From 30b5beab648574ea6c8d914e9105fad0128c71f5 Mon Sep 17 00:00:00 2001 From: tateisu Date: Sun, 15 Jan 2023 16:51:13 +0900 Subject: [PATCH] =?UTF-8?q?runTest=E3=82=92=E4=BD=BF=E3=81=86=E3=80=82dete?= =?UTF-8?q?kt=E3=81=AE=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E7=AF=84?= =?UTF-8?q?=E5=9B=B2=E3=82=92=E5=85=A8=E3=83=A2=E3=82=B8=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=81=A8=E3=83=86=E3=82=B9=E3=83=88=E3=82=B3=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=82=92=E5=90=AB=E3=82=81=E3=82=8B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../jp/juggler/apng/ApngAnimationControl.kt | 45 +- .../java/jp/juggler/apng/ApngBackground.kt | 64 +- .../main/java/jp/juggler/apng/ApngBitmap.kt | 118 +- .../main/java/jp/juggler/apng/ApngChunk.kt | 61 +- .../main/java/jp/juggler/apng/ApngDecoder.kt | 308 +- .../jp/juggler/apng/ApngDecoderCallback.kt | 53 +- .../java/jp/juggler/apng/ApngFrameControl.kt | 87 +- .../java/jp/juggler/apng/ApngImageHeader.kt | 88 +- .../main/java/jp/juggler/apng/ApngPalette.kt | 66 +- .../jp/juggler/apng/ApngTransparentColor.kt | 40 +- .../main/java/jp/juggler/apng/GifDecoder.kt | 1166 ++- .../jp/juggler/apng/GifDecoderCallback.kt | 13 +- .../main/java/jp/juggler/apng/IdatDecoder.kt | 33 +- .../java/jp/juggler/apng/util/BufferPool.kt | 8 +- .../java/jp/juggler/apng/util/ByteSequence.kt | 46 +- .../jp/juggler/apng/util/ByteSequenceQueue.kt | 60 +- .../jp/juggler/apng/util/StreamTokenizer.kt | 118 +- .../src/test/java/jp/juggler/apng/TestApng.kt | 6434 +++++++++-------- .../main/java/jp/juggler/apng/ApngFrames.kt | 995 ++- app/build.gradle | 66 +- .../subwaytooter/ExampleInstrumentedTest.kt | 2 +- .../juggler/subwaytooter/JsonArrayForEach.kt | 8 +- .../juggler/subwaytooter/TestColorString.kt | 2 +- .../subwaytooter/TestMisskeyMentionAndroid.kt | 2 +- .../jp/juggler/subwaytooter/TestTimeParser.kt | 2 +- .../juggler/subwaytooter/TestTootInstance.kt | 94 +- .../juggler/subwaytooter/WordTrieTreeTest.kt | 2 +- .../subwaytooter/api/TestDuplicateMap.kt | 2 +- .../subwaytooter/api/TestTootApiClient.kt | 75 +- .../api/entity/TestEntityUtils.kt | 2 +- .../api/entity/TestTootAccount.kt | 2 +- .../database/TestArrayListSizeBug.kt | 2 +- .../subwaytooter/database/TestDatabase.kt | 2 +- .../testutil/MainDispatcherRule.kt | 32 + .../subwaytooter/testutil/MockInterceptor.kt | 57 + .../subwaytooter/util/TestBucketList.kt | 2 +- .../subwaytooter/util/TestHtmlDecoder.kt | 6 +- .../res/raw/test_toot_instance_mock.json | 226 + .../juggler/subwaytooter/action/Action_Tag.kt | 1 - .../subwaytooter/actmain/SideMenuAdapter.kt | 1 - .../subwaytooter/api/entity/TootInstance.kt | 6 +- .../notification/CheckerWakeLocks.kt | 4 +- .../subwaytooter/util/AttachmentUploader.kt | 1 - .../jp/juggler/subwaytooter/util/PostImpl.kt | 1 - .../juggler/subwaytooter/util/WorkerBase.kt | 4 +- base/build.gradle | 7 + .../java/jp/juggler/base/JugglerBaseTest.kt | 8 +- .../juggler/util/coroutine/AppDispatchers.kt | 14 +- .../juggler/util/coroutine/AsyncActivity.kt | 1 - .../jp/juggler/util/data/CollectionUtils.kt | 3 - .../java/jp/juggler/util/data/ColumnMeta.kt | 4 +- .../java/jp/juggler/util/data/StorageUtils.kt | 2 - .../java/jp/juggler/util/data/WordTrieTree.kt | 10 +- .../java/jp/juggler/util/log/ToastUtils.kt | 4 +- .../colorpicker/AlphaPatternDrawable.kt | 1 - .../colorpicker/ColorPaletteAdapter.kt | 2 +- .../android/colorpicker/ColorPanelView.kt | 19 +- .../android/colorpicker/ColorPickerDialog.kt | 36 +- .../android/colorpicker/ColorPickerView.kt | 8 +- .../ExampleInstrumentedTest.kt | 24 - .../icon_material_symbols/ExampleUnitTest.kt | 17 - .../java/jp/juggler/apng/sample/ActList.kt | 14 +- .../java/jp/juggler/apng/sample/ActViewer.kt | 2 +- .../java/jp/juggler/apng/sample/ApngView.kt | 218 +- 65 files changed, 5548 insertions(+), 5254 deletions(-) create mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MainDispatcherRule.kt create mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MockInterceptor.kt create mode 100644 app/src/androidTest/res/raw/test_toot_instance_mock.json delete mode 100644 icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt delete mode 100644 icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt diff --git a/.gitignore b/.gitignore index bfd32a6b..8b836adf 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,4 @@ detektReport/ app/detekt-*.xml +.idea/androidTestResultsUserPreferences.xml diff --git a/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt b/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt index 59434f8f..b1c456e2 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt @@ -5,33 +5,32 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence class ApngAnimationControl( - + // This must equal the number of `fcTL` chunks. // 0 is not a valid value. // 1 is a valid value for a single-frame APNG. - val numFrames : Int, - + val numFrames: Int, + // if it is 0, the animation should play indefinitely. // If nonzero, the animation should come to rest on the final frame at the end of the last play. - val numPlays : Int = PLAY_INDEFINITELY + val numPlays: Int = PLAY_INDEFINITELY, ) { - - companion object { - const val PLAY_INDEFINITELY = 0 - - internal fun parse(src : ByteSequence) : ApngAnimationControl { - val numFrames = src.readInt32() - val numPlays = src.readInt32() - return ApngAnimationControl( - numFrames = numFrames, - numPlays = numPlays - ) - - } - } - - override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)" - - val isFinite : Boolean - get() = numPlays > PLAY_INDEFINITELY + + companion object { + const val PLAY_INDEFINITELY = 0 + + internal fun parse(src: ByteSequence): ApngAnimationControl { + val numFrames = src.readInt32() + val numPlays = src.readInt32() + return ApngAnimationControl( + numFrames = numFrames, + numPlays = numPlays + ) + } + } + + override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)" + + val isFinite: Boolean + get() = numPlays > PLAY_INDEFINITELY } diff --git a/apng/src/main/java/jp/juggler/apng/ApngBackground.kt b/apng/src/main/java/jp/juggler/apng/ApngBackground.kt index 96185fd6..f38dfb2e 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngBackground.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngBackground.kt @@ -4,36 +4,36 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence -class ApngBackground internal constructor(colorType : ColorType, src : ByteSequence) { - - val red : Int - val green : Int - val blue : Int - val index : Int - - init { - when(colorType) { - ColorType.GREY, ColorType.GREY_ALPHA -> { - val v = src.readUInt16() - red = v - green = v - blue = v - index = - 1 - } - - ColorType.RGB, ColorType.RGBA -> { - red = src.readUInt16() - green = src.readUInt16() - blue = src.readUInt16() - index = - 1 - } - - ColorType.INDEX -> { - red = - 1 - green = - 1 - blue = - 1 - index = src.readUInt8() - } - } - } +class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) { + + val red: Int + val green: Int + val blue: Int + val index: Int + + init { + when (colorType) { + ColorType.GREY, ColorType.GREY_ALPHA -> { + val v = src.readUInt16() + red = v + green = v + blue = v + index = -1 + } + + ColorType.RGB, ColorType.RGBA -> { + red = src.readUInt16() + green = src.readUInt16() + blue = src.readUInt16() + index = -1 + } + + ColorType.INDEX -> { + red = -1 + green = -1 + blue = -1 + index = src.readUInt8() + } + } + } } \ No newline at end of file diff --git a/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt b/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt index f6479701..f29bae2d 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt @@ -2,63 +2,63 @@ package jp.juggler.apng -class ApngBitmap(var width : Int, var height : Int) { - - // each int value contains 0xAARRGGBB - val colors = IntArray(width * height) - - // widthとheightを再指定する。ビットマップはそのまま再利用する - fun reset(width : Int, height : Int) { - val newSize = width * height - if(newSize > colors.size) - throw ApngParseError("can't resize to $width x $height , it's greater than initial size") - this.width = width - this.height = height - // 透明な黒で初期化する - colors.fill(0, fromIndex = 0, toIndex = newSize) - } - - // ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う - inner class Pointer { - - private var pos : Int = 0 - var step : Int = 1 - - fun setPixel(argb : Int) = apply { colors[pos] = argb } - - fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel( - ((a and 255) shl 24) or - ((r and 255) shl 16) or - ((g and 255) shl 8) or - (b and 255) - ) - - fun setOffset(pos : Int = 0, step : Int = 1) = apply { - this.pos = pos - this.step = step - } - - fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step) - - fun plus(x : Int) = apply { pos += x } - - fun next() = plus(step) - - val color : Int - get() = colors[pos] - - val alpha : Int - get() = (colors[pos] shr 24) and 255 - - val red : Int - get() = (colors[pos] shr 16) and 255 - - val green : Int - get() = (colors[pos] shr 8) and 255 - - val blue : Int - get() = (colors[pos]) and 255 - } - - fun pointer() = Pointer() +class ApngBitmap(var width: Int, var height: Int) { + + // each int value contains 0xAARRGGBB + val colors = IntArray(width * height) + + // widthとheightを再指定する。ビットマップはそのまま再利用する + fun reset(width: Int, height: Int) { + val newSize = width * height + if (newSize > colors.size) + throw ApngParseError("can't resize to $width x $height , it's greater than initial size") + this.width = width + this.height = height + // 透明な黒で初期化する + colors.fill(0, fromIndex = 0, toIndex = newSize) + } + + // ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う + inner class Pointer { + + private var pos: Int = 0 + var step: Int = 1 + + fun setPixel(argb: Int) = apply { colors[pos] = argb } + + fun setPixel(a: Int, r: Int, g: Int, b: Int) = setPixel( + ((a and 255) shl 24) or + ((r and 255) shl 16) or + ((g and 255) shl 8) or + (b and 255) + ) + + fun setOffset(pos: Int = 0, step: Int = 1) = apply { + this.pos = pos + this.step = step + } + + fun setXY(x: Int, y: Int, step: Int = 1) = setOffset(x + y * width, step) + + fun plus(x: Int) = apply { pos += x } + + fun next() = plus(step) + + val color: Int + get() = colors[pos] + + val alpha: Int + get() = (colors[pos] shr 24) and 255 + + val red: Int + get() = (colors[pos] shr 16) and 255 + + val green: Int + get() = (colors[pos] shr 8) and 255 + + val blue: Int + get() = (colors[pos]) and 255 + } + + fun pointer() = Pointer() } diff --git a/apng/src/main/java/jp/juggler/apng/ApngChunk.kt b/apng/src/main/java/jp/juggler/apng/ApngChunk.kt index d15702d5..5ac50c09 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngChunk.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngChunk.kt @@ -5,35 +5,34 @@ package jp.juggler.apng import jp.juggler.apng.util.StreamTokenizer import java.util.zip.CRC32 -internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) { - val size : Int - val type : String - - init { - size = tokenizer.readInt32() - val typeBytes = tokenizer.readBytes(4) - type = typeBytes.toString(Charsets.UTF_8) - - crc32.update(typeBytes) - } - - fun readBody(crc32 : CRC32, tokenizer : StreamTokenizer) : ByteArray { - val bytes = tokenizer.readBytes(size) - val crcExpect = tokenizer.readUInt32() - - crc32.update(bytes, 0, size) - val crcActual = crc32.value - if(crcActual != crcExpect) throw ApngParseError("CRC not match.") - - return bytes - } - - fun skipBody(tokenizer : StreamTokenizer) = - tokenizer.skipBytes((size + 4).toLong()) - - - fun checkCRC(tokenizer : StreamTokenizer, crcActual : Long) { - val crcExpect = tokenizer.readUInt32() - if(crcActual != crcExpect) throw ApngParseError("CRC not match.") - } +internal class ApngChunk(crc32: CRC32, tokenizer: StreamTokenizer) { + val size: Int + val type: String + + init { + size = tokenizer.readInt32() + val typeBytes = tokenizer.readBytes(4) + type = typeBytes.toString(Charsets.UTF_8) + + crc32.update(typeBytes, 0, typeBytes.size) + } + + fun readBody(crc32: CRC32, tokenizer: StreamTokenizer): ByteArray { + val bytes = tokenizer.readBytes(size) + val crcExpect = tokenizer.readUInt32() + + crc32.update(bytes, 0, size) + val crcActual = crc32.value + if (crcActual != crcExpect) throw ApngParseError("CRC not match.") + + return bytes + } + + fun skipBody(tokenizer: StreamTokenizer) = + tokenizer.skipBytes((size + 4).toLong()) + + fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) { + val crcExpect = tokenizer.readUInt32() + if (crcActual != crcExpect) throw ApngParseError("CRC not match.") + } } diff --git a/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt b/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt index b14a56f0..22221bf6 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt @@ -7,165 +7,165 @@ import java.io.InputStream import java.util.zip.CRC32 object ApngDecoder { - - private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a) - - fun parseStream( - inStream : InputStream, - callback : ApngDecoderCallback + + private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a) + + fun parseStream( + inStream: InputStream, + callback: ApngDecoderCallback, ) { - val apng = Apng() - val tokenizer = StreamTokenizer(inStream) - - val pngHeader = tokenizer.readBytes(8) - if(! pngHeader.contentEquals(PNG_SIGNATURE)) { - throw ApngParseError("header not match") - } - - var lastSequenceNumber : Int? = null - fun checkSequenceNumber(n : Int) { - val last = lastSequenceNumber - if(last != null && n <= last) { - throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n") - } - lastSequenceNumber = n - } - - val inBuffer = ByteArray(4096) - val inflateBufferPool = BufferPool(8192) - var idatDecoder : IdatDecoder? = null - var fdatDecoder : IdatDecoder? = null - val crc32 = CRC32() - var lastFctl : ApngFrameControl? = null - var bitmap : ApngBitmap? = null - - loop@ while(true) { - crc32.reset() - val chunk = ApngChunk(crc32, tokenizer) - when(chunk.type) { - - "IEND" -> break@loop - - "IHDR" -> { - val header = - ApngImageHeader.parse(ByteSequence(chunk.readBody(crc32, tokenizer))) - bitmap = ApngBitmap(header.width, header.height) - apng.header = header - callback.onHeader(apng, header) - } - - "PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer)) - - "bKGD" -> { - val header = apng.header ?: throw ApngParseError("missing IHDR") - apng.background = ApngBackground( - header.colorType, - ByteSequence(chunk.readBody(crc32, tokenizer)) - ) - } - - "tRNS" -> { - val header = apng.header ?: throw ApngParseError("missing IHDR") - val body = chunk.readBody(crc32, tokenizer) - when(header.colorType) { - ColorType.GREY -> apng.transparentColor = - ApngTransparentColor(true, ByteSequence(body)) - ColorType.RGB -> apng.transparentColor = - ApngTransparentColor(false, ByteSequence(body)) - ColorType.INDEX -> apng.palette?.parseTRNS(body) - ?: throw ApngParseError("missing palette") - else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}") - } - } - - "IDAT" -> { - val header = apng.header ?: throw ApngParseError("missing IHDR") - if(idatDecoder == null) { - bitmap ?: throw ApngParseError("missing bitmap") - bitmap.reset(header.width, header.height) - idatDecoder = IdatDecoder( - apng, - bitmap, - inflateBufferPool, - callback - ) { - callback.onDefaultImage(apng, bitmap) - val fctl = lastFctl - if(fctl != null) { - // IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ - callback.onAnimationFrame(apng, fctl, bitmap) - } - } - } - idatDecoder.addData( - tokenizer.inStream, - chunk.size, - inBuffer, - crc32 - ) - chunk.checkCRC(tokenizer, crc32.value) - } - - "acTL" -> { - val header = apng.header ?: throw ApngParseError("missing IHDR") - val animationControl = ApngAnimationControl - .parse(ByteSequence(chunk.readBody(crc32, tokenizer))) - apng.animationControl = animationControl - callback.onAnimationInfo(apng, header, animationControl) - } - - "fcTL" -> { - val bat = ByteSequence(chunk.readBody(crc32, tokenizer)) - val sequenceNumber = bat.readInt32() - checkSequenceNumber(sequenceNumber) - lastFctl = ApngFrameControl.parse(bat, sequenceNumber) - fdatDecoder = null - } - - "fdAT" -> { - val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT") - if(fdatDecoder == null) { - bitmap ?: throw ApngParseError("missing bitmap") - bitmap.reset(fctl.width, fctl.height) - fdatDecoder = IdatDecoder( - apng, - bitmap, - inflateBufferPool, - callback - ) { - callback.onAnimationFrame(apng, fctl, bitmap) - } - } - val sequenceNumber = tokenizer.readInt32(crc32) - checkSequenceNumber(sequenceNumber) - fdatDecoder.addData( - tokenizer.inStream, - chunk.size - 4, - inBuffer, - crc32 - ) - chunk.checkCRC(tokenizer, crc32.value) - } - - // 無視するチャンク + val apng = Apng() + val tokenizer = StreamTokenizer(inStream) + + val pngHeader = tokenizer.readBytes(8) + if (!pngHeader.contentEquals(PNG_SIGNATURE)) { + throw ApngParseError("header not match") + } + + var lastSequenceNumber: Int? = null + fun checkSequenceNumber(n: Int) { + val last = lastSequenceNumber + if (last != null && n <= last) { + throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n") + } + lastSequenceNumber = n + } + + val inBuffer = ByteArray(4096) + val inflateBufferPool = BufferPool(8192) + var idatDecoder: IdatDecoder? = null + var fdatDecoder: IdatDecoder? = null + val crc32 = CRC32() + var lastFctl: ApngFrameControl? = null + var bitmap: ApngBitmap? = null + + loop@ while (true) { + crc32.reset() + val chunk = ApngChunk(crc32, tokenizer) + when (chunk.type) { + + "IEND" -> break@loop + + "IHDR" -> { + val header = + ApngImageHeader.parse(ByteSequence(chunk.readBody(crc32, tokenizer))) + bitmap = ApngBitmap(header.width, header.height) + apng.header = header + callback.onHeader(apng, header) + } + + "PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer)) + + "bKGD" -> { + val header = apng.header ?: throw ApngParseError("missing IHDR") + apng.background = ApngBackground( + header.colorType, + ByteSequence(chunk.readBody(crc32, tokenizer)) + ) + } + + "tRNS" -> { + val header = apng.header ?: throw ApngParseError("missing IHDR") + val body = chunk.readBody(crc32, tokenizer) + when (header.colorType) { + ColorType.GREY -> apng.transparentColor = + ApngTransparentColor(true, ByteSequence(body)) + ColorType.RGB -> apng.transparentColor = + ApngTransparentColor(false, ByteSequence(body)) + ColorType.INDEX -> apng.palette?.parseTRNS(body) + ?: throw ApngParseError("missing palette") + else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}") + } + } + + "IDAT" -> { + val header = apng.header ?: throw ApngParseError("missing IHDR") + if (idatDecoder == null) { + bitmap ?: throw ApngParseError("missing bitmap") + bitmap.reset(header.width, header.height) + idatDecoder = IdatDecoder( + apng, + bitmap, + inflateBufferPool, + callback + ) { + callback.onDefaultImage(apng, bitmap) + val fctl = lastFctl + if (fctl != null) { + // IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ + callback.onAnimationFrame(apng, fctl, bitmap) + } + } + } + idatDecoder.addData( + tokenizer.inStream, + chunk.size, + inBuffer, + crc32 + ) + chunk.checkCRC(tokenizer, crc32.value) + } + + "acTL" -> { + val header = apng.header ?: throw ApngParseError("missing IHDR") + val animationControl = ApngAnimationControl + .parse(ByteSequence(chunk.readBody(crc32, tokenizer))) + apng.animationControl = animationControl + callback.onAnimationInfo(apng, header, animationControl) + } + + "fcTL" -> { + val bat = ByteSequence(chunk.readBody(crc32, tokenizer)) + val sequenceNumber = bat.readInt32() + checkSequenceNumber(sequenceNumber) + lastFctl = ApngFrameControl.parse(bat, sequenceNumber) + fdatDecoder = null + } + + "fdAT" -> { + val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT") + if (fdatDecoder == null) { + bitmap ?: throw ApngParseError("missing bitmap") + bitmap.reset(fctl.width, fctl.height) + fdatDecoder = IdatDecoder( + apng, + bitmap, + inflateBufferPool, + callback + ) { + callback.onAnimationFrame(apng, fctl, bitmap) + } + } + val sequenceNumber = tokenizer.readInt32(crc32) + checkSequenceNumber(sequenceNumber) + fdatDecoder.addData( + tokenizer.inStream, + chunk.size - 4, + inBuffer, + crc32 + ) + chunk.checkCRC(tokenizer, crc32.value) + } + + // 無視するチャンク "cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information "tEXt", "zTXt", "iTXt", // text information "tIME", // timestamp "hIST", // histogram "pHYs", // Physical pixel dimensions - "sPLT" // Suggested palette (おそらく減色用?) + "sPLT", // Suggested palette (おそらく減色用?) -> chunk.skipBody(tokenizer) - - else -> { - callback.onApngWarning( - "unknown chunk: type=%s,size=0x%x".format( - chunk.type, - chunk.size - ) - ) - chunk.skipBody(tokenizer) - } - } - } - } + + else -> { + callback.onApngWarning( + "unknown chunk: type=%s,size=0x%x".format( + chunk.type, + chunk.size + ) + ) + chunk.skipBody(tokenizer) + } + } + } + } } diff --git a/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt b/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt index 188f733c..8c53d412 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt @@ -1,31 +1,30 @@ package jp.juggler.apng interface ApngDecoderCallback { - - // called for non-fatal warning - fun onApngWarning(message : String) - - // called for debug message - fun onApngDebug(message : String) {} - - fun canApngDebug() : Boolean = false - - // called when PNG image header is detected. - fun onHeader(apng : Apng, header : ApngImageHeader) - - // called when APNG Animation Control is detected. - fun onAnimationInfo( - apng : Apng, - header : ApngImageHeader, - animationControl : ApngAnimationControl - ) - - // called when default image bitmap was rendered. - fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) - - // called when APNG Frame Control is detected and its bitmap was rendered. - // its bitmap may same to default image for first frame. - // ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap) - fun onAnimationFrame(apng : Apng, frameControl : ApngFrameControl, frameBitmap : ApngBitmap) - + + // called for non-fatal warning + fun onApngWarning(message: String) + + // called for debug message + fun onApngDebug(message: String) {} + + fun canApngDebug(): Boolean = false + + // called when PNG image header is detected. + fun onHeader(apng: Apng, header: ApngImageHeader) + + // called when APNG Animation Control is detected. + fun onAnimationInfo( + apng: Apng, + header: ApngImageHeader, + animationControl: ApngAnimationControl, + ) + + // called when default image bitmap was rendered. + fun onDefaultImage(apng: Apng, bitmap: ApngBitmap) + + // called when APNG Frame Control is detected and its bitmap was rendered. + // its bitmap may same to default image for first frame. + // ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap) + fun onAnimationFrame(apng: Apng, frameControl: ApngFrameControl, frameBitmap: ApngBitmap) } diff --git a/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt b/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt index 778a9b02..e364bb2e 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt @@ -4,51 +4,50 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence -class ApngFrameControl ( - val width : Int, - val height : Int, - val xOffset : Int, - val yOffset : Int, - val disposeOp : DisposeOp, - val blendOp : BlendOp, - val sequenceNumber:Int, - val delayMilliseconds: Long +class ApngFrameControl( + val width: Int, + val height: Int, + val xOffset: Int, + val yOffset: Int, + val disposeOp: DisposeOp, + val blendOp: BlendOp, + val sequenceNumber: Int, + val delayMilliseconds: Long, ) { - - companion object{ - internal fun parse(src : ByteSequence, sequenceNumber:Int) :ApngFrameControl{ - val width = src.readInt32() - val height = src.readInt32() - val xOffset = src.readInt32() - val yOffset = src.readInt32() - val delayNum = src.readUInt16() - val delayDen = src.readUInt16().let { if(it == 0) 100 else it } - - var num : Int - - num = src.readUInt8() - val disposeOp = DisposeOp.values().first { it.num == num } - - num = src.readUInt8() - val blendOp = BlendOp.values().first { it.num == num } - return ApngFrameControl( - width =width, - height = height, - xOffset = xOffset, - yOffset = yOffset, - disposeOp = disposeOp, - blendOp = blendOp, - sequenceNumber = sequenceNumber, - delayMilliseconds = when(delayDen) { - 0,1000 -> delayNum.toLong() - else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong() - } - ) - } - } - - override fun toString() = - "ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)" + companion object { + internal fun parse(src: ByteSequence, sequenceNumber: Int): ApngFrameControl { + val width = src.readInt32() + val height = src.readInt32() + val xOffset = src.readInt32() + val yOffset = src.readInt32() + val delayNum = src.readUInt16() + val delayDen = src.readUInt16().let { if (it == 0) 100 else it } + var num: Int + + num = src.readUInt8() + val disposeOp = DisposeOp.values().first { it.num == num } + + num = src.readUInt8() + val blendOp = BlendOp.values().first { it.num == num } + + return ApngFrameControl( + width = width, + height = height, + xOffset = xOffset, + yOffset = yOffset, + disposeOp = disposeOp, + blendOp = blendOp, + sequenceNumber = sequenceNumber, + delayMilliseconds = when (delayDen) { + 0, 1000 -> delayNum.toLong() + else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong() + } + ) + } + } + + override fun toString() = + "ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)" } \ No newline at end of file diff --git a/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt b/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt index 3ab5dd01..fc8fb446 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt @@ -6,50 +6,48 @@ import jp.juggler.apng.util.ByteSequence // information from IHDR chunk. class ApngImageHeader( - val width : Int, - val height : Int, - val bitDepth : Int, - val colorType : ColorType, - val compressionMethod : CompressionMethod, - val filterMethod : FilterMethod, - val interlaceMethod : InterlaceMethod -) { - companion object{ - internal fun parse (src : ByteSequence) :ApngImageHeader{ - - val width = src.readInt32() - val height = src.readInt32() - if(width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small") - - val bitDepth = src.readUInt8() - - var num : Int - // - num = src.readUInt8() - val colorType = ColorType.values().first { it.num == num } - // - num = src.readUInt8() - val compressionMethod = CompressionMethod.values().first { it.num == num } - // - num = src.readUInt8() - val filterMethod = FilterMethod.values().first { it.num == num } - // - num = src.readUInt8() - val interlaceMethod = InterlaceMethod.values().first { it.num == num } + val width: Int, + val height: Int, + val bitDepth: Int, + val colorType: ColorType, + val compressionMethod: CompressionMethod, + val filterMethod: FilterMethod, + val interlaceMethod: InterlaceMethod, +) { + companion object { + internal fun parse(src: ByteSequence): ApngImageHeader { + val width = src.readInt32() + val height = src.readInt32() + if (width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small") - return ApngImageHeader( - width =width, - height = height, - bitDepth = bitDepth, - colorType = colorType, - compressionMethod = compressionMethod, - filterMethod = filterMethod, - interlaceMethod = interlaceMethod - - ) - } - } - - override fun toString() = - "ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)" + val bitDepth = src.readUInt8() + + var num: Int + // + num = src.readUInt8() + val colorType = ColorType.values().first { it.num == num } + // + num = src.readUInt8() + val compressionMethod = CompressionMethod.values().first { it.num == num } + // + num = src.readUInt8() + val filterMethod = FilterMethod.values().first { it.num == num } + // + num = src.readUInt8() + val interlaceMethod = InterlaceMethod.values().first { it.num == num } + + return ApngImageHeader( + width = width, + height = height, + bitDepth = bitDepth, + colorType = colorType, + compressionMethod = compressionMethod, + filterMethod = filterMethod, + interlaceMethod = interlaceMethod, + ) + } + } + + override fun toString() = + "ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)" } diff --git a/apng/src/main/java/jp/juggler/apng/ApngPalette.kt b/apng/src/main/java/jp/juggler/apng/ApngPalette.kt index f7fe3af2..099632a8 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngPalette.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngPalette.kt @@ -6,38 +6,38 @@ import jp.juggler.apng.util.getUInt8 import kotlin.math.min class ApngPalette( - src : ByteArray // repeat of R,G,B + src: ByteArray, // repeat of R,G,B ) { - - companion object { - // full opaque black - const val OPAQUE = 255 shl 24 - } - - val list : IntArray // repeat of 0xAARRGGBB - - var hasAlpha : Boolean = false - - init { - val entryCount = src.size / 3 - list = IntArray(entryCount) - var pos = 0 - for(i in 0 until entryCount) { - list[i] = OPAQUE or - (src.getUInt8(pos) shl 16) or - (src.getUInt8(pos + 1) shl 8) or - src.getUInt8(pos + 2) - pos += 3 - } - } - - override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)" - - // update alpha value from tRNS chunk data - fun parseTRNS(ba : ByteArray) { - hasAlpha = true - for(i in 0 until min(list.size, ba.size)) { - list[i] = (list[i] and 0xffffff) or (ba.getUInt8(i) shl 24) - } - } + + companion object { + // full opaque black + const val OPAQUE = 255 shl 24 + } + + val list: IntArray // repeat of 0xAARRGGBB + + var hasAlpha: Boolean = false + + init { + val entryCount = src.size / 3 + list = IntArray(entryCount) + var pos = 0 + for (i in 0 until entryCount) { + list[i] = OPAQUE or + (src.getUInt8(pos) shl 16) or + (src.getUInt8(pos + 1) shl 8) or + src.getUInt8(pos + 2) + pos += 3 + } + } + + override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)" + + // update alpha value from tRNS chunk data + fun parseTRNS(ba: ByteArray) { + hasAlpha = true + for (i in 0 until min(list.size, ba.size)) { + list[i] = (list[i] and 0xffffff) or (ba.getUInt8(i) shl 24) + } + } } diff --git a/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt b/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt index 1f910468..7e5842b3 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt @@ -4,24 +4,24 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence -class ApngTransparentColor internal constructor(isGreyScale : Boolean, src : ByteSequence) { - val red : Int - val green : Int - val blue : Int - - init { - if(isGreyScale) { - val v = src.readUInt16() - red = v - green = v - blue = v - } else { - red = src.readUInt16() - green = src.readUInt16() - blue = src.readUInt16() - } - } - - fun match(grey : Int) = red == grey - fun match(r : Int, g : Int, b : Int) = (r == red && g == green && b == blue) +class ApngTransparentColor internal constructor(isGreyScale: Boolean, src: ByteSequence) { + val red: Int + val green: Int + val blue: Int + + init { + if (isGreyScale) { + val v = src.readUInt16() + red = v + green = v + blue = v + } else { + red = src.readUInt16() + green = src.readUInt16() + blue = src.readUInt16() + } + } + + fun match(grey: Int) = red == grey + fun match(r: Int, g: Int, b: Int) = (r == red && g == green && b == blue) } \ No newline at end of file diff --git a/apng/src/main/java/jp/juggler/apng/GifDecoder.kt b/apng/src/main/java/jp/juggler/apng/GifDecoder.kt index 0970eb47..3fe754f1 100644 --- a/apng/src/main/java/jp/juggler/apng/GifDecoder.kt +++ b/apng/src/main/java/jp/juggler/apng/GifDecoder.kt @@ -1,7 +1,6 @@ package jp.juggler.apng import java.io.InputStream -import java.lang.StringBuilder import kotlin.math.min // https://raw.githubusercontent.com/rtyley/animated-gif-lib-for-java/master/src/main/java/com/madgag/gif/fmsware/GifDecoder.java @@ -11,588 +10,585 @@ import kotlin.math.min // http://www.theimage.com/animation/pages/disposal3.html // great sample images. -class GifDecoder(val callback : GifDecoderCallback) { - - private class Rectangle(var x : Int = 0, var y : Int = 0, var w : Int = 0, var h : Int = 0) { - - fun set(src : Rectangle) { - this.x = src.x - this.y = src.y - this.w = src.w - this.h = src.h - } - } - - private class Reader(val bis : InputStream) { - - var block = ByteArray(256) // current data block - var blockSize = 0 // block size - - // Reads a single byte from the input stream. - fun byte() : Int = bis.read() - - // Reads next 16-bit value, LSB first - fun UInt16() = byte() or (byte() shl 8) - - fun array(ba : ByteArray, offset : Int = 0, length : Int = ba.size - offset) { - var nRead = 0 - while(nRead < length) { - val delta = bis.read(ba, offset + nRead, length - nRead) - if(delta == - 1) error("unexpected End of Stream") - nRead += delta - } - } - - // Reads specified bytes and compose it to ascii string - fun string(n : Int) : String { - return StringBuilder(n).apply{ - ByteArray(n) - .also{ array(it)} - .forEach { append( Char( it.toInt() and 255)) } - }.toString() - } - - // Reads next variable length block - fun block() : ByteArray { - blockSize = byte() - array(block, 0, blockSize) - return block - } - - // Skips variable length blocks up to and including next zero length block. - fun skipBlock() { - do { - block() - } while(blockSize > 0) - } - } - - // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev - enum class Dispose(val num : Int) { - - Unspecified(0), - DontDispose(1), - RestoreBackground(2), - RestorePrevious(3) - } - - companion object { - private const val MaxStackSize = 4096 - private const val NullCode = - 1 - private const val b0 = 0.toByte() - private const val OPAQUE = 255 shl 24 - } - - private var width = 0 // full image width - private var height = 0 // full image height - private var gctSize = 0 // size of global color table - private var loopCount = 1 // iterations; 0 = repeat forever - - private var gct : IntArray? = null // global color table - - private var bgIndex = 0 // background color index - private var bgColor = 0 // background color - private var lastBgColor = 0 // previous bg color - private var pixelAspect = 0 // pixel aspect ratio - - private var interlace = false // interlace flag - - private var lctFlag = false // local color table flag - private var lctSize = 0 // local color table size - - private val srcRect = Rectangle() // current image position and size - private val lastRect = Rectangle() // last image rect - - // last graphic control extension info - private var dispose = Dispose.Unspecified - private var lastDispose = Dispose.Unspecified - private var transparency = false // use transparent color - private var delay = 0 // delay in milliseconds - private var transIndex = 0 // transparent color index - - // LZW decoder working arrays - private var prefix : ShortArray? = null - private var suffix : ByteArray? = null - private var pixelStack : ByteArray? = null - private var pixels : ByteArray? = null - - private val frames = ArrayList>() - - private var previousImage : ApngBitmap? = null - - // 現在のdispose指定と描画結果を覚えておく - private fun memoryLastDispose(image : ApngBitmap) { - if(dispose != Dispose.RestorePrevious) previousImage = image - lastDispose = dispose - lastRect.set(srcRect) - lastBgColor = bgColor - } - - // 前回のdispose指定を反映する - private fun applyLastDispose(destImage : ApngBitmap) { - - if(lastDispose == Dispose.Unspecified) return - - // restore previous image - val previousImage = this.previousImage - if(previousImage != null) { - System.arraycopy(previousImage.colors, 0, destImage.colors, 0, destImage.colors.size) - } - - if(lastDispose == Dispose.RestoreBackground) { - - // fill lastRect - - val fillColor = if(transparency) { - 0 // assume background is transparent - } else { - lastBgColor // use given background color - } - - for(y in lastRect.y until lastRect.y + lastRect.h) { - val fillStart = y * destImage.width + lastRect.x - val fillWidth = lastRect.w - destImage.colors.fill( - fillColor, - fromIndex = fillStart, - toIndex = fillStart + fillWidth - ) - } - } - } - - // render to ApngBitmap - // may use some previous frame. - private fun render(destImage : ApngBitmap, act : IntArray) { - // expose destination image's pixels as int array - val dest = destImage.colors - - // copy each source line to the appropriate place in the destination - var pass = 1 - var inc = 8 - var iline = 0 - for(i in 0 until srcRect.h) { - var line = i - if(interlace) { - if(iline >= srcRect.h) { - when(++ pass) { - 2 -> { - iline = 4 - } - - 3 -> { - iline = 2 - inc = 4 - } - - 4 -> { - iline = 1 - inc = 2 - } - } - } - line = iline - iline += inc - } - line += srcRect.y - if(line < height) { - - // start of line in source - var sx = i * srcRect.w - - // - val k = line * width - - // loop for dest line. - for(dx in k + srcRect.x until min(k + width, k + srcRect.x + srcRect.w)) { - // map color and insert in destination - val index = pixels !![sx ++].toInt() and 0xff - val c = act[index] - if(c != 0) dest[dx] = c - } - } - } - - } - - /** - * Decodes LZW image data into pixel array. - * Adapted from John Cristy's ImageMagick. - */ - private fun decodeImageData(reader : Reader) { - - // allocate pixel array if need - val nPixels = srcRect.w * srcRect.h - if((pixels?.size ?: 0) < nPixels) pixels = ByteArray(nPixels) - val pixels = this.pixels !! - - if(prefix == null) prefix = ShortArray(MaxStackSize) - if(suffix == null) suffix = ByteArray(MaxStackSize) - if(pixelStack == null) pixelStack = ByteArray(MaxStackSize + 1) - val prefix = this.prefix !! - val suffix = this.suffix !! - val pixelStack = this.pixelStack !! - - // Initialize GIF data stream decoder. - val data_size = reader.byte() - val clear = 1 shl data_size - val end_of_information = clear + 1 - - var available = clear + 2 - var old_code = NullCode - var code_size = data_size + 1 - var code_mask = (1 shl code_size) - 1 - - for(code in 0 until clear) { - prefix[code] = 0 - suffix[code] = code.toByte() - } - - // Decode GIF pixel stream. - var datum = 0 - var bits = 0 - var count = 0 - var first = 0 - var top = 0 - var bi = 0 - var pi = 0 - - var i = 0 - while(i < nPixels) { - if(top == 0) { - if(bits < code_size) { - // Load bytes until there are enough bits for a code. - if(count == 0) { - // Read a new data block. - reader.block() - count = reader.blockSize - if(count <= 0) break - bi = 0 - } - datum += (reader.block[bi].toInt() and 0xff) shl bits - bits += 8 - bi ++ - count -- - continue - } - - // Get the next code. - var code = datum and code_mask - datum = datum shr code_size - bits -= code_size - - // Interpret the code - if((code > available) || (code == end_of_information)) break - - if(code == clear) { - // Reset decoder. - code_size = data_size + 1 - code_mask = (1 shl code_size) - 1 - available = clear + 2 - old_code = NullCode - continue - } - if(old_code == NullCode) { - pixelStack[top ++] = suffix[code] - old_code = code - first = code - continue - } - val in_code = code - - if(code == available) { - pixelStack[top ++] = first.toByte() - code = old_code - } - while(code > clear) { - pixelStack[top ++] = suffix[code] - code = prefix[code].toInt() - } - first = suffix[code].toInt() and 0xff - - // Add a new string to the string table, - - if(available >= MaxStackSize) { - pixelStack[top ++] = first.toByte() - continue - } - pixelStack[top ++] = first.toByte() - prefix[available] = old_code.toShort() - suffix[available] = first.toByte() - available ++ - - if((available and code_mask) == 0 && available < MaxStackSize) { - code_size ++ - code_mask += available - } - - old_code = in_code - } - - // Pop a pixel off the pixel stack. - top -- - pixels[pi ++] = pixelStack[top] - i ++ - } - - // clear missing pixels - for(n in pi until nPixels) { - pixels[n] = b0 - } - } - - /** - * Reads color table as 256 RGB integer values - * - * @param nColors int number of colors to read - * @return int array containing 256 colors (packed ARGB with full alpha) - */ - private fun parseColorTable(reader : Reader, nColors : Int) : IntArray { - val nBytes = 3 * nColors - val c = ByteArray(nBytes) - reader.array(c) - - // max size to avoid bounds checks - val tab = IntArray(256) - var i = 0 - var j = 0 - while(i < nColors) { - val r = c[j].toInt() and 255 - val g = c[j + 1].toInt() and 255 - val b = c[j + 2].toInt() and 255 - j += 3 - tab[i ++] = (OPAQUE or (r shl 16) or (g shl 8) or b) - } - return tab - } - - private fun parseDispose(num : Int) = - Dispose.values().find { it.num == num } ?: error("unknown dispose $num") - - /** - * Reads Graphics Control Extension values - */ - private fun parseGraphicControlExt(reader : Reader) { - reader.byte() // block size - val packed = reader.byte() // packed fields - dispose = parseDispose((packed and 0x1c) shr 2) // disposal method - if(callback.canGifDebug()) callback.onGifDebug("parseGraphicControlExt: frame=${frames.size} dispose=$dispose") - // elect to keep old image if discretionary - if(dispose == Dispose.Unspecified) dispose = Dispose.DontDispose - - transparency = (packed and 1) != 0 - // delay in milliseconds - delay = reader.UInt16() * 10 - // transparent color index - transIndex = reader.byte() - // block terminator - reader.byte() - } - - // Reads Netscape extension to obtain iteration count - private fun readNetscapeExt(reader : Reader) { - do { - val block = reader.block() - if(block[0].toInt() == 1) { - // loop count sub-block - val b1 = block[1].toInt() and 255 - val b2 = block[2].toInt() and 255 - loopCount = ((b2 shl 8) and b1) - } - } while(reader.blockSize > 0) - } - - // Reads next frame image - private fun parseFrame(reader : Reader) { - // (sub)image position & size - srcRect.x = reader.UInt16() - srcRect.y = reader.UInt16() - srcRect.w = reader.UInt16() - srcRect.h = reader.UInt16() - - val packed = reader.byte() - lctFlag = (packed and 0x80) != 0 // 1 - local color table flag - interlace = (packed and 0x40) != 0 // 2 - interlace flag - // 3 - sort flag - // 4-5 - reserved - lctSize = 2 shl (packed and 7) // 6-8 - local color table size - - val act = if(lctFlag) { - // make local table active - parseColorTable(reader, lctSize) - } else { - // make global table active - if(bgIndex == transIndex) bgColor = 0 - gct !! - } - - var save = 0 - if(transparency) { - save = act[transIndex] - act[transIndex] = 0 // set transparent color if specified - } - - decodeImageData(reader) // decode pixel data - reader.skipBlock() - - // add image to frame list - frames.add( - Pair( - ApngFrameControl( - width = width, - height = height, - xOffset = 0, - yOffset = 0, - disposeOp = DisposeOp.None, - blendOp = BlendOp.Source, - sequenceNumber = frames.size, - delayMilliseconds = delay.toLong() - ), - ApngBitmap(width, height).also { - applyLastDispose(it) - render(it, act) // transfer pixel data to image - memoryLastDispose(it) - } - ) - ) - - if(transparency) { - act[transIndex] = save - } - - /** - * Resets frame state for reading next image. - */ - dispose = Dispose.Unspecified - transparency = false - delay = 0 - } - - // read GIF content blocks - private fun readContents(reader : Reader) : ApngAnimationControl { - loopBlocks@ while(true) { - when(val blockCode = reader.byte()) { - // image separator - 0x2C -> parseFrame(reader) - // extension - 0x21 -> when(reader.byte()) { - // graphics control extension - 0xf9 -> parseGraphicControlExt(reader) - // application extension - 0xff -> { - val block = reader.block() - val app = StringBuilder(12) - for(i in 0 until 11) { - app.append( Char( block[i].toInt() and 255 )) - } - if(app.toString() == "NETSCAPE2.0") { - readNetscapeExt(reader) - } else { - reader.skipBlock() // don't care - } - } - - else -> { - // uninteresting extension - reader.skipBlock() - } - } - - // terminator - 0x3b -> break@loopBlocks - - // bad byte, but keep going and see what happens - 0x00 -> { - } - - else -> error("unknown block code $blockCode") - } - } - - return ApngAnimationControl(numFrames = frames.size, numPlays = loopCount) - } - - /** - * Initializes or re-initializes reader - */ - private fun reset() { - frames.clear() - loopCount = ApngAnimationControl.PLAY_INDEFINITELY - gct = null - prefix = null - suffix = null - pixelStack = null - pixels = null - previousImage = null - } - - /** - * Reads GIF file header information. - */ - private fun parseImageHeader(reader : Reader) : ApngImageHeader { - - val id = reader.string(6) - if(! id.startsWith("GIF")) - error("file header not match to GIF.") - - /** - * Reads Logical Screen Descriptor - */ - - // logical screen size - width = reader.UInt16() - height = reader.UInt16() - if(width < 1 || height < 1) error("too small size. ${width}*${height}") - - // packed fields - val packed = reader.byte() - - // global color table used - val gctFlag = (packed and 0x80) != 0 // 1 : global color table flag - // 2-4 : color resolution - // 5 : gct sort flag - gctSize = 2 shl (packed and 7) // 6-8 : gct size - - bgIndex = reader.byte() // background color index - pixelAspect = reader.byte() // pixel aspect ratio - - gct = if(gctFlag) { - val table = parseColorTable(reader, gctSize) - bgColor = table[bgIndex] - table - } else { - bgColor = 0 - null - } - - return ApngImageHeader( - width = this.width, - height = this.height, - bitDepth = 8, - colorType = ColorType.INDEX, - compressionMethod = CompressionMethod.Standard, - filterMethod = FilterMethod.Standard, - interlaceMethod = InterlaceMethod.None - ) - } - - fun parse(src : InputStream) { - - reset() - - val reader = Reader(src) - val header = parseImageHeader(reader) - val animationControl = readContents(reader) - - // GIFは最後まで読まないとフレーム数が分からない - - if(frames.isEmpty()) error("there is no frame.") - callback.onGifHeader(header) - callback.onGifAnimationInfo(header, animationControl) - for(frame in frames) { - callback.onGifAnimationFrame(frame.first, frame.second) - } - - reset() - } +class GifDecoder(val callback: GifDecoderCallback) { + + private class Rectangle(var x: Int = 0, var y: Int = 0, var w: Int = 0, var h: Int = 0) { + + fun set(src: Rectangle) { + this.x = src.x + this.y = src.y + this.w = src.w + this.h = src.h + } + } + + private class Reader(val bis: InputStream) { + + var block = ByteArray(256) // current data block + var blockSize = 0 // block size + + // Reads a single byte from the input stream. + fun byte(): Int = bis.read() + + // Reads next 16-bit value, LSB first + fun uInt16() = byte() or (byte() shl 8) + + fun array(ba: ByteArray, offset: Int = 0, length: Int = ba.size - offset) { + var nRead = 0 + while (nRead < length) { + val delta = bis.read(ba, offset + nRead, length - nRead) + if (delta == -1) error("unexpected End of Stream") + nRead += delta + } + } + + // Reads specified bytes and compose it to ascii string + fun string(n: Int): String { + return StringBuilder(n).apply { + ByteArray(n) + .also { array(it) } + .forEach { append(Char(it.toInt() and 255)) } + }.toString() + } + + // Reads next variable length block + fun block(): ByteArray { + blockSize = byte() + array(block, 0, blockSize) + return block + } + + // Skips variable length blocks up to and including next zero length block. + fun skipBlock() { + do { + block() + } while (blockSize > 0) + } + } + + // 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev + enum class Dispose(val num: Int) { + + Unspecified(0), + DontDispose(1), + RestoreBackground(2), + RestorePrevious(3) + } + + companion object { + private const val MaxStackSize = 4096 + private const val NullCode = -1 + private const val b0 = 0.toByte() + private const val OPAQUE = 255 shl 24 + } + + private var width = 0 // full image width + private var height = 0 // full image height + private var gctSize = 0 // size of global color table + private var loopCount = 1 // iterations; 0 = repeat forever + + private var gct: IntArray? = null // global color table + + private var bgIndex = 0 // background color index + private var bgColor = 0 // background color + private var lastBgColor = 0 // previous bg color + private var pixelAspect = 0 // pixel aspect ratio + + private var interlace = false // interlace flag + + private var lctFlag = false // local color table flag + private var lctSize = 0 // local color table size + + private val srcRect = Rectangle() // current image position and size + private val lastRect = Rectangle() // last image rect + + // last graphic control extension info + private var dispose = Dispose.Unspecified + private var lastDispose = Dispose.Unspecified + private var transparency = false // use transparent color + private var delay = 0 // delay in milliseconds + private var transIndex = 0 // transparent color index + + // LZW decoder working arrays + private var prefix: ShortArray? = null + private var suffix: ByteArray? = null + private var pixelStack: ByteArray? = null + private var pixels: ByteArray? = null + + private val frames = ArrayList>() + + private var previousImage: ApngBitmap? = null + + // 現在のdispose指定と描画結果を覚えておく + private fun memoryLastDispose(image: ApngBitmap) { + if (dispose != Dispose.RestorePrevious) previousImage = image + lastDispose = dispose + lastRect.set(srcRect) + lastBgColor = bgColor + } + + // 前回のdispose指定を反映する + private fun applyLastDispose(destImage: ApngBitmap) { + + if (lastDispose == Dispose.Unspecified) return + + // restore previous image + val previousImage = this.previousImage + if (previousImage != null) { + System.arraycopy(previousImage.colors, 0, destImage.colors, 0, destImage.colors.size) + } + + if (lastDispose == Dispose.RestoreBackground) { + + // fill lastRect + + val fillColor = if (transparency) { + 0 // assume background is transparent + } else { + lastBgColor // use given background color + } + + for (y in lastRect.y until lastRect.y + lastRect.h) { + val fillStart = y * destImage.width + lastRect.x + val fillWidth = lastRect.w + destImage.colors.fill( + fillColor, + fromIndex = fillStart, + toIndex = fillStart + fillWidth + ) + } + } + } + + // render to ApngBitmap + // may use some previous frame. + private fun render(destImage: ApngBitmap, act: IntArray) { + // expose destination image's pixels as int array + val dest = destImage.colors + + // copy each source line to the appropriate place in the destination + var pass = 1 + var inc = 8 + var iLine = 0 + for (i in 0 until srcRect.h) { + var line = i + if (interlace) { + if (iLine >= srcRect.h) { + when (++pass) { + 2 -> { + iLine = 4 + } + 3 -> { + iLine = 2 + inc = 4 + } + 4 -> { + iLine = 1 + inc = 2 + } + } + } + line = iLine + iLine += inc + } + line += srcRect.y + if (line < height) { + + // start of line in source + var sx = i * srcRect.w + + // + val k = line * width + + // loop for dest line. + for (dx in k + srcRect.x until min(k + width, k + srcRect.x + srcRect.w)) { + // map color and insert in destination + val index = pixels!![sx++].toInt() and 0xff + val c = act[index] + if (c != 0) dest[dx] = c + } + } + } + } + + /** + * Decodes LZW image data into pixel array. + * Adapted from John Cristy's ImageMagick. + */ + private fun decodeImageData(reader: Reader) { + + // allocate pixel array if need + val nPixels = srcRect.w * srcRect.h + if ((pixels?.size ?: 0) < nPixels) pixels = ByteArray(nPixels) + val pixels = this.pixels!! + + if (prefix == null) prefix = ShortArray(MaxStackSize) + if (suffix == null) suffix = ByteArray(MaxStackSize) + if (pixelStack == null) pixelStack = ByteArray(MaxStackSize + 1) + val prefix = this.prefix!! + val suffix = this.suffix!! + val pixelStack = this.pixelStack!! + + // Initialize GIF data stream decoder. + val dataSize = reader.byte() + val clear = 1 shl dataSize + val endOfInformation = clear + 1 + + var available = clear + 2 + var oldCode = NullCode + var codeSize = dataSize + 1 + var codeMask = (1 shl codeSize) - 1 + + for (code in 0 until clear) { + prefix[code] = 0 + suffix[code] = code.toByte() + } + + // Decode GIF pixel stream. + var datum = 0 + var bits = 0 + var count = 0 + var first = 0 + var top = 0 + var bi = 0 + var pi = 0 + + var i = 0 + while (i < nPixels) { + if (top == 0) { + if (bits < codeSize) { + // Load bytes until there are enough bits for a code. + if (count == 0) { + // Read a new data block. + reader.block() + count = reader.blockSize + if (count <= 0) break + bi = 0 + } + datum += (reader.block[bi].toInt() and 0xff) shl bits + bits += 8 + bi++ + count-- + continue + } + + // Get the next code. + var code = datum and codeMask + datum = datum shr codeSize + bits -= codeSize + + // Interpret the code + if ((code > available) || (code == endOfInformation)) break + + if (code == clear) { + // Reset decoder. + codeSize = dataSize + 1 + codeMask = (1 shl codeSize) - 1 + available = clear + 2 + oldCode = NullCode + continue + } + if (oldCode == NullCode) { + pixelStack[top++] = suffix[code] + oldCode = code + first = code + continue + } + val inCode = code + + if (code == available) { + pixelStack[top++] = first.toByte() + code = oldCode + } + while (code > clear) { + pixelStack[top++] = suffix[code] + code = prefix[code].toInt() + } + first = suffix[code].toInt() and 0xff + + // Add a new string to the string table, + + if (available >= MaxStackSize) { + pixelStack[top++] = first.toByte() + continue + } + pixelStack[top++] = first.toByte() + prefix[available] = oldCode.toShort() + suffix[available] = first.toByte() + available++ + + if ((available and codeMask) == 0 && available < MaxStackSize) { + codeSize++ + codeMask += available + } + + oldCode = inCode + } + + // Pop a pixel off the pixel stack. + top-- + pixels[pi++] = pixelStack[top] + i++ + } + + // clear missing pixels + for (n in pi until nPixels) { + pixels[n] = b0 + } + } + + /** + * Reads color table as 256 RGB integer values + * + * @param nColors int number of colors to read + * @return int array containing 256 colors (packed ARGB with full alpha) + */ + private fun parseColorTable(reader: Reader, nColors: Int): IntArray { + val nBytes = 3 * nColors + val c = ByteArray(nBytes) + reader.array(c) + + // max size to avoid bounds checks + val tab = IntArray(256) + var i = 0 + var j = 0 + while (i < nColors) { + val r = c[j].toInt() and 255 + val g = c[j + 1].toInt() and 255 + val b = c[j + 2].toInt() and 255 + j += 3 + tab[i++] = (OPAQUE or (r shl 16) or (g shl 8) or b) + } + return tab + } + + private fun parseDispose(num: Int) = + Dispose.values().find { it.num == num } ?: error("unknown dispose $num") + + /** + * Reads Graphics Control Extension values + */ + private fun parseGraphicControlExt(reader: Reader) { + reader.byte() // block size + val packed = reader.byte() // packed fields + dispose = parseDispose((packed and 0x1c) shr 2) // disposal method + if (callback.canGifDebug()) callback.onGifDebug("parseGraphicControlExt: frame=${frames.size} dispose=$dispose") + // elect to keep old image if discretionary + if (dispose == Dispose.Unspecified) dispose = Dispose.DontDispose + + transparency = (packed and 1) != 0 + // delay in milliseconds + delay = reader.uInt16() * 10 + // transparent color index + transIndex = reader.byte() + // block terminator + reader.byte() + } + + // Reads Netscape extension to obtain iteration count + private fun readNetscapeExt(reader: Reader) { + do { + val block = reader.block() + if (block[0].toInt() == 1) { + // loop count sub-block + val b1 = block[1].toInt() and 255 + val b2 = block[2].toInt() and 255 + loopCount = ((b2 shl 8) and b1) + } + } while (reader.blockSize > 0) + } + + // Reads next frame image + private fun parseFrame(reader: Reader) { + // (sub)image position & size + srcRect.x = reader.uInt16() + srcRect.y = reader.uInt16() + srcRect.w = reader.uInt16() + srcRect.h = reader.uInt16() + + val packed = reader.byte() + lctFlag = (packed and 0x80) != 0 // 1 - local color table flag + interlace = (packed and 0x40) != 0 // 2 - interlace flag + // 3 - sort flag + // 4-5 - reserved + lctSize = 2 shl (packed and 7) // 6-8 - local color table size + + val act = if (lctFlag) { + // make local table active + parseColorTable(reader, lctSize) + } else { + // make global table active + if (bgIndex == transIndex) bgColor = 0 + gct!! + } + + var save = 0 + if (transparency) { + save = act[transIndex] + act[transIndex] = 0 // set transparent color if specified + } + + decodeImageData(reader) // decode pixel data + reader.skipBlock() + + // add image to frame list + frames.add( + Pair( + ApngFrameControl( + width = width, + height = height, + xOffset = 0, + yOffset = 0, + disposeOp = DisposeOp.None, + blendOp = BlendOp.Source, + sequenceNumber = frames.size, + delayMilliseconds = delay.toLong() + ), + ApngBitmap(width, height).also { + applyLastDispose(it) + render(it, act) // transfer pixel data to image + memoryLastDispose(it) + } + ) + ) + + if (transparency) { + act[transIndex] = save + } + + /** + * Resets frame state for reading next image. + */ + dispose = Dispose.Unspecified + transparency = false + delay = 0 + } + + // read GIF content blocks + private fun readContents(reader: Reader): ApngAnimationControl { + loopBlocks@ while (true) { + when (val blockCode = reader.byte()) { + // image separator + 0x2C -> parseFrame(reader) + // extension + 0x21 -> when (reader.byte()) { + // graphics control extension + 0xf9 -> parseGraphicControlExt(reader) + // application extension + 0xff -> { + val block = reader.block() + val app = StringBuilder(12) + for (i in 0 until 11) { + app.append(Char(block[i].toInt() and 255)) + } + if (app.toString() == "NETSCAPE2.0") { + readNetscapeExt(reader) + } else { + reader.skipBlock() // don't care + } + } + + else -> { + // uninteresting extension + reader.skipBlock() + } + } + + // terminator + 0x3b -> break@loopBlocks + + // bad byte, but keep going and see what happens + 0x00 -> { + } + + else -> error("unknown block code $blockCode") + } + } + + return ApngAnimationControl(numFrames = frames.size, numPlays = loopCount) + } + + /** + * Initializes or re-initializes reader + */ + private fun reset() { + frames.clear() + loopCount = ApngAnimationControl.PLAY_INDEFINITELY + gct = null + prefix = null + suffix = null + pixelStack = null + pixels = null + previousImage = null + } + + /** + * Reads GIF file header information. + */ + private fun parseImageHeader(reader: Reader): ApngImageHeader { + + val id = reader.string(6) + if (!id.startsWith("GIF")) + error("file header not match to GIF.") + + /** + * Reads Logical Screen Descriptor + */ + + // logical screen size + width = reader.uInt16() + height = reader.uInt16() + if (width < 1 || height < 1) error("too small size. $width*$height") + + // packed fields + val packed = reader.byte() + + // global color table used + val gctFlag = (packed and 0x80) != 0 // 1 : global color table flag + // 2-4 : color resolution + // 5 : gct sort flag + gctSize = 2 shl (packed and 7) // 6-8 : gct size + + bgIndex = reader.byte() // background color index + pixelAspect = reader.byte() // pixel aspect ratio + + gct = if (gctFlag) { + val table = parseColorTable(reader, gctSize) + bgColor = table[bgIndex] + table + } else { + bgColor = 0 + null + } + + return ApngImageHeader( + width = this.width, + height = this.height, + bitDepth = 8, + colorType = ColorType.INDEX, + compressionMethod = CompressionMethod.Standard, + filterMethod = FilterMethod.Standard, + interlaceMethod = InterlaceMethod.None + ) + } + + fun parse(src: InputStream) { + + reset() + + val reader = Reader(src) + val header = parseImageHeader(reader) + val animationControl = readContents(reader) + + // GIFは最後まで読まないとフレーム数が分からない + + if (frames.isEmpty()) error("there is no frame.") + callback.onGifHeader(header) + callback.onGifAnimationInfo(header, animationControl) + for (frame in frames) { + callback.onGifAnimationFrame(frame.first, frame.second) + } + + reset() + } } diff --git a/apng/src/main/java/jp/juggler/apng/GifDecoderCallback.kt b/apng/src/main/java/jp/juggler/apng/GifDecoderCallback.kt index e4fc64b5..db8fc7cf 100644 --- a/apng/src/main/java/jp/juggler/apng/GifDecoderCallback.kt +++ b/apng/src/main/java/jp/juggler/apng/GifDecoderCallback.kt @@ -1,11 +1,10 @@ package jp.juggler.apng interface GifDecoderCallback { - fun onGifWarning(message : String) - fun onGifDebug(message : String) - fun canGifDebug() : Boolean - fun onGifHeader(header : ApngImageHeader) - fun onGifAnimationInfo( header : ApngImageHeader, animationControl : ApngAnimationControl ) - fun onGifAnimationFrame( frameControl : ApngFrameControl, frameBitmap : ApngBitmap ) - + fun onGifWarning(message: String) + fun onGifDebug(message: String) + fun canGifDebug(): Boolean + fun onGifHeader(header: ApngImageHeader) + fun onGifAnimationInfo(header: ApngImageHeader, animationControl: ApngAnimationControl) + fun onGifAnimationFrame(frameControl: ApngFrameControl, frameBitmap: ApngBitmap) } \ No newline at end of file diff --git a/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt b/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt index ded1d9a7..0fa9736f 100644 --- a/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt +++ b/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt @@ -14,7 +14,7 @@ internal class IdatDecoder( private val bitmap: ApngBitmap, private val inflateBufferPool: BufferPool, private val callback: ApngDecoderCallback, - private val onCompleted: () -> Unit + private val onCompleted: () -> Unit, ) { private class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int) @@ -47,9 +47,9 @@ internal class IdatDecoder( } } - private inline fun scanLine1(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { + private inline fun scanLine1(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) { var pos = 1 - var remain = pass_w + var remain = passW while (remain >= 8) { remain -= 8 val v = baLine[pos++].toInt() @@ -74,9 +74,9 @@ internal class IdatDecoder( } } - private inline fun scanLine2(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { + private inline fun scanLine2(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) { var pos = 1 - var remain = pass_w + var remain = passW while (remain >= 4) { remain -= 4 val v = baLine[pos++].toInt() @@ -93,9 +93,9 @@ internal class IdatDecoder( } } - private inline fun scanLine4(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { + private inline fun scanLine4(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) { var pos = 1 - var remain = pass_w + var remain = passW while (remain >= 2) { remain -= 2 val v = baLine[pos++].toInt() @@ -108,14 +108,13 @@ internal class IdatDecoder( } } - private inline fun scanLine8(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) { + private inline fun scanLine8(baLine: ByteArray, passW: Int, block: (v: Int) -> Unit) { var pos = 1 - var remain = pass_w + var remain = passW while (remain-- > 0) { block(baLine.getUInt8(pos++)) } } - } private val inflater = Inflater() @@ -405,7 +404,7 @@ internal class IdatDecoder( val filterNum = baLine.getUInt8(0) - // if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType") + // if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType") when (FilterType.values().first { it.num == filterNum }) { FilterType.None -> { @@ -424,7 +423,6 @@ internal class IdatDecoder( // val y = passInfo.yStart + passInfo.yStep * passY // callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}") // } - } } @@ -464,7 +462,6 @@ internal class IdatDecoder( // val y = passInfo.yStart + passInfo.yStep * passY // callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}") // } - } } } @@ -498,7 +495,7 @@ internal class IdatDecoder( inStream: InputStream, size: Int, inBuffer: ByteArray, - crc32: CRC32 + crc32: CRC32, ) { var foundEnd = false var inRemain = size @@ -541,11 +538,11 @@ internal class IdatDecoder( inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated)) // キューに追加したデータをScanLine単位で消費する - @Suppress("ControlFlowWithEmptyBody") - while (!isCompleted && readScanLine()){ - } + @Suppress("ControlFlowWithEmptyBody", "EmptyWhileBlock") + while (!isCompleted && readScanLine()) { + } - if (isCompleted) { + if (isCompleted) { inflateBufferQueue.clear() break } diff --git a/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt b/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt index 88b21052..f0bca14d 100644 --- a/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt +++ b/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt @@ -2,8 +2,8 @@ package jp.juggler.apng.util import java.util.* -internal class BufferPool(private val arraySize : Int) { - private val list = LinkedList() - fun obtain() : ByteArray = if(list.isEmpty()) ByteArray(arraySize) else list.removeFirst() - fun recycle(array : ByteArray?) = array?.let { list.add(it) } +internal class BufferPool(private val arraySize: Int) { + private val list = LinkedList() + fun obtain(): ByteArray = if (list.isEmpty()) ByteArray(arraySize) else list.removeFirst() + fun recycle(array: ByteArray?) = array?.let { list.add(it) } } diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt b/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt index 02b6ea64..9fe7d4c8 100644 --- a/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt +++ b/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt @@ -2,32 +2,32 @@ package jp.juggler.apng.util import jp.juggler.apng.ApngParseError -internal fun ByteArray.getUInt8(pos : Int) = get(pos).toInt() and 255 +internal fun ByteArray.getUInt8(pos: Int) = get(pos).toInt() and 255 -internal fun ByteArray.getUInt16(pos : Int) = (getUInt8(pos) shl 8) or getUInt8(pos + 1) +internal fun ByteArray.getUInt16(pos: Int) = (getUInt8(pos) shl 8) or getUInt8(pos + 1) -internal fun ByteArray.getInt32(pos : Int) = (getUInt8(pos) shl 24) or - (getUInt8(pos + 1) shl 16) or - (getUInt8(pos + 2) shl 8) or - getUInt8(pos + 3) +internal fun ByteArray.getInt32(pos: Int) = (getUInt8(pos) shl 24) or + (getUInt8(pos + 1) shl 16) or + (getUInt8(pos + 2) shl 8) or + getUInt8(pos + 3) internal class ByteSequence( - val array : ByteArray, - var offset : Int, - var length : Int + val array: ByteArray, + var offset: Int, + var length: Int, ) { - - constructor(ba : ByteArray) : this(ba, 0, ba.size) - - private inline fun readX(dataSize : Int, block : () -> T) : T { - if(length < dataSize) throw ApngParseError("readX: unexpected end") - val v = block() - offset += dataSize - length -= dataSize - return v - } - - fun readUInt8() = readX(1) { array.getUInt8(offset) } - fun readUInt16() = readX(2) { array.getUInt16(offset) } - fun readInt32() = readX(4) { array.getInt32(offset) } + + constructor(ba: ByteArray) : this(ba, 0, ba.size) + + private inline fun readX(dataSize: Int, block: () -> T): T { + if (length < dataSize) throw ApngParseError("readX: unexpected end") + val v = block() + offset += dataSize + length -= dataSize + return v + } + + fun readUInt8() = readX(1) { array.getUInt8(offset) } + fun readUInt16() = readX(2) { array.getUInt16(offset) } + fun readInt32() = readX(4) { array.getInt32(offset) } } diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt b/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt index 10447d1e..e8056ced 100644 --- a/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt +++ b/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt @@ -3,34 +3,34 @@ package jp.juggler.apng.util import java.util.* import kotlin.math.min -internal class ByteSequenceQueue(private val bufferRecycler : (ByteSequence) -> Unit) { - - private val list = LinkedList() - - val remain : Int - get() = list.sumOf { it.length } - - fun add(range : ByteSequence) =list.add(range) - - fun clear() = list.onEach(bufferRecycler).clear() - - fun readBytes(dst : ByteArray, offset : Int, length : Int) : Int { - var dstOffset = offset - var dstRemain = length - while(dstRemain > 0 && list.isNotEmpty()) { - val item = list.first() - if(item.length <= 0) { - bufferRecycler(item) - list.removeFirst() - } else { - val delta = min(item.length, dstRemain) - System.arraycopy(item.array, item.offset, dst, dstOffset, delta) - dstOffset += delta - dstRemain -= delta - item.offset += delta - item.length -= delta - } - } - return length - dstRemain - } +internal class ByteSequenceQueue(private val bufferRecycler: (ByteSequence) -> Unit) { + + private val list = LinkedList() + + val remain: Int + get() = list.sumOf { it.length } + + fun add(range: ByteSequence) = list.add(range) + + fun clear() = list.onEach(bufferRecycler).clear() + + fun readBytes(dst: ByteArray, offset: Int, length: Int): Int { + var dstOffset = offset + var dstRemain = length + while (dstRemain > 0 && list.isNotEmpty()) { + val item = list.first() + if (item.length <= 0) { + bufferRecycler(item) + list.removeFirst() + } else { + val delta = min(item.length, dstRemain) + System.arraycopy(item.array, item.offset, dst, dstOffset, delta) + dstOffset += delta + dstRemain -= delta + item.offset += delta + item.length -= delta + } + } + return length - dstRemain + } } diff --git a/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt b/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt index dfd9eb5d..2ff1d3b0 100644 --- a/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt +++ b/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt @@ -4,63 +4,63 @@ import jp.juggler.apng.ApngParseError import java.io.InputStream import java.util.zip.CRC32 -internal class StreamTokenizer(val inStream : InputStream) { - - fun skipBytes(size : Long) { - var nRead = 0L - while(true) { - val remain = size - nRead - if(remain <= 0) break - val delta = inStream.skip(size - nRead) - if(delta <= 0) throw ApngParseError("skipBytes: unexpected EoS") - nRead += delta - } - } - - fun readBytes(size : Int) : ByteArray { - val dst = ByteArray(size) - var nRead = 0 - while(true) { - val remain = size - nRead - if(remain <= 0) break - val delta = inStream.read(dst, nRead, size - nRead) - if(delta < 0) throw ApngParseError("readBytes: unexpected EoS") - nRead += delta - } - return dst - } - - private fun readByte() : Int { - val b = inStream.read() - if(b == - 1) throw ApngParseError("readByte: unexpected EoS") - return b and 0xff - } - - fun readInt32() : Int { - val b0 = readByte() - val b1 = readByte() - val b2 = readByte() - val b3 = readByte() - - return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 - } - - fun readInt32(crc32 : CRC32) : Int { - val ba = readBytes(4) - crc32.update(ba) - val b0 = ba[0].toInt() and 255 - val b1 = ba[1].toInt() and 255 - val b2 = ba[2].toInt() and 255 - val b3 = ba[3].toInt() and 255 - - return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 - } - - fun readUInt32() : Long { - val b0 = readByte() - val b1 = readByte() - val b2 = readByte() - val b3 = readByte() - return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong() - } +internal class StreamTokenizer(val inStream: InputStream) { + + fun skipBytes(size: Long) { + var nRead = 0L + while (true) { + val remain = size - nRead + if (remain <= 0) break + val delta = inStream.skip(size - nRead) + if (delta <= 0) throw ApngParseError("skipBytes: unexpected EoS") + nRead += delta + } + } + + fun readBytes(size: Int): ByteArray { + val dst = ByteArray(size) + var nRead = 0 + while (true) { + val remain = size - nRead + if (remain <= 0) break + val delta = inStream.read(dst, nRead, size - nRead) + if (delta < 0) throw ApngParseError("readBytes: unexpected EoS") + nRead += delta + } + return dst + } + + private fun readByte(): Int { + val b = inStream.read() + if (b == -1) throw ApngParseError("readByte: unexpected EoS") + return b and 0xff + } + + fun readInt32(): Int { + val b0 = readByte() + val b1 = readByte() + val b2 = readByte() + val b3 = readByte() + + return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 + } + + fun readInt32(crc32: CRC32): Int { + val ba = readBytes(4) + crc32.update(ba) + val b0 = ba[0].toInt() and 255 + val b1 = ba[1].toInt() and 255 + val b2 = ba[2].toInt() and 255 + val b3 = ba[3].toInt() and 255 + + return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 + } + + fun readUInt32(): Long { + val b0 = readByte() + val b1 = readByte() + val b2 = readByte() + val b3 = readByte() + return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong() + } } \ No newline at end of file diff --git a/apng/src/test/java/jp/juggler/apng/TestApng.kt b/apng/src/test/java/jp/juggler/apng/TestApng.kt index 5a4b8841..07f1b063 100644 --- a/apng/src/test/java/jp/juggler/apng/TestApng.kt +++ b/apng/src/test/java/jp/juggler/apng/TestApng.kt @@ -1,4 +1,5 @@ @file:Suppress("LocalVariableName") + package jp.juggler.apng import org.junit.Assert.* @@ -7,3225 +8,3226 @@ import java.io.BufferedInputStream import java.io.File import java.io.FileInputStream +@Suppress("LargeClass") class TestApng { - - companion object { - - private val imageData = intArrayOf( - 0xffff, - 0xffff, - 0x0, - 0xf7bd, - 0xffff, - 0x0, - 0xef7b, - 0xffff, - 0x0, - 0xe739, - 0xffff, - 0x0, - 0xdef7, - 0xffff, - 0x0, - 0xd6b5, - 0xffff, - 0x0, - 0xce73, - 0xffff, - 0x0, - 0xc631, - 0xffff, - 0x0, - 0xbdef, - 0xffff, - 0x0, - 0xb5ad, - 0xffff, - 0x0, - 0xad6b, - 0xffff, - 0x0, - 0xa529, - 0xffff, - 0x0, - 0x9ce7, - 0xffff, - 0x0, - 0x94a5, - 0xffff, - 0x0, - 0x8c63, - 0xffff, - 0x0, - 0x8421, - 0xffff, - 0x0, - 0x7bde, - 0xffff, - 0x0, - 0x739c, - 0xffff, - 0x0, - 0x6b5a, - 0xffff, - 0x0, - 0x6318, - 0xffff, - 0x0, - 0x5ad6, - 0xffff, - 0x0, - 0x5294, - 0xffff, - 0x0, - 0x4a52, - 0xffff, - 0x0, - 0x4210, - 0xffff, - 0x0, - 0x39ce, - 0xffff, - 0x0, - 0x318c, - 0xffff, - 0x0, - 0x294a, - 0xffff, - 0x0, - 0x2108, - 0xffff, - 0x0, - 0x18c6, - 0xffff, - 0x0, - 0x1084, - 0xffff, - 0x0, - 0x842, - 0xffff, - 0x0, - 0x0, - 0xffff, - 0x0, - 0xffff, - 0xf7bd, - 0x0, - 0xf7bd, - 0xf7bd, - 0x0, - 0xef7b, - 0xf7bd, - 0x0, - 0xe739, - 0xf7bd, - 0x0, - 0xdef7, - 0xf7bd, - 0x0, - 0xd6b5, - 0xf7bd, - 0x0, - 0xce73, - 0xf7bd, - 0x0, - 0xc631, - 0xf7bd, - 0x0, - 0xbdef, - 0xf7bd, - 0x0, - 0xb5ad, - 0xf7bd, - 0x0, - 0xad6b, - 0xf7bd, - 0x0, - 0xa529, - 0xf7bd, - 0x0, - 0x9ce7, - 0xf7bd, - 0x0, - 0x94a5, - 0xf7bd, - 0x0, - 0x8c63, - 0xf7bd, - 0x0, - 0x8421, - 0xf7bd, - 0x0, - 0x7bde, - 0xf7bd, - 0x0, - 0x739c, - 0xf7bd, - 0x0, - 0x6b5a, - 0xf7bd, - 0x0, - 0x6318, - 0xf7bd, - 0x0, - 0x5ad6, - 0xf7bd, - 0x0, - 0x5294, - 0xf7bd, - 0x0, - 0x4a52, - 0xf7bd, - 0x0, - 0x4210, - 0xf7bd, - 0x0, - 0x39ce, - 0xf7bd, - 0x0, - 0x318c, - 0xf7bd, - 0x0, - 0x294a, - 0xf7bd, - 0x0, - 0x2108, - 0xf7bd, - 0x0, - 0x18c6, - 0xf7bd, - 0x0, - 0x1084, - 0xf7bd, - 0x0, - 0x842, - 0xf7bd, - 0x0, - 0x0, - 0xf7bd, - 0x842, - 0xffff, - 0xef7b, - 0x0, - 0xf7bd, - 0xef7b, - 0x0, - 0xef7b, - 0xef7b, - 0x0, - 0xe739, - 0xef7b, - 0x0, - 0xdef7, - 0xef7b, - 0x0, - 0xd6b5, - 0xef7b, - 0x0, - 0xce73, - 0xef7b, - 0x0, - 0xc631, - 0xef7b, - 0x0, - 0xbdef, - 0xef7b, - 0x0, - 0xb5ad, - 0xef7b, - 0x0, - 0xad6b, - 0xef7b, - 0x0, - 0xa529, - 0xef7b, - 0x0, - 0x9ce7, - 0xef7b, - 0x0, - 0x94a5, - 0xef7b, - 0x0, - 0x8c63, - 0xef7b, - 0x0, - 0x8421, - 0xef7b, - 0x0, - 0x7bde, - 0xef7b, - 0x0, - 0x739c, - 0xef7b, - 0x0, - 0x6b5a, - 0xef7b, - 0x0, - 0x6318, - 0xef7b, - 0x0, - 0x5ad6, - 0xef7b, - 0x0, - 0x5294, - 0xef7b, - 0x0, - 0x4a52, - 0xef7b, - 0x0, - 0x4210, - 0xef7b, - 0x0, - 0x39ce, - 0xef7b, - 0x0, - 0x318c, - 0xef7b, - 0x0, - 0x294a, - 0xef7b, - 0x0, - 0x2108, - 0xef7b, - 0x0, - 0x18c6, - 0xef7b, - 0x0, - 0x1084, - 0xef7b, - 0x0, - 0x842, - 0xef7b, - 0x842, - 0x0, - 0xef7b, - 0x1084, - 0xffff, - 0xe739, - 0x0, - 0xf7bd, - 0xe739, - 0x0, - 0xef7b, - 0xe739, - 0x0, - 0xe739, - 0xe739, - 0x0, - 0xdef7, - 0xe739, - 0x0, - 0xd6b5, - 0xe739, - 0x0, - 0xce73, - 0xe739, - 0x0, - 0xc631, - 0xe739, - 0x0, - 0xbdef, - 0xe739, - 0x0, - 0xb5ad, - 0xe739, - 0x0, - 0xad6b, - 0xe739, - 0x0, - 0xa529, - 0xe739, - 0x0, - 0x9ce7, - 0xe739, - 0x0, - 0x94a5, - 0xe739, - 0x0, - 0x8c63, - 0xe739, - 0x0, - 0x8421, - 0xe739, - 0x0, - 0x7bde, - 0xe739, - 0x0, - 0x739c, - 0xe739, - 0x0, - 0x6b5a, - 0xe739, - 0x0, - 0x6318, - 0xe739, - 0x0, - 0x5ad6, - 0xe739, - 0x0, - 0x5294, - 0xe739, - 0x0, - 0x4a52, - 0xe739, - 0x0, - 0x4210, - 0xe739, - 0x0, - 0x39ce, - 0xe739, - 0x0, - 0x318c, - 0xe739, - 0x0, - 0x294a, - 0xe739, - 0x0, - 0x2108, - 0xe739, - 0x0, - 0x18c6, - 0xe739, - 0x0, - 0x1084, - 0xe739, - 0x842, - 0x842, - 0xe739, - 0x1084, - 0x0, - 0xe739, - 0x18c6, - 0xffff, - 0xdef7, - 0x0, - 0xf7bd, - 0xdef7, - 0x0, - 0xef7b, - 0xdef7, - 0x0, - 0xe739, - 0xdef7, - 0x0, - 0xdef7, - 0xdef7, - 0x0, - 0xd6b5, - 0xdef7, - 0x0, - 0xce73, - 0xdef7, - 0x0, - 0xc631, - 0xdef7, - 0x0, - 0xbdef, - 0xdef7, - 0x0, - 0xb5ad, - 0xdef7, - 0x0, - 0xad6b, - 0xdef7, - 0x0, - 0xa529, - 0xdef7, - 0x0, - 0x9ce7, - 0xdef7, - 0x0, - 0x94a5, - 0xdef7, - 0x0, - 0x8c63, - 0xdef7, - 0x0, - 0x8421, - 0xdef7, - 0x0, - 0x7bde, - 0xdef7, - 0x0, - 0x739c, - 0xdef7, - 0x0, - 0x6b5a, - 0xdef7, - 0x0, - 0x6318, - 0xdef7, - 0x0, - 0x5ad6, - 0xdef7, - 0x0, - 0x5294, - 0xdef7, - 0x0, - 0x4a52, - 0xdef7, - 0x0, - 0x4210, - 0xdef7, - 0x0, - 0x39ce, - 0xdef7, - 0x0, - 0x318c, - 0xdef7, - 0x0, - 0x294a, - 0xdef7, - 0x0, - 0x2108, - 0xdef7, - 0x0, - 0x18c6, - 0xdef7, - 0x842, - 0x1084, - 0xdef7, - 0x1084, - 0x842, - 0xdef7, - 0x18c6, - 0x0, - 0xdef7, - 0x2108, - 0xffff, - 0xd6b5, - 0x0, - 0xf7bd, - 0xd6b5, - 0x0, - 0xef7b, - 0xd6b5, - 0x0, - 0xe739, - 0xd6b5, - 0x0, - 0xdef7, - 0xd6b5, - 0x0, - 0xd6b5, - 0xd6b5, - 0x0, - 0xce73, - 0xd6b5, - 0x0, - 0xc631, - 0xd6b5, - 0x0, - 0xbdef, - 0xd6b5, - 0x0, - 0xb5ad, - 0xd6b5, - 0x0, - 0xad6b, - 0xd6b5, - 0x0, - 0xa529, - 0xd6b5, - 0x0, - 0x9ce7, - 0xd6b5, - 0x0, - 0x94a5, - 0xd6b5, - 0x0, - 0x8c63, - 0xd6b5, - 0x0, - 0x8421, - 0xd6b5, - 0x0, - 0x7bde, - 0xd6b5, - 0x0, - 0x739c, - 0xd6b5, - 0x0, - 0x6b5a, - 0xd6b5, - 0x0, - 0x6318, - 0xd6b5, - 0x0, - 0x5ad6, - 0xd6b5, - 0x0, - 0x5294, - 0xd6b5, - 0x0, - 0x4a52, - 0xd6b5, - 0x0, - 0x4210, - 0xd6b5, - 0x0, - 0x39ce, - 0xd6b5, - 0x0, - 0x318c, - 0xd6b5, - 0x0, - 0x294a, - 0xd6b5, - 0x0, - 0x2108, - 0xd6b5, - 0x842, - 0x18c6, - 0xd6b5, - 0x1084, - 0x1084, - 0xd6b5, - 0x18c6, - 0x842, - 0xd6b5, - 0x2108, - 0x0, - 0xd6b5, - 0x294a, - 0xffff, - 0xce73, - 0x0, - 0xf7bd, - 0xce73, - 0x0, - 0xef7b, - 0xce73, - 0x0, - 0xe739, - 0xce73, - 0x0, - 0xdef7, - 0xce73, - 0x0, - 0xd6b5, - 0xce73, - 0x0, - 0xce73, - 0xce73, - 0x0, - 0xc631, - 0xce73, - 0x0, - 0xbdef, - 0xce73, - 0x0, - 0xb5ad, - 0xce73, - 0x0, - 0xad6b, - 0xce73, - 0x0, - 0xa529, - 0xce73, - 0x0, - 0x9ce7, - 0xce73, - 0x0, - 0x94a5, - 0xce73, - 0x0, - 0x8c63, - 0xce73, - 0x0, - 0x8421, - 0xce73, - 0x0, - 0x7bde, - 0xce73, - 0x0, - 0x739c, - 0xce73, - 0x0, - 0x6b5a, - 0xce73, - 0x0, - 0x6318, - 0xce73, - 0x0, - 0x5ad6, - 0xce73, - 0x0, - 0x5294, - 0xce73, - 0x0, - 0x4a52, - 0xce73, - 0x0, - 0x4210, - 0xce73, - 0x0, - 0x39ce, - 0xce73, - 0x0, - 0x318c, - 0xce73, - 0x0, - 0x294a, - 0xce73, - 0x842, - 0x2108, - 0xce73, - 0x1084, - 0x18c6, - 0xce73, - 0x18c6, - 0x1084, - 0xce73, - 0x2108, - 0x842, - 0xce73, - 0x294a, - 0x0, - 0xce73, - 0x318c, - 0xffff, - 0xc631, - 0x0, - 0xf7bd, - 0xc631, - 0x0, - 0xef7b, - 0xc631, - 0x0, - 0xe739, - 0xc631, - 0x0, - 0xdef7, - 0xc631, - 0x0, - 0xd6b5, - 0xc631, - 0x0, - 0xce73, - 0xc631, - 0x0, - 0xc631, - 0xc631, - 0x0, - 0xbdef, - 0xc631, - 0x0, - 0xb5ad, - 0xc631, - 0x0, - 0xad6b, - 0xc631, - 0x0, - 0xa529, - 0xc631, - 0x0, - 0x9ce7, - 0xc631, - 0x0, - 0x94a5, - 0xc631, - 0x0, - 0x8c63, - 0xc631, - 0x0, - 0x8421, - 0xc631, - 0x0, - 0x7bde, - 0xc631, - 0x0, - 0x739c, - 0xc631, - 0x0, - 0x6b5a, - 0xc631, - 0x0, - 0x6318, - 0xc631, - 0x0, - 0x5ad6, - 0xc631, - 0x0, - 0x5294, - 0xc631, - 0x0, - 0x4a52, - 0xc631, - 0x0, - 0x4210, - 0xc631, - 0x0, - 0x39ce, - 0xc631, - 0x0, - 0x318c, - 0xc631, - 0x842, - 0x294a, - 0xc631, - 0x1084, - 0x2108, - 0xc631, - 0x18c6, - 0x18c6, - 0xc631, - 0x2108, - 0x1084, - 0xc631, - 0x294a, - 0x842, - 0xc631, - 0x318c, - 0x0, - 0xc631, - 0x39ce, - 0xffff, - 0xbdef, - 0x0, - 0xf7bd, - 0xbdef, - 0x0, - 0xef7b, - 0xbdef, - 0x0, - 0xe739, - 0xbdef, - 0x0, - 0xdef7, - 0xbdef, - 0x0, - 0xd6b5, - 0xbdef, - 0x0, - 0xce73, - 0xbdef, - 0x0, - 0xc631, - 0xbdef, - 0x0, - 0xbdef, - 0xbdef, - 0x0, - 0xb5ad, - 0xbdef, - 0x0, - 0xad6b, - 0xbdef, - 0x0, - 0xa529, - 0xbdef, - 0x0, - 0x9ce7, - 0xbdef, - 0x0, - 0x94a5, - 0xbdef, - 0x0, - 0x8c63, - 0xbdef, - 0x0, - 0x8421, - 0xbdef, - 0x0, - 0x7bde, - 0xbdef, - 0x0, - 0x739c, - 0xbdef, - 0x0, - 0x6b5a, - 0xbdef, - 0x0, - 0x6318, - 0xbdef, - 0x0, - 0x5ad6, - 0xbdef, - 0x0, - 0x5294, - 0xbdef, - 0x0, - 0x4a52, - 0xbdef, - 0x0, - 0x4210, - 0xbdef, - 0x0, - 0x39ce, - 0xbdef, - 0x842, - 0x318c, - 0xbdef, - 0x1084, - 0x294a, - 0xbdef, - 0x18c6, - 0x2108, - 0xbdef, - 0x2108, - 0x18c6, - 0xbdef, - 0x294a, - 0x1084, - 0xbdef, - 0x318c, - 0x842, - 0xbdef, - 0x39ce, - 0x0, - 0xbdef, - 0x4210, - 0xffff, - 0xb5ad, - 0x0, - 0xf7bd, - 0xb5ad, - 0x0, - 0xef7b, - 0xb5ad, - 0x0, - 0xe739, - 0xb5ad, - 0x0, - 0xdef7, - 0xb5ad, - 0x0, - 0xd6b5, - 0xb5ad, - 0x0, - 0xce73, - 0xb5ad, - 0x0, - 0xc631, - 0xb5ad, - 0x0, - 0xbdef, - 0xb5ad, - 0x0, - 0xb5ad, - 0xb5ad, - 0x0, - 0xad6b, - 0xb5ad, - 0x0, - 0xa529, - 0xb5ad, - 0x0, - 0x9ce7, - 0xb5ad, - 0x0, - 0x94a5, - 0xb5ad, - 0x0, - 0x8c63, - 0xb5ad, - 0x0, - 0x8421, - 0xb5ad, - 0x0, - 0x7bde, - 0xb5ad, - 0x0, - 0x739c, - 0xb5ad, - 0x0, - 0x6b5a, - 0xb5ad, - 0x0, - 0x6318, - 0xb5ad, - 0x0, - 0x5ad6, - 0xb5ad, - 0x0, - 0x5294, - 0xb5ad, - 0x0, - 0x4a52, - 0xb5ad, - 0x0, - 0x4210, - 0xb5ad, - 0x842, - 0x39ce, - 0xb5ad, - 0x1084, - 0x318c, - 0xb5ad, - 0x18c6, - 0x294a, - 0xb5ad, - 0x2108, - 0x2108, - 0xb5ad, - 0x294a, - 0x18c6, - 0xb5ad, - 0x318c, - 0x1084, - 0xb5ad, - 0x39ce, - 0x842, - 0xb5ad, - 0x4210, - 0x0, - 0xb5ad, - 0x4a52, - 0xffff, - 0xad6b, - 0x0, - 0xf7bd, - 0xad6b, - 0x0, - 0xef7b, - 0xad6b, - 0x0, - 0xe739, - 0xad6b, - 0x0, - 0xdef7, - 0xad6b, - 0x0, - 0xd6b5, - 0xad6b, - 0x0, - 0xce73, - 0xad6b, - 0x0, - 0xc631, - 0xad6b, - 0x0, - 0xbdef, - 0xad6b, - 0x0, - 0xb5ad, - 0xad6b, - 0x0, - 0xad6b, - 0xad6b, - 0x0, - 0xa529, - 0xad6b, - 0x0, - 0x9ce7, - 0xad6b, - 0x0, - 0x94a5, - 0xad6b, - 0x0, - 0x8c63, - 0xad6b, - 0x0, - 0x8421, - 0xad6b, - 0x0, - 0x7bde, - 0xad6b, - 0x0, - 0x739c, - 0xad6b, - 0x0, - 0x6b5a, - 0xad6b, - 0x0, - 0x6318, - 0xad6b, - 0x0, - 0x5ad6, - 0xad6b, - 0x0, - 0x5294, - 0xad6b, - 0x0, - 0x4a52, - 0xad6b, - 0x842, - 0x4210, - 0xad6b, - 0x1084, - 0x39ce, - 0xad6b, - 0x18c6, - 0x318c, - 0xad6b, - 0x2108, - 0x294a, - 0xad6b, - 0x294a, - 0x2108, - 0xad6b, - 0x318c, - 0x18c6, - 0xad6b, - 0x39ce, - 0x1084, - 0xad6b, - 0x4210, - 0x842, - 0xad6b, - 0x4a52, - 0x0, - 0xad6b, - 0x5294, - 0xffff, - 0xa529, - 0x0, - 0xf7bd, - 0xa529, - 0x0, - 0xef7b, - 0xa529, - 0x0, - 0xe739, - 0xa529, - 0x0, - 0xdef7, - 0xa529, - 0x0, - 0xd6b5, - 0xa529, - 0x0, - 0xce73, - 0xa529, - 0x0, - 0xc631, - 0xa529, - 0x0, - 0xbdef, - 0xa529, - 0x0, - 0xb5ad, - 0xa529, - 0x0, - 0xad6b, - 0xa529, - 0x0, - 0xa529, - 0xa529, - 0x0, - 0x9ce7, - 0xa529, - 0x0, - 0x94a5, - 0xa529, - 0x0, - 0x8c63, - 0xa529, - 0x0, - 0x8421, - 0xa529, - 0x0, - 0x7bde, - 0xa529, - 0x0, - 0x739c, - 0xa529, - 0x0, - 0x6b5a, - 0xa529, - 0x0, - 0x6318, - 0xa529, - 0x0, - 0x5ad6, - 0xa529, - 0x0, - 0x5294, - 0xa529, - 0x842, - 0x4a52, - 0xa529, - 0x1084, - 0x4210, - 0xa529, - 0x18c6, - 0x39ce, - 0xa529, - 0x2108, - 0x318c, - 0xa529, - 0x294a, - 0x294a, - 0xa529, - 0x318c, - 0x2108, - 0xa529, - 0x39ce, - 0x18c6, - 0xa529, - 0x4210, - 0x1084, - 0xa529, - 0x4a52, - 0x842, - 0xa529, - 0x5294, - 0x0, - 0xa529, - 0x5ad6, - 0xffff, - 0x9ce7, - 0x0, - 0xf7bd, - 0x9ce7, - 0x0, - 0xef7b, - 0x9ce7, - 0x0, - 0xe739, - 0x9ce7, - 0x0, - 0xdef7, - 0x9ce7, - 0x0, - 0xd6b5, - 0x9ce7, - 0x0, - 0xce73, - 0x9ce7, - 0x0, - 0xc631, - 0x9ce7, - 0x0, - 0xbdef, - 0x9ce7, - 0x0, - 0xb5ad, - 0x9ce7, - 0x0, - 0xad6b, - 0x9ce7, - 0x0, - 0xa529, - 0x9ce7, - 0x0, - 0x9ce7, - 0x9ce7, - 0x0, - 0x94a5, - 0x9ce7, - 0x0, - 0x8c63, - 0x9ce7, - 0x0, - 0x8421, - 0x9ce7, - 0x0, - 0x7bde, - 0x9ce7, - 0x0, - 0x739c, - 0x9ce7, - 0x0, - 0x6b5a, - 0x9ce7, - 0x0, - 0x6318, - 0x9ce7, - 0x0, - 0x5ad6, - 0x9ce7, - 0x842, - 0x5294, - 0x9ce7, - 0x1084, - 0x4a52, - 0x9ce7, - 0x18c6, - 0x4210, - 0x9ce7, - 0x2108, - 0x39ce, - 0x9ce7, - 0x294a, - 0x318c, - 0x9ce7, - 0x318c, - 0x294a, - 0x9ce7, - 0x39ce, - 0x2108, - 0x9ce7, - 0x4210, - 0x18c6, - 0x9ce7, - 0x4a52, - 0x1084, - 0x9ce7, - 0x5294, - 0x842, - 0x9ce7, - 0x5ad6, - 0x0, - 0x9ce7, - 0x6318, - 0xffff, - 0x94a5, - 0x0, - 0xf7bd, - 0x94a5, - 0x0, - 0xef7b, - 0x94a5, - 0x0, - 0xe739, - 0x94a5, - 0x0, - 0xdef7, - 0x94a5, - 0x0, - 0xd6b5, - 0x94a5, - 0x0, - 0xce73, - 0x94a5, - 0x0, - 0xc631, - 0x94a5, - 0x0, - 0xbdef, - 0x94a5, - 0x0, - 0xb5ad, - 0x94a5, - 0x0, - 0xad6b, - 0x94a5, - 0x0, - 0xa529, - 0x94a5, - 0x0, - 0x9ce7, - 0x94a5, - 0x0, - 0x94a5, - 0x94a5, - 0x0, - 0x8c63, - 0x94a5, - 0x0, - 0x8421, - 0x94a5, - 0x0, - 0x7bde, - 0x94a5, - 0x0, - 0x739c, - 0x94a5, - 0x0, - 0x6b5a, - 0x94a5, - 0x0, - 0x6318, - 0x94a5, - 0x842, - 0x5ad6, - 0x94a5, - 0x1084, - 0x5294, - 0x94a5, - 0x18c6, - 0x4a52, - 0x94a5, - 0x2108, - 0x4210, - 0x94a5, - 0x294a, - 0x39ce, - 0x94a5, - 0x318c, - 0x318c, - 0x94a5, - 0x39ce, - 0x294a, - 0x94a5, - 0x4210, - 0x2108, - 0x94a5, - 0x4a52, - 0x18c6, - 0x94a5, - 0x5294, - 0x1084, - 0x94a5, - 0x5ad6, - 0x842, - 0x94a5, - 0x6318, - 0x0, - 0x94a5, - 0x6b5a, - 0xffff, - 0x8c63, - 0x0, - 0xf7bd, - 0x8c63, - 0x0, - 0xef7b, - 0x8c63, - 0x0, - 0xe739, - 0x8c63, - 0x0, - 0xdef7, - 0x8c63, - 0x0, - 0xd6b5, - 0x8c63, - 0x0, - 0xce73, - 0x8c63, - 0x0, - 0xc631, - 0x8c63, - 0x0, - 0xbdef, - 0x8c63, - 0x0, - 0xb5ad, - 0x8c63, - 0x0, - 0xad6b, - 0x8c63, - 0x0, - 0xa529, - 0x8c63, - 0x0, - 0x9ce7, - 0x8c63, - 0x0, - 0x94a5, - 0x8c63, - 0x0, - 0x8c63, - 0x8c63, - 0x0, - 0x8421, - 0x8c63, - 0x0, - 0x7bde, - 0x8c63, - 0x0, - 0x739c, - 0x8c63, - 0x0, - 0x6b5a, - 0x8c63, - 0x842, - 0x6318, - 0x8c63, - 0x1084, - 0x5ad6, - 0x8c63, - 0x18c6, - 0x5294, - 0x8c63, - 0x2108, - 0x4a52, - 0x8c63, - 0x294a, - 0x4210, - 0x8c63, - 0x318c, - 0x39ce, - 0x8c63, - 0x39ce, - 0x318c, - 0x8c63, - 0x4210, - 0x294a, - 0x8c63, - 0x4a52, - 0x2108, - 0x8c63, - 0x5294, - 0x18c6, - 0x8c63, - 0x5ad6, - 0x1084, - 0x8c63, - 0x6318, - 0x842, - 0x8c63, - 0x6b5a, - 0x0, - 0x8c63, - 0x739c, - 0xffff, - 0x8421, - 0x0, - 0xf7bd, - 0x8421, - 0x0, - 0xef7b, - 0x8421, - 0x0, - 0xe739, - 0x8421, - 0x0, - 0xdef7, - 0x8421, - 0x0, - 0xd6b5, - 0x8421, - 0x0, - 0xce73, - 0x8421, - 0x0, - 0xc631, - 0x8421, - 0x0, - 0xbdef, - 0x8421, - 0x0, - 0xb5ad, - 0x8421, - 0x0, - 0xad6b, - 0x8421, - 0x0, - 0xa529, - 0x8421, - 0x0, - 0x9ce7, - 0x8421, - 0x0, - 0x94a5, - 0x8421, - 0x0, - 0x8c63, - 0x8421, - 0x0, - 0x8421, - 0x8421, - 0x0, - 0x7bde, - 0x8421, - 0x0, - 0x739c, - 0x8421, - 0x842, - 0x6b5a, - 0x8421, - 0x1084, - 0x6318, - 0x8421, - 0x18c6, - 0x5ad6, - 0x8421, - 0x2108, - 0x5294, - 0x8421, - 0x294a, - 0x4a52, - 0x8421, - 0x318c, - 0x4210, - 0x8421, - 0x39ce, - 0x39ce, - 0x8421, - 0x4210, - 0x318c, - 0x8421, - 0x4a52, - 0x294a, - 0x8421, - 0x5294, - 0x2108, - 0x8421, - 0x5ad6, - 0x18c6, - 0x8421, - 0x6318, - 0x1084, - 0x8421, - 0x6b5a, - 0x842, - 0x8421, - 0x739c, - 0x0, - 0x8421, - 0x7bde, - 0xffff, - 0x7bde, - 0x0, - 0xf7bd, - 0x7bde, - 0x0, - 0xef7b, - 0x7bde, - 0x0, - 0xe739, - 0x7bde, - 0x0, - 0xdef7, - 0x7bde, - 0x0, - 0xd6b5, - 0x7bde, - 0x0, - 0xce73, - 0x7bde, - 0x0, - 0xc631, - 0x7bde, - 0x0, - 0xbdef, - 0x7bde, - 0x0, - 0xb5ad, - 0x7bde, - 0x0, - 0xad6b, - 0x7bde, - 0x0, - 0xa529, - 0x7bde, - 0x0, - 0x9ce7, - 0x7bde, - 0x0, - 0x94a5, - 0x7bde, - 0x0, - 0x8c63, - 0x7bde, - 0x0, - 0x8421, - 0x7bde, - 0x0, - 0x7bde, - 0x7bde, - 0x842, - 0x739c, - 0x7bde, - 0x1084, - 0x6b5a, - 0x7bde, - 0x18c6, - 0x6318, - 0x7bde, - 0x2108, - 0x5ad6, - 0x7bde, - 0x294a, - 0x5294, - 0x7bde, - 0x318c, - 0x4a52, - 0x7bde, - 0x39ce, - 0x4210, - 0x7bde, - 0x4210, - 0x39ce, - 0x7bde, - 0x4a52, - 0x318c, - 0x7bde, - 0x5294, - 0x294a, - 0x7bde, - 0x5ad6, - 0x2108, - 0x7bde, - 0x6318, - 0x18c6, - 0x7bde, - 0x6b5a, - 0x1084, - 0x7bde, - 0x739c, - 0x842, - 0x7bde, - 0x7bde, - 0x0, - 0x7bde, - 0x8421, - 0xffff, - 0x739c, - 0x0, - 0xf7bd, - 0x739c, - 0x0, - 0xef7b, - 0x739c, - 0x0, - 0xe739, - 0x739c, - 0x0, - 0xdef7, - 0x739c, - 0x0, - 0xd6b5, - 0x739c, - 0x0, - 0xce73, - 0x739c, - 0x0, - 0xc631, - 0x739c, - 0x0, - 0xbdef, - 0x739c, - 0x0, - 0xb5ad, - 0x739c, - 0x0, - 0xad6b, - 0x739c, - 0x0, - 0xa529, - 0x739c, - 0x0, - 0x9ce7, - 0x739c, - 0x0, - 0x94a5, - 0x739c, - 0x0, - 0x8c63, - 0x739c, - 0x0, - 0x8421, - 0x739c, - 0x842, - 0x7bde, - 0x739c, - 0x1084, - 0x739c, - 0x739c, - 0x18c6, - 0x6b5a, - 0x739c, - 0x2108, - 0x6318, - 0x739c, - 0x294a, - 0x5ad6, - 0x739c, - 0x318c, - 0x5294, - 0x739c, - 0x39ce, - 0x4a52, - 0x739c, - 0x4210, - 0x4210, - 0x739c, - 0x4a52, - 0x39ce, - 0x739c, - 0x5294, - 0x318c, - 0x739c, - 0x5ad6, - 0x294a, - 0x739c, - 0x6318, - 0x2108, - 0x739c, - 0x6b5a, - 0x18c6, - 0x739c, - 0x739c, - 0x1084, - 0x739c, - 0x7bde, - 0x842, - 0x739c, - 0x8421, - 0x0, - 0x739c, - 0x8c63, - 0xffff, - 0x6b5a, - 0x0, - 0xf7bd, - 0x6b5a, - 0x0, - 0xef7b, - 0x6b5a, - 0x0, - 0xe739, - 0x6b5a, - 0x0, - 0xdef7, - 0x6b5a, - 0x0, - 0xd6b5, - 0x6b5a, - 0x0, - 0xce73, - 0x6b5a, - 0x0, - 0xc631, - 0x6b5a, - 0x0, - 0xbdef, - 0x6b5a, - 0x0, - 0xb5ad, - 0x6b5a, - 0x0, - 0xad6b, - 0x6b5a, - 0x0, - 0xa529, - 0x6b5a, - 0x0, - 0x9ce7, - 0x6b5a, - 0x0, - 0x94a5, - 0x6b5a, - 0x0, - 0x8c63, - 0x6b5a, - 0x842, - 0x8421, - 0x6b5a, - 0x1084, - 0x7bde, - 0x6b5a, - 0x18c6, - 0x739c, - 0x6b5a, - 0x2108, - 0x6b5a, - 0x6b5a, - 0x294a, - 0x6318, - 0x6b5a, - 0x318c, - 0x5ad6, - 0x6b5a, - 0x39ce, - 0x5294, - 0x6b5a, - 0x4210, - 0x4a52, - 0x6b5a, - 0x4a52, - 0x4210, - 0x6b5a, - 0x5294, - 0x39ce, - 0x6b5a, - 0x5ad6, - 0x318c, - 0x6b5a, - 0x6318, - 0x294a, - 0x6b5a, - 0x6b5a, - 0x2108, - 0x6b5a, - 0x739c, - 0x18c6, - 0x6b5a, - 0x7bde, - 0x1084, - 0x6b5a, - 0x8421, - 0x842, - 0x6b5a, - 0x8c63, - 0x0, - 0x6b5a, - 0x94a5, - 0xffff, - 0x6318, - 0x0, - 0xf7bd, - 0x6318, - 0x0, - 0xef7b, - 0x6318, - 0x0, - 0xe739, - 0x6318, - 0x0, - 0xdef7, - 0x6318, - 0x0, - 0xd6b5, - 0x6318, - 0x0, - 0xce73, - 0x6318, - 0x0, - 0xc631, - 0x6318, - 0x0, - 0xbdef, - 0x6318, - 0x0, - 0xb5ad, - 0x6318, - 0x0, - 0xad6b, - 0x6318, - 0x0, - 0xa529, - 0x6318, - 0x0, - 0x9ce7, - 0x6318, - 0x0, - 0x94a5, - 0x6318, - 0x842, - 0x8c63, - 0x6318, - 0x1084, - 0x8421, - 0x6318, - 0x18c6, - 0x7bde, - 0x6318, - 0x2108, - 0x739c, - 0x6318, - 0x294a, - 0x6b5a, - 0x6318, - 0x318c, - 0x6318, - 0x6318, - 0x39ce, - 0x5ad6, - 0x6318, - 0x4210, - 0x5294, - 0x6318, - 0x4a52, - 0x4a52, - 0x6318, - 0x5294, - 0x4210, - 0x6318, - 0x5ad6, - 0x39ce, - 0x6318, - 0x6318, - 0x318c, - 0x6318, - 0x6b5a, - 0x294a, - 0x6318, - 0x739c, - 0x2108, - 0x6318, - 0x7bde, - 0x18c6, - 0x6318, - 0x8421, - 0x1084, - 0x6318, - 0x8c63, - 0x842, - 0x6318, - 0x94a5, - 0x0, - 0x6318, - 0x9ce7, - 0xffff, - 0x5ad6, - 0x0, - 0xf7bd, - 0x5ad6, - 0x0, - 0xef7b, - 0x5ad6, - 0x0, - 0xe739, - 0x5ad6, - 0x0, - 0xdef7, - 0x5ad6, - 0x0, - 0xd6b5, - 0x5ad6, - 0x0, - 0xce73, - 0x5ad6, - 0x0, - 0xc631, - 0x5ad6, - 0x0, - 0xbdef, - 0x5ad6, - 0x0, - 0xb5ad, - 0x5ad6, - 0x0, - 0xad6b, - 0x5ad6, - 0x0, - 0xa529, - 0x5ad6, - 0x0, - 0x9ce7, - 0x5ad6, - 0x842, - 0x94a5, - 0x5ad6, - 0x1084, - 0x8c63, - 0x5ad6, - 0x18c6, - 0x8421, - 0x5ad6, - 0x2108, - 0x7bde, - 0x5ad6, - 0x294a, - 0x739c, - 0x5ad6, - 0x318c, - 0x6b5a, - 0x5ad6, - 0x39ce, - 0x6318, - 0x5ad6, - 0x4210, - 0x5ad6, - 0x5ad6, - 0x4a52, - 0x5294, - 0x5ad6, - 0x5294, - 0x4a52, - 0x5ad6, - 0x5ad6, - 0x4210, - 0x5ad6, - 0x6318, - 0x39ce, - 0x5ad6, - 0x6b5a, - 0x318c, - 0x5ad6, - 0x739c, - 0x294a, - 0x5ad6, - 0x7bde, - 0x2108, - 0x5ad6, - 0x8421, - 0x18c6, - 0x5ad6, - 0x8c63, - 0x1084, - 0x5ad6, - 0x94a5, - 0x842, - 0x5ad6, - 0x9ce7, - 0x0, - 0x5ad6, - 0xa529, - 0xffff, - 0x5294, - 0x0, - 0xf7bd, - 0x5294, - 0x0, - 0xef7b, - 0x5294, - 0x0, - 0xe739, - 0x5294, - 0x0, - 0xdef7, - 0x5294, - 0x0, - 0xd6b5, - 0x5294, - 0x0, - 0xce73, - 0x5294, - 0x0, - 0xc631, - 0x5294, - 0x0, - 0xbdef, - 0x5294, - 0x0, - 0xb5ad, - 0x5294, - 0x0, - 0xad6b, - 0x5294, - 0x0, - 0xa529, - 0x5294, - 0x842, - 0x9ce7, - 0x5294, - 0x1084, - 0x94a5, - 0x5294, - 0x18c6, - 0x8c63, - 0x5294, - 0x2108, - 0x8421, - 0x5294, - 0x294a, - 0x7bde, - 0x5294, - 0x318c, - 0x739c, - 0x5294, - 0x39ce, - 0x6b5a, - 0x5294, - 0x4210, - 0x6318, - 0x5294, - 0x4a52, - 0x5ad6, - 0x5294, - 0x5294, - 0x5294, - 0x5294, - 0x5ad6, - 0x4a52, - 0x5294, - 0x6318, - 0x4210, - 0x5294, - 0x6b5a, - 0x39ce, - 0x5294, - 0x739c, - 0x318c, - 0x5294, - 0x7bde, - 0x294a, - 0x5294, - 0x8421, - 0x2108, - 0x5294, - 0x8c63, - 0x18c6, - 0x5294, - 0x94a5, - 0x1084, - 0x5294, - 0x9ce7, - 0x842, - 0x5294, - 0xa529, - 0x0, - 0x5294, - 0xad6b, - 0xffff, - 0x4a52, - 0x0, - 0xf7bd, - 0x4a52, - 0x0, - 0xef7b, - 0x4a52, - 0x0, - 0xe739, - 0x4a52, - 0x0, - 0xdef7, - 0x4a52, - 0x0, - 0xd6b5, - 0x4a52, - 0x0, - 0xce73, - 0x4a52, - 0x0, - 0xc631, - 0x4a52, - 0x0, - 0xbdef, - 0x4a52, - 0x0, - 0xb5ad, - 0x4a52, - 0x0, - 0xad6b, - 0x4a52, - 0x842, - 0xa529, - 0x4a52, - 0x1084, - 0x9ce7, - 0x4a52, - 0x18c6, - 0x94a5, - 0x4a52, - 0x2108, - 0x8c63, - 0x4a52, - 0x294a, - 0x8421, - 0x4a52, - 0x318c, - 0x7bde, - 0x4a52, - 0x39ce, - 0x739c, - 0x4a52, - 0x4210, - 0x6b5a, - 0x4a52, - 0x4a52, - 0x6318, - 0x4a52, - 0x5294, - 0x5ad6, - 0x4a52, - 0x5ad6, - 0x5294, - 0x4a52, - 0x6318, - 0x4a52, - 0x4a52, - 0x6b5a, - 0x4210, - 0x4a52, - 0x739c, - 0x39ce, - 0x4a52, - 0x7bde, - 0x318c, - 0x4a52, - 0x8421, - 0x294a, - 0x4a52, - 0x8c63, - 0x2108, - 0x4a52, - 0x94a5, - 0x18c6, - 0x4a52, - 0x9ce7, - 0x1084, - 0x4a52, - 0xa529, - 0x842, - 0x4a52, - 0xad6b, - 0x0, - 0x4a52, - 0xb5ad, - 0xffff, - 0x4210, - 0x0, - 0xf7bd, - 0x4210, - 0x0, - 0xef7b, - 0x4210, - 0x0, - 0xe739, - 0x4210, - 0x0, - 0xdef7, - 0x4210, - 0x0, - 0xd6b5, - 0x4210, - 0x0, - 0xce73, - 0x4210, - 0x0, - 0xc631, - 0x4210, - 0x0, - 0xbdef, - 0x4210, - 0x0, - 0xb5ad, - 0x4210, - 0x842, - 0xad6b, - 0x4210, - 0x1084, - 0xa529, - 0x4210, - 0x18c6, - 0x9ce7, - 0x4210, - 0x2108, - 0x94a5, - 0x4210, - 0x294a, - 0x8c63, - 0x4210, - 0x318c, - 0x8421, - 0x4210, - 0x39ce, - 0x7bde, - 0x4210, - 0x4210, - 0x739c, - 0x4210, - 0x4a52, - 0x6b5a, - 0x4210, - 0x5294, - 0x6318, - 0x4210, - 0x5ad6, - 0x5ad6, - 0x4210, - 0x6318, - 0x5294, - 0x4210, - 0x6b5a, - 0x4a52, - 0x4210, - 0x739c, - 0x4210, - 0x4210, - 0x7bde, - 0x39ce, - 0x4210, - 0x8421, - 0x318c, - 0x4210, - 0x8c63, - 0x294a, - 0x4210, - 0x94a5, - 0x2108, - 0x4210, - 0x9ce7, - 0x18c6, - 0x4210, - 0xa529, - 0x1084, - 0x4210, - 0xad6b, - 0x842, - 0x4210, - 0xb5ad, - 0x0, - 0x4210, - 0xbdef, - 0xffff, - 0x39ce, - 0x0, - 0xf7bd, - 0x39ce, - 0x0, - 0xef7b, - 0x39ce, - 0x0, - 0xe739, - 0x39ce, - 0x0, - 0xdef7, - 0x39ce, - 0x0, - 0xd6b5, - 0x39ce, - 0x0, - 0xce73, - 0x39ce, - 0x0, - 0xc631, - 0x39ce, - 0x0, - 0xbdef, - 0x39ce, - 0x842, - 0xb5ad, - 0x39ce, - 0x1084, - 0xad6b, - 0x39ce, - 0x18c6, - 0xa529, - 0x39ce, - 0x2108, - 0x9ce7, - 0x39ce, - 0x294a, - 0x94a5, - 0x39ce, - 0x318c, - 0x8c63, - 0x39ce, - 0x39ce, - 0x8421, - 0x39ce, - 0x4210, - 0x7bde, - 0x39ce, - 0x4a52, - 0x739c, - 0x39ce, - 0x5294, - 0x6b5a, - 0x39ce, - 0x5ad6, - 0x6318, - 0x39ce, - 0x6318, - 0x5ad6, - 0x39ce, - 0x6b5a, - 0x5294, - 0x39ce, - 0x739c, - 0x4a52, - 0x39ce, - 0x7bde, - 0x4210, - 0x39ce, - 0x8421, - 0x39ce, - 0x39ce, - 0x8c63, - 0x318c, - 0x39ce, - 0x94a5, - 0x294a, - 0x39ce, - 0x9ce7, - 0x2108, - 0x39ce, - 0xa529, - 0x18c6, - 0x39ce, - 0xad6b, - 0x1084, - 0x39ce, - 0xb5ad, - 0x842, - 0x39ce, - 0xbdef, - 0x0, - 0x39ce, - 0xc631, - 0xffff, - 0x318c, - 0x0, - 0xf7bd, - 0x318c, - 0x0, - 0xef7b, - 0x318c, - 0x0, - 0xe739, - 0x318c, - 0x0, - 0xdef7, - 0x318c, - 0x0, - 0xd6b5, - 0x318c, - 0x0, - 0xce73, - 0x318c, - 0x0, - 0xc631, - 0x318c, - 0x842, - 0xbdef, - 0x318c, - 0x1084, - 0xb5ad, - 0x318c, - 0x18c6, - 0xad6b, - 0x318c, - 0x2108, - 0xa529, - 0x318c, - 0x294a, - 0x9ce7, - 0x318c, - 0x318c, - 0x94a5, - 0x318c, - 0x39ce, - 0x8c63, - 0x318c, - 0x4210, - 0x8421, - 0x318c, - 0x4a52, - 0x7bde, - 0x318c, - 0x5294, - 0x739c, - 0x318c, - 0x5ad6, - 0x6b5a, - 0x318c, - 0x6318, - 0x6318, - 0x318c, - 0x6b5a, - 0x5ad6, - 0x318c, - 0x739c, - 0x5294, - 0x318c, - 0x7bde, - 0x4a52, - 0x318c, - 0x8421, - 0x4210, - 0x318c, - 0x8c63, - 0x39ce, - 0x318c, - 0x94a5, - 0x318c, - 0x318c, - 0x9ce7, - 0x294a, - 0x318c, - 0xa529, - 0x2108, - 0x318c, - 0xad6b, - 0x18c6, - 0x318c, - 0xb5ad, - 0x1084, - 0x318c, - 0xbdef, - 0x842, - 0x318c, - 0xc631, - 0x0, - 0x318c, - 0xce73, - 0xffff, - 0x294a, - 0x0, - 0xf7bd, - 0x294a, - 0x0, - 0xef7b, - 0x294a, - 0x0, - 0xe739, - 0x294a, - 0x0, - 0xdef7, - 0x294a, - 0x0, - 0xd6b5, - 0x294a, - 0x0, - 0xce73, - 0x294a, - 0x842, - 0xc631, - 0x294a, - 0x1084, - 0xbdef, - 0x294a, - 0x18c6, - 0xb5ad, - 0x294a, - 0x2108, - 0xad6b, - 0x294a, - 0x294a, - 0xa529, - 0x294a, - 0x318c, - 0x9ce7, - 0x294a, - 0x39ce, - 0x94a5, - 0x294a, - 0x4210, - 0x8c63, - 0x294a, - 0x4a52, - 0x8421, - 0x294a, - 0x5294, - 0x7bde, - 0x294a, - 0x5ad6, - 0x739c, - 0x294a, - 0x6318, - 0x6b5a, - 0x294a, - 0x6b5a, - 0x6318, - 0x294a, - 0x739c, - 0x5ad6, - 0x294a, - 0x7bde, - 0x5294, - 0x294a, - 0x8421, - 0x4a52, - 0x294a, - 0x8c63, - 0x4210, - 0x294a, - 0x94a5, - 0x39ce, - 0x294a, - 0x9ce7, - 0x318c, - 0x294a, - 0xa529, - 0x294a, - 0x294a, - 0xad6b, - 0x2108, - 0x294a, - 0xb5ad, - 0x18c6, - 0x294a, - 0xbdef, - 0x1084, - 0x294a, - 0xc631, - 0x842, - 0x294a, - 0xce73, - 0x0, - 0x294a, - 0xd6b5, - 0xffff, - 0x2108, - 0x0, - 0xf7bd, - 0x2108, - 0x0, - 0xef7b, - 0x2108, - 0x0, - 0xe739, - 0x2108, - 0x0, - 0xdef7, - 0x2108, - 0x0, - 0xd6b5, - 0x2108, - 0x842, - 0xce73, - 0x2108, - 0x1084, - 0xc631, - 0x2108, - 0x18c6, - 0xbdef, - 0x2108, - 0x2108, - 0xb5ad, - 0x2108, - 0x294a, - 0xad6b, - 0x2108, - 0x318c, - 0xa529, - 0x2108, - 0x39ce, - 0x9ce7, - 0x2108, - 0x4210, - 0x94a5, - 0x2108, - 0x4a52, - 0x8c63, - 0x2108, - 0x5294, - 0x8421, - 0x2108, - 0x5ad6, - 0x7bde, - 0x2108, - 0x6318, - 0x739c, - 0x2108, - 0x6b5a, - 0x6b5a, - 0x2108, - 0x739c, - 0x6318, - 0x2108, - 0x7bde, - 0x5ad6, - 0x2108, - 0x8421, - 0x5294, - 0x2108, - 0x8c63, - 0x4a52, - 0x2108, - 0x94a5, - 0x4210, - 0x2108, - 0x9ce7, - 0x39ce, - 0x2108, - 0xa529, - 0x318c, - 0x2108, - 0xad6b, - 0x294a, - 0x2108, - 0xb5ad, - 0x2108, - 0x2108, - 0xbdef, - 0x18c6, - 0x2108, - 0xc631, - 0x1084, - 0x2108, - 0xce73, - 0x842, - 0x2108, - 0xd6b5, - 0x0, - 0x2108, - 0xdef7, - 0xffff, - 0x18c6, - 0x0, - 0xf7bd, - 0x18c6, - 0x0, - 0xef7b, - 0x18c6, - 0x0, - 0xe739, - 0x18c6, - 0x0, - 0xdef7, - 0x18c6, - 0x842, - 0xd6b5, - 0x18c6, - 0x1084, - 0xce73, - 0x18c6, - 0x18c6, - 0xc631, - 0x18c6, - 0x2108, - 0xbdef, - 0x18c6, - 0x294a, - 0xb5ad, - 0x18c6, - 0x318c, - 0xad6b, - 0x18c6, - 0x39ce, - 0xa529, - 0x18c6, - 0x4210, - 0x9ce7, - 0x18c6, - 0x4a52, - 0x94a5, - 0x18c6, - 0x5294, - 0x8c63, - 0x18c6, - 0x5ad6, - 0x8421, - 0x18c6, - 0x6318, - 0x7bde, - 0x18c6, - 0x6b5a, - 0x739c, - 0x18c6, - 0x739c, - 0x6b5a, - 0x18c6, - 0x7bde, - 0x6318, - 0x18c6, - 0x8421, - 0x5ad6, - 0x18c6, - 0x8c63, - 0x5294, - 0x18c6, - 0x94a5, - 0x4a52, - 0x18c6, - 0x9ce7, - 0x4210, - 0x18c6, - 0xa529, - 0x39ce, - 0x18c6, - 0xad6b, - 0x318c, - 0x18c6, - 0xb5ad, - 0x294a, - 0x18c6, - 0xbdef, - 0x2108, - 0x18c6, - 0xc631, - 0x18c6, - 0x18c6, - 0xce73, - 0x1084, - 0x18c6, - 0xd6b5, - 0x842, - 0x18c6, - 0xdef7, - 0x0, - 0x18c6, - 0xe739, - 0xffff, - 0x1084, - 0x0, - 0xf7bd, - 0x1084, - 0x0, - 0xef7b, - 0x1084, - 0x0, - 0xe739, - 0x1084, - 0x842, - 0xdef7, - 0x1084, - 0x1084, - 0xd6b5, - 0x1084, - 0x18c6, - 0xce73, - 0x1084, - 0x2108, - 0xc631, - 0x1084, - 0x294a, - 0xbdef, - 0x1084, - 0x318c, - 0xb5ad, - 0x1084, - 0x39ce, - 0xad6b, - 0x1084, - 0x4210, - 0xa529, - 0x1084, - 0x4a52, - 0x9ce7, - 0x1084, - 0x5294, - 0x94a5, - 0x1084, - 0x5ad6, - 0x8c63, - 0x1084, - 0x6318, - 0x8421, - 0x1084, - 0x6b5a, - 0x7bde, - 0x1084, - 0x739c, - 0x739c, - 0x1084, - 0x7bde, - 0x6b5a, - 0x1084, - 0x8421, - 0x6318, - 0x1084, - 0x8c63, - 0x5ad6, - 0x1084, - 0x94a5, - 0x5294, - 0x1084, - 0x9ce7, - 0x4a52, - 0x1084, - 0xa529, - 0x4210, - 0x1084, - 0xad6b, - 0x39ce, - 0x1084, - 0xb5ad, - 0x318c, - 0x1084, - 0xbdef, - 0x294a, - 0x1084, - 0xc631, - 0x2108, - 0x1084, - 0xce73, - 0x18c6, - 0x1084, - 0xd6b5, - 0x1084, - 0x1084, - 0xdef7, - 0x842, - 0x1084, - 0xe739, - 0x0, - 0x1084, - 0xef7b, - 0xffff, - 0x842, - 0x0, - 0xf7bd, - 0x842, - 0x0, - 0xef7b, - 0x842, - 0x842, - 0xe739, - 0x842, - 0x1084, - 0xdef7, - 0x842, - 0x18c6, - 0xd6b5, - 0x842, - 0x2108, - 0xce73, - 0x842, - 0x294a, - 0xc631, - 0x842, - 0x318c, - 0xbdef, - 0x842, - 0x39ce, - 0xb5ad, - 0x842, - 0x4210, - 0xad6b, - 0x842, - 0x4a52, - 0xa529, - 0x842, - 0x5294, - 0x9ce7, - 0x842, - 0x5ad6, - 0x94a5, - 0x842, - 0x6318, - 0x8c63, - 0x842, - 0x6b5a, - 0x8421, - 0x842, - 0x739c, - 0x7bde, - 0x842, - 0x7bde, - 0x739c, - 0x842, - 0x8421, - 0x6b5a, - 0x842, - 0x8c63, - 0x6318, - 0x842, - 0x94a5, - 0x5ad6, - 0x842, - 0x9ce7, - 0x5294, - 0x842, - 0xa529, - 0x4a52, - 0x842, - 0xad6b, - 0x4210, - 0x842, - 0xb5ad, - 0x39ce, - 0x842, - 0xbdef, - 0x318c, - 0x842, - 0xc631, - 0x294a, - 0x842, - 0xce73, - 0x2108, - 0x842, - 0xd6b5, - 0x18c6, - 0x842, - 0xdef7, - 0x1084, - 0x842, - 0xe739, - 0x842, - 0x842, - 0xef7b, - 0x0, - 0x842, - 0xf7bd, - 0xffff, - 0x0, - 0x0, - 0xf7bd, - 0x0, - 0x842, - 0xef7b, - 0x0, - 0x1084, - 0xe739, - 0x0, - 0x18c6, - 0xdef7, - 0x0, - 0x2108, - 0xd6b5, - 0x0, - 0x294a, - 0xce73, - 0x0, - 0x318c, - 0xc631, - 0x0, - 0x39ce, - 0xbdef, - 0x0, - 0x4210, - 0xb5ad, - 0x0, - 0x4a52, - 0xad6b, - 0x0, - 0x5294, - 0xa529, - 0x0, - 0x5ad6, - 0x9ce7, - 0x0, - 0x6318, - 0x94a5, - 0x0, - 0x6b5a, - 0x8c63, - 0x0, - 0x739c, - 0x8421, - 0x0, - 0x7bde, - 0x7bde, - 0x0, - 0x8421, - 0x739c, - 0x0, - 0x8c63, - 0x6b5a, - 0x0, - 0x94a5, - 0x6318, - 0x0, - 0x9ce7, - 0x5ad6, - 0x0, - 0xa529, - 0x5294, - 0x0, - 0xad6b, - 0x4a52, - 0x0, - 0xb5ad, - 0x4210, - 0x0, - 0xbdef, - 0x39ce, - 0x0, - 0xc631, - 0x318c, - 0x0, - 0xce73, - 0x294a, - 0x0, - 0xd6b5, - 0x2108, - 0x0, - 0xdef7, - 0x18c6, - 0x0, - 0xe739, - 0x1084, - 0x0, - 0xef7b, - 0x842, - 0x0, - 0xf7bd, - 0x0, - 0x0, - 0xffff - - ) - } - - private fun getResourceFile(path : String) : File { - return File(this.javaClass.classLoader.getResource(path).path) - } - - @Test - fun testByteMod() { - // Int.toByte() は Int値の下位8ビットをByte型で表現する。クリッピングされたりはしない - // ただしKotlinもJavaと同じでbyteは符号付きしかないので面倒くさい… - for(i in - 256 .. 256) { - val b = i.toByte() - val i2 = b.toInt() and 255 - assertEquals(i and 255, i2) - } - } - - @Test - fun testApng1() { - FileInputStream(getResourceFile("walk.apng")).use { inStream -> - ApngDecoder.parseStream( - BufferedInputStream(inStream), - object : ApngDecoderCallback { - override fun onApngWarning(message : String) { - println(message) - } - - override fun onApngDebug(message : String) { - println(message) - } - - override fun canApngDebug() : Boolean { - return true - } - - override fun onAnimationInfo( - apng : Apng, - header : ApngImageHeader, - animationControl : ApngAnimationControl + + companion object { + + private val imageData = intArrayOf( + 0xffff, + 0xffff, + 0x0, + 0xf7bd, + 0xffff, + 0x0, + 0xef7b, + 0xffff, + 0x0, + 0xe739, + 0xffff, + 0x0, + 0xdef7, + 0xffff, + 0x0, + 0xd6b5, + 0xffff, + 0x0, + 0xce73, + 0xffff, + 0x0, + 0xc631, + 0xffff, + 0x0, + 0xbdef, + 0xffff, + 0x0, + 0xb5ad, + 0xffff, + 0x0, + 0xad6b, + 0xffff, + 0x0, + 0xa529, + 0xffff, + 0x0, + 0x9ce7, + 0xffff, + 0x0, + 0x94a5, + 0xffff, + 0x0, + 0x8c63, + 0xffff, + 0x0, + 0x8421, + 0xffff, + 0x0, + 0x7bde, + 0xffff, + 0x0, + 0x739c, + 0xffff, + 0x0, + 0x6b5a, + 0xffff, + 0x0, + 0x6318, + 0xffff, + 0x0, + 0x5ad6, + 0xffff, + 0x0, + 0x5294, + 0xffff, + 0x0, + 0x4a52, + 0xffff, + 0x0, + 0x4210, + 0xffff, + 0x0, + 0x39ce, + 0xffff, + 0x0, + 0x318c, + 0xffff, + 0x0, + 0x294a, + 0xffff, + 0x0, + 0x2108, + 0xffff, + 0x0, + 0x18c6, + 0xffff, + 0x0, + 0x1084, + 0xffff, + 0x0, + 0x842, + 0xffff, + 0x0, + 0x0, + 0xffff, + 0x0, + 0xffff, + 0xf7bd, + 0x0, + 0xf7bd, + 0xf7bd, + 0x0, + 0xef7b, + 0xf7bd, + 0x0, + 0xe739, + 0xf7bd, + 0x0, + 0xdef7, + 0xf7bd, + 0x0, + 0xd6b5, + 0xf7bd, + 0x0, + 0xce73, + 0xf7bd, + 0x0, + 0xc631, + 0xf7bd, + 0x0, + 0xbdef, + 0xf7bd, + 0x0, + 0xb5ad, + 0xf7bd, + 0x0, + 0xad6b, + 0xf7bd, + 0x0, + 0xa529, + 0xf7bd, + 0x0, + 0x9ce7, + 0xf7bd, + 0x0, + 0x94a5, + 0xf7bd, + 0x0, + 0x8c63, + 0xf7bd, + 0x0, + 0x8421, + 0xf7bd, + 0x0, + 0x7bde, + 0xf7bd, + 0x0, + 0x739c, + 0xf7bd, + 0x0, + 0x6b5a, + 0xf7bd, + 0x0, + 0x6318, + 0xf7bd, + 0x0, + 0x5ad6, + 0xf7bd, + 0x0, + 0x5294, + 0xf7bd, + 0x0, + 0x4a52, + 0xf7bd, + 0x0, + 0x4210, + 0xf7bd, + 0x0, + 0x39ce, + 0xf7bd, + 0x0, + 0x318c, + 0xf7bd, + 0x0, + 0x294a, + 0xf7bd, + 0x0, + 0x2108, + 0xf7bd, + 0x0, + 0x18c6, + 0xf7bd, + 0x0, + 0x1084, + 0xf7bd, + 0x0, + 0x842, + 0xf7bd, + 0x0, + 0x0, + 0xf7bd, + 0x842, + 0xffff, + 0xef7b, + 0x0, + 0xf7bd, + 0xef7b, + 0x0, + 0xef7b, + 0xef7b, + 0x0, + 0xe739, + 0xef7b, + 0x0, + 0xdef7, + 0xef7b, + 0x0, + 0xd6b5, + 0xef7b, + 0x0, + 0xce73, + 0xef7b, + 0x0, + 0xc631, + 0xef7b, + 0x0, + 0xbdef, + 0xef7b, + 0x0, + 0xb5ad, + 0xef7b, + 0x0, + 0xad6b, + 0xef7b, + 0x0, + 0xa529, + 0xef7b, + 0x0, + 0x9ce7, + 0xef7b, + 0x0, + 0x94a5, + 0xef7b, + 0x0, + 0x8c63, + 0xef7b, + 0x0, + 0x8421, + 0xef7b, + 0x0, + 0x7bde, + 0xef7b, + 0x0, + 0x739c, + 0xef7b, + 0x0, + 0x6b5a, + 0xef7b, + 0x0, + 0x6318, + 0xef7b, + 0x0, + 0x5ad6, + 0xef7b, + 0x0, + 0x5294, + 0xef7b, + 0x0, + 0x4a52, + 0xef7b, + 0x0, + 0x4210, + 0xef7b, + 0x0, + 0x39ce, + 0xef7b, + 0x0, + 0x318c, + 0xef7b, + 0x0, + 0x294a, + 0xef7b, + 0x0, + 0x2108, + 0xef7b, + 0x0, + 0x18c6, + 0xef7b, + 0x0, + 0x1084, + 0xef7b, + 0x0, + 0x842, + 0xef7b, + 0x842, + 0x0, + 0xef7b, + 0x1084, + 0xffff, + 0xe739, + 0x0, + 0xf7bd, + 0xe739, + 0x0, + 0xef7b, + 0xe739, + 0x0, + 0xe739, + 0xe739, + 0x0, + 0xdef7, + 0xe739, + 0x0, + 0xd6b5, + 0xe739, + 0x0, + 0xce73, + 0xe739, + 0x0, + 0xc631, + 0xe739, + 0x0, + 0xbdef, + 0xe739, + 0x0, + 0xb5ad, + 0xe739, + 0x0, + 0xad6b, + 0xe739, + 0x0, + 0xa529, + 0xe739, + 0x0, + 0x9ce7, + 0xe739, + 0x0, + 0x94a5, + 0xe739, + 0x0, + 0x8c63, + 0xe739, + 0x0, + 0x8421, + 0xe739, + 0x0, + 0x7bde, + 0xe739, + 0x0, + 0x739c, + 0xe739, + 0x0, + 0x6b5a, + 0xe739, + 0x0, + 0x6318, + 0xe739, + 0x0, + 0x5ad6, + 0xe739, + 0x0, + 0x5294, + 0xe739, + 0x0, + 0x4a52, + 0xe739, + 0x0, + 0x4210, + 0xe739, + 0x0, + 0x39ce, + 0xe739, + 0x0, + 0x318c, + 0xe739, + 0x0, + 0x294a, + 0xe739, + 0x0, + 0x2108, + 0xe739, + 0x0, + 0x18c6, + 0xe739, + 0x0, + 0x1084, + 0xe739, + 0x842, + 0x842, + 0xe739, + 0x1084, + 0x0, + 0xe739, + 0x18c6, + 0xffff, + 0xdef7, + 0x0, + 0xf7bd, + 0xdef7, + 0x0, + 0xef7b, + 0xdef7, + 0x0, + 0xe739, + 0xdef7, + 0x0, + 0xdef7, + 0xdef7, + 0x0, + 0xd6b5, + 0xdef7, + 0x0, + 0xce73, + 0xdef7, + 0x0, + 0xc631, + 0xdef7, + 0x0, + 0xbdef, + 0xdef7, + 0x0, + 0xb5ad, + 0xdef7, + 0x0, + 0xad6b, + 0xdef7, + 0x0, + 0xa529, + 0xdef7, + 0x0, + 0x9ce7, + 0xdef7, + 0x0, + 0x94a5, + 0xdef7, + 0x0, + 0x8c63, + 0xdef7, + 0x0, + 0x8421, + 0xdef7, + 0x0, + 0x7bde, + 0xdef7, + 0x0, + 0x739c, + 0xdef7, + 0x0, + 0x6b5a, + 0xdef7, + 0x0, + 0x6318, + 0xdef7, + 0x0, + 0x5ad6, + 0xdef7, + 0x0, + 0x5294, + 0xdef7, + 0x0, + 0x4a52, + 0xdef7, + 0x0, + 0x4210, + 0xdef7, + 0x0, + 0x39ce, + 0xdef7, + 0x0, + 0x318c, + 0xdef7, + 0x0, + 0x294a, + 0xdef7, + 0x0, + 0x2108, + 0xdef7, + 0x0, + 0x18c6, + 0xdef7, + 0x842, + 0x1084, + 0xdef7, + 0x1084, + 0x842, + 0xdef7, + 0x18c6, + 0x0, + 0xdef7, + 0x2108, + 0xffff, + 0xd6b5, + 0x0, + 0xf7bd, + 0xd6b5, + 0x0, + 0xef7b, + 0xd6b5, + 0x0, + 0xe739, + 0xd6b5, + 0x0, + 0xdef7, + 0xd6b5, + 0x0, + 0xd6b5, + 0xd6b5, + 0x0, + 0xce73, + 0xd6b5, + 0x0, + 0xc631, + 0xd6b5, + 0x0, + 0xbdef, + 0xd6b5, + 0x0, + 0xb5ad, + 0xd6b5, + 0x0, + 0xad6b, + 0xd6b5, + 0x0, + 0xa529, + 0xd6b5, + 0x0, + 0x9ce7, + 0xd6b5, + 0x0, + 0x94a5, + 0xd6b5, + 0x0, + 0x8c63, + 0xd6b5, + 0x0, + 0x8421, + 0xd6b5, + 0x0, + 0x7bde, + 0xd6b5, + 0x0, + 0x739c, + 0xd6b5, + 0x0, + 0x6b5a, + 0xd6b5, + 0x0, + 0x6318, + 0xd6b5, + 0x0, + 0x5ad6, + 0xd6b5, + 0x0, + 0x5294, + 0xd6b5, + 0x0, + 0x4a52, + 0xd6b5, + 0x0, + 0x4210, + 0xd6b5, + 0x0, + 0x39ce, + 0xd6b5, + 0x0, + 0x318c, + 0xd6b5, + 0x0, + 0x294a, + 0xd6b5, + 0x0, + 0x2108, + 0xd6b5, + 0x842, + 0x18c6, + 0xd6b5, + 0x1084, + 0x1084, + 0xd6b5, + 0x18c6, + 0x842, + 0xd6b5, + 0x2108, + 0x0, + 0xd6b5, + 0x294a, + 0xffff, + 0xce73, + 0x0, + 0xf7bd, + 0xce73, + 0x0, + 0xef7b, + 0xce73, + 0x0, + 0xe739, + 0xce73, + 0x0, + 0xdef7, + 0xce73, + 0x0, + 0xd6b5, + 0xce73, + 0x0, + 0xce73, + 0xce73, + 0x0, + 0xc631, + 0xce73, + 0x0, + 0xbdef, + 0xce73, + 0x0, + 0xb5ad, + 0xce73, + 0x0, + 0xad6b, + 0xce73, + 0x0, + 0xa529, + 0xce73, + 0x0, + 0x9ce7, + 0xce73, + 0x0, + 0x94a5, + 0xce73, + 0x0, + 0x8c63, + 0xce73, + 0x0, + 0x8421, + 0xce73, + 0x0, + 0x7bde, + 0xce73, + 0x0, + 0x739c, + 0xce73, + 0x0, + 0x6b5a, + 0xce73, + 0x0, + 0x6318, + 0xce73, + 0x0, + 0x5ad6, + 0xce73, + 0x0, + 0x5294, + 0xce73, + 0x0, + 0x4a52, + 0xce73, + 0x0, + 0x4210, + 0xce73, + 0x0, + 0x39ce, + 0xce73, + 0x0, + 0x318c, + 0xce73, + 0x0, + 0x294a, + 0xce73, + 0x842, + 0x2108, + 0xce73, + 0x1084, + 0x18c6, + 0xce73, + 0x18c6, + 0x1084, + 0xce73, + 0x2108, + 0x842, + 0xce73, + 0x294a, + 0x0, + 0xce73, + 0x318c, + 0xffff, + 0xc631, + 0x0, + 0xf7bd, + 0xc631, + 0x0, + 0xef7b, + 0xc631, + 0x0, + 0xe739, + 0xc631, + 0x0, + 0xdef7, + 0xc631, + 0x0, + 0xd6b5, + 0xc631, + 0x0, + 0xce73, + 0xc631, + 0x0, + 0xc631, + 0xc631, + 0x0, + 0xbdef, + 0xc631, + 0x0, + 0xb5ad, + 0xc631, + 0x0, + 0xad6b, + 0xc631, + 0x0, + 0xa529, + 0xc631, + 0x0, + 0x9ce7, + 0xc631, + 0x0, + 0x94a5, + 0xc631, + 0x0, + 0x8c63, + 0xc631, + 0x0, + 0x8421, + 0xc631, + 0x0, + 0x7bde, + 0xc631, + 0x0, + 0x739c, + 0xc631, + 0x0, + 0x6b5a, + 0xc631, + 0x0, + 0x6318, + 0xc631, + 0x0, + 0x5ad6, + 0xc631, + 0x0, + 0x5294, + 0xc631, + 0x0, + 0x4a52, + 0xc631, + 0x0, + 0x4210, + 0xc631, + 0x0, + 0x39ce, + 0xc631, + 0x0, + 0x318c, + 0xc631, + 0x842, + 0x294a, + 0xc631, + 0x1084, + 0x2108, + 0xc631, + 0x18c6, + 0x18c6, + 0xc631, + 0x2108, + 0x1084, + 0xc631, + 0x294a, + 0x842, + 0xc631, + 0x318c, + 0x0, + 0xc631, + 0x39ce, + 0xffff, + 0xbdef, + 0x0, + 0xf7bd, + 0xbdef, + 0x0, + 0xef7b, + 0xbdef, + 0x0, + 0xe739, + 0xbdef, + 0x0, + 0xdef7, + 0xbdef, + 0x0, + 0xd6b5, + 0xbdef, + 0x0, + 0xce73, + 0xbdef, + 0x0, + 0xc631, + 0xbdef, + 0x0, + 0xbdef, + 0xbdef, + 0x0, + 0xb5ad, + 0xbdef, + 0x0, + 0xad6b, + 0xbdef, + 0x0, + 0xa529, + 0xbdef, + 0x0, + 0x9ce7, + 0xbdef, + 0x0, + 0x94a5, + 0xbdef, + 0x0, + 0x8c63, + 0xbdef, + 0x0, + 0x8421, + 0xbdef, + 0x0, + 0x7bde, + 0xbdef, + 0x0, + 0x739c, + 0xbdef, + 0x0, + 0x6b5a, + 0xbdef, + 0x0, + 0x6318, + 0xbdef, + 0x0, + 0x5ad6, + 0xbdef, + 0x0, + 0x5294, + 0xbdef, + 0x0, + 0x4a52, + 0xbdef, + 0x0, + 0x4210, + 0xbdef, + 0x0, + 0x39ce, + 0xbdef, + 0x842, + 0x318c, + 0xbdef, + 0x1084, + 0x294a, + 0xbdef, + 0x18c6, + 0x2108, + 0xbdef, + 0x2108, + 0x18c6, + 0xbdef, + 0x294a, + 0x1084, + 0xbdef, + 0x318c, + 0x842, + 0xbdef, + 0x39ce, + 0x0, + 0xbdef, + 0x4210, + 0xffff, + 0xb5ad, + 0x0, + 0xf7bd, + 0xb5ad, + 0x0, + 0xef7b, + 0xb5ad, + 0x0, + 0xe739, + 0xb5ad, + 0x0, + 0xdef7, + 0xb5ad, + 0x0, + 0xd6b5, + 0xb5ad, + 0x0, + 0xce73, + 0xb5ad, + 0x0, + 0xc631, + 0xb5ad, + 0x0, + 0xbdef, + 0xb5ad, + 0x0, + 0xb5ad, + 0xb5ad, + 0x0, + 0xad6b, + 0xb5ad, + 0x0, + 0xa529, + 0xb5ad, + 0x0, + 0x9ce7, + 0xb5ad, + 0x0, + 0x94a5, + 0xb5ad, + 0x0, + 0x8c63, + 0xb5ad, + 0x0, + 0x8421, + 0xb5ad, + 0x0, + 0x7bde, + 0xb5ad, + 0x0, + 0x739c, + 0xb5ad, + 0x0, + 0x6b5a, + 0xb5ad, + 0x0, + 0x6318, + 0xb5ad, + 0x0, + 0x5ad6, + 0xb5ad, + 0x0, + 0x5294, + 0xb5ad, + 0x0, + 0x4a52, + 0xb5ad, + 0x0, + 0x4210, + 0xb5ad, + 0x842, + 0x39ce, + 0xb5ad, + 0x1084, + 0x318c, + 0xb5ad, + 0x18c6, + 0x294a, + 0xb5ad, + 0x2108, + 0x2108, + 0xb5ad, + 0x294a, + 0x18c6, + 0xb5ad, + 0x318c, + 0x1084, + 0xb5ad, + 0x39ce, + 0x842, + 0xb5ad, + 0x4210, + 0x0, + 0xb5ad, + 0x4a52, + 0xffff, + 0xad6b, + 0x0, + 0xf7bd, + 0xad6b, + 0x0, + 0xef7b, + 0xad6b, + 0x0, + 0xe739, + 0xad6b, + 0x0, + 0xdef7, + 0xad6b, + 0x0, + 0xd6b5, + 0xad6b, + 0x0, + 0xce73, + 0xad6b, + 0x0, + 0xc631, + 0xad6b, + 0x0, + 0xbdef, + 0xad6b, + 0x0, + 0xb5ad, + 0xad6b, + 0x0, + 0xad6b, + 0xad6b, + 0x0, + 0xa529, + 0xad6b, + 0x0, + 0x9ce7, + 0xad6b, + 0x0, + 0x94a5, + 0xad6b, + 0x0, + 0x8c63, + 0xad6b, + 0x0, + 0x8421, + 0xad6b, + 0x0, + 0x7bde, + 0xad6b, + 0x0, + 0x739c, + 0xad6b, + 0x0, + 0x6b5a, + 0xad6b, + 0x0, + 0x6318, + 0xad6b, + 0x0, + 0x5ad6, + 0xad6b, + 0x0, + 0x5294, + 0xad6b, + 0x0, + 0x4a52, + 0xad6b, + 0x842, + 0x4210, + 0xad6b, + 0x1084, + 0x39ce, + 0xad6b, + 0x18c6, + 0x318c, + 0xad6b, + 0x2108, + 0x294a, + 0xad6b, + 0x294a, + 0x2108, + 0xad6b, + 0x318c, + 0x18c6, + 0xad6b, + 0x39ce, + 0x1084, + 0xad6b, + 0x4210, + 0x842, + 0xad6b, + 0x4a52, + 0x0, + 0xad6b, + 0x5294, + 0xffff, + 0xa529, + 0x0, + 0xf7bd, + 0xa529, + 0x0, + 0xef7b, + 0xa529, + 0x0, + 0xe739, + 0xa529, + 0x0, + 0xdef7, + 0xa529, + 0x0, + 0xd6b5, + 0xa529, + 0x0, + 0xce73, + 0xa529, + 0x0, + 0xc631, + 0xa529, + 0x0, + 0xbdef, + 0xa529, + 0x0, + 0xb5ad, + 0xa529, + 0x0, + 0xad6b, + 0xa529, + 0x0, + 0xa529, + 0xa529, + 0x0, + 0x9ce7, + 0xa529, + 0x0, + 0x94a5, + 0xa529, + 0x0, + 0x8c63, + 0xa529, + 0x0, + 0x8421, + 0xa529, + 0x0, + 0x7bde, + 0xa529, + 0x0, + 0x739c, + 0xa529, + 0x0, + 0x6b5a, + 0xa529, + 0x0, + 0x6318, + 0xa529, + 0x0, + 0x5ad6, + 0xa529, + 0x0, + 0x5294, + 0xa529, + 0x842, + 0x4a52, + 0xa529, + 0x1084, + 0x4210, + 0xa529, + 0x18c6, + 0x39ce, + 0xa529, + 0x2108, + 0x318c, + 0xa529, + 0x294a, + 0x294a, + 0xa529, + 0x318c, + 0x2108, + 0xa529, + 0x39ce, + 0x18c6, + 0xa529, + 0x4210, + 0x1084, + 0xa529, + 0x4a52, + 0x842, + 0xa529, + 0x5294, + 0x0, + 0xa529, + 0x5ad6, + 0xffff, + 0x9ce7, + 0x0, + 0xf7bd, + 0x9ce7, + 0x0, + 0xef7b, + 0x9ce7, + 0x0, + 0xe739, + 0x9ce7, + 0x0, + 0xdef7, + 0x9ce7, + 0x0, + 0xd6b5, + 0x9ce7, + 0x0, + 0xce73, + 0x9ce7, + 0x0, + 0xc631, + 0x9ce7, + 0x0, + 0xbdef, + 0x9ce7, + 0x0, + 0xb5ad, + 0x9ce7, + 0x0, + 0xad6b, + 0x9ce7, + 0x0, + 0xa529, + 0x9ce7, + 0x0, + 0x9ce7, + 0x9ce7, + 0x0, + 0x94a5, + 0x9ce7, + 0x0, + 0x8c63, + 0x9ce7, + 0x0, + 0x8421, + 0x9ce7, + 0x0, + 0x7bde, + 0x9ce7, + 0x0, + 0x739c, + 0x9ce7, + 0x0, + 0x6b5a, + 0x9ce7, + 0x0, + 0x6318, + 0x9ce7, + 0x0, + 0x5ad6, + 0x9ce7, + 0x842, + 0x5294, + 0x9ce7, + 0x1084, + 0x4a52, + 0x9ce7, + 0x18c6, + 0x4210, + 0x9ce7, + 0x2108, + 0x39ce, + 0x9ce7, + 0x294a, + 0x318c, + 0x9ce7, + 0x318c, + 0x294a, + 0x9ce7, + 0x39ce, + 0x2108, + 0x9ce7, + 0x4210, + 0x18c6, + 0x9ce7, + 0x4a52, + 0x1084, + 0x9ce7, + 0x5294, + 0x842, + 0x9ce7, + 0x5ad6, + 0x0, + 0x9ce7, + 0x6318, + 0xffff, + 0x94a5, + 0x0, + 0xf7bd, + 0x94a5, + 0x0, + 0xef7b, + 0x94a5, + 0x0, + 0xe739, + 0x94a5, + 0x0, + 0xdef7, + 0x94a5, + 0x0, + 0xd6b5, + 0x94a5, + 0x0, + 0xce73, + 0x94a5, + 0x0, + 0xc631, + 0x94a5, + 0x0, + 0xbdef, + 0x94a5, + 0x0, + 0xb5ad, + 0x94a5, + 0x0, + 0xad6b, + 0x94a5, + 0x0, + 0xa529, + 0x94a5, + 0x0, + 0x9ce7, + 0x94a5, + 0x0, + 0x94a5, + 0x94a5, + 0x0, + 0x8c63, + 0x94a5, + 0x0, + 0x8421, + 0x94a5, + 0x0, + 0x7bde, + 0x94a5, + 0x0, + 0x739c, + 0x94a5, + 0x0, + 0x6b5a, + 0x94a5, + 0x0, + 0x6318, + 0x94a5, + 0x842, + 0x5ad6, + 0x94a5, + 0x1084, + 0x5294, + 0x94a5, + 0x18c6, + 0x4a52, + 0x94a5, + 0x2108, + 0x4210, + 0x94a5, + 0x294a, + 0x39ce, + 0x94a5, + 0x318c, + 0x318c, + 0x94a5, + 0x39ce, + 0x294a, + 0x94a5, + 0x4210, + 0x2108, + 0x94a5, + 0x4a52, + 0x18c6, + 0x94a5, + 0x5294, + 0x1084, + 0x94a5, + 0x5ad6, + 0x842, + 0x94a5, + 0x6318, + 0x0, + 0x94a5, + 0x6b5a, + 0xffff, + 0x8c63, + 0x0, + 0xf7bd, + 0x8c63, + 0x0, + 0xef7b, + 0x8c63, + 0x0, + 0xe739, + 0x8c63, + 0x0, + 0xdef7, + 0x8c63, + 0x0, + 0xd6b5, + 0x8c63, + 0x0, + 0xce73, + 0x8c63, + 0x0, + 0xc631, + 0x8c63, + 0x0, + 0xbdef, + 0x8c63, + 0x0, + 0xb5ad, + 0x8c63, + 0x0, + 0xad6b, + 0x8c63, + 0x0, + 0xa529, + 0x8c63, + 0x0, + 0x9ce7, + 0x8c63, + 0x0, + 0x94a5, + 0x8c63, + 0x0, + 0x8c63, + 0x8c63, + 0x0, + 0x8421, + 0x8c63, + 0x0, + 0x7bde, + 0x8c63, + 0x0, + 0x739c, + 0x8c63, + 0x0, + 0x6b5a, + 0x8c63, + 0x842, + 0x6318, + 0x8c63, + 0x1084, + 0x5ad6, + 0x8c63, + 0x18c6, + 0x5294, + 0x8c63, + 0x2108, + 0x4a52, + 0x8c63, + 0x294a, + 0x4210, + 0x8c63, + 0x318c, + 0x39ce, + 0x8c63, + 0x39ce, + 0x318c, + 0x8c63, + 0x4210, + 0x294a, + 0x8c63, + 0x4a52, + 0x2108, + 0x8c63, + 0x5294, + 0x18c6, + 0x8c63, + 0x5ad6, + 0x1084, + 0x8c63, + 0x6318, + 0x842, + 0x8c63, + 0x6b5a, + 0x0, + 0x8c63, + 0x739c, + 0xffff, + 0x8421, + 0x0, + 0xf7bd, + 0x8421, + 0x0, + 0xef7b, + 0x8421, + 0x0, + 0xe739, + 0x8421, + 0x0, + 0xdef7, + 0x8421, + 0x0, + 0xd6b5, + 0x8421, + 0x0, + 0xce73, + 0x8421, + 0x0, + 0xc631, + 0x8421, + 0x0, + 0xbdef, + 0x8421, + 0x0, + 0xb5ad, + 0x8421, + 0x0, + 0xad6b, + 0x8421, + 0x0, + 0xa529, + 0x8421, + 0x0, + 0x9ce7, + 0x8421, + 0x0, + 0x94a5, + 0x8421, + 0x0, + 0x8c63, + 0x8421, + 0x0, + 0x8421, + 0x8421, + 0x0, + 0x7bde, + 0x8421, + 0x0, + 0x739c, + 0x8421, + 0x842, + 0x6b5a, + 0x8421, + 0x1084, + 0x6318, + 0x8421, + 0x18c6, + 0x5ad6, + 0x8421, + 0x2108, + 0x5294, + 0x8421, + 0x294a, + 0x4a52, + 0x8421, + 0x318c, + 0x4210, + 0x8421, + 0x39ce, + 0x39ce, + 0x8421, + 0x4210, + 0x318c, + 0x8421, + 0x4a52, + 0x294a, + 0x8421, + 0x5294, + 0x2108, + 0x8421, + 0x5ad6, + 0x18c6, + 0x8421, + 0x6318, + 0x1084, + 0x8421, + 0x6b5a, + 0x842, + 0x8421, + 0x739c, + 0x0, + 0x8421, + 0x7bde, + 0xffff, + 0x7bde, + 0x0, + 0xf7bd, + 0x7bde, + 0x0, + 0xef7b, + 0x7bde, + 0x0, + 0xe739, + 0x7bde, + 0x0, + 0xdef7, + 0x7bde, + 0x0, + 0xd6b5, + 0x7bde, + 0x0, + 0xce73, + 0x7bde, + 0x0, + 0xc631, + 0x7bde, + 0x0, + 0xbdef, + 0x7bde, + 0x0, + 0xb5ad, + 0x7bde, + 0x0, + 0xad6b, + 0x7bde, + 0x0, + 0xa529, + 0x7bde, + 0x0, + 0x9ce7, + 0x7bde, + 0x0, + 0x94a5, + 0x7bde, + 0x0, + 0x8c63, + 0x7bde, + 0x0, + 0x8421, + 0x7bde, + 0x0, + 0x7bde, + 0x7bde, + 0x842, + 0x739c, + 0x7bde, + 0x1084, + 0x6b5a, + 0x7bde, + 0x18c6, + 0x6318, + 0x7bde, + 0x2108, + 0x5ad6, + 0x7bde, + 0x294a, + 0x5294, + 0x7bde, + 0x318c, + 0x4a52, + 0x7bde, + 0x39ce, + 0x4210, + 0x7bde, + 0x4210, + 0x39ce, + 0x7bde, + 0x4a52, + 0x318c, + 0x7bde, + 0x5294, + 0x294a, + 0x7bde, + 0x5ad6, + 0x2108, + 0x7bde, + 0x6318, + 0x18c6, + 0x7bde, + 0x6b5a, + 0x1084, + 0x7bde, + 0x739c, + 0x842, + 0x7bde, + 0x7bde, + 0x0, + 0x7bde, + 0x8421, + 0xffff, + 0x739c, + 0x0, + 0xf7bd, + 0x739c, + 0x0, + 0xef7b, + 0x739c, + 0x0, + 0xe739, + 0x739c, + 0x0, + 0xdef7, + 0x739c, + 0x0, + 0xd6b5, + 0x739c, + 0x0, + 0xce73, + 0x739c, + 0x0, + 0xc631, + 0x739c, + 0x0, + 0xbdef, + 0x739c, + 0x0, + 0xb5ad, + 0x739c, + 0x0, + 0xad6b, + 0x739c, + 0x0, + 0xa529, + 0x739c, + 0x0, + 0x9ce7, + 0x739c, + 0x0, + 0x94a5, + 0x739c, + 0x0, + 0x8c63, + 0x739c, + 0x0, + 0x8421, + 0x739c, + 0x842, + 0x7bde, + 0x739c, + 0x1084, + 0x739c, + 0x739c, + 0x18c6, + 0x6b5a, + 0x739c, + 0x2108, + 0x6318, + 0x739c, + 0x294a, + 0x5ad6, + 0x739c, + 0x318c, + 0x5294, + 0x739c, + 0x39ce, + 0x4a52, + 0x739c, + 0x4210, + 0x4210, + 0x739c, + 0x4a52, + 0x39ce, + 0x739c, + 0x5294, + 0x318c, + 0x739c, + 0x5ad6, + 0x294a, + 0x739c, + 0x6318, + 0x2108, + 0x739c, + 0x6b5a, + 0x18c6, + 0x739c, + 0x739c, + 0x1084, + 0x739c, + 0x7bde, + 0x842, + 0x739c, + 0x8421, + 0x0, + 0x739c, + 0x8c63, + 0xffff, + 0x6b5a, + 0x0, + 0xf7bd, + 0x6b5a, + 0x0, + 0xef7b, + 0x6b5a, + 0x0, + 0xe739, + 0x6b5a, + 0x0, + 0xdef7, + 0x6b5a, + 0x0, + 0xd6b5, + 0x6b5a, + 0x0, + 0xce73, + 0x6b5a, + 0x0, + 0xc631, + 0x6b5a, + 0x0, + 0xbdef, + 0x6b5a, + 0x0, + 0xb5ad, + 0x6b5a, + 0x0, + 0xad6b, + 0x6b5a, + 0x0, + 0xa529, + 0x6b5a, + 0x0, + 0x9ce7, + 0x6b5a, + 0x0, + 0x94a5, + 0x6b5a, + 0x0, + 0x8c63, + 0x6b5a, + 0x842, + 0x8421, + 0x6b5a, + 0x1084, + 0x7bde, + 0x6b5a, + 0x18c6, + 0x739c, + 0x6b5a, + 0x2108, + 0x6b5a, + 0x6b5a, + 0x294a, + 0x6318, + 0x6b5a, + 0x318c, + 0x5ad6, + 0x6b5a, + 0x39ce, + 0x5294, + 0x6b5a, + 0x4210, + 0x4a52, + 0x6b5a, + 0x4a52, + 0x4210, + 0x6b5a, + 0x5294, + 0x39ce, + 0x6b5a, + 0x5ad6, + 0x318c, + 0x6b5a, + 0x6318, + 0x294a, + 0x6b5a, + 0x6b5a, + 0x2108, + 0x6b5a, + 0x739c, + 0x18c6, + 0x6b5a, + 0x7bde, + 0x1084, + 0x6b5a, + 0x8421, + 0x842, + 0x6b5a, + 0x8c63, + 0x0, + 0x6b5a, + 0x94a5, + 0xffff, + 0x6318, + 0x0, + 0xf7bd, + 0x6318, + 0x0, + 0xef7b, + 0x6318, + 0x0, + 0xe739, + 0x6318, + 0x0, + 0xdef7, + 0x6318, + 0x0, + 0xd6b5, + 0x6318, + 0x0, + 0xce73, + 0x6318, + 0x0, + 0xc631, + 0x6318, + 0x0, + 0xbdef, + 0x6318, + 0x0, + 0xb5ad, + 0x6318, + 0x0, + 0xad6b, + 0x6318, + 0x0, + 0xa529, + 0x6318, + 0x0, + 0x9ce7, + 0x6318, + 0x0, + 0x94a5, + 0x6318, + 0x842, + 0x8c63, + 0x6318, + 0x1084, + 0x8421, + 0x6318, + 0x18c6, + 0x7bde, + 0x6318, + 0x2108, + 0x739c, + 0x6318, + 0x294a, + 0x6b5a, + 0x6318, + 0x318c, + 0x6318, + 0x6318, + 0x39ce, + 0x5ad6, + 0x6318, + 0x4210, + 0x5294, + 0x6318, + 0x4a52, + 0x4a52, + 0x6318, + 0x5294, + 0x4210, + 0x6318, + 0x5ad6, + 0x39ce, + 0x6318, + 0x6318, + 0x318c, + 0x6318, + 0x6b5a, + 0x294a, + 0x6318, + 0x739c, + 0x2108, + 0x6318, + 0x7bde, + 0x18c6, + 0x6318, + 0x8421, + 0x1084, + 0x6318, + 0x8c63, + 0x842, + 0x6318, + 0x94a5, + 0x0, + 0x6318, + 0x9ce7, + 0xffff, + 0x5ad6, + 0x0, + 0xf7bd, + 0x5ad6, + 0x0, + 0xef7b, + 0x5ad6, + 0x0, + 0xe739, + 0x5ad6, + 0x0, + 0xdef7, + 0x5ad6, + 0x0, + 0xd6b5, + 0x5ad6, + 0x0, + 0xce73, + 0x5ad6, + 0x0, + 0xc631, + 0x5ad6, + 0x0, + 0xbdef, + 0x5ad6, + 0x0, + 0xb5ad, + 0x5ad6, + 0x0, + 0xad6b, + 0x5ad6, + 0x0, + 0xa529, + 0x5ad6, + 0x0, + 0x9ce7, + 0x5ad6, + 0x842, + 0x94a5, + 0x5ad6, + 0x1084, + 0x8c63, + 0x5ad6, + 0x18c6, + 0x8421, + 0x5ad6, + 0x2108, + 0x7bde, + 0x5ad6, + 0x294a, + 0x739c, + 0x5ad6, + 0x318c, + 0x6b5a, + 0x5ad6, + 0x39ce, + 0x6318, + 0x5ad6, + 0x4210, + 0x5ad6, + 0x5ad6, + 0x4a52, + 0x5294, + 0x5ad6, + 0x5294, + 0x4a52, + 0x5ad6, + 0x5ad6, + 0x4210, + 0x5ad6, + 0x6318, + 0x39ce, + 0x5ad6, + 0x6b5a, + 0x318c, + 0x5ad6, + 0x739c, + 0x294a, + 0x5ad6, + 0x7bde, + 0x2108, + 0x5ad6, + 0x8421, + 0x18c6, + 0x5ad6, + 0x8c63, + 0x1084, + 0x5ad6, + 0x94a5, + 0x842, + 0x5ad6, + 0x9ce7, + 0x0, + 0x5ad6, + 0xa529, + 0xffff, + 0x5294, + 0x0, + 0xf7bd, + 0x5294, + 0x0, + 0xef7b, + 0x5294, + 0x0, + 0xe739, + 0x5294, + 0x0, + 0xdef7, + 0x5294, + 0x0, + 0xd6b5, + 0x5294, + 0x0, + 0xce73, + 0x5294, + 0x0, + 0xc631, + 0x5294, + 0x0, + 0xbdef, + 0x5294, + 0x0, + 0xb5ad, + 0x5294, + 0x0, + 0xad6b, + 0x5294, + 0x0, + 0xa529, + 0x5294, + 0x842, + 0x9ce7, + 0x5294, + 0x1084, + 0x94a5, + 0x5294, + 0x18c6, + 0x8c63, + 0x5294, + 0x2108, + 0x8421, + 0x5294, + 0x294a, + 0x7bde, + 0x5294, + 0x318c, + 0x739c, + 0x5294, + 0x39ce, + 0x6b5a, + 0x5294, + 0x4210, + 0x6318, + 0x5294, + 0x4a52, + 0x5ad6, + 0x5294, + 0x5294, + 0x5294, + 0x5294, + 0x5ad6, + 0x4a52, + 0x5294, + 0x6318, + 0x4210, + 0x5294, + 0x6b5a, + 0x39ce, + 0x5294, + 0x739c, + 0x318c, + 0x5294, + 0x7bde, + 0x294a, + 0x5294, + 0x8421, + 0x2108, + 0x5294, + 0x8c63, + 0x18c6, + 0x5294, + 0x94a5, + 0x1084, + 0x5294, + 0x9ce7, + 0x842, + 0x5294, + 0xa529, + 0x0, + 0x5294, + 0xad6b, + 0xffff, + 0x4a52, + 0x0, + 0xf7bd, + 0x4a52, + 0x0, + 0xef7b, + 0x4a52, + 0x0, + 0xe739, + 0x4a52, + 0x0, + 0xdef7, + 0x4a52, + 0x0, + 0xd6b5, + 0x4a52, + 0x0, + 0xce73, + 0x4a52, + 0x0, + 0xc631, + 0x4a52, + 0x0, + 0xbdef, + 0x4a52, + 0x0, + 0xb5ad, + 0x4a52, + 0x0, + 0xad6b, + 0x4a52, + 0x842, + 0xa529, + 0x4a52, + 0x1084, + 0x9ce7, + 0x4a52, + 0x18c6, + 0x94a5, + 0x4a52, + 0x2108, + 0x8c63, + 0x4a52, + 0x294a, + 0x8421, + 0x4a52, + 0x318c, + 0x7bde, + 0x4a52, + 0x39ce, + 0x739c, + 0x4a52, + 0x4210, + 0x6b5a, + 0x4a52, + 0x4a52, + 0x6318, + 0x4a52, + 0x5294, + 0x5ad6, + 0x4a52, + 0x5ad6, + 0x5294, + 0x4a52, + 0x6318, + 0x4a52, + 0x4a52, + 0x6b5a, + 0x4210, + 0x4a52, + 0x739c, + 0x39ce, + 0x4a52, + 0x7bde, + 0x318c, + 0x4a52, + 0x8421, + 0x294a, + 0x4a52, + 0x8c63, + 0x2108, + 0x4a52, + 0x94a5, + 0x18c6, + 0x4a52, + 0x9ce7, + 0x1084, + 0x4a52, + 0xa529, + 0x842, + 0x4a52, + 0xad6b, + 0x0, + 0x4a52, + 0xb5ad, + 0xffff, + 0x4210, + 0x0, + 0xf7bd, + 0x4210, + 0x0, + 0xef7b, + 0x4210, + 0x0, + 0xe739, + 0x4210, + 0x0, + 0xdef7, + 0x4210, + 0x0, + 0xd6b5, + 0x4210, + 0x0, + 0xce73, + 0x4210, + 0x0, + 0xc631, + 0x4210, + 0x0, + 0xbdef, + 0x4210, + 0x0, + 0xb5ad, + 0x4210, + 0x842, + 0xad6b, + 0x4210, + 0x1084, + 0xa529, + 0x4210, + 0x18c6, + 0x9ce7, + 0x4210, + 0x2108, + 0x94a5, + 0x4210, + 0x294a, + 0x8c63, + 0x4210, + 0x318c, + 0x8421, + 0x4210, + 0x39ce, + 0x7bde, + 0x4210, + 0x4210, + 0x739c, + 0x4210, + 0x4a52, + 0x6b5a, + 0x4210, + 0x5294, + 0x6318, + 0x4210, + 0x5ad6, + 0x5ad6, + 0x4210, + 0x6318, + 0x5294, + 0x4210, + 0x6b5a, + 0x4a52, + 0x4210, + 0x739c, + 0x4210, + 0x4210, + 0x7bde, + 0x39ce, + 0x4210, + 0x8421, + 0x318c, + 0x4210, + 0x8c63, + 0x294a, + 0x4210, + 0x94a5, + 0x2108, + 0x4210, + 0x9ce7, + 0x18c6, + 0x4210, + 0xa529, + 0x1084, + 0x4210, + 0xad6b, + 0x842, + 0x4210, + 0xb5ad, + 0x0, + 0x4210, + 0xbdef, + 0xffff, + 0x39ce, + 0x0, + 0xf7bd, + 0x39ce, + 0x0, + 0xef7b, + 0x39ce, + 0x0, + 0xe739, + 0x39ce, + 0x0, + 0xdef7, + 0x39ce, + 0x0, + 0xd6b5, + 0x39ce, + 0x0, + 0xce73, + 0x39ce, + 0x0, + 0xc631, + 0x39ce, + 0x0, + 0xbdef, + 0x39ce, + 0x842, + 0xb5ad, + 0x39ce, + 0x1084, + 0xad6b, + 0x39ce, + 0x18c6, + 0xa529, + 0x39ce, + 0x2108, + 0x9ce7, + 0x39ce, + 0x294a, + 0x94a5, + 0x39ce, + 0x318c, + 0x8c63, + 0x39ce, + 0x39ce, + 0x8421, + 0x39ce, + 0x4210, + 0x7bde, + 0x39ce, + 0x4a52, + 0x739c, + 0x39ce, + 0x5294, + 0x6b5a, + 0x39ce, + 0x5ad6, + 0x6318, + 0x39ce, + 0x6318, + 0x5ad6, + 0x39ce, + 0x6b5a, + 0x5294, + 0x39ce, + 0x739c, + 0x4a52, + 0x39ce, + 0x7bde, + 0x4210, + 0x39ce, + 0x8421, + 0x39ce, + 0x39ce, + 0x8c63, + 0x318c, + 0x39ce, + 0x94a5, + 0x294a, + 0x39ce, + 0x9ce7, + 0x2108, + 0x39ce, + 0xa529, + 0x18c6, + 0x39ce, + 0xad6b, + 0x1084, + 0x39ce, + 0xb5ad, + 0x842, + 0x39ce, + 0xbdef, + 0x0, + 0x39ce, + 0xc631, + 0xffff, + 0x318c, + 0x0, + 0xf7bd, + 0x318c, + 0x0, + 0xef7b, + 0x318c, + 0x0, + 0xe739, + 0x318c, + 0x0, + 0xdef7, + 0x318c, + 0x0, + 0xd6b5, + 0x318c, + 0x0, + 0xce73, + 0x318c, + 0x0, + 0xc631, + 0x318c, + 0x842, + 0xbdef, + 0x318c, + 0x1084, + 0xb5ad, + 0x318c, + 0x18c6, + 0xad6b, + 0x318c, + 0x2108, + 0xa529, + 0x318c, + 0x294a, + 0x9ce7, + 0x318c, + 0x318c, + 0x94a5, + 0x318c, + 0x39ce, + 0x8c63, + 0x318c, + 0x4210, + 0x8421, + 0x318c, + 0x4a52, + 0x7bde, + 0x318c, + 0x5294, + 0x739c, + 0x318c, + 0x5ad6, + 0x6b5a, + 0x318c, + 0x6318, + 0x6318, + 0x318c, + 0x6b5a, + 0x5ad6, + 0x318c, + 0x739c, + 0x5294, + 0x318c, + 0x7bde, + 0x4a52, + 0x318c, + 0x8421, + 0x4210, + 0x318c, + 0x8c63, + 0x39ce, + 0x318c, + 0x94a5, + 0x318c, + 0x318c, + 0x9ce7, + 0x294a, + 0x318c, + 0xa529, + 0x2108, + 0x318c, + 0xad6b, + 0x18c6, + 0x318c, + 0xb5ad, + 0x1084, + 0x318c, + 0xbdef, + 0x842, + 0x318c, + 0xc631, + 0x0, + 0x318c, + 0xce73, + 0xffff, + 0x294a, + 0x0, + 0xf7bd, + 0x294a, + 0x0, + 0xef7b, + 0x294a, + 0x0, + 0xe739, + 0x294a, + 0x0, + 0xdef7, + 0x294a, + 0x0, + 0xd6b5, + 0x294a, + 0x0, + 0xce73, + 0x294a, + 0x842, + 0xc631, + 0x294a, + 0x1084, + 0xbdef, + 0x294a, + 0x18c6, + 0xb5ad, + 0x294a, + 0x2108, + 0xad6b, + 0x294a, + 0x294a, + 0xa529, + 0x294a, + 0x318c, + 0x9ce7, + 0x294a, + 0x39ce, + 0x94a5, + 0x294a, + 0x4210, + 0x8c63, + 0x294a, + 0x4a52, + 0x8421, + 0x294a, + 0x5294, + 0x7bde, + 0x294a, + 0x5ad6, + 0x739c, + 0x294a, + 0x6318, + 0x6b5a, + 0x294a, + 0x6b5a, + 0x6318, + 0x294a, + 0x739c, + 0x5ad6, + 0x294a, + 0x7bde, + 0x5294, + 0x294a, + 0x8421, + 0x4a52, + 0x294a, + 0x8c63, + 0x4210, + 0x294a, + 0x94a5, + 0x39ce, + 0x294a, + 0x9ce7, + 0x318c, + 0x294a, + 0xa529, + 0x294a, + 0x294a, + 0xad6b, + 0x2108, + 0x294a, + 0xb5ad, + 0x18c6, + 0x294a, + 0xbdef, + 0x1084, + 0x294a, + 0xc631, + 0x842, + 0x294a, + 0xce73, + 0x0, + 0x294a, + 0xd6b5, + 0xffff, + 0x2108, + 0x0, + 0xf7bd, + 0x2108, + 0x0, + 0xef7b, + 0x2108, + 0x0, + 0xe739, + 0x2108, + 0x0, + 0xdef7, + 0x2108, + 0x0, + 0xd6b5, + 0x2108, + 0x842, + 0xce73, + 0x2108, + 0x1084, + 0xc631, + 0x2108, + 0x18c6, + 0xbdef, + 0x2108, + 0x2108, + 0xb5ad, + 0x2108, + 0x294a, + 0xad6b, + 0x2108, + 0x318c, + 0xa529, + 0x2108, + 0x39ce, + 0x9ce7, + 0x2108, + 0x4210, + 0x94a5, + 0x2108, + 0x4a52, + 0x8c63, + 0x2108, + 0x5294, + 0x8421, + 0x2108, + 0x5ad6, + 0x7bde, + 0x2108, + 0x6318, + 0x739c, + 0x2108, + 0x6b5a, + 0x6b5a, + 0x2108, + 0x739c, + 0x6318, + 0x2108, + 0x7bde, + 0x5ad6, + 0x2108, + 0x8421, + 0x5294, + 0x2108, + 0x8c63, + 0x4a52, + 0x2108, + 0x94a5, + 0x4210, + 0x2108, + 0x9ce7, + 0x39ce, + 0x2108, + 0xa529, + 0x318c, + 0x2108, + 0xad6b, + 0x294a, + 0x2108, + 0xb5ad, + 0x2108, + 0x2108, + 0xbdef, + 0x18c6, + 0x2108, + 0xc631, + 0x1084, + 0x2108, + 0xce73, + 0x842, + 0x2108, + 0xd6b5, + 0x0, + 0x2108, + 0xdef7, + 0xffff, + 0x18c6, + 0x0, + 0xf7bd, + 0x18c6, + 0x0, + 0xef7b, + 0x18c6, + 0x0, + 0xe739, + 0x18c6, + 0x0, + 0xdef7, + 0x18c6, + 0x842, + 0xd6b5, + 0x18c6, + 0x1084, + 0xce73, + 0x18c6, + 0x18c6, + 0xc631, + 0x18c6, + 0x2108, + 0xbdef, + 0x18c6, + 0x294a, + 0xb5ad, + 0x18c6, + 0x318c, + 0xad6b, + 0x18c6, + 0x39ce, + 0xa529, + 0x18c6, + 0x4210, + 0x9ce7, + 0x18c6, + 0x4a52, + 0x94a5, + 0x18c6, + 0x5294, + 0x8c63, + 0x18c6, + 0x5ad6, + 0x8421, + 0x18c6, + 0x6318, + 0x7bde, + 0x18c6, + 0x6b5a, + 0x739c, + 0x18c6, + 0x739c, + 0x6b5a, + 0x18c6, + 0x7bde, + 0x6318, + 0x18c6, + 0x8421, + 0x5ad6, + 0x18c6, + 0x8c63, + 0x5294, + 0x18c6, + 0x94a5, + 0x4a52, + 0x18c6, + 0x9ce7, + 0x4210, + 0x18c6, + 0xa529, + 0x39ce, + 0x18c6, + 0xad6b, + 0x318c, + 0x18c6, + 0xb5ad, + 0x294a, + 0x18c6, + 0xbdef, + 0x2108, + 0x18c6, + 0xc631, + 0x18c6, + 0x18c6, + 0xce73, + 0x1084, + 0x18c6, + 0xd6b5, + 0x842, + 0x18c6, + 0xdef7, + 0x0, + 0x18c6, + 0xe739, + 0xffff, + 0x1084, + 0x0, + 0xf7bd, + 0x1084, + 0x0, + 0xef7b, + 0x1084, + 0x0, + 0xe739, + 0x1084, + 0x842, + 0xdef7, + 0x1084, + 0x1084, + 0xd6b5, + 0x1084, + 0x18c6, + 0xce73, + 0x1084, + 0x2108, + 0xc631, + 0x1084, + 0x294a, + 0xbdef, + 0x1084, + 0x318c, + 0xb5ad, + 0x1084, + 0x39ce, + 0xad6b, + 0x1084, + 0x4210, + 0xa529, + 0x1084, + 0x4a52, + 0x9ce7, + 0x1084, + 0x5294, + 0x94a5, + 0x1084, + 0x5ad6, + 0x8c63, + 0x1084, + 0x6318, + 0x8421, + 0x1084, + 0x6b5a, + 0x7bde, + 0x1084, + 0x739c, + 0x739c, + 0x1084, + 0x7bde, + 0x6b5a, + 0x1084, + 0x8421, + 0x6318, + 0x1084, + 0x8c63, + 0x5ad6, + 0x1084, + 0x94a5, + 0x5294, + 0x1084, + 0x9ce7, + 0x4a52, + 0x1084, + 0xa529, + 0x4210, + 0x1084, + 0xad6b, + 0x39ce, + 0x1084, + 0xb5ad, + 0x318c, + 0x1084, + 0xbdef, + 0x294a, + 0x1084, + 0xc631, + 0x2108, + 0x1084, + 0xce73, + 0x18c6, + 0x1084, + 0xd6b5, + 0x1084, + 0x1084, + 0xdef7, + 0x842, + 0x1084, + 0xe739, + 0x0, + 0x1084, + 0xef7b, + 0xffff, + 0x842, + 0x0, + 0xf7bd, + 0x842, + 0x0, + 0xef7b, + 0x842, + 0x842, + 0xe739, + 0x842, + 0x1084, + 0xdef7, + 0x842, + 0x18c6, + 0xd6b5, + 0x842, + 0x2108, + 0xce73, + 0x842, + 0x294a, + 0xc631, + 0x842, + 0x318c, + 0xbdef, + 0x842, + 0x39ce, + 0xb5ad, + 0x842, + 0x4210, + 0xad6b, + 0x842, + 0x4a52, + 0xa529, + 0x842, + 0x5294, + 0x9ce7, + 0x842, + 0x5ad6, + 0x94a5, + 0x842, + 0x6318, + 0x8c63, + 0x842, + 0x6b5a, + 0x8421, + 0x842, + 0x739c, + 0x7bde, + 0x842, + 0x7bde, + 0x739c, + 0x842, + 0x8421, + 0x6b5a, + 0x842, + 0x8c63, + 0x6318, + 0x842, + 0x94a5, + 0x5ad6, + 0x842, + 0x9ce7, + 0x5294, + 0x842, + 0xa529, + 0x4a52, + 0x842, + 0xad6b, + 0x4210, + 0x842, + 0xb5ad, + 0x39ce, + 0x842, + 0xbdef, + 0x318c, + 0x842, + 0xc631, + 0x294a, + 0x842, + 0xce73, + 0x2108, + 0x842, + 0xd6b5, + 0x18c6, + 0x842, + 0xdef7, + 0x1084, + 0x842, + 0xe739, + 0x842, + 0x842, + 0xef7b, + 0x0, + 0x842, + 0xf7bd, + 0xffff, + 0x0, + 0x0, + 0xf7bd, + 0x0, + 0x842, + 0xef7b, + 0x0, + 0x1084, + 0xe739, + 0x0, + 0x18c6, + 0xdef7, + 0x0, + 0x2108, + 0xd6b5, + 0x0, + 0x294a, + 0xce73, + 0x0, + 0x318c, + 0xc631, + 0x0, + 0x39ce, + 0xbdef, + 0x0, + 0x4210, + 0xb5ad, + 0x0, + 0x4a52, + 0xad6b, + 0x0, + 0x5294, + 0xa529, + 0x0, + 0x5ad6, + 0x9ce7, + 0x0, + 0x6318, + 0x94a5, + 0x0, + 0x6b5a, + 0x8c63, + 0x0, + 0x739c, + 0x8421, + 0x0, + 0x7bde, + 0x7bde, + 0x0, + 0x8421, + 0x739c, + 0x0, + 0x8c63, + 0x6b5a, + 0x0, + 0x94a5, + 0x6318, + 0x0, + 0x9ce7, + 0x5ad6, + 0x0, + 0xa529, + 0x5294, + 0x0, + 0xad6b, + 0x4a52, + 0x0, + 0xb5ad, + 0x4210, + 0x0, + 0xbdef, + 0x39ce, + 0x0, + 0xc631, + 0x318c, + 0x0, + 0xce73, + 0x294a, + 0x0, + 0xd6b5, + 0x2108, + 0x0, + 0xdef7, + 0x18c6, + 0x0, + 0xe739, + 0x1084, + 0x0, + 0xef7b, + 0x842, + 0x0, + 0xf7bd, + 0x0, + 0x0, + 0xffff + + ) + } + + private fun getResourceFile(path: String): File { + return File(this.javaClass.classLoader.getResource(path).path) + } + + @Test + fun testByteMod() { + // Int.toByte() は Int値の下位8ビットをByte型で表現する。クリッピングされたりはしない + // ただしKotlinもJavaと同じでbyteは符号付きしかないので面倒くさい… + for (i in -256..256) { + val b = i.toByte() + val i2 = b.toInt() and 255 + assertEquals(i and 255, i2) + } + } + + @Test + fun testApng1() { + FileInputStream(getResourceFile("walk.apng")).use { inStream -> + ApngDecoder.parseStream( + BufferedInputStream(inStream), + object : ApngDecoderCallback { + override fun onApngWarning(message: String) { + println(message) + } + + override fun onApngDebug(message: String) { + println(message) + } + + override fun canApngDebug(): Boolean { + return true + } + + override fun onAnimationInfo( + apng: Apng, + header: ApngImageHeader, + animationControl: ApngAnimationControl, ) { - println("animationControl=$animationControl") - } - - override fun onHeader(apng : Apng, header : ApngImageHeader) { - println("header=$header") - } - - override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) { - println("onDefaultImage w=${bitmap.width},h=${bitmap.height}") - } - - override fun onAnimationFrame( - apng : Apng, - frameControl : ApngFrameControl, - bitmap : ApngBitmap + println("animationControl=$animationControl") + } + + override fun onHeader(apng: Apng, header: ApngImageHeader) { + println("header=$header") + } + + override fun onDefaultImage(apng: Apng, bitmap: ApngBitmap) { + println("onDefaultImage w=${bitmap.width},h=${bitmap.height}") + } + + override fun onAnimationFrame( + apng: Apng, + frameControl: ApngFrameControl, + bitmap: ApngBitmap, ) { - println("onAnimationFrame frameControl=$frameControl") - println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}") - } - } - ) - } - } - - @Test - fun test16bitPaeth() { - - FileInputStream(getResourceFile("basi2c16.png")).use { inStream -> - ApngDecoder.parseStream( - BufferedInputStream(inStream), - object : ApngDecoderCallback { - override fun onApngWarning(message : String) { - println(message) - } - - override fun onApngDebug(message : String) { - println(message) - } - - override fun canApngDebug() : Boolean { - return true - } - - override fun onAnimationInfo( - apng : Apng, - header : ApngImageHeader, - animationControl : ApngAnimationControl + println("onAnimationFrame frameControl=$frameControl") + println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}") + } + } + ) + } + } + + @Test + fun test16bitPaeth() { + + FileInputStream(getResourceFile("basi2c16.png")).use { inStream -> + ApngDecoder.parseStream( + BufferedInputStream(inStream), + object : ApngDecoderCallback { + override fun onApngWarning(message: String) { + println(message) + } + + override fun onApngDebug(message: String) { + println(message) + } + + override fun canApngDebug(): Boolean { + return true + } + + override fun onAnimationInfo( + apng: Apng, + header: ApngImageHeader, + animationControl: ApngAnimationControl, ) { - println("animationControl=$animationControl") - } - - override fun onHeader(apng : Apng, header : ApngImageHeader) { - println("header=$header") - } - - override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) { - println("onDefaultImage w=${bitmap.width},h=${bitmap.height}") - - val w = bitmap.width - val h = bitmap.height - val dstPos = bitmap.pointer() - for(y in 0 until h) { - for(x in 0 until w) { - val srcPos = (x + y * w) * 3 - val src_r = imageData[srcPos + 0] - val src_g = imageData[srcPos + 1] - val src_b = imageData[srcPos + 2] - dstPos.setXY(x, y) - val dst_r = dstPos.red - val dst_g = dstPos.green - val dst_b = dstPos.blue - assertEquals( - "xy=$x,$y color=${"0x%x".format(dstPos.color)} r", - src_r shr 8, - dst_r - ) - assertEquals( - "xy=$x,$y color=${"0x%x".format(dstPos.color)} g", - src_g shr 8, - dst_g - ) - assertEquals( - "xy=$x,$y color=${"0x%x".format(dstPos.color)} b", - src_b shr 8, - dst_b - ) - } - } - } - - override fun onAnimationFrame( - apng : Apng, - frameControl : ApngFrameControl, - bitmap : ApngBitmap + println("animationControl=$animationControl") + } + + override fun onHeader(apng: Apng, header: ApngImageHeader) { + println("header=$header") + } + + override fun onDefaultImage(apng: Apng, bitmap: ApngBitmap) { + println("onDefaultImage w=${bitmap.width},h=${bitmap.height}") + + val w = bitmap.width + val h = bitmap.height + val dstPos = bitmap.pointer() + for (y in 0 until h) { + for (x in 0 until w) { + val srcPos = (x + y * w) * 3 + val src_r = imageData[srcPos + 0] + val src_g = imageData[srcPos + 1] + val src_b = imageData[srcPos + 2] + dstPos.setXY(x, y) + val dst_r = dstPos.red + val dst_g = dstPos.green + val dst_b = dstPos.blue + assertEquals( + "xy=$x,$y color=${"0x%x".format(dstPos.color)} r", + src_r shr 8, + dst_r + ) + assertEquals( + "xy=$x,$y color=${"0x%x".format(dstPos.color)} g", + src_g shr 8, + dst_g + ) + assertEquals( + "xy=$x,$y color=${"0x%x".format(dstPos.color)} b", + src_b shr 8, + dst_b + ) + } + } + } + + override fun onAnimationFrame( + apng: Apng, + frameControl: ApngFrameControl, + bitmap: ApngBitmap, ) { - println("onAnimationFrame frameControl=$frameControl") - println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}") - } - } - ) - } - } + println("onAnimationFrame frameControl=$frameControl") + println("onAnimationFrame w=${bitmap.width},h=${bitmap.height}") + } + } + ) + } + } } diff --git a/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt index 35ee0821..ba4cc1b6 100644 --- a/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt +++ b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt @@ -1,514 +1,505 @@ package jp.juggler.apng -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.PorterDuff -import android.graphics.PorterDuffXfermode -import android.graphics.Rect +import android.graphics.* import android.util.Log - import java.io.InputStream -import java.util.ArrayList import kotlin.math.max import kotlin.math.min class ApngFrames private constructor( - private val pixelSizeMax : Int = 0, - private val debug : Boolean = false + private val pixelSizeMax: Int = 0, + private val debug: Boolean = false, ) : ApngDecoderCallback, GifDecoderCallback { - - companion object { - - private const val TAG = "ApngFrames" - - // ループしない画像の場合は3秒でまたループさせる - private const val DELAY_AFTER_END = 3000L - - // アニメーションフレームの描画に使う - private val sPaintDontBlend = Paint().apply { - xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC) - isFilterBitmap = true - } - private val sPaintClear = Paint().apply { - xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) - color = 0 - } - - private fun createBlankBitmap(w : Int, h : Int) = - Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) - - private fun scale(max : Int, num : Int, den : Int) = - (max.toFloat() * num.toFloat() / den.toFloat() + 0.5f).toInt() - - private fun scaleBitmap( - size_max : Int, - src : Bitmap, - recycleSrc : Boolean = true // true: ownership of "src" will be moved or recycled. - ) : Bitmap { - - val wSrc = src.width - val hSrc = src.height - if(size_max <= 0 || (wSrc <= size_max && hSrc <= size_max)) { - return if(recycleSrc) { - src - } else { - src.copy(Bitmap.Config.ARGB_8888, false) - } - } - - val wDst : Int - val hDst : Int - if(wSrc >= hSrc) { - wDst = size_max - hDst = max(1, scale(size_max, hSrc, wSrc)) - } else { - hDst = size_max - wDst = max(1, scale(size_max, wSrc, hSrc)) - } - //Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst") - - val b2 = createBlankBitmap(wDst, hDst) - val canvas = Canvas(b2) - val rectSrc = Rect(0, 0, wSrc, hSrc) - val rectDst = Rect(0, 0, wDst, hDst) - canvas.drawBitmap(src, rectSrc, rectDst, sPaintDontBlend) - - if(recycleSrc) src.recycle() - - return b2 - } - - private fun toAndroidBitmap(src : ApngBitmap) = - Bitmap.createBitmap( - src.colors, // int[] 配列 - 0, // offset - src.width, //stride - src.width, // width - src.height, //height - Bitmap.Config.ARGB_8888 - ) - - private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) = - scaleBitmap(size_max, toAndroidBitmap(src)) - - private fun parseApng( - inStream : InputStream, - pixelSizeMax : Int, - debug : Boolean = false - ) : ApngFrames { - val result = ApngFrames(pixelSizeMax, debug) - try { - ApngDecoder.parseStream(inStream, result) - result.onParseComplete() - return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true } - ?: throw RuntimeException("APNG has no image") - } catch(ex : Throwable) { - result.dispose() - throw ex - } - - } - - private fun parseGif( - inStream : InputStream, - pixelSizeMax : Int, - debug : Boolean = false - ) : ApngFrames { - val result = ApngFrames(pixelSizeMax, debug) - try { - GifDecoder(result).parse(inStream) - result.onParseComplete() - return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true } - ?: throw RuntimeException("GIF has no image") - } catch(ex : Throwable) { - result.dispose() - throw ex - } - } - private val apngHeadKey = byteArrayOf(0x89.toByte(),0x50) - private val gifHeadKey = "GIF".toByteArray(Charsets.UTF_8) + companion object { - private fun matchBytes(ba1:ByteArray,ba2:ByteArray,length:Int=min(ba1.size,ba2.size)):Boolean{ - for( i in 0 until length){ - if( ba1[i] != ba2[i] ) return false - } - return true - } - - fun parse( - pixelSizeMax : Int, - debug : Boolean = false, - opener : () -> InputStream? - ) : ApngFrames? { - - val buf = ByteArray(8) { 0.toByte() } - opener()?.use { it.read(buf, 0, buf.size) } - - if(buf.size >= 8 && matchBytes(buf, apngHeadKey) ) { - return opener()?.use { parseApng(it, pixelSizeMax, debug) } - } - - if(buf.size >= 6 && matchBytes(buf, gifHeadKey) ) { - return opener()?.use { parseGif(it, pixelSizeMax, debug) } - } - - return null - } - } - - private var header : ApngImageHeader? = null - private var animationControl : ApngAnimationControl? = null - - // width,height (after resized) - var width : Int = 1 - private set - - var height : Int = 1 - private set - - class Frame( - val bitmap : Bitmap, - val timeStart : Long, - val timeWidth : Long - ) - - var frames : ArrayList? = null - - @Suppress("MemberVisibilityCanBePrivate") - val numFrames : Int - get() = frames?.size ?: 1 - - @Suppress("unused") - val hasMultipleFrame : Boolean - get() = numFrames > 1 - - private var timeTotal = 0L - - private lateinit var canvas : Canvas - - private var canvasBitmap : Bitmap? = null - - // 再生速度の調整 - private var durationScale = 1f - - // APNGじゃなかった場合に使われる - private var defaultImage : Bitmap? = null - set(value) { - field = value - if(value != null) { - width = value.width - height = value.height - } - } - - constructor(bitmap : Bitmap) : this() { - defaultImage = bitmap - } - - private fun onParseComplete() { - canvasBitmap?.recycle() - canvasBitmap = null - - val frames = this.frames - if(frames != null) { - if(frames.size > 1) { - defaultImage?.recycle() - defaultImage = null - } else if(frames.size == 1) { - defaultImage?.recycle() - defaultImage = frames.first().bitmap - frames.clear() - } - } - } - - fun dispose() { - canvasBitmap?.recycle() - defaultImage?.recycle() - frames?.forEach { it.bitmap.recycle() } - } - - class FindFrameResult { - var bitmap : Bitmap? = null // may null - var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE - } - - // シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する - @Suppress("unused") - fun findFrame(result : FindFrameResult, t : Long) { - - if(defaultImage != null) { - result.bitmap = defaultImage - result.delay = Long.MAX_VALUE - return - } - - val animationControl = this.animationControl - val frames = this.frames - - if(animationControl == null || frames == null || frames.isEmpty()) { - // ここは通らないはず… - result.bitmap = null - result.delay = Long.MAX_VALUE - return - } - - val frameCount = frames.size - - val isFinite = animationControl.isFinite - val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1 - val endWait = if(isFinite) DELAY_AFTER_END else 0L - val timeTotalLoop = max(1, timeTotal * repeatSequenceCount + endWait) - - val tf = (max(0, t) / durationScale).toLong() - - // 全体の繰り返し時刻で余りを計算 - val tl = tf % timeTotalLoop - - if(tl >= timeTotalLoop - endWait) { - // 終端で待機状態 - result.bitmap = frames[frameCount - 1].bitmap - result.delay = (0.5f + (timeTotalLoop - tl) * durationScale).toLong() - return - } - - // 1ループの繰り返し時刻で余りを計算 - val tt = tl % timeTotal - - // フレームリストを時刻で二分探索 - var s = 0 - var e = frameCount - while(e - s > 1) { - val mid = s + e shr 1 - val frame = frames[mid] - // log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.timeStart,frame.timeStart+frame.timeWidth ); - if(tt < frame.timeStart) { - e = mid - } else if(tt >= frame.timeStart + frame.timeWidth) { - s = mid + 1 - } else { - s = mid - break - } - } - s = if(s < 0) 0 else if(s >= frameCount - 1) frameCount - 1 else s - val frame = frames[s] - val delay = frame.timeStart + frame.timeWidth - tt - result.bitmap = frames[s].bitmap - result.delay = (0.5f + durationScale * max(0f, delay.toFloat())).toLong() - - // log.d("findFrame tf=%d,tl=%d/%d,tt=%d/%d,s=%d,w=%d,delay=%d",tf,tl,loop_total,tt,timeTotal,s,frame.timeWidth,result.delay); - } - - ///////////////////////////////////////////////////// - // implements ApngDecoderCallback - - override fun onApngWarning(message : String) { - Log.w(TAG, message) - } - - override fun onApngDebug(message : String) { - Log.d(TAG, message) - } - - override fun canApngDebug() : Boolean = debug - - override fun onHeader(apng : Apng, header : ApngImageHeader) { - this.header = header - } - - override fun onAnimationInfo( - apng : Apng, - header : ApngImageHeader, - animationControl : ApngAnimationControl - ) { - if(debug) { - Log.d(TAG, "onAnimationInfo") - } - this.animationControl = animationControl - this.frames = ArrayList(animationControl.numFrames) - - val canvasBitmap = createBlankBitmap(header.width, header.height) - this.canvasBitmap = canvasBitmap - this.canvas = Canvas(canvasBitmap) - } - - override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) { - if(debug) { - Log.d(TAG, "onDefaultImage") - } - defaultImage?.recycle() - defaultImage = toAndroidBitmap(bitmap, pixelSizeMax) - } - - override fun onAnimationFrame( - apng : Apng, - frameControl : ApngFrameControl, - frameBitmap : ApngBitmap - ) { - if(debug) { - Log.d( - TAG, - "onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}" - ) - } - val frames = this.frames ?: return - val canvasBitmap = this.canvasBitmap ?: return - - val disposeOp = when { - - // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. - frameControl.disposeOp == DisposeOp.Previous && frames.isEmpty() -> DisposeOp.Background - - else -> frameControl.disposeOp - } - - val previous : Bitmap? = when(disposeOp) { - DisposeOp.Previous -> Bitmap.createBitmap( - canvasBitmap, - frameControl.xOffset, - frameControl.yOffset, - frameControl.width, - frameControl.height - ) - else -> null - } - - try { - - val frameBitmapAndroid = toAndroidBitmap(frameBitmap) - - try { - - canvas.drawBitmap( - frameBitmapAndroid, - frameControl.xOffset.toFloat(), - frameControl.yOffset.toFloat(), - when(frameControl.blendOp) { - // all color components of the frame, including alpha, - // overwrite the current contents of the frame's output buffer region. - BlendOp.Source -> sPaintDontBlend - // the frame should be composited onto the output buffer based on its alpha, - // using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. - BlendOp.Over -> null - } - ) - - val frame = Frame( - bitmap = scaleBitmap(pixelSizeMax, canvasBitmap, recycleSrc = false), - timeStart = timeTotal, - timeWidth = max(1L, frameControl.delayMilliseconds) - ) - frames.add(frame) - timeTotal += frame.timeWidth - - when(disposeOp) { - - // no disposal is done on this frame before rendering the next; - // the contents of the output buffer are left as is. - DisposeOp.None -> { - } - - // the frame's region of the output buffer is - // to be cleared to fully transparent black - // before rendering the next frame. - DisposeOp.Background -> { - val rect = Rect() - rect.left = frameControl.xOffset - rect.top = frameControl.yOffset - rect.right = frameControl.xOffset + frameControl.width - rect.bottom = frameControl.yOffset + frameControl.height - canvas.drawRect(rect, sPaintClear) - // canvas.drawColor(0, PorterDuff.Mode.CLEAR) - } - - // the frame's region of the output buffer is - // to be reverted to the previous contents - // before rendering the next frame. - DisposeOp.Previous -> if(previous != null) { - canvas.drawBitmap( - previous, - frameControl.xOffset.toFloat(), - frameControl.yOffset.toFloat(), - sPaintDontBlend - ) - } - - } - - } finally { - frameBitmapAndroid.recycle() - } - } finally { - previous?.recycle() - } - } - - /////////////////////////////////////////////////////////////////////// - // Gif support - - override fun onGifWarning(message : String) { - Log.w(TAG, message) - } - - override fun onGifDebug(message : String) { - Log.d(TAG, message) - } - - override fun canGifDebug() : Boolean = debug - - override fun onGifHeader(header : ApngImageHeader) { - this.header = header - } - - override fun onGifAnimationInfo( - header : ApngImageHeader, - animationControl : ApngAnimationControl - ) { - if(debug) { - Log.d(TAG, "onAnimationInfo") - } - this.animationControl = animationControl - this.frames = ArrayList(animationControl.numFrames) - val canvasBitmap = createBlankBitmap(header.width, header.height) - this.canvasBitmap = canvasBitmap - this.canvas = Canvas(canvasBitmap) - } - - override fun onGifAnimationFrame( - frameControl : ApngFrameControl, - frameBitmap : ApngBitmap - ) { - if(debug) { - Log.d( - TAG, - "onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}" - ) - } - val frames = this.frames ?: return - - if(frames.isEmpty()) { - defaultImage?.recycle() - defaultImage = toAndroidBitmap(frameBitmap, pixelSizeMax) - // ここでwidth,heightがセットされる - } - - val frameBitmapAndroid = toAndroidBitmap(frameBitmap) - try { - val frame = Frame( - bitmap = scaleBitmap(pixelSizeMax, frameBitmapAndroid, recycleSrc = false), - timeStart = timeTotal, - timeWidth = max(1L, frameControl.delayMilliseconds) - ) - frames.add(frame) - timeTotal += frame.timeWidth - - } finally { - frameBitmapAndroid.recycle() - } - - } - + private const val TAG = "ApngFrames" + + // ループしない画像の場合は3秒でまたループさせる + private const val DELAY_AFTER_END = 3000L + + // アニメーションフレームの描画に使う + private val sPaintDontBlend = Paint().apply { + xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC) + isFilterBitmap = true + } + private val sPaintClear = Paint().apply { + xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + color = 0 + } + + private fun createBlankBitmap(w: Int, h: Int) = + Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) + + private fun scale(max: Int, num: Int, den: Int) = + (max.toFloat() * num.toFloat() / den.toFloat() + 0.5f).toInt() + + private fun scaleBitmap( + sizeMax: Int, + src: Bitmap, + recycleSrc: Boolean = true, // true: ownership of "src" will be moved or recycled. + ): Bitmap { + + val wSrc = src.width + val hSrc = src.height + if (sizeMax <= 0 || (wSrc <= sizeMax && hSrc <= sizeMax)) { + return if (recycleSrc) { + src + } else { + src.copy(Bitmap.Config.ARGB_8888, false) + } + } + + val wDst: Int + val hDst: Int + if (wSrc >= hSrc) { + wDst = sizeMax + hDst = max(1, scale(sizeMax, hSrc, wSrc)) + } else { + hDst = sizeMax + wDst = max(1, scale(sizeMax, wSrc, hSrc)) + } + //Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst") + + val b2 = createBlankBitmap(wDst, hDst) + val canvas = Canvas(b2) + val rectSrc = Rect(0, 0, wSrc, hSrc) + val rectDst = Rect(0, 0, wDst, hDst) + canvas.drawBitmap(src, rectSrc, rectDst, sPaintDontBlend) + + if (recycleSrc) src.recycle() + + return b2 + } + + private fun toAndroidBitmap(src: ApngBitmap) = + Bitmap.createBitmap( + src.colors, // int[] 配列 + 0, // offset + src.width, //stride + src.width, // width + src.height, //height + Bitmap.Config.ARGB_8888 + ) + + private fun toAndroidBitmap(src: ApngBitmap, sizeMax: Int) = + scaleBitmap(sizeMax, toAndroidBitmap(src)) + + private fun parseApng( + inStream: InputStream, + pixelSizeMax: Int, + debug: Boolean = false, + ): ApngFrames { + val result = ApngFrames(pixelSizeMax, debug) + try { + ApngDecoder.parseStream(inStream, result) + result.onParseComplete() + return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true } + ?: error("APNG has no image") + } catch (ex: Throwable) { + result.dispose() + throw ex + } + } + + private fun parseGif( + inStream: InputStream, + pixelSizeMax: Int, + debug: Boolean = false, + ): ApngFrames { + val result = ApngFrames(pixelSizeMax, debug) + try { + GifDecoder(result).parse(inStream) + result.onParseComplete() + return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true } + ?: error("GIF has no image") + } catch (ex: Throwable) { + result.dispose() + throw ex + } + } + + private val apngHeadKey = byteArrayOf(0x89.toByte(), 0x50) + private val gifHeadKey = "GIF".toByteArray(Charsets.UTF_8) + + private fun matchBytes( + ba1: ByteArray, + ba2: ByteArray, + length: Int = min(ba1.size, ba2.size), + ): Boolean { + for (i in 0 until length) { + if (ba1[i] != ba2[i]) return false + } + return true + } + + fun parse( + pixelSizeMax: Int, + debug: Boolean = false, + opener: () -> InputStream?, + ): ApngFrames? { + + val buf = ByteArray(8) { 0.toByte() } + opener()?.use { it.read(buf, 0, buf.size) } + + if (buf.size >= 8 && matchBytes(buf, apngHeadKey)) { + return opener()?.use { parseApng(it, pixelSizeMax, debug) } + } + + if (buf.size >= 6 && matchBytes(buf, gifHeadKey)) { + return opener()?.use { parseGif(it, pixelSizeMax, debug) } + } + + return null + } + } + + private var header: ApngImageHeader? = null + private var animationControl: ApngAnimationControl? = null + + // width,height (after resized) + var width: Int = 1 + private set + + var height: Int = 1 + private set + + class Frame( + val bitmap: Bitmap, + val timeStart: Long, + val timeWidth: Long, + ) + + var frames: ArrayList? = null + + @Suppress("MemberVisibilityCanBePrivate") + val numFrames: Int + get() = frames?.size ?: 1 + + @Suppress("unused") + val hasMultipleFrame: Boolean + get() = numFrames > 1 + + private var timeTotal = 0L + + private lateinit var canvas: Canvas + + private var canvasBitmap: Bitmap? = null + + // 再生速度の調整 + private var durationScale = 1f + + // APNGじゃなかった場合に使われる + private var defaultImage: Bitmap? = null + set(value) { + field = value + if (value != null) { + width = value.width + height = value.height + } + } + + constructor(bitmap: Bitmap) : this() { + defaultImage = bitmap + } + + private fun onParseComplete() { + canvasBitmap?.recycle() + canvasBitmap = null + + val frames = this.frames + if (frames != null) { + if (frames.size > 1) { + defaultImage?.recycle() + defaultImage = null + } else if (frames.size == 1) { + defaultImage?.recycle() + defaultImage = frames.first().bitmap + frames.clear() + } + } + } + + fun dispose() { + canvasBitmap?.recycle() + defaultImage?.recycle() + frames?.forEach { it.bitmap.recycle() } + } + + class FindFrameResult { + var bitmap: Bitmap? = null // may null + var delay: Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE + } + + // シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する + @Suppress("unused") + fun findFrame(result: FindFrameResult, t: Long) { + + if (defaultImage != null) { + result.bitmap = defaultImage + result.delay = Long.MAX_VALUE + return + } + + val animationControl = this.animationControl + val frames = this.frames + + if (animationControl == null || frames == null || frames.isEmpty()) { + // ここは通らないはず… + result.bitmap = null + result.delay = Long.MAX_VALUE + return + } + + val frameCount = frames.size + + val isFinite = animationControl.isFinite + val repeatSequenceCount = if (isFinite) animationControl.numPlays else 1 + val endWait = if (isFinite) DELAY_AFTER_END else 0L + val timeTotalLoop = max(1, timeTotal * repeatSequenceCount + endWait) + + val tf = (max(0, t) / durationScale).toLong() + + // 全体の繰り返し時刻で余りを計算 + val tl = tf % timeTotalLoop + + if (tl >= timeTotalLoop - endWait) { + // 終端で待機状態 + result.bitmap = frames[frameCount - 1].bitmap + result.delay = (0.5f + (timeTotalLoop - tl) * durationScale).toLong() + return + } + + // 1ループの繰り返し時刻で余りを計算 + val tt = tl % timeTotal + + // フレームリストを時刻で二分探索 + var s = 0 + var e = frameCount + while (e - s > 1) { + val mid = s + e shr 1 + val frame = frames[mid] + // log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.timeStart,frame.timeStart+frame.timeWidth ); + if (tt < frame.timeStart) { + e = mid + } else if (tt >= frame.timeStart + frame.timeWidth) { + s = mid + 1 + } else { + s = mid + break + } + } + s = if (s < 0) 0 else if (s >= frameCount - 1) frameCount - 1 else s + val frame = frames[s] + val delay = frame.timeStart + frame.timeWidth - tt + result.bitmap = frames[s].bitmap + result.delay = (0.5f + durationScale * max(0f, delay.toFloat())).toLong() + + // log.d("findFrame tf=%d,tl=%d/%d,tt=%d/%d,s=%d,w=%d,delay=%d",tf,tl,loop_total,tt,timeTotal,s,frame.timeWidth,result.delay); + } + + ///////////////////////////////////////////////////// + // implements ApngDecoderCallback + + override fun onApngWarning(message: String) { + Log.w(TAG, message) + } + + override fun onApngDebug(message: String) { + Log.d(TAG, message) + } + + override fun canApngDebug(): Boolean = debug + + override fun onHeader(apng: Apng, header: ApngImageHeader) { + this.header = header + } + + override fun onAnimationInfo( + apng: Apng, + header: ApngImageHeader, + animationControl: ApngAnimationControl, + ) { + if (debug) { + Log.d(TAG, "onAnimationInfo") + } + this.animationControl = animationControl + this.frames = ArrayList(animationControl.numFrames) + + val canvasBitmap = createBlankBitmap(header.width, header.height) + this.canvasBitmap = canvasBitmap + this.canvas = Canvas(canvasBitmap) + } + + override fun onDefaultImage(apng: Apng, bitmap: ApngBitmap) { + if (debug) { + Log.d(TAG, "onDefaultImage") + } + defaultImage?.recycle() + defaultImage = toAndroidBitmap(bitmap, pixelSizeMax) + } + + override fun onAnimationFrame( + apng: Apng, + frameControl: ApngFrameControl, + frameBitmap: ApngBitmap, + ) { + if (debug) { + Log.d( + TAG, + "onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}" + ) + } + val frames = this.frames ?: return + val canvasBitmap = this.canvasBitmap ?: return + + val disposeOp = when { + + // If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND. + frameControl.disposeOp == DisposeOp.Previous && frames.isEmpty() -> DisposeOp.Background + + else -> frameControl.disposeOp + } + + val previous: Bitmap? = when (disposeOp) { + DisposeOp.Previous -> Bitmap.createBitmap( + canvasBitmap, + frameControl.xOffset, + frameControl.yOffset, + frameControl.width, + frameControl.height + ) + else -> null + } + + try { + + val frameBitmapAndroid = toAndroidBitmap(frameBitmap) + + try { + + canvas.drawBitmap( + frameBitmapAndroid, + frameControl.xOffset.toFloat(), + frameControl.yOffset.toFloat(), + when (frameControl.blendOp) { + // all color components of the frame, including alpha, + // overwrite the current contents of the frame's output buffer region. + BlendOp.Source -> sPaintDontBlend + // the frame should be composited onto the output buffer based on its alpha, + // using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2]. + BlendOp.Over -> null + } + ) + + val frame = Frame( + bitmap = scaleBitmap(pixelSizeMax, canvasBitmap, recycleSrc = false), + timeStart = timeTotal, + timeWidth = max(1L, frameControl.delayMilliseconds) + ) + frames.add(frame) + timeTotal += frame.timeWidth + + when (disposeOp) { + + // no disposal is done on this frame before rendering the next; + // the contents of the output buffer are left as is. + DisposeOp.None -> { + } + + // the frame's region of the output buffer is + // to be cleared to fully transparent black + // before rendering the next frame. + DisposeOp.Background -> { + val rect = Rect() + rect.left = frameControl.xOffset + rect.top = frameControl.yOffset + rect.right = frameControl.xOffset + frameControl.width + rect.bottom = frameControl.yOffset + frameControl.height + canvas.drawRect(rect, sPaintClear) + // canvas.drawColor(0, PorterDuff.Mode.CLEAR) + } + + // the frame's region of the output buffer is + // to be reverted to the previous contents + // before rendering the next frame. + DisposeOp.Previous -> if (previous != null) { + canvas.drawBitmap( + previous, + frameControl.xOffset.toFloat(), + frameControl.yOffset.toFloat(), + sPaintDontBlend + ) + } + } + } finally { + frameBitmapAndroid.recycle() + } + } finally { + previous?.recycle() + } + } + + /////////////////////////////////////////////////////////////////////// + // Gif support + + override fun onGifWarning(message: String) { + Log.w(TAG, message) + } + + override fun onGifDebug(message: String) { + Log.d(TAG, message) + } + + override fun canGifDebug(): Boolean = debug + + override fun onGifHeader(header: ApngImageHeader) { + this.header = header + } + + override fun onGifAnimationInfo( + header: ApngImageHeader, + animationControl: ApngAnimationControl, + ) { + if (debug) { + Log.d(TAG, "onAnimationInfo") + } + this.animationControl = animationControl + this.frames = ArrayList(animationControl.numFrames) + val canvasBitmap = createBlankBitmap(header.width, header.height) + this.canvasBitmap = canvasBitmap + this.canvas = Canvas(canvasBitmap) + } + + override fun onGifAnimationFrame( + frameControl: ApngFrameControl, + frameBitmap: ApngBitmap, + ) { + if (debug) { + Log.d( + TAG, + "onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}" + ) + } + val frames = this.frames ?: return + + if (frames.isEmpty()) { + defaultImage?.recycle() + defaultImage = toAndroidBitmap(frameBitmap, pixelSizeMax) + // ここでwidth,heightがセットされる + } + + val frameBitmapAndroid = toAndroidBitmap(frameBitmap) + try { + val frame = Frame( + bitmap = scaleBitmap(pixelSizeMax, frameBitmapAndroid, recycleSrc = false), + timeStart = timeTotal, + timeWidth = max(1L, frameControl.delayMilliseconds) + ) + frames.add(frame) + timeTotal += frame.timeWidth + } finally { + frameBitmapAndroid.recycle() + } + } } diff --git a/app/build.gradle b/app/build.gradle index 775a33a0..e9f351ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -135,6 +135,7 @@ dependencies { implementation project(':apng_android') implementation fileTree(include: ['*.aar'], dir: 'src/main/libs') + // App1 とサーバ情報カラムで使う api "org.conscrypt:conscrypt-android:2.5.2" @@ -143,19 +144,48 @@ dependencies { kapt "com.github.bumptech.glide:compiler:$glideVersion" detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detekt_version") + + testImplementation(project(":base")) + androidTestImplementation(project(":base")) + + testApi "androidx.arch.core:core-testing:$arch_version" + testApi "junit:junit:$junit_version" + testApi "org.jetbrains.kotlin:kotlin-test" + testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version" + + androidTestApi "androidx.test.espresso:espresso-core:3.5.1" + androidTestApi "androidx.test.ext:junit-ktx:1.1.5" + androidTestApi "androidx.test.ext:junit:1.1.5" + androidTestApi "androidx.test.ext:truth:1.5.0" + androidTestApi "androidx.test:core-ktx:1.5.0" + androidTestApi "androidx.test:core:$androidx_test_version" + androidTestApi "androidx.test:runner:1.5.2" + androidTestApi "org.jetbrains.kotlin:kotlin-test" + androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version" + + // To use android test orchestrator + androidTestUtil "androidx.test:orchestrator:1.4.2" + + testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") { + exclude group: "com.squareup.okio", module: "okio" + exclude group: "com.squareup.okhttp3", module: "okhttp" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8" + } + androidTestApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") { + exclude group: "com.squareup.okio", module: "okio" + exclude group: "com.squareup.okhttp3", module: "okhttp" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-common" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jdk8" + } } repositories { mavenCentral() } -// detekt -def projectSource = file(projectDir) -def configFile = files("$rootDir/config/detekt/config.yml") -def baselineFile = file("$rootDir/config/detekt/baseline.xml") -def kotlinFiles = "**/*.kt" -def resourceFiles = "**/resources/**" -def buildFiles = "**/build/**" tasks.register("detektAll", Detekt) { description = "Custom DETEKT build for all modules" @@ -173,12 +203,30 @@ tasks.register("detektAll", Detekt) { // preconfigure defaults buildUponDefaultConfig = true - setSource(projectSource) + def configFile = files("$rootDir/config/detekt/config.yml") config.setFrom(configFile) + + def baselineFile = file("$rootDir/config/detekt/baseline.xml") if (baselineFile.isFile()) { baseline.set(baselineFile) } - include(kotlinFiles) + + source = files( + "$rootDir/apng/src", + "$rootDir/apng_android/src", + "$rootDir/app/src", + "$rootDir/base/src", + "$rootDir/colorpicker/src", + "$rootDir/emoji/src", + "$rootDir/icon_material_symbols/src", + "$rootDir/sample_apng/src", + ) + +// def kotlinFiles = "**/*.kt" +// include(kotlinFiles) + + def resourceFiles = "**/resources/**" + def buildFiles = "**/build/**" exclude(resourceFiles, buildFiles) reports { html.enabled = true diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt index 2116bfd5..f3ef6c0a 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/ExampleInstrumentedTest.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt index 5f7ded69..cd31ab9b 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/JsonArrayForEach.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter -import androidx.test.runner.AndroidJUnit4 -import jp.juggler.util.jsonArray +import androidx.test.ext.junit.runners.AndroidJUnit4 +import jp.juggler.util.data.* import org.jetbrains.anko.collections.forEachReversedByIndex import org.jetbrains.anko.collections.forEachReversedWithIndex import org.junit.Assert.assertEquals @@ -19,7 +19,7 @@ class JsonArrayForEach { @Test @Throws(Exception::class) fun test() { - val array = jsonArray { + val array = buildJsonArray { add("a") add("b") add(null) @@ -59,4 +59,4 @@ class JsonArrayForEach { assertEquals(count, 24) } -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt index c50d909c..916bdb2d 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter import android.graphics.Color -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.jrummyapps.android.colorpicker.parseColorString import org.junit.Assert.assertEquals import org.junit.Test diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt index 2793de90..08687137 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt @@ -1,6 +1,6 @@ package jp.juggler.subwaytooter -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import jp.juggler.subwaytooter.api.entity.TootAccount import jp.juggler.util.data.asciiPatternString import org.junit.Assert.assertEquals diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTimeParser.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTimeParser.kt index ad359a3b..ddf6e88c 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTimeParser.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTimeParser.kt @@ -2,7 +2,7 @@ package jp.juggler.subwaytooter import androidx.test.platform.app.InstrumentationRegistry import jp.juggler.subwaytooter.api.entity.TootStatus -import jp.juggler.util.asciiRegex +import jp.juggler.util.data.asciiRegex import org.junit.Assert.assertEquals import org.junit.Test diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt index 4f918d32..a3316d68 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt @@ -1,55 +1,48 @@ package jp.juggler.subwaytooter +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 import jp.juggler.subwaytooter.api.TootApiCallback import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.table.SavedAccount +import jp.juggler.subwaytooter.testutil.MainDispatcherRule +import jp.juggler.subwaytooter.testutil.MockInterceptor import jp.juggler.subwaytooter.util.SimpleHttpClientImpl -import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.log.LogCategory -import jp.juggler.util.network.MySslSocketFactory -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import okhttp3.ConnectionSpec -import okhttp3.OkHttpClient +import kotlinx.coroutines.test.runTest +import okhttp3.* import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.util.* -import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class TestTootInstance { companion object { private val log = LogCategory("TestTootInstance") + } -// val cookieJar = JavaNetCookieJar(CookieManager().apply { -// setCookiePolicy(CookiePolicy.ACCEPT_ALL) -// CookieHandler.setDefault(this) -// }) + // テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる + // プロパティは記述順に初期化されることに注意 + @get:Rule + val mainDispatcherRule = MainDispatcherRule() - private val okHttp = OkHttpClient.Builder() - .connectTimeout(60.toLong(), TimeUnit.SECONDS) - .readTimeout(60.toLong(), TimeUnit.SECONDS) - .writeTimeout(60.toLong(), TimeUnit.SECONDS) - .pingInterval(10, TimeUnit.SECONDS) - .connectionSpecs( - Collections.singletonList( - ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .allEnabledCipherSuites() - .allEnabledTlsVersions() - .build() - ) - ) - .sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager) - .build() + private val client by lazy { + val mockInterceptor = MockInterceptor( + // テストアプリのコンテキスト + context = InstrumentationRegistry.getInstrumentation().context!!, + // テストアプリ中のリソースID + rawId = jp.juggler.subwaytooter.test.R.raw.test_toot_instance_mock, + ) - private val dummyClientCallback = object : TootApiCallback { + val okHttp = OkHttpClient.Builder().addInterceptor(mockInterceptor).build() + + val dummyClientCallback = object : TootApiCallback { override suspend fun isApiCancelled() = false override suspend fun publishApiProgress(s: String) { @@ -61,9 +54,8 @@ class TestTootInstance { } } - private val appContext = InstrumentationRegistry.getInstrumentation().targetContext!! - - val client = TootApiClient( + val appContext = InstrumentationRegistry.getInstrumentation().targetContext!! + TootApiClient( context = appContext, httpClient = SimpleHttpClientImpl(appContext, okHttp), callback = dummyClientCallback @@ -76,34 +68,26 @@ class TestTootInstance { */ @Test - fun testWithoutAccount() { - runBlocking { - withContext(AppDispatchers.io) { - suspend fun a(host: Host) { - val (ti, ri) = TootInstance.getEx(client, hostArg = host) - assertNotNull(ti) - assertNull(ri?.error) - ti!!.run { log.d("$instanceType $uri $version") } - } - a(Host.parse("mastodon.juggler.jp")) - a(Host.parse("misskey.io")) - } + fun instanceByHostname() = runTest { + suspend fun a(host: Host) { + val (ti, ri) = TootInstance.getEx(client, hostArg = host) + assertNull("no error", ri?.error) + assertNotNull("instance information", ti) + ti!!.run { log.d("$instanceType $uri $version") } } + a(Host.parse("mastodon.juggler.jp")) + a(Host.parse("misskey.io")) } @Test - fun testWithAccount() { - runBlocking { - withContext(AppDispatchers.io) { - suspend fun a(account: SavedAccount) { - val (ti, ri) = TootInstance.getEx(client, account = account) - assertNull(ri?.error) - assertNotNull(ti) - ti!!.run { log.d("${account.acct} $instanceType $uri $version") } - } - a(SavedAccount(45, "tateisu@mastodon.juggler.jp")) - a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12)) - } + fun testWithAccount() = runTest { + suspend fun a(account: SavedAccount) { + val (ti, ri) = TootInstance.getEx(client, account = account) + assertNull(ri?.error) + assertNotNull(ti) + ti!!.run { log.d("${account.acct} $instanceType $uri $version") } } + a(SavedAccount(45, "tateisu@mastodon.juggler.jp")) + a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12)) } } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt index a7dacbad..a59994f5 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/WordTrieTreeTest.kt @@ -1,6 +1,6 @@ package jp.juggler.subwaytooter -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import jp.juggler.util.data.CharacterGroup import jp.juggler.util.data.WordTrieTree import org.junit.Assert.assertEquals diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt index fdf0997f..9eed9098 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter.api import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.data.* diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt index d69a3a69..010d1f6e 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestTootApiClient.kt @@ -2,18 +2,19 @@ package jp.juggler.subwaytooter.api +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 import jp.juggler.subwaytooter.api.entity.Host import jp.juggler.subwaytooter.api.entity.TootInstance import jp.juggler.subwaytooter.table.SavedAccount +import jp.juggler.subwaytooter.testutil.MainDispatcherRule import jp.juggler.subwaytooter.util.SimpleHttpClient import jp.juggler.util.data.JsonObject import jp.juggler.util.data.buildJsonArray import jp.juggler.util.data.buildJsonObject import jp.juggler.util.log.LogCategory import jp.juggler.util.network.MEDIA_TYPE_JSON -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.ResponseBody.Companion.toResponseBody @@ -21,6 +22,7 @@ import okio.Buffer import okio.BufferedSource import okio.ByteString import org.junit.Assert.* +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.util.concurrent.atomic.AtomicReference @@ -28,6 +30,12 @@ import java.util.concurrent.atomic.AtomicReference @Suppress("MemberVisibilityCanPrivate") @RunWith(AndroidJUnit4::class) class TestTootApiClient { + + // テスト毎に書くと複数テストで衝突するので、MainDispatcherRuleに任せる + // プロパティは記述順に初期化されることに注意 + @get:Rule + val mainDispatcherRule = MainDispatcherRule() + companion object { private val log = LogCategory("TestTootApiClient") } @@ -229,14 +237,21 @@ class TestTootApiClient { .build() } - else -> - Response.Builder() - .request(request) - .protocol(Protocol.HTTP_1_1) - .code(200) - .message("status-message") - .body(request.url.toString().toResponseBody(mediaTypeTextPlain)) - .build() + "/api/meta" -> Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(404) + .message("not found") + .body("""{"error":"404 not found"}""".toResponseBody(MEDIA_TYPE_JSON)) + .build() + + else -> Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(200) + .message("status-message") + .body(request.url.toString().toResponseBody(mediaTypeTextPlain)) + .build() } }, @@ -519,7 +534,7 @@ class TestTootApiClient { @Test fun testIsApiCancelled() { - runBlocking { + runTest { var flag = 0 var progressString: String? = null var progressValue: Int? = null @@ -559,7 +574,7 @@ class TestTootApiClient { @Test fun testSendRequest() { - runBlocking { + runTest { val callback = ProgressRecordTootApiCallback() @@ -637,7 +652,7 @@ class TestTootApiClient { @Test fun testReadBodyString() { - runBlocking { + runTest { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( appContext, @@ -745,7 +760,7 @@ class TestTootApiClient { @Test fun testParseString() { - runBlocking { + runTest { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( @@ -865,7 +880,7 @@ class TestTootApiClient { @Test fun testParseJson() { - runBlocking { + runTest { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( appContext, @@ -1046,7 +1061,7 @@ class TestTootApiClient { @Test fun testRegisterClient() { - runBlocking { + runTest { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( appContext, @@ -1064,7 +1079,7 @@ class TestTootApiClient { assertEquals(null, result?.error) var jsonObject = result?.jsonObject assertNotNull(jsonObject) - if (jsonObject == null) return@runBlocking + if (jsonObject == null) return@runTest val clientInfo = jsonObject // clientCredential の作成 @@ -1073,7 +1088,7 @@ class TestTootApiClient { assertEquals(null, result?.error) val clientCredential = result?.string assertNotNull(clientCredential) - if (clientCredential == null) return@runBlocking + if (clientCredential == null) return@runTest clientInfo[TootApiClient.KEY_CLIENT_CREDENTIAL] = clientCredential // clientCredential の検証 @@ -1082,7 +1097,7 @@ class TestTootApiClient { assertEquals(null, result?.error) jsonObject = result?.jsonObject assertNotNull(jsonObject) // 中味は別に見てない。jsonObjectなら良いらしい - if (jsonObject == null) return@runBlocking + if (jsonObject == null) return@runTest var url: String? @@ -1095,7 +1110,7 @@ class TestTootApiClient { result = client.authentication1(clientName) url = result?.string assertNotNull(url) - if (url == null) return@runBlocking + if (url == null) return@runTest println(url) // ブラウザからコールバックで受け取ったcodeを処理する @@ -1103,29 +1118,29 @@ class TestTootApiClient { result = client.authentication2Mastodon(clientName, "DUMMY_CODE", refToken) jsonObject = result?.jsonObject assertNotNull(jsonObject) - if (jsonObject == null) return@runBlocking + if (jsonObject == null) return@runTest println(jsonObject.toString()) // 認証できたならアクセストークンがある val tokenInfo = result?.tokenInfo assertNotNull(tokenInfo) - if (tokenInfo == null) return@runBlocking + if (tokenInfo == null) return@runTest val accessToken = tokenInfo.string("access_token") assertNotNull(accessToken) - if (accessToken == null) return@runBlocking + if (accessToken == null) return@runTest // アカウント手動入力でログインする場合はこの関数を直接呼び出す result = client.getUserCredential(accessToken, tokenInfo) jsonObject = result?.jsonObject assertNotNull(jsonObject) - if (jsonObject == null) return@runBlocking + if (jsonObject == null) return@runTest println(jsonObject.toString()) } } @Test fun testGetInstanceInformation() { - runBlocking { + runTest { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( appContext, @@ -1135,8 +1150,8 @@ class TestTootApiClient { val instance = Host.parse("unit-test") client.apiHost = instance val (instanceInfo, instanceResult) = TootInstance.get(client) - assertNotNull(instanceInfo) - assertNotNull(instanceResult) + assertNull("no error", instanceResult?.error) + assertNotNull("instance info", instanceInfo) val json = instanceResult?.jsonObject if (json != null) println(json.toString()) } @@ -1144,7 +1159,7 @@ class TestTootApiClient { @Test fun testGetHttp() { - runBlocking { + runTest { val callback = ProgressRecordTootApiCallback() val client = TootApiClient( appContext, @@ -1160,7 +1175,7 @@ class TestTootApiClient { @Test fun testRequest() { - runBlocking { + runTest { val tokenInfo = JsonObject() tokenInfo["access_token"] = "DUMMY_ACCESS_TOKEN" @@ -1188,7 +1203,7 @@ class TestTootApiClient { @Test fun testWebSocket() { - runBlocking { + runTest { val tokenInfo = buildJsonObject { put("access_token", "DUMMY_ACCESS_TOKEN") } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt index 3c20a59b..9e07ac38 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter.api.entity import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.data.* diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt index 44cb0c46..11a9fb08 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestTootAccount.kt @@ -1,6 +1,6 @@ package jp.juggler.subwaytooter.api.entity -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import jp.juggler.subwaytooter.util.LinkHelper import org.junit.Assert.assertEquals import org.junit.Test diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt index 9f8281bb..8e674862 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestArrayListSizeBug.kt @@ -1,6 +1,6 @@ package jp.juggler.subwaytooter.database -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt index 422bf5e6..2257809a 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt @@ -4,7 +4,7 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import jp.juggler.subwaytooter.global.DB_VERSION import jp.juggler.subwaytooter.global.TABLE_LIST import org.junit.Assert.assertNull diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MainDispatcherRule.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MainDispatcherRule.kt new file mode 100644 index 00000000..697e0bee --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MainDispatcherRule.kt @@ -0,0 +1,32 @@ +package jp.juggler.subwaytooter.testutil + +import jp.juggler.util.coroutine.AppDispatchers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * Dispatchers.Main のテスト中の置き換えを複数テストで衝突しないようにルール化する + * https://developer.android.com/kotlin/coroutines/test?hl=ja + */ +@OptIn(ExperimentalCoroutinesApi::class) +class MainDispatcherRule( + /** + * UnconfinedTestDispatcher か StandardTestDispatcher のどちらかを指定する + */ + val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) : TestWatcher() { + override fun starting(description: Description) { + Dispatchers.setMain(testDispatcher) + AppDispatchers.setTest(testDispatcher) + } + + override fun finished(description: Description) { + Dispatchers.resetMain() + } +} diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MockInterceptor.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MockInterceptor.kt new file mode 100644 index 00000000..117a596b --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/testutil/MockInterceptor.kt @@ -0,0 +1,57 @@ +package jp.juggler.subwaytooter.testutil + +import android.content.Context +import androidx.annotation.RawRes +import jp.juggler.util.data.JsonObject +import jp.juggler.util.data.decodeJsonObject +import jp.juggler.util.data.decodeUTF8 +import jp.juggler.util.data.encodeUTF8 +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Protocol +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.IOException + +class MockInterceptor(private val mockJsonMap: JsonObject) : Interceptor { + constructor(context: Context, @RawRes rawId: Int) : this( + context.resources.openRawResource(rawId) + .use { it.readBytes() } + .decodeUTF8() + .decodeJsonObject() + ) + + override fun intercept(chain: Interceptor.Chain): Response { + val url = chain.request().url.toString() + return when (val resultValue = mockJsonMap[url]) { + null -> throw IOException("missing mockJson for $url") + is JsonObject -> Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_2) + .code(200) + .message("OK") + .body( + resultValue.toString().encodeUTF8() + .toResponseBody("application/json".toMediaType()) + ).build() + is Number -> Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_2) + .code(resultValue.toInt()) + .message("error $resultValue") + .body( + """{"error":$resultValue}""".encodeUTF8() + .toResponseBody("application/json".toMediaType()) + ).build() + else -> Response.Builder() + .request(chain.request()) + .protocol(Protocol.HTTP_2) + .code(500) + .message("unknonw result type: $resultValue") + .body( + """{"error":$resultValue}""".encodeUTF8() + .toResponseBody("application/json".toMediaType()) + ).build() + } + } +} diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt index fdb051d9..d6bdec21 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestBucketList.kt @@ -1,6 +1,6 @@ package jp.juggler.subwaytooter.util -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt index f698aca5..ad9a80ad 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/util/TestHtmlDecoder.kt @@ -1,9 +1,9 @@ package jp.juggler.subwaytooter.util import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 import jp.juggler.subwaytooter.api.entity.Host -import jp.juggler.util.neatSpaces +import jp.juggler.util.data.* import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -17,7 +17,7 @@ class TestHtmlDecoder { val start: Int, val end: Int, val flags: Int, - val text: String + val text: String, ) { override fun toString() = "[$start..$end) $flags ${span.javaClass.simpleName} $text" } diff --git a/app/src/androidTest/res/raw/test_toot_instance_mock.json b/app/src/androidTest/res/raw/test_toot_instance_mock.json new file mode 100644 index 00000000..00888dfa --- /dev/null +++ b/app/src/androidTest/res/raw/test_toot_instance_mock.json @@ -0,0 +1,226 @@ +{ + "https://mastodon.juggler.jp/api/v1/instance": { + "uri": "mastodon.juggler.jp", + "title": "juggler.jp Mastodon サービス", + "short_description": "日本語で楽しめるMastodonサーバを提供しています。", + "description": "日本語で楽しめるMastodonサーバを提供しています。\u003ca href=\"https://mastodon.juggler.jp/terms\"\u003e利用規約\u003c/a\u003eを読んでからサインアップしてください。", + "email": "tateisu+juggler@gmail.com", + "version": "4.0.2", + "urls": { + "streaming_api": "wss://mastodon.juggler.jp" + }, + "stats": { + "user_count": 1799, + "status_count": 725461, + "domain_count": 11626 + }, + "thumbnail": "https://m1j.zzz.ac/site_uploads/files/000/000/001/@1x/6f06b912bd963e28.png", + "languages": [ + "ja" + ], + "registrations": true, + "approval_required": false, + "invites_enabled": true, + "configuration": { + "accounts": { + "max_featured_tags": 10 + }, + "statuses": { + "max_characters": 500, + "max_media_attachments": 4, + "characters_reserved_per_url": 23 + }, + "media_attachments": { + "supported_mime_types": [ + "image/jpeg", + "image/png", + "image/gif", + "image/heic", + "image/heif", + "image/webp", + "image/avif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/vnd.wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf" + ], + "image_size_limit": 10485760, + "image_matrix_limit": 16777216, + "video_size_limit": 41943040, + "video_frame_rate_limit": 60, + "video_matrix_limit": 2304000 + }, + "polls": { + "max_options": 4, + "max_characters_per_option": 50, + "min_expiration": 300, + "max_expiration": 2629746 + } + }, + "contact_account": { + "id": "33698", + "username": "juggler", + "acct": "juggler", + "display_name": "mastodon.juggler.jp運営情報", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2017-09-10T00:00:00.000Z", + "note": "\u003cp\u003emastodon.juggler.jp の運営情報アカウントです。\u003cbr /\u003eメンテナンス中の進捗は \u003ca href=\"https://fedibird.com/@tateisu\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003efedibird.com/@tateisu\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e や \u003ca href=\"https://twitter.com/tateisu\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003etwitter.com/tateisu\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e で報告するかもしれません。\u003cbr /\u003e\u003ca href=\"https://mastodon.juggler.jp/tags/%E9%81%8B%E5%96%B6%E6%83%85%E5%A0%B1\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003e運営情報\u003c/span\u003e\u003c/a\u003e \u003ca href=\"https://mastodon.juggler.jp/tags/Mastodon\" class=\"mention hashtag\" rel=\"tag\"\u003e#\u003cspan\u003eMastodon\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e", + "url": "https://mastodon.juggler.jp/@juggler", + "avatar": "https://m1j.zzz.ac/accounts/avatars/000/033/698/original/a2db8e1efe4dec82.jpg", + "avatar_static": "https://m1j.zzz.ac/accounts/avatars/000/033/698/original/a2db8e1efe4dec82.jpg", + "header": "https://m1j.zzz.ac/accounts/headers/000/033/698/original/6396b4dcb2f3b580.png", + "header_static": "https://m1j.zzz.ac/accounts/headers/000/033/698/original/6396b4dcb2f3b580.png", + "followers_count": 605, + "following_count": 0, + "statuses_count": 215, + "last_status_at": "2023-01-07", + "noindex": false, + "emojis": [], + "fields": [ + { + "name": "Fantia", + "value": "\u003ca href=\"https://fantia.jp/fanclubs/8239\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003efantia.jp/fanclubs/8239\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e", + "verified_at": null + }, + { + "name": "Amazon干し芋", + "value": "\u003ca href=\"http://amzn.asia/axmNivM\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttp://\u003c/span\u003e\u003cspan class=\"\"\u003eamzn.asia/axmNivM\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e", + "verified_at": null + } + ] + }, + "rules": [ + { + "id": "1", + "text": "日本の法律や判例で禁止されてることはダメです。ただし親告罪は関係者からの通報がない場合は黙認する場合があります。" + }, + { + "id": "3", + "text": "(ネット上のアバターを含む)実在する人格へのなりすましを禁止します。" + }, + { + "id": "4", + "text": "サーバ運営を妨害する行為はダメです。これはアカウント大量作成、無差別フォロー、無差別ブースト、無差別ファボを含みます。\r\n" + }, + { + "id": "7", + "text": "(GDPR免責) あなたがデータを削除したい場合、我々は可能な範囲で対応します。しかし連合先のサーバ上のデータの削除について完全な責任を持つことは非常に困難です。これはEUのGDPRに厳密には適合しません。ユーザはこれに同意した上でアカウントを作成したものとみなします。" + }, + { + "id": "8", + "text": "公開TLからのサイレンス対象。セールス、政治活動、宗教勧誘、SEO目的の投稿が多い人。公開TLに自動投稿するボット。オカルトやスピリチュアルや愚痴ばかり書いて住民からの呼びかけにはほとんど答えない人。アバターアイコンや名前が食事時に見たくない感じの人。その他管理者が公開TLに不適切であると判断したアカウントはサイレンス、凍結、停止などの処置を行う場合があります。" + }, + { + "id": "10", + "text": "年齢が14歳未満のユーザはこのサーバを利用できません。" + } + ] + }, + "https://misskey.io/api/v1/instance": 404, + "https://misskey.io/api/meta": { + "driveCapacityPerLocalUserMb": 0, + "driveCapacityPerRemoteUserMb": 0, + "cacheRemoteFiles": true, + "emailRequiredForSignup": true, + "enableHcaptcha": true, + "hcaptchaSiteKey": "string", + "enableRecaptcha": true, + "recaptchaSiteKey": "string", + "swPublickey": "string", + "mascotImageUrl": "/assets/ai.png", + "bannerUrl": "string", + "errorImageUrl": "https://xn--931a.moe/aiart/yubitun.png", + "iconUrl": "string", + "maxNoteTextLength": 0, + "emojis": [ + { + "id": "string", + "aliases": [ + "string" + ], + "category": "string", + "host": "string", + "url": "string" + } + ], + "ads": [ + { + "place": "string", + "url": "string", + "imageUrl": "string" + } + ], + "enableEmail": true, + "enableTwitterIntegration": true, + "enableGithubIntegration": true, + "enableDiscordIntegration": true, + "enableServiceWorker": true, + "translatorAvailable": true, + "proxyAccountName": "string", + "userStarForReactionFallback": true, + "pinnedUsers": [ + "string" + ], + "hiddenTags": [ + "string" + ], + "blockedHosts": [ + "string" + ], + "hcaptchaSecretKey": "string", + "recaptchaSecretKey": "string", + "sensitiveMediaDetection": "string", + "sensitiveMediaDetectionSensitivity": "string", + "setSensitiveFlagAutomatically": true, + "enableSensitiveMediaDetectionForVideos": true, + "proxyAccountId": "string", + "twitterConsumerKey": "string", + "twitterConsumerSecret": "string", + "githubClientId": "string", + "githubClientSecret": "string", + "discordClientId": "string", + "discordClientSecret": "string", + "summaryProxy": "string", + "email": "string", + "smtpSecure": true, + "smtpHost": "string", + "smtpPort": "string", + "smtpUser": "string", + "smtpPass": "string", + "swPrivateKey": "string", + "useObjectStorage": true, + "objectStorageBaseUrl": "string", + "objectStorageBucket": "string", + "objectStoragePrefix": "string", + "objectStorageEndpoint": "string", + "objectStorageRegion": "string", + "objectStoragePort": 0, + "objectStorageAccessKey": "string", + "objectStorageSecretKey": "string", + "objectStorageUseSSL": true, + "objectStorageUseProxy": true, + "objectStorageSetPublicRead": true, + "enableIpLogging": true, + "enableActiveEmailValidation": true + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt index 78214553..f717d1f4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt @@ -23,7 +23,6 @@ import jp.juggler.util.log.LogCategory import jp.juggler.util.log.showToast import jp.juggler.util.network.toFormRequestBody import jp.juggler.util.network.toPost -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext private val log = LogCategory("Action_Tag") diff --git a/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt b/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt index 92fe1879..edb7fafc 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt @@ -43,7 +43,6 @@ import jp.juggler.util.log.showToast import jp.juggler.util.ui.activity import jp.juggler.util.ui.attrColor import jp.juggler.util.ui.createColoredDrawable -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jetbrains.anko.backgroundColor import java.lang.ref.WeakReference diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt index 2e74a0a3..130640d7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt @@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.LinkHelper import jp.juggler.subwaytooter.util.VersionString +import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe import jp.juggler.util.coroutine.launchDefault import jp.juggler.util.data.* import jp.juggler.util.log.LogCategory @@ -15,7 +16,6 @@ import jp.juggler.util.log.withCaption import jp.juggler.util.network.toPostRequestBuilder import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay -import kotlinx.coroutines.withTimeout import okhttp3.Request import java.util.regex.Pattern import kotlin.coroutines.Continuation @@ -431,7 +431,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { try { val req = requestQueue.receive() val r = try { - withTimeout(30000L) { + withTimeoutSafe(30000L) { handleRequest(req) } } catch (ex: Throwable) { @@ -481,7 +481,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { val cacheEntry = (hostArg ?: account?.apiHost ?: client.apiHost)?.getCacheEntry() ?: return tiError("missing host.") - return withTimeout(30000L) { + return withTimeoutSafe(30000L) { suspendCoroutine { cont -> QueuedRequest(cont, allowPixelfed) { cached -> diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/CheckerWakeLocks.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/CheckerWakeLocks.kt index a729d1f2..38b60408 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/CheckerWakeLocks.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/CheckerWakeLocks.kt @@ -8,11 +8,11 @@ import android.net.wifi.WifiManager import android.os.Build import android.os.PowerManager import jp.juggler.subwaytooter.App1 +import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe import jp.juggler.util.log.* import jp.juggler.util.systemService import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay -import kotlinx.coroutines.withTimeout class CheckerWakeLocks(contextArg: Context) { companion object { @@ -118,7 +118,7 @@ class CheckerWakeLocks(contextArg: Context) { suspend fun checkConnection() { var connectionState: String? = null try { - withTimeout(10000L) { + withTimeoutSafe(10000L) { while (true) { connectionState = appState.networkTracker.connectionState ?: break // null if connected diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt index bb68f8c5..e7cc6249 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt @@ -29,7 +29,6 @@ import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPut import jp.juggler.util.network.toPutRequestBuilder import jp.juggler.util.ui.* -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.delay diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt index 15de5575..0db4b865 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt @@ -21,7 +21,6 @@ import jp.juggler.util.network.MEDIA_TYPE_JSON import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.ui.* import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import okhttp3.Request diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/WorkerBase.kt b/app/src/main/java/jp/juggler/subwaytooter/util/WorkerBase.kt index b0ff7470..e9da2231 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/WorkerBase.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/WorkerBase.kt @@ -1,10 +1,10 @@ package jp.juggler.subwaytooter.util +import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe import jp.juggler.util.coroutine.launchDefault import kotlinx.coroutines.Job import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.withTimeout abstract class WorkerBase( private val waiter: Channel = Channel(capacity = Channel.CONFLATED), @@ -17,7 +17,7 @@ abstract class WorkerBase( abstract suspend fun run() suspend fun waitEx(ms: Long) = try { - withTimeout(ms) { waiter.receive() } + withTimeoutSafe(ms) { waiter.receive() } } catch (ignored: TimeoutCancellationException) { null } diff --git a/base/build.gradle b/base/build.gradle index a6292439..7028e1f3 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -111,11 +111,18 @@ dependencies { testApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version" androidTestApi "androidx.test.espresso:espresso-core:3.5.1" + androidTestApi "androidx.test.ext:junit-ktx:1.1.5" androidTestApi "androidx.test.ext:junit:1.1.5" + androidTestApi "androidx.test.ext:truth:1.5.0" + androidTestApi "androidx.test:core-ktx:1.5.0" androidTestApi "androidx.test:core:$androidx_test_version" + androidTestApi "androidx.test:runner:1.5.2" androidTestApi "org.jetbrains.kotlin:kotlin-test" androidTestApi "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinx_coroutines_version" + // To use android test orchestrator + androidTestUtil "androidx.test:orchestrator:1.4.2" + testApi("com.squareup.okhttp3:mockwebserver:$okhttpVersion") { exclude group: "com.squareup.okio", module: "okio" exclude group: "com.squareup.okhttp3", module: "okhttp" diff --git a/base/src/androidTest/java/jp/juggler/base/JugglerBaseTest.kt b/base/src/androidTest/java/jp/juggler/base/JugglerBaseTest.kt index a7637f25..41512011 100644 --- a/base/src/androidTest/java/jp/juggler/base/JugglerBaseTest.kt +++ b/base/src/androidTest/java/jp/juggler/base/JugglerBaseTest.kt @@ -1,16 +1,14 @@ package jp.juggler.base -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import jp.juggler.base.JugglerBase.Companion.jugglerBase import jp.juggler.base.JugglerBase.Companion.jugglerBaseNullable import jp.juggler.base.JugglerBase.Companion.prepareJugglerBase - +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -23,6 +21,6 @@ class JugglerBaseTest { assertNotNull("JubblerBase is initialized for a test.", jugglerBaseNullable) val appContext = InstrumentationRegistry.getInstrumentation().targetContext appContext.prepareJugglerBase - assertNotNull( "JubblerBase is initialized after prepare.",jugglerBase) + assertNotNull("JubblerBase is initialized after prepare.", jugglerBase) } } diff --git a/base/src/main/java/jp/juggler/util/coroutine/AppDispatchers.kt b/base/src/main/java/jp/juggler/util/coroutine/AppDispatchers.kt index ae3657f9..65cadbb1 100644 --- a/base/src/main/java/jp/juggler/util/coroutine/AppDispatchers.kt +++ b/base/src/main/java/jp/juggler/util/coroutine/AppDispatchers.kt @@ -1,7 +1,6 @@ package jp.juggler.util.coroutine -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* /** * Test時にdispatcherを差し替えられるようにする @@ -33,4 +32,15 @@ object AppDispatchers { default = testDispatcher io = testDispatcher } + + /** + * withTimeout はrunTest内部だと即座に例外を出すので、 + * テスト中はタイムアウトをチェックしないようにする + * https://stackoverflow.com/questions/70658926/how-to-use-kotlinx-coroutines-withtimeout-in-kotlinx-coroutines-test-runtest + */ + suspend fun withTimeoutSafe(timeMillis: Long, block: suspend CoroutineScope.() -> T) = + when (io) { + Dispatchers.IO -> withTimeout(timeMillis, block) + else -> coroutineScope { block() } + } } diff --git a/base/src/main/java/jp/juggler/util/coroutine/AsyncActivity.kt b/base/src/main/java/jp/juggler/util/coroutine/AsyncActivity.kt index a078c859..fb213ac6 100644 --- a/base/src/main/java/jp/juggler/util/coroutine/AsyncActivity.kt +++ b/base/src/main/java/jp/juggler/util/coroutine/AsyncActivity.kt @@ -3,7 +3,6 @@ package jp.juggler.util.coroutine import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlin.coroutines.CoroutineContext diff --git a/base/src/main/java/jp/juggler/util/data/CollectionUtils.kt b/base/src/main/java/jp/juggler/util/data/CollectionUtils.kt index 4662cc69..e5e108ab 100644 --- a/base/src/main/java/jp/juggler/util/data/CollectionUtils.kt +++ b/base/src/main/java/jp/juggler/util/data/CollectionUtils.kt @@ -1,7 +1,5 @@ package jp.juggler.util.data -import java.util.LinkedHashMap - // same as x?.let{ dst.add(it) } fun T.addTo(dst: ArrayList) = dst.add(this) @@ -34,4 +32,3 @@ fun MutableCollection.removeFirst(check: (T) -> Boolean): T? { } return null } - diff --git a/base/src/main/java/jp/juggler/util/data/ColumnMeta.kt b/base/src/main/java/jp/juggler/util/data/ColumnMeta.kt index ca325872..e5358491 100644 --- a/base/src/main/java/jp/juggler/util/data/ColumnMeta.kt +++ b/base/src/main/java/jp/juggler/util/data/ColumnMeta.kt @@ -59,8 +59,8 @@ fun Cursor.getBlobOrNull(keyIdx: Int) = fun Cursor.getBlobOrNull(key: String) = getBlobOrNull(getColumnIndex(key)) -fun Cursor.columnIndexOrThrow(key:String)= - getColumnIndex(key).takeIf{it>=0L} ?: error("missing column $key") +fun Cursor.columnIndexOrThrow(key: String) = + getColumnIndex(key).takeIf { it >= 0L } ?: error("missing column $key") ///////////////////////////////////////////////////////////// diff --git a/base/src/main/java/jp/juggler/util/data/StorageUtils.kt b/base/src/main/java/jp/juggler/util/data/StorageUtils.kt index 4335e059..6b67834d 100644 --- a/base/src/main/java/jp/juggler/util/data/StorageUtils.kt +++ b/base/src/main/java/jp/juggler/util/data/StorageUtils.kt @@ -7,8 +7,6 @@ import android.net.Uri import android.provider.OpenableColumns import android.webkit.MimeTypeMap import androidx.annotation.RawRes -import jp.juggler.util.data.getStringOrNull -import jp.juggler.util.data.notEmpty import jp.juggler.util.log.LogCategory import okhttp3.internal.closeQuietly import java.io.InputStream diff --git a/base/src/main/java/jp/juggler/util/data/WordTrieTree.kt b/base/src/main/java/jp/juggler/util/data/WordTrieTree.kt index 221b0ce3..0441eb3c 100644 --- a/base/src/main/java/jp/juggler/util/data/WordTrieTree.kt +++ b/base/src/main/java/jp/juggler/util/data/WordTrieTree.kt @@ -69,7 +69,7 @@ class WordTrieTree { // 単語の追加 fun add( s: String, - tag:Any?=null, + tag: Any? = null, validator: (src: CharSequence, start: Int, end: Int) -> Boolean = EMPTY_VALIDATOR, ) { val t = CharacterGroup.Tokenizer().reset(s, 0, s.length) @@ -92,9 +92,9 @@ class WordTrieTree { } // タグを覚える - if(tag!=null){ + if (tag != null) { val tags = node.matchTags - ?: ArrayList().also{ node.matchTags = it} + ?: ArrayList().also { node.matchTags = it } tags.add(tag) } @@ -136,7 +136,7 @@ class WordTrieTree { if (matchWord != null && node.validator(t.text, start, t.offset)) { // マッチしたことを覚えておく - dst = Match(start, t.offset, matchWord ,node.matchTags) + dst = Match(start, t.offset, matchWord, node.matchTags) // ミュート用途の場合、ひとつでも単語にマッチすればより長い探索は必要ない if (allowShortMatch) break @@ -205,7 +205,7 @@ class WordTrieTree { } @OptIn(ExperimentalContracts::class) -fun WordTrieTree?.isNullOrEmpty() :Boolean { +fun WordTrieTree?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } diff --git a/base/src/main/java/jp/juggler/util/log/ToastUtils.kt b/base/src/main/java/jp/juggler/util/log/ToastUtils.kt index 02db3152..0d658c00 100644 --- a/base/src/main/java/jp/juggler/util/log/ToastUtils.kt +++ b/base/src/main/java/jp/juggler/util/log/ToastUtils.kt @@ -12,11 +12,11 @@ import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import jp.juggler.util.coroutine.AppDispatchers.withTimeoutSafe import jp.juggler.util.coroutine.runOnMainLooper import kotlinx.coroutines.CancellationException import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeout import me.drakeet.support.toast.ToastCompat import java.lang.ref.WeakReference import kotlin.coroutines.resume @@ -84,7 +84,7 @@ fun initializeToastUtils(app: Application) { */ suspend fun Animation.startAndAwait(duration: Long, v: View) = try { - withTimeout(duration + 333L) { + withTimeoutSafe(duration + 333L) { suspendCancellableCoroutine { cont -> v.clearAnimation() this@startAndAwait.duration = duration diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt index 437bb8bf..40564acc 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt @@ -37,7 +37,6 @@ internal class AlphaPatternDrawable(private val rectangleSize: Int) : Drawable() */ private var bitmap: Bitmap? = null - /** * This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on. * We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt index 4e9889ed..25e9aac2 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt @@ -28,7 +28,7 @@ internal class ColorPaletteAdapter( val colors: IntArray, var selectedPosition: Int, @ColorShape val colorShape: Int, - val listener: (Int)->Unit + val listener: (Int) -> Unit, ) : BaseAdapter() { override fun getCount(): Int = colors.size diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt index 32a2c153..de366396 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt @@ -37,7 +37,7 @@ import androidx.core.view.ViewCompat class ColorPanelView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyle: Int = 0 + defStyle: Int = 0, ) : View(context, attrs, defStyle) { companion object { @@ -97,7 +97,6 @@ class ColorPanelView @JvmOverloads constructor( } } - init { val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView) shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE) @@ -117,11 +116,8 @@ class ColorPanelView @JvmOverloads constructor( borderColor = typedArray.getColor(0, borderColor) typedArray.recycle() } - - } - public override fun onSaveInstanceState(): Parcelable { val state = Bundle() state.putParcelable("instanceState", super.onSaveInstanceState()) @@ -138,7 +134,6 @@ class ColorPanelView @JvmOverloads constructor( } } - override fun onDraw(canvas: Canvas) { borderPaint.color = borderColor colorPaint.color = color @@ -236,7 +231,6 @@ class ColorPanelView @JvmOverloads constructor( alphaPattern.setBounds(left, top, right, bottom) } - /** * Set the original color. This is only used for previewing colors. * @@ -264,13 +258,12 @@ class ColorPanelView @JvmOverloads constructor( val screenWidth = context.resources.displayMetrics.widthPixels referenceX = screenWidth - referenceX // mirror } - val hint = StringBuilder("#") - if (Color.alpha(color) != 255) { - hint.append(Integer.toHexString(color).uppercase()) - } else { - hint.append(String.format("%06X", 0xFFFFFF and color).uppercase()) + val hexText = when { + Color.alpha(color) == 255 -> "%06X".format(color and 0xFFFFFF) + else -> Integer.toHexString(color) } - val cheatSheet = Toast.makeText(context, hint.toString(), Toast.LENGTH_SHORT) + val hint = "#${hexText.uppercase()}" + val cheatSheet = Toast.makeText(context, hint, Toast.LENGTH_SHORT) if (midy < displayFrame.height()) { // Show along the top; follow action buttons cheatSheet.setGravity( diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt index 590021bb..4df2c86f 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt @@ -265,11 +265,10 @@ class ColorPickerDialog : super.onSaveInstanceState(outState) } - // region Custom Picker private fun createPickerView(): View { - val args = arguments ?: throw RuntimeException("createPickerView: args is null") - val activity = activity ?: throw RuntimeException("createPickerView: activity is null") + val args = arguments ?: error("createPickerView: args is null") + val activity = activity ?: error("createPickerView: activity is null") val contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null) colorPicker = contentView.findViewById(R.id.cpv_color_picker_view) val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old) @@ -376,14 +375,13 @@ class ColorPickerDialog : } private fun setHex(color: Int) { - if (showAlphaSlider) { - hexEditText!!.setText(String.format("%08X", color)) - } else { - hexEditText!!.setText(String.format("%06X", 0xFFFFFF and color)) + val hexText = when { + showAlphaSlider -> "%08X".format(color) + else -> "%06X".format(color and 0xFFFFFF) } + hexEditText?.setText(hexText) } - // -- endregion -- // region Presets Picker private fun createPresetsView(): View { @@ -399,13 +397,13 @@ class ColorPickerDialog : shadesLayout?.visibility = View.GONE contentView.findViewById(R.id.shades_divider).visibility = View.GONE } - adapter = ColorPaletteAdapter(presets, selectedItemPosition, colorShape){ - when(it){ - color -> { + adapter = ColorPaletteAdapter(presets, selectedItemPosition, colorShape) { + when (it) { + color -> { colorPickerDialogListener?.onColorSelected(dialogId, color) dismiss() } - else ->{ + else -> { color = it if (showColorShades) { createColorShades(color) @@ -519,18 +517,18 @@ class ColorPickerDialog : } private fun shadeColor(@ColorInt color: Int, percent: Double): Int { - val hex = String.format("#%06X", 0xFFFFFF and color) + val hex = "#%06X".format(color and 0xFFFFFF) val f = hex.substring(1).toLong(16) val t = (if (percent < 0) 0 else 255).toDouble() val p = if (percent < 0) percent * -1 else percent - val R = f shr 16 - val G = f shr 8 and 0x00FF - val B = f and 0x0000FF + val cR = f shr 16 + val cG = f shr 8 and 0x00FF + val cB = f and 0x0000FF return Color.argb( Color.alpha(color), - ((t - R) * p).roundToInt() + R.toInt(), - ((t - G) * p).roundToInt() + G.toInt(), - ((t - B) * p).roundToInt() + B.toInt(), + ((t - cR) * p).roundToInt() + cR.toInt(), + ((t - cG) * p).roundToInt() + cG.toInt(), + ((t - cB) * p).roundToInt() + cB.toInt(), ) } diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt index d7faf630..6128e8e8 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt @@ -502,13 +502,13 @@ class ColorPickerView @JvmOverloads constructor( return p } - private fun satValToPoint(sat: Float, `val`: Float): Point { - val rect = satValRect - val height = rect!!.height().toFloat() + private fun satValToPoint(sat: Float, inValue: Float): Point { + val rect = satValRect!! + val height = rect.height().toFloat() val width = rect.width().toFloat() val p = Point() p.x = (sat * width + rect.left).toInt() - p.y = ((1f - `val`) * height + rect.top).toInt() + p.y = ((1f - inValue) * height + rect.top).toInt() return p } diff --git a/icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt b/icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt deleted file mode 100644 index 652bf87e..00000000 --- a/icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package jp.juggler.icon_material_symbols - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("jp.juggler.icon_material_symbols.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt b/icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt deleted file mode 100644 index 2ac8d5db..00000000 --- a/icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package jp.juggler.icon_material_symbols - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt b/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt index 06ff3bce..56a4f562 100644 --- a/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt +++ b/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt @@ -78,11 +78,11 @@ class ActList : AppCompatActivity(), CoroutineScope { grantResults: IntArray, ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) { - if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { - // 特に何もしてないらしい - } - } +// if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) { +// if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) { +// // 特に何もしてないらしい +// } +// } } private fun load() = launch { @@ -153,14 +153,14 @@ class ActList : AppCompatActivity(), CoroutineScope { inner class MyViewHolder( viewRoot: View, - _activity: ActList, + actList: ActList, ) { private val tvCaption: TextView = viewRoot.findViewById(R.id.tvCaption) private val apngView: ApngView = viewRoot.findViewById(R.id.apngView) init { - apngView.timeAnimationStart = _activity.timeAnimationStart + apngView.timeAnimationStart = actList.timeAnimationStart } private var lastId: Int = 0 diff --git a/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt b/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt index 8c02deef..cc6bf243 100644 --- a/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt +++ b/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt @@ -125,7 +125,7 @@ class ActViewer : AsyncActivity() { Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}") val bitmap = f.bitmap - FileOutputStream(File(dir, "${title}_${i}.png")).use { fo -> + FileOutputStream(File(dir, "${title}_$i.png")).use { fo -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, fo) } diff --git a/sample_apng/src/main/java/jp/juggler/apng/sample/ApngView.kt b/sample_apng/src/main/java/jp/juggler/apng/sample/ApngView.kt index 885135ef..25549ee4 100644 --- a/sample_apng/src/main/java/jp/juggler/apng/sample/ApngView.kt +++ b/sample_apng/src/main/java/jp/juggler/apng/sample/ApngView.kt @@ -10,119 +10,117 @@ import android.view.View import jp.juggler.apng.ApngFrames import kotlin.math.max -class ApngView : View{ - - var timeAnimationStart :Long =0L - - var apngFrames : ApngFrames? = null - set(value) { - field = value - initializeScale() - } - - constructor(context : Context) : super(context, null) { - init(context) - } - constructor(context : Context, attrs : AttributeSet?) : super(context, attrs, 0) { - init(context) - } - constructor(context : Context, attrs : AttributeSet?, defStyle : Int) : super(context, attrs, defStyle) { - init(context) - } - - private var wView : Float = 1f - - private var hView : Float = 1f - - private var aspectView: Float = 1f - - private var wImage : Float = 1f - - private var hImage : Float = 1f - - private var aspectImage : Float = 1f - +class ApngView : View { - - private var currentScale : Float = 1f - - private var currentTransX :Float = 0f - private var currentTransY :Float = 0f - - - private val drawMatrix = Matrix() - - private val paint = Paint() - - private val findFrameResult = ApngFrames.FindFrameResult() - + var timeAnimationStart: Long = 0L + var apngFrames: ApngFrames? = null + set(value) { + field = value + initializeScale() + } - private fun init(@Suppress("UNUSED_PARAMETER") context:Context){ - // - } - - override fun onSizeChanged(w : Int, h : Int, oldw : Int, oldh : Int) { - super.onSizeChanged(w, h, oldw, oldh) - - wView = max(1, w).toFloat() - hView = max(1, h).toFloat() - aspectView = wView / hView - - initializeScale() - } - - private fun initializeScale(){ - val apngFrames = this.apngFrames - if( apngFrames != null) { - wImage = max(1, apngFrames.width).toFloat() - hImage = max(1, apngFrames.height).toFloat() - aspectImage = wImage / hImage - - currentScale = if(aspectView > aspectImage) { - hView / hImage - } else { - wView / wImage - } - - val wDraw = wImage * currentScale - val hDraw = hImage * currentScale - - currentTransX = (wView - wDraw) / 2f - currentTransY = (hView - hDraw) / 2f - - } else { - currentScale = 1f - currentTransX = 0f - currentTransY = 0f - } - invalidate() - } - + constructor(context: Context) : super(context, null) { + init(context) + } - override fun onDraw(canvas : Canvas) { - super.onDraw(canvas) - - val apngFrames = this.apngFrames - if(apngFrames != null ){ - - val t = SystemClock.elapsedRealtime() - timeAnimationStart + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) { + init(context) + } - apngFrames.findFrame(findFrameResult,t) - val delay = findFrameResult.delay - val bitmap = findFrameResult.bitmap - if( bitmap != null) { - drawMatrix.reset() - drawMatrix.postScale(currentScale, currentScale) - drawMatrix.postTranslate(currentTransX, currentTransY) - paint.isFilterBitmap = currentScale < 4f - canvas.drawBitmap(bitmap, drawMatrix, paint) - - if( delay != Long.MAX_VALUE){ - postInvalidateDelayed(max(1L,delay)) - } - } - } - } - + constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init(context) + } + + private var wView: Float = 1f + + private var hView: Float = 1f + + private var aspectView: Float = 1f + + private var wImage: Float = 1f + + private var hImage: Float = 1f + + private var aspectImage: Float = 1f + + private var currentScale: Float = 1f + + private var currentTransX: Float = 0f + private var currentTransY: Float = 0f + + private val drawMatrix = Matrix() + + private val paint = Paint() + + private val findFrameResult = ApngFrames.FindFrameResult() + + private fun init(@Suppress("UNUSED_PARAMETER") context: Context) { + // + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + wView = max(1, w).toFloat() + hView = max(1, h).toFloat() + aspectView = wView / hView + + initializeScale() + } + + private fun initializeScale() { + val apngFrames = this.apngFrames + if (apngFrames != null) { + wImage = max(1, apngFrames.width).toFloat() + hImage = max(1, apngFrames.height).toFloat() + aspectImage = wImage / hImage + + currentScale = if (aspectView > aspectImage) { + hView / hImage + } else { + wView / wImage + } + + val wDraw = wImage * currentScale + val hDraw = hImage * currentScale + + currentTransX = (wView - wDraw) / 2f + currentTransY = (hView - hDraw) / 2f + } else { + currentScale = 1f + currentTransX = 0f + currentTransY = 0f + } + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val apngFrames = this.apngFrames + if (apngFrames != null) { + + val t = SystemClock.elapsedRealtime() - timeAnimationStart + + apngFrames.findFrame(findFrameResult, t) + val delay = findFrameResult.delay + val bitmap = findFrameResult.bitmap + if (bitmap != null) { + drawMatrix.reset() + drawMatrix.postScale(currentScale, currentScale) + drawMatrix.postTranslate(currentTransX, currentTransY) + paint.isFilterBitmap = currentScale < 4f + canvas.drawBitmap(bitmap, drawMatrix, paint) + + if (delay != Long.MAX_VALUE) { + postInvalidateDelayed(max(1L, delay)) + } + } + } + } } \ No newline at end of file