diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml
index 98f88d09..ce1b3284 100644
--- a/.idea/dictionaries/tateisu.xml
+++ b/.idea/dictionaries/tateisu.xml
@@ -29,7 +29,9 @@
hashtag
hashtags
hohoemi
+ idat
idempotency
+ ihdr
kenglxn
mailto
mimumedon
@@ -37,6 +39,7 @@
noto
nsfw
openclose
+ paeth
pleroma
poller
proc
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 76bada1a..ac4a8428 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -9,6 +9,7 @@
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
index 02444022..b8ed0ec6 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -4,6 +4,7 @@
+
diff --git a/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt b/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt
index 15790e79..5f695dbf 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt
@@ -2,10 +2,10 @@
package jp.juggler.apng
-import jp.juggler.apng.util.ByteArrayTokenizer
+import jp.juggler.apng.util.ByteSequence
-class ApngAnimationControl internal constructor(bat: ByteArrayTokenizer) {
+class ApngAnimationControl internal constructor(src: ByteSequence) {
companion object {
const val PLAY_INDEFINITELY =0
@@ -21,8 +21,8 @@ class ApngAnimationControl internal constructor(bat: ByteArrayTokenizer) {
val numPlays: Int
init {
- numFrames = bat.readInt32()
- numPlays = bat.readInt32()
+ numFrames = src.readInt32()
+ numPlays = src.readInt32()
}
override fun toString() ="ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
diff --git a/apng/src/main/java/jp/juggler/apng/ApngBackground.kt b/apng/src/main/java/jp/juggler/apng/ApngBackground.kt
index 58c12128..05db9d34 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngBackground.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngBackground.kt
@@ -2,9 +2,9 @@
package jp.juggler.apng
-import jp.juggler.apng.util.ByteArrayTokenizer
+import jp.juggler.apng.util.ByteSequence
-class ApngBackground internal constructor(colorType: ColorType, bat: ByteArrayTokenizer) {
+class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) {
val red: Int
val green: Int
@@ -14,23 +14,23 @@ class ApngBackground internal constructor(colorType: ColorType, bat: ByteArrayTo
init {
when (colorType) {
ColorType.GREY, ColorType.GREY_ALPHA -> {
- val v = bat.readUInt16()
+ val v = src.readUInt16()
red = v
green = v
blue = v
index = -1
}
ColorType.RGB, ColorType.RGBA -> {
- red = bat.readUInt16()
- green = bat.readUInt16()
- blue = bat.readUInt16()
+ red = src.readUInt16()
+ green = src.readUInt16()
+ blue = src.readUInt16()
index = -1
}
ColorType.INDEX -> {
red = -1
green = -1
blue = -1
- index = bat.readUInt8()
+ index = src.readUInt8()
}
}
}
diff --git a/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt b/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt
index 912137a5..9f1b435d 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt
@@ -1,35 +1,72 @@
+@file:Suppress("MemberVisibilityCanBePrivate", "unused")
+
package jp.juggler.apng
-class ApngBitmap(var width: Int, var height: Int) {
-
- val colors = IntArray( width * height)
-
- fun reset(width: Int, height: Int) {
- val newSize = width * height
- if( newSize > colors.size )
- throw ParseError("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) {
-
- fun plusX(x: Int): Pointer {
- pos += x
- return this
- }
-
- fun setPixel(a: Int, r: Int, g: Int, b: Int): Pointer {
- colors[pos] = ((a and 255) shl 24) or ((r and 255) shl 16) or ((g and 255) shl 8) or (b and 255)
- return this
- }
-
- fun setPixel(a: Byte, r: Byte, g: Byte, b: Byte): Pointer {
- colors[pos] = ((a.toInt() and 255) shl 24) or ((r.toInt() and 255) shl 16) or ((g.toInt() and 255) shl 8) or (b.toInt() and 255)
- return this
- }
- }
-
- fun pointer(x: Int, y: Int) = Pointer( x + y * width )
+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 ParseError("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) : Pointer {
+ // if( pos == width) println("setPixel 0x%x".format(argb))
+ colors[pos] = argb
+ return this
+ }
+
+ 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) : Pointer {
+ this.pos = pos
+ this.step = step
+ return this
+ }
+
+ fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step)
+
+ fun plus(x : Int) : Pointer {
+ pos += x
+ return this
+ }
+
+ 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/ApngDecoder.kt b/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt
index cab1d065..508dcd86 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt
@@ -2,146 +2,167 @@
package jp.juggler.apng
-import jp.juggler.apng.util.BufferPool
-import jp.juggler.apng.util.ByteArrayTokenizer
-import jp.juggler.apng.util.StreamTokenizer
+import jp.juggler.apng.util.*
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
- ) {
- val apng = Apng()
- val tokenizer = StreamTokenizer(_inStream)
-
- val pngHeader = tokenizer.readBytes(8)
- if (!pngHeader.contentEquals(PNG_SIGNATURE)) {
- throw ParseError("header not match")
- }
-
- var lastSequenceNumber: Int? = null
- fun checkSequenceNumber(n: Int) {
- val last = lastSequenceNumber
- if (last != null && n <= last) {
- throw ParseError("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(ByteArrayTokenizer(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 ParseError("missing IHDR")
- apng.background = ApngBackground(header.colorType, ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
- }
-
- "tRNS" -> {
- val header = apng.header ?: throw ParseError("missing IHDR")
- val body = chunk.readBody(crc32, tokenizer)
- when (header.colorType) {
- ColorType.GREY -> apng.transparentColor = ApngTransparentColor(true, ByteArrayTokenizer(body))
- ColorType.RGB -> apng.transparentColor = ApngTransparentColor(false, ByteArrayTokenizer(body))
- ColorType.INDEX -> apng.palette?.parseTRNS(body) ?: throw ParseError("missing palette")
- else -> callback.log("tRNS ignored. colorType =${header.colorType}")
- }
- }
-
- "IDAT" -> {
- val header = apng.header ?: throw ParseError("missing IHDR")
- if (idatDecoder == null) {
- bitmap ?: throw ParseError("missing bitmap")
- bitmap.reset(header.width, header.height)
- idatDecoder = IdatDecoder(apng, bitmap, inflateBufferPool) {
- 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 animationControl = ApngAnimationControl(ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
- apng.animationControl = animationControl
- callback.onAnimationInfo(apng, animationControl)
- }
-
- "fcTL" -> {
- val bat = ByteArrayTokenizer(chunk.readBody(crc32, tokenizer))
- checkSequenceNumber(bat.readInt32())
- lastFctl = ApngFrameControl(bat)
- fdatDecoder = null
- }
-
- "fdAT" -> {
- val fctl = lastFctl ?: throw ParseError("missing fCTL before fdAT")
- if (fdatDecoder == null) {
- bitmap ?: throw ParseError("missing bitmap")
- bitmap.reset(fctl.width, fctl.height)
- fdatDecoder = IdatDecoder(apng, bitmap, inflateBufferPool) {
- callback.onAnimationFrame(apng, fctl, bitmap)
- }
- }
- checkSequenceNumber(tokenizer.readInt32(crc32))
- 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 (おそらく減色用?)
- -> chunk.skipBody(tokenizer)
-
- else -> {
- callback.log("unknown chunk: type=%s,size=0x%x".format(chunk.type, chunk.size))
- chunk.skipBody(tokenizer)
- }
- }
- }
- }
+
+ 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 ParseError("header not match")
+ }
+
+ var lastSequenceNumber : Int? = null
+ fun checkSequenceNumber(n : Int) {
+ val last = lastSequenceNumber
+ if(last != null && n <= last) {
+ throw ParseError("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(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 ParseError("missing IHDR")
+ apng.background = ApngBackground(
+ header.colorType,
+ ByteSequence(chunk.readBody(crc32, tokenizer))
+ )
+ }
+
+ "tRNS" -> {
+ val header = apng.header ?: throw ParseError("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 ParseError("missing palette")
+ else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}")
+ }
+ }
+
+ "IDAT" -> {
+ val header = apng.header ?: throw ParseError("missing IHDR")
+ if(idatDecoder == null) {
+ bitmap ?: throw ParseError("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 ParseError("missing IHDR")
+ val animationControl =
+ ApngAnimationControl(ByteSequence(chunk.readBody(crc32, tokenizer)))
+ apng.animationControl = animationControl
+ callback.onAnimationInfo(apng, header,animationControl)
+ }
+
+ "fcTL" -> {
+ val bat = ByteSequence(chunk.readBody(crc32, tokenizer))
+ checkSequenceNumber(bat.readInt32())
+ lastFctl = ApngFrameControl(bat)
+ fdatDecoder = null
+ }
+
+ "fdAT" -> {
+ val fctl = lastFctl ?: throw ParseError("missing fCTL before fdAT")
+ if(fdatDecoder == null) {
+ bitmap ?: throw ParseError("missing bitmap")
+ bitmap.reset(fctl.width, fctl.height)
+ fdatDecoder = IdatDecoder(
+ apng,
+ bitmap,
+ inflateBufferPool,
+ callback
+ ) {
+ callback.onAnimationFrame(apng, fctl, bitmap)
+ }
+ }
+ checkSequenceNumber(tokenizer.readInt32(crc32))
+ 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 (おそらく減色用?)
+ -> 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 4a3b2d95..ad30e0ba 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt
@@ -1,9 +1,31 @@
package jp.juggler.apng
-interface ApngDecoderCallback{
- fun onHeader(apng: Apng, header: ApngImageHeader)
- fun onAnimationInfo(apng: Apng, animationControl: ApngAnimationControl)
- fun onDefaultImage(apng: Apng, bitmap: ApngBitmap)
- fun onAnimationFrame(apng: Apng, frameControl: ApngFrameControl, bitmap: ApngBitmap)
- fun log(message:String)
+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, bitmap : ApngBitmap)
+
}
diff --git a/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt b/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt
index cfc60d77..262211c8 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt
@@ -2,10 +2,10 @@
package jp.juggler.apng
-import jp.juggler.apng.util.ByteArrayTokenizer
+import jp.juggler.apng.util.ByteSequence
-class ApngFrameControl internal constructor(bat: ByteArrayTokenizer) {
+class ApngFrameControl internal constructor(src: ByteSequence) {
val width: Int
val height: Int
@@ -17,19 +17,19 @@ class ApngFrameControl internal constructor(bat: ByteArrayTokenizer) {
val blendOp: BlendOp
init {
- width = bat.readInt32()
- height = bat.readInt32()
- xOffset = bat.readInt32()
- yOffset = bat.readInt32()
- delayNum = bat.readUInt16()
- delayDen = bat.readUInt16().let{ if(it==0) 100 else it}
+ width = src.readInt32()
+ height = src.readInt32()
+ xOffset = src.readInt32()
+ yOffset = src.readInt32()
+ delayNum = src.readUInt16()
+ delayDen = src.readUInt16().let{ if(it==0) 100 else it}
var num:Int
- num = bat.readUInt8()
+ num = src.readUInt8()
disposeOp = DisposeOp.values().first{it.num==num}
- num = bat.readUInt8()
+ num = src.readUInt8()
blendOp = BlendOp.values().first{it.num==num}
}
diff --git a/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt b/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt
index ae737292..f524476d 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt
@@ -2,10 +2,10 @@
package jp.juggler.apng
-import jp.juggler.apng.util.ByteArrayTokenizer
+import jp.juggler.apng.util.ByteSequence
-
-class ApngImageHeader internal constructor(bat: ByteArrayTokenizer) {
+// information from IHDR chunk.
+class ApngImageHeader internal constructor(src: ByteSequence) {
val width: Int
val height: Int
val bitDepth: Int
@@ -16,22 +16,24 @@ class ApngImageHeader internal constructor(bat: ByteArrayTokenizer) {
init {
- width = bat.readInt32()
- height = bat.readInt32()
- bitDepth = bat.readUInt8()
+ width = src.readInt32()
+ height = src.readInt32()
+ if(width <=0 || height <=0 ) throw ParseError("w=$width,h=$height is too small")
+
+ bitDepth = src.readUInt8()
var num:Int
//
- num =bat.readUInt8()
+ num =src.readUInt8()
colorType = ColorType.values().first { it.num==num }
//
- num =bat.readUInt8()
+ num =src.readUInt8()
compressionMethod = CompressionMethod.values().first { it.num==num }
//
- num =bat.readUInt8()
+ num =src.readUInt8()
filterMethod = FilterMethod.values().first { it.num==num }
//
- num =bat.readUInt8()
+ num =src.readUInt8()
interlaceMethod = InterlaceMethod.values().first { it.num==num }
}
diff --git a/apng/src/main/java/jp/juggler/apng/ApngPalette.kt b/apng/src/main/java/jp/juggler/apng/ApngPalette.kt
index 8ca82b16..217d7fb6 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngPalette.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngPalette.kt
@@ -2,28 +2,40 @@
package jp.juggler.apng
+import jp.juggler.apng.util.getUInt8
-class ApngPalette(rgb: ByteArray) {
- val list: ByteArray
- var hasAlpha: Boolean = false
-
- init {
- val entryCount = rgb.size / 3
- list = ByteArray(4 * entryCount)
- for (i in 0 until entryCount) {
- list[i * 4] = 255.toByte()
- list[i * 4 + 1] = rgb[i * 3 + 0]
- list[i * 4 + 2] = rgb[i * 3 + 1]
- list[i * 4 + 3] = rgb[i * 3 + 2]
- }
- }
-
- override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
-
- fun parseTRNS(ba: ByteArray) {
- hasAlpha = true
- for (i in 0 until Math.min(list.size, ba.size)) {
- list[i * 4] = ba[i]
- }
- }
+class ApngPalette(
+ 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 Math.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 042db12b..d10f58a2 100644
--- a/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt
+++ b/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt
@@ -1,21 +1,23 @@
+@file:Suppress("MemberVisibilityCanBePrivate")
+
package jp.juggler.apng
-import jp.juggler.apng.util.ByteArrayTokenizer
+import jp.juggler.apng.util.ByteSequence
-class ApngTransparentColor internal constructor(isGreyScale:Boolean, bat: ByteArrayTokenizer) {
+class ApngTransparentColor internal constructor(isGreyScale:Boolean, src: ByteSequence) {
val red:Int
val green:Int
val blue:Int
init{
if( isGreyScale){
- val v = bat.readUInt16()
+ val v = src.readUInt16()
red =v
green =v
blue =v
}else{
- red =bat.readUInt16()
- green =bat.readUInt16()
- blue =bat.readUInt16()
+ red =src.readUInt16()
+ green =src.readUInt16()
+ blue =src.readUInt16()
}
}
diff --git a/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt b/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt
index 9d41e7c6..c04225ba 100644
--- a/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt
+++ b/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt
@@ -2,611 +2,623 @@
package jp.juggler.apng
-import jp.juggler.apng.util.BufferPool
-import jp.juggler.apng.util.ByteArrayQueue
-import jp.juggler.apng.util.ByteArrayRange
+import jp.juggler.apng.util.*
import java.io.InputStream
import java.util.*
import java.util.zip.CRC32
import java.util.zip.Inflater
internal class IdatDecoder(
- apng: Apng,
- private val bitmap: ApngBitmap,
- private val inflateBufferPool: BufferPool,
- private val onCompleted: () -> Unit
+ apng : Apng,
+ private val bitmap : ApngBitmap,
+ private val inflateBufferPool : BufferPool,
+ private val callback : ApngDecoderCallback,
+ private val onCompleted : () -> Unit
) {
-
- class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int)
-
- companion object {
-
- private val passInfoList = listOf(
- PassInfo(1, 0, 1, 0), // [0]:no interlacing
- PassInfo(8, 0, 8, 0), // Adam7 pass 1
- PassInfo(8, 4, 8, 0), // Adam7 pass 2
- PassInfo(4, 0, 8, 4), // Adam7 pass 3
- PassInfo(4, 2, 4, 0), // Adam7 pass 4
- PassInfo(2, 0, 4, 2), // Adam7 pass 5
- PassInfo(2, 1, 2, 0), // Adam7 pass 6
- PassInfo(1, 0, 2, 1) // Adam7 pass 7
-
- )
-
- private fun abs(v: Int) = if (v >= 0) v else -v
-
- private val dummyPaletteData = ByteArray(0)
-
- // a = left, b = above, c = upper left
- private fun paeth(a: Int, b: Int, c: Int): Int {
- val p = a + b - c
- val pa = abs(p - a)
- val pb = abs(p - b)
- val pc = abs(p - c)
- return when {
- (pa <= pb && pa <= pc) -> a
- (pb <= pc) -> b
- else -> c
- }
- }
-
- private inline fun scanLine1(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (pass_w - x >= 8) {
- val v = baLine[pos++].toInt()
- block((v shr 7) and 1)
- block((v shr 6) and 1)
- block((v shr 5) and 1)
- block((v shr 4) and 1)
- block((v shr 3) and 1)
- block((v shr 2) and 1)
- block((v shr 1) and 1)
- block(v and 1)
- x += 8
- }
- val remain = pass_w - x
- if (remain > 0) {
- val v = baLine[pos].toInt()
- block((v shr 7) and 1)
- if (remain > 1) block((v shr 6) and 1)
- if (remain > 2) block((v shr 5) and 1)
- if (remain > 3) block((v shr 4) and 1)
- if (remain > 4) block((v shr 3) and 1)
- if (remain > 5) block((v shr 2) and 1)
- if (remain > 6) block((v shr 1) and 1)
- }
- }
-
- private inline fun scanLine2(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (pass_w - x >= 4) {
- val v = baLine[pos++].toInt()
- block((v shr 6) and 3)
- block((v shr 4) and 3)
- block((v shr 2) and 3)
- block(v and 3)
- x += 4
- }
- val remain = pass_w - x
- if (remain > 0) {
- val v = baLine[pos].toInt()
- block((v shr 6) and 3)
- if (remain > 1) block((v shr 4) and 3)
- if (remain > 2) block((v shr 2) and 3)
- }
- }
-
- private inline fun scanLine4(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (pass_w - x >= 2) {
- val v = baLine[pos++].toInt()
- block((v shr 4) and 15)
- block(v and 15)
- x += 2
- }
- val remain = pass_w - x
- if (remain > 0) {
- val v = baLine[pos].toInt()
- block((v shr 4) and 15)
- }
- }
-
- private inline fun scanLine8(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val v = baLine[pos++].toInt()
- block(v and 255)
- ++x
- }
- }
-
- private fun parseUInt16(ba: ByteArray, pos: Int): Int {
- val b0 = ba[pos].toInt() and 255
- val b1 = ba[pos].toInt() and 255
- return (b0 shl 8) or b1
- }
-
- private inline fun scanLine16(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val v = parseUInt16(baLine, pos)
- pos += 2
- block(v)
- ++x
- }
- }
-
- private inline fun scanLineRGB8(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val r = baLine[pos].toInt() and 255
- val g = baLine[pos + 1].toInt() and 255
- val b = baLine[pos + 2].toInt() and 255
- pos += 3
- block(r, g, b)
- ++x
- }
- }
-
- private inline fun scanLineRGB16(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val r = parseUInt16(baLine, pos)
- val g = parseUInt16(baLine, pos + 2)
- val b = parseUInt16(baLine, pos + 4)
- pos += 6
- block(r, g, b)
- ++x
- }
- }
-
- private inline fun scanLineRGBA8(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int, a: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val r = baLine[pos].toInt() and 255
- val g = baLine[pos + 1].toInt() and 255
- val b = baLine[pos + 2].toInt() and 255
- val a = baLine[pos + 3].toInt() and 255
- pos += 4
- block(r, g, b, a)
- ++x
- }
- }
-
- private inline fun scanLineRGBA16(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int, a: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val r = parseUInt16(baLine, pos)
- val g = parseUInt16(baLine, pos + 2)
- val b = parseUInt16(baLine, pos + 4)
- val a = parseUInt16(baLine, pos + 6)
- pos += 8
- block(r, g, b, a)
- ++x
- }
- }
-
- private inline fun scanLineGA8(baLine: ByteArray, pass_w: Int, block: (g: Int, a: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val g = baLine[pos].toInt() and 255
- val a = baLine[pos + 1].toInt() and 255
- pos += 2
- block(g, a)
- ++x
- }
- }
-
- private inline fun scanLineGA16(baLine: ByteArray, pass_w: Int, block: (g: Int, a: Int) -> Unit) {
- var pos = 1
- var x = 0
- while (x < pass_w) {
- val g = parseUInt16(baLine, pos)
- val a = parseUInt16(baLine, pos + 2)
- pos += 4
- block(g, a)
- ++x
- }
- }
- }
-
- private val inflater = Inflater()
- private val bytesQueue = ByteArrayQueue { inflateBufferPool.recycle(it.array) }
- private val colorType: ColorType
- private val bitDepth: Int
- private val plteData: ByteArray
- private val sampleBits: Int
- private val sampleBytes: Int
- private val scanLineBytesMax: Int
- private val linePool = LinkedList()
- private val transparentCheckerGrey: (v: Int) -> Int
- private val transparentCheckerRGB: (r: Int, g: Int, b: Int) -> Int
- private val renderScanLineFunc: (baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) -> Unit
-
- private var pass: Int
- private lateinit var passInfo: PassInfo
- private var passWidth: Int = 0
- private var passHeight: Int = 0
- private var passY: Int = 0
- private var scanLineBytes: Int = 0
- private var baPreviousLine: ByteArray? = null
- private var isCompleted = false
-
- init {
- val header = requireNotNull(apng.header)
- this.colorType = header.colorType
- this.bitDepth = header.bitDepth
-
- this.plteData = if (colorType == ColorType.INDEX) {
- apng.palette?.list
- ?: throw ParseError("missing ApngPalette for index color")
- } else {
- dummyPaletteData
- }
-
- sampleBits = when (colorType) {
- ColorType.GREY,ColorType.INDEX -> bitDepth
- ColorType.GREY_ALPHA -> bitDepth * 2
- ColorType.RGB -> bitDepth * 3
- ColorType.RGBA -> bitDepth * 4
- }
-
- sampleBytes = (sampleBits + 7) / 8
- scanLineBytesMax = 1 + (bitmap.width * sampleBits + 7) / 8
-
- linePool.add(ByteArray(scanLineBytesMax))
- linePool.add(ByteArray(scanLineBytesMax))
-
- this.pass = when (header.interlaceMethod) {
- InterlaceMethod.None -> 0
- InterlaceMethod.Standard -> 1
- }
-
- val transparentColor = apng.transparentColor
- transparentCheckerGrey = if (transparentColor != null) {
- { v: Int -> if (transparentColor.match(v)) 0 else 255 }
- } else {
- { _: Int -> 255 }
- }
-
- transparentCheckerRGB = if (transparentColor != null) {
- { r: Int, g: Int, b: Int -> if (transparentColor.match(r,g,b)) 0 else 255 }
- } else {
- { _: Int, _: Int, _: Int -> 255 }
- }
-
- renderScanLineFunc = selectRenderFunc()
-
- initializePass()
- }
-
- private fun renderGrey1(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine1(baLine, passWidth) { v ->
- val g8 = if (v == 0) 0 else 255
- val a8 = transparentCheckerGrey(v)
- bitmapPointer.setPixel(a8, g8, g8, g8)
- .plusX(xStep)
- }
- }
-
- private fun renderGrey2(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine2(baLine, passWidth) { v ->
- val g8 = v or (v shl 2) or (v shl 4) or (v shl 6)
- val a8 = transparentCheckerGrey(v)
- bitmapPointer.setPixel(a8, g8, g8, g8)
- .plusX(xStep)
- }
- }
-
- private fun renderGrey4(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine4(baLine, passWidth) { v ->
- val g8 = v or (v shl 4)
- val a8 = transparentCheckerGrey(v)
- bitmapPointer.setPixel(a8, g8, g8, g8)
- .plusX(xStep)
- }
- }
-
- private fun renderGrey8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine8(baLine, passWidth) { v ->
- val a8 = transparentCheckerGrey(v)
- bitmapPointer.setPixel(a8, v, v, v)
- .plusX(xStep)
- }
- }
-
- private fun renderGrey16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine16(baLine, passWidth) { v ->
- val g8 = v shr 8
- val a8 = transparentCheckerGrey(v)
- bitmapPointer.setPixel(a8, g8, g8, g8)
- .plusX(xStep)
- }
- }
-
- private fun renderRGB8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLineRGB8(baLine, passWidth) { r, g, b ->
- val a8 = transparentCheckerRGB(r, g, b)
- bitmapPointer.setPixel(a8, r, g, b)
- .plusX(xStep)
- }
- }
-
- private fun renderRGB16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLineRGB16(baLine, passWidth) { r, g, b ->
- val a8 = transparentCheckerRGB(r, g, b)
- bitmapPointer.setPixel(a8, r shr 8, g shr 8, b shr 8)
- .plusX(xStep)
- }
- }
-
- private fun renderIndex1(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine1(baLine, passWidth) { v ->
- val plteOffset = v * 4
- bitmapPointer.setPixel(
- plteData[plteOffset],
- plteData[plteOffset + 1],
- plteData[plteOffset + 2],
- plteData[plteOffset + 3]
- )
- .plusX(xStep)
- }
- }
-
- private fun renderIndex2(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine2(baLine, passWidth) { v ->
- val plteOffset = v * 4
- bitmapPointer.setPixel(
- plteData[plteOffset],
- plteData[plteOffset + 1],
- plteData[plteOffset + 2],
- plteData[plteOffset + 3]
- )
- .plusX(xStep)
- }
- }
-
- private fun renderIndex4(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine4(baLine, passWidth) { v ->
- val plteOffset = v * 4
- bitmapPointer.setPixel(
- plteData[plteOffset],
- plteData[plteOffset + 1],
- plteData[plteOffset + 2],
- plteData[plteOffset + 3]
- )
- .plusX(xStep)
- }
- }
-
- private fun renderIndex8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLine8(baLine, passWidth) { v ->
- val plteOffset = v * 4
- bitmapPointer.setPixel(
- plteData[plteOffset],
- plteData[plteOffset + 1],
- plteData[plteOffset + 2],
- plteData[plteOffset + 3]
- )
- .plusX(xStep)
- }
- }
-
- private fun renderGA8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLineGA8(baLine, passWidth) { g, a ->
- bitmapPointer.setPixel(a, g, g, g)
- .plusX(xStep)
- }
- }
-
- private fun renderGA16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLineGA16(baLine, passWidth) { g, a ->
- val g8 = g shr 8
- bitmapPointer.setPixel(a shr 8, g8, g8, g8)
- .plusX(xStep)
- }
- }
-
- private fun renderRGBA8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLineRGBA8(baLine, passWidth) { r, g, b, a ->
- bitmapPointer.setPixel(a, r, g, b)
- .plusX(xStep)
- }
- }
-
- private fun renderRGBA16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
- scanLineRGBA16(baLine, passWidth) { r, g, b, a ->
- bitmapPointer.setPixel(a shr 8, r shr 8, g shr 8, b shr 8)
- .plusX(xStep)
- }
- }
-
- private fun colorBitsNotSupported(): Nothing {
- throw ParseError("bitDepth $bitDepth is not supported for $colorType")
- }
-
- private fun selectRenderFunc() = when (colorType) {
- ColorType.GREY -> when (bitDepth) {
- 1 -> ::renderGrey1
- 2 -> ::renderGrey2
- 4 -> ::renderGrey4
- 8 -> ::renderGrey8
- 16 -> ::renderGrey16
- else -> colorBitsNotSupported()
- }
- ColorType.RGB -> when (bitDepth) {
- 8 -> ::renderRGB8
- 16 -> ::renderRGB16
- else -> colorBitsNotSupported()
- }
- ColorType.INDEX -> when (bitDepth) {
- 1 -> ::renderIndex1
- 2 -> ::renderIndex2
- 4 -> ::renderIndex4
- 8 -> ::renderIndex8
- else -> colorBitsNotSupported()
- }
- ColorType.GREY_ALPHA -> when (bitDepth) {
- 8 -> ::renderGA8
- 16 -> ::renderGA16
- else -> colorBitsNotSupported()
- }
- ColorType.RGBA -> when (bitDepth) {
- 8 -> ::renderRGBA8
- 16 -> ::renderRGBA16
- else -> colorBitsNotSupported()
- }
- }
-
- private fun initializePass() {
- passInfo = passInfoList[pass]
- passWidth = (bitmap.width + passInfo.xStep - passInfo.xStart - 1) / passInfo.xStep
- passHeight = (bitmap.height + passInfo.yStep - passInfo.yStart - 1) / passInfo.yStep
- passY = 0
- scanLineBytes = 1 + (passWidth * sampleBits + 7) / 8
-
- baPreviousLine?.let { linePool.add(it) }
- baPreviousLine = null
-
- if (passWidth <= 0 || passHeight <= 0) {
- incrementPassOrComplete()
- }
- }
-
- private fun incrementPassOrComplete(){
- if (pass in 1..6) {
- ++pass
- initializePass()
- } else if (!isCompleted) {
- isCompleted = true
- onCompleted()
- }
- }
-
-
- private fun readScanLine(): Boolean {
- if (bytesQueue.remain < scanLineBytes) return false
-
- val baLine = linePool.removeFirst()
- bytesQueue.readBytes(baLine, 0, scanLineBytes)
-
- val filterNum = baLine[0].toInt() and 255
- val filterType = FilterType.values().first { it.num == filterNum }
-
- when (filterType) {
- FilterType.None -> {
- }
- FilterType.Sub -> {
- for (pos in 1 until scanLineBytes) {
- val vLeft = if (pos <= sampleBytes) 0 else baLine[pos - sampleBytes].toInt() and 255
- val vCur = baLine[pos].toInt() and 255
- baLine[pos] = (vCur + vLeft).toByte()
- }
- }
- FilterType.Up -> {
- for (pos in 1 until scanLineBytes) {
- val vUp = (baPreviousLine?.get(pos)?.toInt() ?: 0) and 255
- val vCur = baLine[pos].toInt() and 255
- baLine[pos] = (vCur + vUp).toByte()
- }
- }
- FilterType.Average -> {
- for (pos in 1 until scanLineBytes) {
- val vLeft = if (pos <= sampleBytes) 0 else baLine[pos - sampleBytes].toInt() and 255
- val vUp = (baPreviousLine?.get(pos)?.toInt() ?: 0) and 255
- val vCur = baLine[pos].toInt() and 255
- baLine[pos] = (vCur + ((vLeft + vUp) shr 1)).toByte()
- }
- }
- FilterType.Paeth -> {
- for (pos in 1 until scanLineBytes) {
- val vLeft = if (pos <= sampleBytes) 0 else baLine[pos - sampleBytes].toInt() and 255
- val vUp = (baPreviousLine?.get(pos)?.toInt() ?: 0) and 255
- val vUpperLeft = if (pos <= sampleBytes) 0 else (baPreviousLine?.get(pos - sampleBytes)?.toInt()
- ?: 0) and 255
- val vCur = baLine[pos].toInt() and 255
- baLine[pos] = (vCur + paeth(vLeft, vUp, vUpperLeft)).toByte()
- }
- }
- }
-
- // render scanline
- renderScanLineFunc(
- baLine,
- bitmap.pointer(
- passInfo.xStart,
- passInfo.yStart + passInfo.yStep * passY
- ),
- passInfo.xStep
- )
- // save previous line
- baPreviousLine?.let { linePool.add(it) }
- baPreviousLine = baLine
-
- // complete pass?
- if (++passY >= passHeight) {
- incrementPassOrComplete()
- }
-
- return true
- }
-
-
- // returns CRC32 value
- fun addData(
- inStream: InputStream,
- size: Int,
- inBuffer: ByteArray,
- crc32: CRC32
- ){
- var foundEnd = false
- var inRemain = size
- while (inRemain > 0 && !foundEnd) {
- // read from inStream( max 4096 byte)
- var nRead = 0
- val nReadMax = Math.min(inBuffer.size, inRemain)
- while (nRead < nReadMax) {
- val delta = inStream.read(inBuffer, nRead, nReadMax - nRead)
- if (delta < 0) {
- foundEnd = true
- break
- }
- nRead += delta
- }
- if (nRead > 0) {
- inRemain -= nRead
- crc32.update(inBuffer, 0, nRead)
-
- // inflate
- inflater.setInput(inBuffer, 0, nRead)
- while (!inflater.needsInput()) {
- val inflateBuffer = inflateBufferPool.obtain()
- val delta = inflater.inflate(inflateBuffer)
- if (delta > 0) {
- bytesQueue.add(ByteArrayRange(inflateBuffer, 0, delta))
- } else {
- inflateBufferPool.recycle(inflateBuffer)
- }
- }
-
- // read scanLine
- while (!isCompleted && readScanLine()) {
- }
-
- if (isCompleted) {
- bytesQueue.clear()
- }
- }
- }
- }
+
+ private class PassInfo(val xStep : Int, val xStart : Int, val yStep : Int, val yStart : Int)
+
+ companion object {
+
+ private val passInfoList = listOf(
+ PassInfo(8, 0, 8, 0), // [0] Adam7 pass 1
+ PassInfo(8, 4, 8, 0), // [1] Adam7 pass 2
+ PassInfo(4, 0, 8, 4), // [2] Adam7 pass 3
+ PassInfo(4, 2, 4, 0), // [3] Adam7 pass 4
+ PassInfo(2, 0, 4, 2), // [4] Adam7 pass 5
+ PassInfo(2, 1, 2, 0), // [5] Adam7 pass 6
+ PassInfo(1, 0, 2, 1), // [6] Adam7 pass 7
+ PassInfo(1, 0, 1, 0) // [7] no interlacing
+ )
+
+ private val dummyPaletteData = IntArray(0)
+
+
+ private fun abs(v : Int) = if(v >= 0) v else - v
+
+ // a = left, b = above, c = upper left
+ private fun paeth(a : Int, b : Int, c : Int) : Int {
+ val p = a + b - c
+ val pa = abs(p - a)
+ val pb = abs(p - b)
+ val pc = abs(p - c)
+ return when {
+ (pa <= pb && pa <= pc) -> a
+ (pb <= pc) -> b
+ else -> c
+ }
+ }
+
+ private inline fun scanLine1(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
+ var pos = 1
+ var x = 0
+ while(pass_w - x >= 8) {
+ val v = baLine[pos ++].toInt()
+ block((v shr 7) and 1)
+ block((v shr 6) and 1)
+ block((v shr 5) and 1)
+ block((v shr 4) and 1)
+ block((v shr 3) and 1)
+ block((v shr 2) and 1)
+ block((v shr 1) and 1)
+ block(v and 1)
+ x += 8
+ }
+ val remain = pass_w - x
+ if(remain > 0) {
+ val v = baLine[pos].toInt()
+ block((v shr 7) and 1)
+ if(remain > 1) block((v shr 6) and 1)
+ if(remain > 2) block((v shr 5) and 1)
+ if(remain > 3) block((v shr 4) and 1)
+ if(remain > 4) block((v shr 3) and 1)
+ if(remain > 5) block((v shr 2) and 1)
+ if(remain > 6) block((v shr 1) and 1)
+ }
+ }
+
+ private inline fun scanLine2(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
+ var pos = 1
+ var x = 0
+ while(pass_w - x >= 4) {
+ val v = baLine[pos ++].toInt()
+ block((v shr 6) and 3)
+ block((v shr 4) and 3)
+ block((v shr 2) and 3)
+ block(v and 3)
+ x += 4
+ }
+ val remain = pass_w - x
+ if(remain > 0) {
+ val v = baLine[pos].toInt()
+ block((v shr 6) and 3)
+ if(remain > 1) block((v shr 4) and 3)
+ if(remain > 2) block((v shr 2) and 3)
+ }
+ }
+
+ private inline fun scanLine4(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
+ var pos = 1
+ var x = 0
+ while(pass_w - x >= 2) {
+ val v = baLine[pos ++].toInt()
+ block((v shr 4) and 15)
+ block(v and 15)
+ x += 2
+ }
+ val remain = pass_w - x
+ if(remain > 0) {
+ val v = baLine[pos].toInt()
+ block((v shr 4) and 15)
+ }
+ }
+
+ private inline fun scanLine8(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(baLine.getUInt8(pos ++))
+ }
+ }
+
+ private inline fun scanLine16(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(baLine.getUInt16(pos))
+ pos += 2
+ }
+ }
+
+ private inline fun scanLineRGB8(
+ baLine : ByteArray,
+ pass_w : Int,
+ block : (r : Int, g : Int, b : Int) -> Unit
+ ) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(
+ baLine.getUInt8(pos),
+ baLine.getUInt8(pos + 1),
+ baLine.getUInt8(pos + 2)
+ )
+ pos += 3
+ }
+ }
+
+ private inline fun scanLineRGB16(
+ baLine : ByteArray,
+ pass_w : Int,
+ block : (r : Int, g : Int, b : Int) -> Unit
+ ) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(
+ baLine.getUInt16(pos),
+ baLine.getUInt16(pos + 2),
+ baLine.getUInt16(pos + 4)
+ )
+ pos += 6
+ }
+ }
+
+ private inline fun scanLineRGBA8(
+ baLine : ByteArray,
+ pass_w : Int,
+ block : (r : Int, g : Int, b : Int, a : Int) -> Unit
+ ) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(
+ baLine.getUInt8(pos),
+ baLine.getUInt8(pos + 1),
+ baLine.getUInt8(pos + 2),
+ baLine.getUInt8(pos + 3)
+ )
+ pos += 4
+ }
+ }
+
+ private inline fun scanLineRGBA16(
+ baLine : ByteArray,
+ pass_w : Int,
+ block : (r : Int, g : Int, b : Int, a : Int) -> Unit
+ ) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(
+ baLine.getUInt16(pos),
+ baLine.getUInt16(pos + 2),
+ baLine.getUInt16(pos + 4),
+ baLine.getUInt16(pos + 6)
+ )
+ pos += 8
+ }
+ }
+
+ private inline fun scanLineGA8(
+ baLine : ByteArray,
+ pass_w : Int,
+ block : (g : Int, a : Int) -> Unit
+ ) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(
+ baLine.getUInt8(pos),
+ baLine.getUInt8(pos + 1)
+ )
+ pos += 2
+ }
+ }
+
+ private inline fun scanLineGA16(
+ baLine : ByteArray,
+ pass_w : Int,
+ block : (g : Int, a : Int) -> Unit
+ ) {
+ var pos = 1
+ for(x in 0 until pass_w) {
+ block(
+ baLine.getUInt16(pos),
+ baLine.getUInt16(pos + 2)
+ )
+ pos += 4
+ }
+ }
+ }
+
+ private val inflater = Inflater()
+ private val inflateBufferQueue = ByteSequenceQueue { inflateBufferPool.recycle(it.array) }
+ private val colorType : ColorType
+ private val bitDepth : Int
+ private val paletteData : IntArray
+ private val sampleBits : Int
+ private val sampleBytes : Int
+ private val scanLineBytesMax : Int
+ private val linePool = LinkedList()
+ private val transparentCheckerGrey : (v : Int) -> Int
+ private val transparentCheckerRGB : (r : Int, g : Int, b : Int) -> Int
+ private val renderScanLineFunc : (baLine : ByteArray) -> Unit
+ private val bitmapPointer = bitmap.pointer()
+
+ private var pass : Int
+ private lateinit var passInfo : PassInfo
+ private var passWidth : Int = 0
+ private var passHeight : Int = 0
+ private var passY : Int = 0
+ private var scanLineBytes : Int = 0
+ private var baPreviousLine : ByteArray? = null
+ private var isCompleted = false
+
+ init {
+ val header = requireNotNull(apng.header)
+ this.colorType = header.colorType
+ this.bitDepth = header.bitDepth
+
+ this.paletteData = if(colorType == ColorType.INDEX) {
+ apng.palette?.list
+ ?: throw ParseError("missing ApngPalette for index color")
+ } else {
+ dummyPaletteData
+ }
+
+ sampleBits = when(colorType) {
+ ColorType.GREY, ColorType.INDEX -> bitDepth
+ ColorType.GREY_ALPHA -> bitDepth * 2
+ ColorType.RGB -> bitDepth * 3
+ ColorType.RGBA -> bitDepth * 4
+ }
+
+ sampleBytes = (sampleBits + 7) / 8
+ scanLineBytesMax = 1 + (bitmap.width * sampleBits + 7) / 8
+
+ linePool.add(ByteArray(scanLineBytesMax))
+ linePool.add(ByteArray(scanLineBytesMax))
+
+ val transparentColor = apng.transparentColor
+
+ transparentCheckerGrey = if(transparentColor != null) {
+ { v : Int -> if(transparentColor.match(v)) 0 else 255 }
+ } else {
+ { _ : Int -> 255 }
+ }
+
+ transparentCheckerRGB = if(transparentColor != null) {
+ { r : Int, g : Int, b : Int -> if(transparentColor.match(r, g, b)) 0 else 255 }
+ } else {
+ { _ : Int, _ : Int, _ : Int -> 255 }
+ }
+
+ renderScanLineFunc = selectRenderFunc()
+
+ pass = when(header.interlaceMethod) {
+ InterlaceMethod.Standard -> 0
+ InterlaceMethod.None -> 7
+ }
+
+ initializePass()
+ }
+
+ private fun renderGrey1(baLine : ByteArray) {
+ scanLine1(baLine, passWidth) { v ->
+ val g8 = if(v == 0) 0 else 255
+ val a8 = transparentCheckerGrey(v)
+ bitmapPointer.setPixel(a8, g8, g8, g8).next()
+ }
+ }
+
+ private fun renderGrey2(baLine : ByteArray) {
+ scanLine2(baLine, passWidth) { v ->
+ val g8 = v or (v shl 2) or (v shl 4) or (v shl 6)
+ val a8 = transparentCheckerGrey(v)
+ bitmapPointer.setPixel(a8, g8, g8, g8).next()
+ }
+ }
+
+ private fun renderGrey4(baLine : ByteArray) {
+ scanLine4(baLine, passWidth) { v ->
+ val g8 = v or (v shl 4)
+ val a8 = transparentCheckerGrey(v)
+ bitmapPointer.setPixel(a8, g8, g8, g8).next()
+ }
+ }
+
+ private fun renderGrey8(baLine : ByteArray) {
+ scanLine8(baLine, passWidth) { v ->
+ val a8 = transparentCheckerGrey(v)
+ bitmapPointer.setPixel(a8, v, v, v).next()
+ }
+ }
+
+ private fun renderGrey16(baLine : ByteArray) {
+ scanLine16(baLine, passWidth) { v ->
+ val g8 = v shr 8
+ val a8 = transparentCheckerGrey(v)
+ bitmapPointer.setPixel(a8, g8, g8, g8).next()
+ }
+ }
+
+ private fun renderRGB8(baLine : ByteArray) {
+ scanLineRGB8(baLine, passWidth) { r, g, b ->
+ val a8 = transparentCheckerRGB(r, g, b)
+ bitmapPointer.setPixel(a8, r, g, b).next()
+ }
+ }
+
+ private fun renderRGB16(baLine : ByteArray) {
+ scanLineRGB16(baLine, passWidth) { r, g, b ->
+ val a8 = transparentCheckerRGB(r, g, b)
+ bitmapPointer.setPixel(a8, r shr 8, g shr 8, b shr 8).next()
+ }
+ }
+
+ private fun renderIndex1(baLine : ByteArray) {
+ scanLine1(baLine, passWidth) { v ->
+ bitmapPointer.setPixel(paletteData[v]).next()
+ }
+ }
+
+ private fun renderIndex2(baLine : ByteArray) {
+ scanLine2(baLine, passWidth) { v ->
+ bitmapPointer.setPixel( paletteData[v] ).next()
+ }
+ }
+
+ private fun renderIndex4(baLine : ByteArray) {
+ scanLine4(baLine, passWidth) { v ->
+ bitmapPointer.setPixel(paletteData[v]).next()
+ }
+ }
+
+ private fun renderIndex8(baLine : ByteArray) {
+ scanLine8(baLine, passWidth) { v ->
+ bitmapPointer.setPixel(paletteData[v]).next()
+ }
+ }
+
+ private fun renderGA8(baLine : ByteArray) {
+ scanLineGA8(baLine, passWidth) { g, a ->
+ bitmapPointer.setPixel(a, g, g, g).next()
+ }
+ }
+
+ private fun renderGA16(baLine : ByteArray) {
+ scanLineGA16(baLine, passWidth) { g, a ->
+ val g8 = g shr 8
+ bitmapPointer.setPixel(a shr 8, g8, g8, g8).next()
+ }
+ }
+
+ private fun renderRGBA8(baLine : ByteArray) {
+ scanLineRGBA8(baLine, passWidth) { r, g, b, a ->
+ bitmapPointer.setPixel(a, r, g, b).next()
+ }
+ }
+
+ private fun renderRGBA16(baLine : ByteArray) {
+ scanLineRGBA16(baLine, passWidth) { r, g, b, a ->
+ bitmapPointer.setPixel(a shr 8, r shr 8, g shr 8, b shr 8).next()
+ }
+ }
+
+ private fun colorBitsNotSupported() : Nothing {
+ throw ParseError("bitDepth $bitDepth is not supported for $colorType")
+ }
+
+ private fun selectRenderFunc() = when(colorType) {
+ ColorType.GREY -> when(bitDepth) {
+ 1 -> ::renderGrey1
+ 2 -> ::renderGrey2
+ 4 -> ::renderGrey4
+ 8 -> ::renderGrey8
+ 16 -> ::renderGrey16
+ else -> colorBitsNotSupported()
+ }
+ ColorType.RGB -> when(bitDepth) {
+ 8 -> ::renderRGB8
+ 16 -> ::renderRGB16
+ else -> colorBitsNotSupported()
+ }
+ ColorType.INDEX -> when(bitDepth) {
+ 1 -> ::renderIndex1
+ 2 -> ::renderIndex2
+ 4 -> ::renderIndex4
+ 8 -> ::renderIndex8
+ else -> colorBitsNotSupported()
+ }
+ ColorType.GREY_ALPHA -> when(bitDepth) {
+ 8 -> ::renderGA8
+ 16 -> ::renderGA16
+ else -> colorBitsNotSupported()
+ }
+ ColorType.RGBA -> when(bitDepth) {
+ 8 -> ::renderRGBA8
+ 16 -> ::renderRGBA16
+ else -> colorBitsNotSupported()
+ }
+ }
+
+ private fun initializePass() {
+ passInfo = passInfoList[pass]
+ passWidth = (bitmap.width + passInfo.xStep - passInfo.xStart - 1) / passInfo.xStep
+ passHeight = (bitmap.height + passInfo.yStep - passInfo.yStart - 1) / passInfo.yStep
+ passY = 0
+ scanLineBytes = 1 + (passWidth * sampleBits + 7) / 8
+
+ baPreviousLine?.let { linePool.add(it) }
+ baPreviousLine = null
+
+ if(passWidth <= 0 || passHeight <= 0) {
+ if( callback.canApngDebug() ) callback.onApngDebug("pass $pass is empty. size=${passWidth}x${passHeight} ")
+ incrementPassOrComplete()
+ }
+ }
+
+ private fun incrementPassOrComplete() {
+ if(pass < 6) {
+ ++ pass
+ initializePass()
+ } else if(! isCompleted) {
+ isCompleted = true
+ onCompleted()
+ }
+ }
+
+ // スキャンラインを読む。行を処理したらtrueを返す
+ private fun readScanLine() : Boolean {
+
+ if(inflateBufferQueue.remain < scanLineBytes){
+ // not yet enough data to process scanline
+ return false
+ }
+
+ val baLine = linePool.removeFirst()
+ inflateBufferQueue.readBytes(baLine, 0, scanLineBytes)
+
+ val filterNum = baLine.getUInt8(0)
+ val filterType = FilterType.values().first { it.num == filterNum }
+
+// if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType")
+
+ when(filterType) {
+ FilterType.None -> {
+ }
+
+ FilterType.Sub -> {
+ for(pos in 1 until scanLineBytes) {
+ val vCur = baLine.getUInt8(pos)
+ val leftPos = pos -sampleBytes
+ val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos)
+
+
+ baLine[pos] = (vCur + vLeft).toByte()
+
+// if( callback.canApngDebug() ){
+// val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes)
+// 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}")
+// }
+
+ }
+ }
+
+ FilterType.Up -> {
+ val baPreviousLine=this.baPreviousLine
+ for(pos in 1 until scanLineBytes) {
+ val vCur = baLine.getUInt8(pos)
+ val vUp = baPreviousLine?.getUInt8(pos) ?: 0
+ baLine[pos] = (vCur + vUp).toByte()
+ }
+ }
+
+ FilterType.Average -> {
+ val baPreviousLine=this.baPreviousLine
+ for(pos in 1 until scanLineBytes) {
+ val vCur = baLine.getUInt8(pos)
+ val leftPos = pos -sampleBytes
+ val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos)
+ val vUp = baPreviousLine?.getUInt8(pos) ?: 0
+ baLine[pos] = (vCur + ((vLeft + vUp) shr 1)).toByte()
+ }
+ }
+
+ FilterType.Paeth -> {
+ val baPreviousLine=this.baPreviousLine
+ for(pos in 1 until scanLineBytes) {
+ val vCur = baLine.getUInt8(pos)
+ val leftPos = pos -sampleBytes
+ val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos)
+ val vUp = baPreviousLine?.getUInt8(pos) ?: 0
+ val vUpperLeft = if(leftPos <=0 ) 0 else baPreviousLine?.getUInt8(leftPos) ?: 0
+
+ baLine[pos] = (vCur + paeth(vLeft, vUp, vUpperLeft)).toByte()
+
+// if( callback.canApngDebug() ){
+// val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes)
+// 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)}")
+// }
+
+ }
+ }
+ }
+
+ // render scanline
+ bitmapPointer.setXY(
+ x=passInfo.xStart,
+ y=passInfo.yStart + passInfo.yStep * passY,
+ step=passInfo.xStep
+ )
+ renderScanLineFunc(baLine)
+
+ // save previous line
+ baPreviousLine?.let { linePool.add(it) }
+ baPreviousLine = baLine
+
+ if(++ passY >= passHeight) {
+ // pass completed
+ incrementPassOrComplete()
+ }
+
+ return true
+ }
+
+ // - 複数のIDATチャンクを順に読む
+ // - deflate圧縮をデコード(複数のチャンクの場合連続したものとして扱う)
+ // - interlace passに合わせてスキャンライン単位に分割
+ // - filterをデコードして
+ // - ビットマップにレンダリング
+ fun addData(
+ inStream : InputStream,
+ size : Int,
+ inBuffer : ByteArray,
+ crc32 : CRC32
+ ) {
+ var foundEnd = false
+ var inRemain = size
+ while(inRemain > 0 && ! foundEnd) {
+
+ // inBufferのサイズに合わせて読み込む
+ var nRead = 0
+ val nReadMax = Math.min(inBuffer.size, inRemain)
+ while(true) {
+
+ val remain = nReadMax - nRead
+ if(remain <= 0) break
+
+ val delta = inStream.read(inBuffer, nRead, remain)
+ if(delta < 0) {
+ foundEnd = true
+ break
+ }
+ nRead += delta
+ }
+
+ if(nRead <= 0) continue
+
+ inRemain -= nRead
+
+ // 読んだらCRC計算する
+ crc32.update(inBuffer, 0, nRead)
+
+ // zlibのdeflateをデコードする
+ inflater.setInput(inBuffer, 0, nRead)
+ while(! inflater.needsInput()) {
+ val inflateBuffer = inflateBufferPool.obtain()
+ val delta = inflater.inflate(inflateBuffer)
+ if(delta > 0) {
+ inflateBufferQueue.add(ByteSequence(inflateBuffer, 0, delta))
+ } else {
+ inflateBufferPool.recycle(inflateBuffer)
+ }
+ }
+
+ // inflateBufferQueue からScanLine単位で読んでレンダリング
+ while(! isCompleted && readScanLine()) {
+ }
+
+ // チャンクに余計なデータがあった場合でも終端まで読む
+ if(isCompleted) inflateBufferQueue.clear()
+ }
+
+ if(isCompleted) inflateBufferQueue.clear()
+ }
}
diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteArrayQueue.kt b/apng/src/main/java/jp/juggler/apng/util/ByteArrayQueue.kt
deleted file mode 100644
index 935ed554..00000000
--- a/apng/src/main/java/jp/juggler/apng/util/ByteArrayQueue.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package jp.juggler.apng.util
-
-import java.util.*
-
-internal class ByteArrayQueue(private val bufferRecycler :(ByteArrayRange)->Unit) {
-
- private val list = LinkedList()
-
- val remain: Int
- get() = list.sumBy { it.remain }
-
- fun add(range: ByteArrayRange) {
- list.add(range)
- }
-
- fun clear() {
- for( item in list ){
- bufferRecycler(item)
- }
- list.clear()
- }
-
- fun readBytes(dst: ByteArray, offset: Int, length: Int): Int {
- var nRead = 0
- while (nRead < length && list.isNotEmpty()) {
- val item = list.first()
- if (item.remain <= 0) {
- bufferRecycler(item)
- list.removeFirst()
- } else {
- val delta = Math.min(item.remain, length - nRead)
- System.arraycopy(item.array, item.start, dst, offset + nRead, delta)
- item.start += delta
- item.remain -= delta
- nRead += delta
- }
- }
- return nRead
- }
-}
diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteArrayRange.kt b/apng/src/main/java/jp/juggler/apng/util/ByteArrayRange.kt
deleted file mode 100644
index e4125e5f..00000000
--- a/apng/src/main/java/jp/juggler/apng/util/ByteArrayRange.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package jp.juggler.apng.util
-
-internal class ByteArrayRange(
- val array: ByteArray,
- var start: Int,
- var remain: Int
-)
diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteArrayTokenizer.kt b/apng/src/main/java/jp/juggler/apng/util/ByteArrayTokenizer.kt
deleted file mode 100644
index 18c76dc8..00000000
--- a/apng/src/main/java/jp/juggler/apng/util/ByteArrayTokenizer.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package jp.juggler.apng.util
-
-import jp.juggler.apng.ParseError
-
-
-internal class ByteArrayTokenizer(ba: ByteArray) {
- private val array: ByteArray = ba
- private val arraySize: Int = ba.size
- private var pos = 0
-
- val size: Int
- get()= arraySize
-
- val remain: Int
- get()= arraySize -pos
-
- fun skipBytes(size: Int) {
- pos += size
- }
-
- fun readBytes(size: Int): ByteArrayRange {
- if (pos + size > arraySize) {
- throw ParseError("readBytes: unexpected EoS")
- }
- val result = ByteArrayRange(array, pos, size)
- pos+=size
- return result
- }
-
- private fun readByte(): Int {
- if (pos >= arraySize) {
- throw ParseError("readBytes: unexpected EoS")
- }
- return array[pos++].toInt() 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 readUInt16(): Int {
- val b0 = readByte()
- val b1 = readByte()
- return (b0 shl 8) or b1
- }
-
- fun readUInt8() = readByte()
-
-
-}
\ No newline at end of file
diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt b/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt
new file mode 100644
index 00000000..8c1c3958
--- /dev/null
+++ b/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt
@@ -0,0 +1,33 @@
+package jp.juggler.apng.util
+
+import jp.juggler.apng.ParseError
+
+internal fun ByteArray.getUInt8(pos : Int) = this[pos].toInt() and 255
+
+internal fun ByteArray.getUInt16(pos : Int) = (this.getUInt8(pos) shl 8) or this.getUInt8(pos + 1)
+
+internal fun ByteArray.getInt32(pos : Int) = (this.getUInt8(pos) shl 24) or
+ (this.getUInt8(pos + 1) shl 16) or
+ (this.getUInt8(pos + 2) shl 8) or
+ this.getUInt8(pos + 3)
+
+internal class ByteSequence(
+ 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 ParseError("readIntX: 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
new file mode 100644
index 00000000..cb0d9157
--- /dev/null
+++ b/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt
@@ -0,0 +1,40 @@
+package jp.juggler.apng.util
+
+import java.util.*
+
+internal class ByteSequenceQueue(private val bufferRecycler :(ByteSequence)->Unit) {
+
+ private val list = LinkedList()
+
+ val remain: Int
+ get() = list.sumBy { it.length }
+
+ fun add(range: ByteSequence) {
+ list.add(range)
+ }
+
+ fun clear() {
+ for( item in list ){
+ bufferRecycler(item)
+ }
+ list.clear()
+ }
+
+ fun readBytes(dst: ByteArray, offset: Int, length: Int): Int {
+ var nRead = 0
+ while (nRead < length && list.isNotEmpty()) {
+ val item = list.first()
+ if (item.length <= 0) {
+ bufferRecycler(item)
+ list.removeFirst()
+ continue
+ }
+ val delta = Math.min(item.length, length - nRead)
+ System.arraycopy(item.array, item.offset, dst, offset + nRead, delta)
+ item.offset += delta
+ item.length -= delta
+ nRead += delta
+ }
+ return nRead
+ }
+}
diff --git a/apng_android/.gitignore b/apng_android/.gitignore
new file mode 100644
index 00000000..3543521e
--- /dev/null
+++ b/apng_android/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/apng_android/build.gradle b/apng_android/build.gradle
new file mode 100644
index 00000000..4095ca11
--- /dev/null
+++ b/apng_android/build.gradle
@@ -0,0 +1,42 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion 27
+ buildToolsVersion '27.0.3'
+
+ defaultConfig {
+ targetSdkVersion 27
+ minSdkVersion 21
+
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.1'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
+
+ api project(':apng')
+ // 'api' に指定した依存関係はこのライブラリの利用者に公開されます
+ // 'implementation' に指定した依存関係はこのライブラリの利用者に公開されません
+
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/apng_android/proguard-rules.pro b/apng_android/proguard-rules.pro
new file mode 100644
index 00000000..6e7ffa99
--- /dev/null
+++ b/apng_android/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/apng_android/src/androidTest/java/jp/juggler/apng/ExampleInstrumentedTest.java b/apng_android/src/androidTest/java/jp/juggler/apng/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..98d6e451
--- /dev/null
+++ b/apng_android/src/androidTest/java/jp/juggler/apng/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package jp.juggler.apng;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception{
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals( "jp.juggler.apng.test", appContext.getPackageName() );
+ }
+}
diff --git a/apng_android/src/main/AndroidManifest.xml b/apng_android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..4ee8b651
--- /dev/null
+++ b/apng_android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
diff --git a/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt
new file mode 100644
index 00000000..c91cc3e3
--- /dev/null
+++ b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt
@@ -0,0 +1,388 @@
+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.util.Log
+
+import java.io.InputStream
+import java.util.ArrayList
+
+class ApngFrames private constructor(
+ private val pixelSizeMax : Int = 0,
+ private val debug:Boolean =false
+) : ApngDecoderCallback {
+
+ companion object {
+
+ private const val TAG = "ApngFrames"
+
+ // ループしない画像の場合は3秒でまたループさせる
+ private const val DELAY_AFTER_END = 3000L
+
+ // アニメーションフレームの描画に使う
+ private val sSrcModePaint : Paint by lazy {
+ val paint = Paint()
+ paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
+ paint.isFilterBitmap = true
+ paint
+ }
+
+ private fun createBlankBitmap(w : Int, h : Int) : Bitmap {
+ return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
+ }
+
+ // WARNING: ownership of "src" will be moved or recycled.
+ private fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? {
+ if(src == null) return null
+
+ val wSrc = src.width
+ val hSrc = src.height
+ if(size_max <= 0 || wSrc <= size_max && hSrc <= size_max) return src
+
+ val wDst : Int
+ val hDst : Int
+ if(wSrc >= hSrc) {
+ wDst = size_max
+ hDst = Math.max(
+ 1,
+ (size_max.toFloat() * hSrc.toFloat() / wSrc.toFloat() + 0.5f).toInt()
+ )
+ } else {
+ hDst = size_max
+ wDst = Math.max(
+ 1,
+ (size_max.toFloat() * wSrc.toFloat() / hSrc.toFloat() + 0.5f).toInt()
+ )
+ }
+ 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,
+ sSrcModePaint
+ )
+ src.recycle()
+ return b2
+ }
+
+ private fun toBitmap(src : ApngBitmap) : Bitmap {
+ return Bitmap.createBitmap(
+ src.colors, // int[] 配列
+ 0, // offset
+ src.width, //stride
+ src.width, // width
+ src.height, //height
+ Bitmap.Config.ARGB_8888
+ )
+ }
+
+ private fun toBitmap(src : ApngBitmap, size_max : Int) : Bitmap? {
+ return scaleBitmap(
+ toBitmap(
+ src
+ ), size_max
+ )
+ }
+
+ @Suppress("unused")
+ fun parseApng(inStream : InputStream, pixelSizeMax : Int,debug:Boolean=false) : ApngFrames {
+ val result = ApngFrames(pixelSizeMax,debug)
+ try {
+ ApngDecoder.parseStream(inStream, result)
+ result.onParseComplete()
+ return if( result.defaultImage != null || result.frames?.isNotEmpty() == true ){
+ result
+ }else{
+ throw RuntimeException("APNG has no image")
+ }
+ } catch(ex : Throwable) {
+ result.dispose()
+ throw ex
+ }
+
+ }
+ }
+
+ private var header : ApngImageHeader? = null
+ private var animationControl : ApngAnimationControl? = null
+
+ val width : Int
+ get() = Math.min( pixelSizeMax, header?.width ?: 1)
+
+ val height : Int
+ get() = Math.min( pixelSizeMax, header?.height ?: 1)
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ val numFrames : Int
+ get() = animationControl?.numFrames ?: 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
+
+ private class Frame(
+ internal val bitmap : Bitmap,
+ internal val time_start : Long,
+ internal val time_width : Long
+ )
+
+ private var frames : ArrayList? = null
+
+ @Suppress("unused")
+ constructor(bitmap : Bitmap) : this() {
+ 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() {
+ defaultImage?.recycle()
+ canvasBitmap?.recycle()
+
+ val frames = this.frames
+ if(frames != null) {
+ for(f in frames) {
+ f.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) {
+ // この場合は既に mBitmapNonAnimation が用意されてるはずだ
+ result.bitmap = null
+ result.delay = Long.MAX_VALUE
+ return
+ }
+
+ val frameCount = frames.size
+
+ val isFinite = ! animationControl.isPlayIndefinitely
+ val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
+ val endWait = if(isFinite) DELAY_AFTER_END else 0L
+ val timeTotalLoop = Math.max(1,timeTotal * repeatSequenceCount + endWait)
+
+ val tf = (if(0.5f + t < 0f) 0f else 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.time_start,frame.time_start+frame.time_width );
+ if(tt < frame.time_start) {
+ e = mid
+ } else if(tt >= frame.time_start + frame.time_width) {
+ 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.time_start + frame.time_width - tt
+ result.bitmap = frames[s].bitmap
+ result.delay = (0.5f + durationScale * Math.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.time_width,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
+ ) {
+ this.animationControl = animationControl
+
+ val canvasBitmap =
+ createBlankBitmap(header.width, header.height)
+ this.canvasBitmap = canvasBitmap
+ this.canvas = Canvas(canvasBitmap)
+ this.frames = ArrayList(animationControl.numFrames)
+ }
+
+ override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
+ defaultImage?.recycle()
+ defaultImage = toBitmap(bitmap, pixelSizeMax)
+ }
+
+ override fun onAnimationFrame(
+ apng : Apng,
+ frameControl : ApngFrameControl,
+ bitmap : ApngBitmap
+ ) {
+ val frames = this.frames ?: return
+ val canvasBitmap = this.canvasBitmap ?: return
+
+ // APNGのフレーム画像をAndroidの形式に変換する。この段階ではリサイズしない
+ val bitmapNative = toBitmap(bitmap)
+
+ val previous : Bitmap? = if(frameControl.disposeOp == DisposeOp.Previous) {
+ // Capture the current bitmap region IF it needs to be reverted after rendering
+ Bitmap.createBitmap(
+ canvasBitmap,
+ frameControl.xOffset,
+ frameControl.yOffset,
+ frameControl.width,
+ frameControl.height
+ )
+ } else {
+ null
+ }
+
+ val paint = if(frameControl.blendOp == BlendOp.Source) {
+ sSrcModePaint // SRC_OVER, not blend
+ } else {
+ null // (for blend, leave paint null)
+ }
+
+ // Draw the new frame into place
+ canvas.drawBitmap(
+ bitmapNative,
+ frameControl.xOffset.toFloat(),
+ frameControl.yOffset.toFloat(),
+ paint
+ )
+
+ // Extract a drawable from the canvas. Have to copy the current bitmap.
+ // Store the drawable in the sequence of frames
+ val timeStart = timeTotal
+ val timeWidth = Math.max(1L, frameControl.delayMilliseconds)
+ timeTotal += timeWidth
+
+ val scaledBitmap =
+ scaleBitmap(
+ canvasBitmap.copy(
+ Bitmap.Config.ARGB_8888,
+ false
+ ), pixelSizeMax
+ )
+ if(scaledBitmap != null) {
+ frames.add(Frame(scaledBitmap, timeStart, timeWidth))
+ }
+
+ // Now "dispose" of the frame in preparation for the next.
+ // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
+
+ when(frameControl.disposeOp) {
+ DisposeOp.None -> {
+ }
+
+ DisposeOp.Background ->
+ // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
+ //System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
+ // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
+ //if (true || isFull) {
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR) // Clear to fully transparent black
+
+ DisposeOp.Previous ->
+ // APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
+ //System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
+ // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
+ // Put the original section back
+ if(previous != null) {
+ canvas.drawBitmap(
+ previous, frameControl.xOffset.toFloat(), frameControl.yOffset.toFloat(),
+ sSrcModePaint
+ )
+ previous.recycle()
+ }
+
+ else -> {
+ // 0: Default should never happen
+
+ // APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
+ //System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
+ // do nothing
+ // } else {
+ // Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
+ // paint = new Paint();
+ // paint.setColor(0);
+ // paint.setStyle(Paint.Style.FILL);
+ // canvas.drawRect(rt, paint);
+ // }
+
+ }
+ }
+ }
+
+}
diff --git a/apng_android/src/main/res/values/strings.xml b/apng_android/src/main/res/values/strings.xml
new file mode 100644
index 00000000..3a35720c
--- /dev/null
+++ b/apng_android/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ apng_android
+
diff --git a/apng_android/src/test/java/jp/juggler/apng/ExampleUnitTest.java b/apng_android/src/test/java/jp/juggler/apng/ExampleUnitTest.java
new file mode 100644
index 00000000..7b609a07
--- /dev/null
+++ b/apng_android/src/test/java/jp/juggler/apng/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package jp.juggler.apng;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception{
+ assertEquals( 4, 2 + 2 );
+ }
+}
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 0f5b606d..0c583145 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -64,10 +64,10 @@ dependencies {
exclude group: 'com.android.support', module: 'support-annotations'
})
- compile project(':exif')
- compile project(':colorpicker')
- compile project(':emoji')
- compile project(':apng')
+ implementation project(':exif')
+ implementation project(':colorpicker')
+ implementation project(':emoji')
+ implementation project(':apng_android')
compile 'com.android.support:support-v4:27.0.2'
compile 'com.android.support:appcompat-v7:27.0.2'
diff --git a/app/src/main/java/android/support/v7/widget/ListRecyclerView.kt b/app/src/main/java/android/support/v7/widget/ListRecyclerView.kt
index 3c3f43f2..c563c7d4 100644
--- a/app/src/main/java/android/support/v7/widget/ListRecyclerView.kt
+++ b/app/src/main/java/android/support/v7/widget/ListRecyclerView.kt
@@ -6,7 +6,7 @@ import android.util.AttributeSet
class ListRecyclerView : RecyclerView {
companion object {
- // private val log = LogCategory("ListRecyclerView")
+ // private val warning = LogCategory("ListRecyclerView")
}
constructor(context : Context) : super(context)
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt
index c8762379..7ec8c663 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt
@@ -1936,7 +1936,7 @@ class ActMain : AppCompatActivity()
App1.openCustomTab(this, opener.url)
} catch(ex : Throwable) {
- // log.trace( ex );
+ // warning.trace( ex );
log.e(ex, "openChromeTab failed. url=%s", opener.url)
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt
index d8b3a543..c0d2c275 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt
@@ -107,12 +107,12 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
override fun onLoadingChanged(isLoading : Boolean) {
// かなり頻繁に呼ばれる
- // log.d( "exoPlayer onLoadingChanged %s" ,isLoading );
+ // warning.d( "exoPlayer onLoadingChanged %s" ,isLoading );
}
override fun onPlayerStateChanged(playWhenReady : Boolean, playbackState : Int) {
// かなり頻繁に呼ばれる
- // log.d( "exoPlayer onPlayerStateChanged %s %s", playWhenReady, playbackState );
+ // warning.d( "exoPlayer onPlayerStateChanged %s %s", playWhenReady, playbackState );
if(playWhenReady && playbackState == Player.STATE_BUFFERING) {
val now = SystemClock.elapsedRealtime()
if(now - buffering_last_shown >= short_limit && exoPlayer.duration >= short_limit) {
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt
index ce2ff344..55358933 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt
@@ -124,7 +124,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
// Intent takeVideoIntent = new Intent( MediaStore.ACTION_VIDEO_CAPTURE );
// startActivityForResult( takeVideoIntent, REQUEST_CODE_VIDEO );
// }catch( Throwable ex ){
- // log.trace( ex );
+ // warning.trace( ex );
// Utils.showToast( this, ex, "opening video app failed." );
// }
// }
diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.kt b/app/src/main/java/jp/juggler/subwaytooter/App1.kt
index ae78b881..0568ab49 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/App1.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/App1.kt
@@ -182,7 +182,7 @@ class App1 : Application() {
// int memory = am.getMemoryClass();
// int largeMemory = am.getLargeMemoryClass();
// // どちらも単位はMB
- // log.d("MemoryClass=%d, LargeMemoryClass = %d",memory,largeMemory);
+ // warning.d("MemoryClass=%d, LargeMemoryClass = %d",memory,largeMemory);
//
// int maxSize;
// if( am.isLowRamDevice() ){
diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppState.kt b/app/src/main/java/jp/juggler/subwaytooter/AppState.kt
index 7ce64bc6..7666c372 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/AppState.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/AppState.kt
@@ -391,16 +391,16 @@ class AppState(internal val context : Context, internal val pref : SharedPrefere
// tts.setOnUtteranceProgressListener( new UtteranceProgressListener() {
// @Override public void onStart( String utteranceId ){
- // log.d( "UtteranceProgressListener.onStart id=%s", utteranceId );
+ // warning.d( "UtteranceProgressListener.onStart id=%s", utteranceId );
// }
//
// @Override public void onDone( String utteranceId ){
- // log.d( "UtteranceProgressListener.onDone id=%s", utteranceId );
+ // warning.d( "UtteranceProgressListener.onDone id=%s", utteranceId );
// handler.post( proc_flushSpeechQueue );
// }
//
// @Override public void onError( String utteranceId ){
- // log.d( "UtteranceProgressListener.onError id=%s", utteranceId );
+ // warning.d( "UtteranceProgressListener.onError id=%s", utteranceId );
// handler.post( proc_flushSpeechQueue );
// }
// } );
diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.kt b/app/src/main/java/jp/juggler/subwaytooter/Column.kt
index 0623d849..4c0408c2 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/Column.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/Column.kt
@@ -3461,7 +3461,7 @@ class Column(
} else if(holder_sp.adapterIndex == 0 && holder_sp.offset == 0) {
// スクロール位置が先頭なら先頭にする
log.d(
- "mergeStreamingMessage: has VH. keep head. pos=%s,offset=%s"
+ "mergeStreamingMessage: has VH. keep head. offset=%s,offset=%s"
, holder_sp.adapterIndex
, holder_sp.offset
)
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt
index b2702b5f..e04002a6 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.kt
@@ -341,7 +341,7 @@ class ColumnViewHolder(
}
val sp = column.scroll_save ?: //復元後にもここを通るがこれは正常である
- // log.d( "restoreScrollPosition [%d] %s , column has no saved scroll position.", page_idx, column.getColumnName( true ) );
+ // warning.d( "restoreScrollPosition [%d] %s , column has no saved scroll position.", page_idx, column.getColumnName( true ) );
return
column.scroll_save = null
diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.kt b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.kt
index 89162b66..ed4402a3 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.kt
@@ -172,7 +172,7 @@ internal class ItemListAdapter(
// 変更リストを順番に通知する
for(c in changeList) {
val adapterIndex = column.toAdapterIndex(c.listIndex)
- log.d("notifyChange: ChangeType=${c.type} pos=$adapterIndex,count=${c.count}")
+ log.d("notifyChange: ChangeType=${c.type} offset=$adapterIndex,count=${c.count}")
when(c.type) {
AdapterChangeType.RangeInsert -> notifyItemRangeInserted(adapterIndex, c.count)
AdapterChangeType.RangeRemove -> notifyItemRangeRemoved(adapterIndex, c.count)
@@ -204,17 +204,17 @@ internal class ItemListAdapter(
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position : Int, count : Int) {
- log.d("notifyChange: notifyItemRangeInserted pos=$position,count=$count")
+ log.d("notifyChange: notifyItemRangeInserted offset=$position,count=$count")
notifyItemRangeInserted(position, count)
}
override fun onRemoved(position : Int, count : Int) {
- log.d("notifyChange: notifyItemRangeRemoved pos=$position,count=$count")
+ log.d("notifyChange: notifyItemRangeRemoved offset=$position,count=$count")
notifyItemRangeRemoved(position, count)
}
override fun onChanged(position : Int, count : Int, payload : Any?) {
- log.d("notifyChange: notifyItemRangeChanged pos=$position,count=$count")
+ log.d("notifyChange: notifyItemRangeChanged offset=$position,count=$count")
notifyItemRangeChanged(position, count, payload)
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt
index 4d1276cd..7171d141 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.kt
@@ -1373,7 +1373,7 @@ class PollingWorker private constructor(c : Context) {
val type = src.parseString("type")
if(id <= nr.nid_read) {
- // log.d("update_sub: ignore data that id=%s, <= read id %s ",id,nr.nid_read);
+ // warning.d("update_sub: ignore data that id=%s, <= read id %s ",id,nr.nid_read);
return
} else {
log.d("update_sub: found data that id=%s, > read id %s ", id, nr.nid_read)
diff --git a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt
index a3302dbc..7d91de3c 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/StreamReader.kt
@@ -100,7 +100,7 @@ internal class StreamReader(
* Invoked when a text (type `0x1`) message has been received.
*/
override fun onMessage(webSocket : WebSocket, text : String) {
- // log.d( "WebSocket onMessage. url=%s, message=%s", webSocket.request().url(), text );
+ // warning.d( "WebSocket onMessage. url=%s, message=%s", webSocket.request().url(), text );
try {
val obj = text.toJsonObject()
diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt
index bf472a0f..ecb283fb 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt
@@ -93,7 +93,7 @@ open class TootApiResult(
while(m.find()) {
val url = m.group(1)
val rel = m.group(2)
- // log.d("Link %s,%s",rel,url);
+ // warning.d("Link %s,%s",rel,url);
if("next" == rel) link_older = url
if("prev" == rel) link_newer = url
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt
index 6d73dcd3..02ba51eb 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/span/EmojiImageSpan.kt
@@ -15,7 +15,7 @@ class EmojiImageSpan(context : Context, private val res_id : Int) : ReplacementS
companion object {
- // private static final LogCategory log = new LogCategory( "EmojiImageSpan" );
+ // private static final LogCategory warning = new LogCategory( "EmojiImageSpan" );
// static DynamicDrawableSpan x = null;
diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt
index 3a1aed11..88cf0f39 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt
@@ -6,10 +6,10 @@ import android.graphics.Rect
import android.graphics.RectF
import android.support.annotation.IntRange
import android.text.style.ReplacementSpan
+import jp.juggler.apng.ApngFrames
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.Pref
-import jp.juggler.subwaytooter.util.ApngFrames
import jp.juggler.subwaytooter.util.LogCategory
import java.lang.ref.WeakReference
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt
index e1d9464e..3d762431 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/LogData.kt
@@ -8,7 +8,7 @@ import android.util.Log
object LogData {
private const val TAG = "SubwayTooter"
- internal const val table = "log"
+ internal const val table = "warning"
private const val COL_TIME = "t"
private const val COL_LEVEL = "l"
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt b/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt
index f38f73aa..1ac9ad6d 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/MutedApp.kt
@@ -88,7 +88,7 @@ object MutedApp {
// cursor.close();
// }
// }catch( Throwable ex ){
- // log.e( ex, "load failed." );
+ // warning.e( ex, "load failed." );
// }
// return false;
// }
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt b/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt
index 4ddfc19b..b52dabc9 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/MutedWord.kt
@@ -88,7 +88,7 @@ object MutedWord {
// cursor.close();
// }
// }catch( Throwable ex ){
- // log.e( ex, "load failed." );
+ // warning.e( ex, "load failed." );
// }
// return false;
// }
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt b/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt
index 24282f3d..67315317 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/TagSet.kt
@@ -55,7 +55,7 @@ object TagSet {
// App1.database.delete(table, COL_TIME_SAVE + "", arrayOf(expire.toString()))
//
// } catch(ex : Throwable) {
- // log.e(ex, "deleteOld failed.")
+ // warning.e(ex, "deleteOld failed.")
// }
// }
@@ -67,7 +67,7 @@ object TagSet {
// cv.put( COL_ACCT, acct );
// App1.getDB().replace( table, null, cv );
// }catch( Throwable ex ){
- // log.e( ex, "save failed." );
+ // warning.e( ex, "save failed." );
// }
// }
diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt b/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt
index 6eaf46c9..5deafe90 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/table/UserRelation.kt
@@ -221,7 +221,7 @@ class UserRelation private constructor() {
// try{
// App1.getDB().delete( table, COL_NAME + "=?", new String[]{ name } );
// }catch( Throwable ex ){
- // log.e( ex, "delete failed." );
+ // warning.e( ex, "delete failed." );
// }
// }
//
@@ -241,7 +241,7 @@ class UserRelation private constructor() {
// }
// }
// }catch( Throwable ex ){
- // log.e(ex,"getNameSet() failed.")
+ // warning.e(ex,"getNameSet() failed.")
// }
// return dst;
// }
@@ -267,7 +267,7 @@ class UserRelation private constructor() {
// cursor.close();
// }
// }catch( Throwable ex ){
- // log.e( ex, "load failed." );
+ // warning.e( ex, "load failed." );
// }
// return false;
// }
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/APNGFrames.kt b/app/src/main/java/jp/juggler/subwaytooter/util/APNGFrames.kt
deleted file mode 100644
index 77ac2ae8..00000000
--- a/app/src/main/java/jp/juggler/subwaytooter/util/APNGFrames.kt
+++ /dev/null
@@ -1,810 +0,0 @@
-package jp.juggler.subwaytooter.util
-
-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.util.Log
-import jp.juggler.apng.*
-
-import java.io.InputStream
-import java.util.ArrayList
-
-class ApngFrames(private val pixelSizeMax : Int = 0) : ApngDecoderCallback {
-
- companion object {
-
- private const val TAG = "ApngFrames"
-
- // ループしない画像の場合は3秒でまたループさせる
- private const val DELAY_AFTER_END = 3000L
-
- // アニメーションフレームの描画に使う
- private val sSrcModePaint : Paint by lazy {
- val paint = Paint()
- paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
- paint.isFilterBitmap = true
- paint
- }
-
- private fun createBlankBitmap(w : Int, h : Int) : Bitmap {
- return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
- }
-
- // WARNING: ownership of "src" will be moved or recycled.
- private fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? {
- if(src == null) return null
-
- if(size_max <= 0) return src
-
- val wSrc = src.width
- val hSrc = src.height
- if(wSrc <= size_max && hSrc <= size_max) return src
-
- val wDst : Int
- val hDst : Int
- if(wSrc >= hSrc) {
- wDst = size_max
- hDst = Math.max(
- 1,
- (size_max.toFloat() * hSrc.toFloat() / wSrc.toFloat() + 0.5f).toInt()
- )
- } else {
- hDst = size_max
- wDst = Math.max(
- 1,
- (size_max.toFloat() * wSrc.toFloat() / hSrc.toFloat() + 0.5f).toInt()
- )
- }
-
- 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, sSrcModePaint)
- src.recycle()
- return b2
- }
-
- private fun toBitmap(src : ApngBitmap) : Bitmap {
- return Bitmap.createBitmap(
- src.colors, // int[] 配列
- 0, // offset
- src.width, //stride
- src.width, // width
- src.height, //height
- Bitmap.Config.ARGB_8888
- )
- }
-
- private fun toBitmap(src : ApngBitmap, size_max : Int) : Bitmap? {
- return scaleBitmap(toBitmap(src), size_max)
- }
-
- @Suppress("unused")
- fun parseApng(inStream : InputStream, size_max : Int) : ApngFrames {
- val result = ApngFrames(size_max)
- try {
- ApngDecoder.parseStream(inStream, result)
- result.onParseComplete()
- return if( result.defaultImage != null || result.frames?.isNotEmpty() == true ){
- result
- }else{
- throw RuntimeException("APNG has no image")
- }
- } catch(ex : Throwable) {
- result.dispose()
- throw ex
- }
-
- }
- }
-
- private var header : ApngImageHeader? = null
- private var animationControl : ApngAnimationControl? = null
-
- val width : Int
- get() = header?.width ?: 0
-
- val height : Int
- get() = header?.height ?: 0
-
- @Suppress("MemberVisibilityCanBePrivate")
- val numFrames : Int
- get() = animationControl?.numFrames ?: 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
-
- private class Frame(
- internal val bitmap : Bitmap,
- internal val time_start : Long,
- internal val time_width : Long
- )
-
- private var frames : ArrayList? = null
-
- @Suppress("unused")
- constructor(bitmap : Bitmap) : this() {
- 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() {
- defaultImage?.recycle()
- canvasBitmap?.recycle()
-
- val frames = this.frames
- if(frames != null) {
- for(f in frames) {
- f.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) {
- // この場合は既に mBitmapNonAnimation が用意されてるはずだ
- result.bitmap = null
- result.delay = Long.MAX_VALUE
- return
- }
-
- val frameCount = frames.size
-
- val isFinite = ! animationControl.isPlayIndefinitely
- val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
- val endWait = if(isFinite) DELAY_AFTER_END else 0L
- val timeTotalLoop = Math.max(1,timeTotal * repeatSequenceCount + endWait)
-
- val tf = (if(0.5f + t < 0f) 0f else 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.time_start,frame.time_start+frame.time_width );
- if(tt < frame.time_start) {
- e = mid
- } else if(tt >= frame.time_start + frame.time_width) {
- 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.time_start + frame.time_width - tt
- result.bitmap = frames[s].bitmap
- result.delay = (0.5f + durationScale * Math.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.time_width,result.delay);
- }
-
- /////////////////////////////////////////////////////
- // implements ApngDecoderCallback
-
- override fun log(message : String) {
- Log.d(ApngFrames.TAG, message)
- }
-
- override fun onHeader(apng : Apng, header : ApngImageHeader) {
- this.header = header
- }
-
- override fun onAnimationInfo(
- apng : Apng,
- animationControl : ApngAnimationControl
- ) {
- this.animationControl = animationControl
-
- val canvasBitmap = ApngFrames.createBlankBitmap(width, height)
- this.canvasBitmap = canvasBitmap
- this.canvas = Canvas(canvasBitmap)
- this.frames = ArrayList(animationControl.numFrames)
- }
-
- override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
- defaultImage?.recycle()
- defaultImage = ApngFrames.toBitmap(bitmap, pixelSizeMax)
- }
-
- override fun onAnimationFrame(
- apng : Apng,
- frameControl : ApngFrameControl,
- bitmap : ApngBitmap
- ) {
- val frames = this.frames ?: return
- val canvasBitmap = this.canvasBitmap ?: return
-
- // APNGのフレーム画像をAndroidの形式に変換する。この段階ではリサイズしない
- val bitmapNative = ApngFrames.toBitmap(bitmap)
-
- val previous : Bitmap? = if(frameControl.disposeOp == DisposeOp.Previous) {
- // Capture the current bitmap region IF it needs to be reverted after rendering
- Bitmap.createBitmap(
- canvasBitmap,
- frameControl.xOffset,
- frameControl.yOffset,
- frameControl.width,
- frameControl.height
- )
- } else {
- null
- }
-
- val paint = if(frameControl.blendOp == BlendOp.Source) {
- ApngFrames.sSrcModePaint // SRC_OVER, not blend
- } else {
- null // (for blend, leave paint null)
- }
-
- // Draw the new frame into place
- canvas.drawBitmap(
- bitmapNative,
- frameControl.xOffset.toFloat(),
- frameControl.yOffset.toFloat(),
- paint
- )
-
- // Extract a drawable from the canvas. Have to copy the current bitmap.
- // Store the drawable in the sequence of frames
- val timeStart = timeTotal
- val timeWidth = Math.max(1L, frameControl.delayMilliseconds)
- timeTotal += timeWidth
-
- val scaledBitmap =
- scaleBitmap(canvasBitmap.copy(Bitmap.Config.ARGB_8888, false), pixelSizeMax)
- if(scaledBitmap != null) {
- frames.add(Frame(scaledBitmap, timeStart, timeWidth))
- }
-
- // Now "dispose" of the frame in preparation for the next.
- // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
-
- when(frameControl.disposeOp) {
- DisposeOp.None -> {
- }
-
- DisposeOp.Background ->
- // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
- //System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
- // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
- //if (true || isFull) {
- canvas.drawColor(0, PorterDuff.Mode.CLEAR) // Clear to fully transparent black
-
- DisposeOp.Previous ->
- // APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
- //System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
- // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
- // Put the original section back
- if(previous != null) {
- canvas.drawBitmap(
- previous, frameControl.xOffset.toFloat(), frameControl.yOffset.toFloat(),
- ApngFrames.sSrcModePaint
- )
- previous.recycle()
- }
-
- else -> {
- // 0: Default should never happen
-
- // APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
- //System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
- // do nothing
- // } else {
- // Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
- // paint = new Paint();
- // paint.setColor(0);
- // paint.setStyle(Paint.Style.FILL);
- // canvas.drawRect(rt, paint);
- // }
-
- }
- }
- }
-
-}
-
-
-//package jp.juggler.subwaytooter.util
-//
-//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 net.ellerton.japng.PngScanlineBuffer
-//import net.ellerton.japng.argb8888.Argb8888Bitmap
-//import net.ellerton.japng.argb8888.Argb8888Processor
-//import net.ellerton.japng.argb8888.Argb8888Processors
-//import net.ellerton.japng.argb8888.Argb8888ScanlineProcessor
-//import net.ellerton.japng.argb8888.BasicArgb8888Director
-//import net.ellerton.japng.chunks.PngAnimationControl
-//import net.ellerton.japng.chunks.PngFrameControl
-//import net.ellerton.japng.chunks.PngHeader
-//import net.ellerton.japng.error.PngException
-//import net.ellerton.japng.reader.DefaultPngChunkReader
-//import net.ellerton.japng.reader.PngReadHelper
-//
-//import java.io.InputStream
-//import java.util.ArrayList
-//
-//// APNGを解釈した結果を保持する
-//// (フレーム数分のbitmapと時間情報)
-//
-//class APNGFrames {
-//
-// companion object {
-//
-// internal val log = LogCategory("APNGFrames")
-//
-// // ループしない画像の場合は3秒でまたループさせる
-// private const val DELAY_AFTER_END = 3000L
-//
-// /**
-// * Keep a 1x1 transparent image around as reference for creating a scaled starting bitmap.
-// * Considering this because of some reported OutOfMemory errors, and this post:
-// *
-// *
-// * http://stackoverflow.com/a/8527745/963195
-// *
-// *
-// * Specifically: "NEVER use Bitmap.createBitmap(width, height, Config.ARGB_8888). I mean NEVER!"
-// *
-// *
-// * Instead the 1x1 image (68 bytes of resources) is scaled up to the needed size.
-// * Whether or not this fixes the OOM problems is TBD...
-// */
-// //static Bitmap sOnePxTransparent;
-// internal val sSrcModePaint : Paint by lazy{
-// val paint = Paint()
-// paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
-// paint.isFilterBitmap = true
-// paint
-// }
-//
-// internal fun createBlankBitmap(w : Int, h : Int) : Bitmap {
-// return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
-// }
-//
-// // WARNING: ownership of "src" will be moved or recycled.
-// internal fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? {
-// if(src == null) return null
-//
-// val src_w = src.width
-// val src_h = src.height
-// if(src_w <= size_max && src_h <= size_max) return src
-//
-// var dst_w : Int
-// var dst_h : Int
-// if(src_w >= src_h) {
-// dst_w = size_max
-// dst_h = (0.5f + src_h * size_max / src_w.toFloat()).toInt()
-// if(dst_h < 1) dst_h = 1
-// } else {
-// dst_h = size_max
-// dst_w = (0.5f + src_w * size_max / src_h.toFloat()).toInt()
-// if(dst_w < 1) dst_w = 1
-// }
-//
-// // この方法だとリークがあるらしい???
-// // http://stackoverflow.com/a/8527745/963195
-// // return Bitmap.createScaledBitmap( src, dst_w , dst_h , true );
-//
-// val b2 = createBlankBitmap(dst_w, dst_h)
-// val canvas = Canvas(b2)
-// val rect_src = Rect(0, 0, src_w, src_h)
-// val rect_dst = Rect(0, 0, dst_w, dst_h)
-// canvas.drawBitmap(src, rect_src, rect_dst, sSrcModePaint)
-// src.recycle()
-// return b2
-// }
-//
-// internal fun toBitmap(src : Argb8888Bitmap) : Bitmap {
-// val offset = 0
-// val stride = src.width
-// return Bitmap.createBitmap(src.pixelArray, offset, stride, src.width, src.height, Bitmap.Config.ARGB_8888)
-// }
-//
-// internal fun toBitmap(src : Argb8888Bitmap, size_max : Int) : Bitmap? {
-// return scaleBitmap(toBitmap(src), size_max)
-// }
-//
-// /////////////////////////////////////////////////////////////////////
-//
-// // entry point is here
-// @Throws(PngException::class)
-// internal fun parseAPNG( inStream : InputStream, size_max : Int) : APNGFrames? {
-// val handler = APNGParseEventHandler(size_max)
-// try {
-// val processor = Argb8888Processor(handler)
-// val reader = DefaultPngChunkReader(processor)
-// val result = PngReadHelper.read(inStream, reader)
-// result?.onParseComplete()
-// return result
-// } catch(ex : Throwable) {
-// handler.dispose()
-// throw ex
-// }
-//
-// }
-// }
-//
-// // ピクセルサイズ制限
-// private var mPixelSizeMax : Int = 0
-//
-// // APNGじゃなかった場合に使われる
-// private var mBitmapNonAnimation : Bitmap? = null
-//
-// private lateinit var header : PngHeader
-// private lateinit var scanlineProcessor : Argb8888ScanlineProcessor
-// private lateinit var canvas : Canvas
-//
-// private var canvasBitmap : Bitmap? = null
-// private var currentFrame : PngFrameControl? = null
-// private var animationControl : PngAnimationControl? = null
-//
-// private var time_total = 0L
-//
-// private var frames : ArrayList? = null
-//
-// ///////////////////////////////////////////////////////////////
-//
-// // 再生速度の調整
-// private var durationScale = 1f
-//
-// private val numFrames : Int
-// get() = animationControl?.numFrames ?: 1
-//
-// val hasMultipleFrame : Boolean
-// get() = numFrames > 1
-//
-// private class Frame(
-// internal val bitmap : Bitmap,
-// internal val time_start : Long,
-// internal val time_width : Long
-// )
-//
-// ///////////////////////////////////////////////////////////////
-//
-// internal constructor(bitmap : Bitmap) {
-// this.mBitmapNonAnimation = bitmap
-// }
-//
-// internal constructor(
-// header : PngHeader, scanlineProcessor : Argb8888ScanlineProcessor, animationControl : PngAnimationControl, size_max : Int
-// ) {
-// this.header = header
-// this.scanlineProcessor = scanlineProcessor
-// this.animationControl = animationControl
-// this.mPixelSizeMax = size_max
-//
-// this.canvasBitmap = createBlankBitmap(header.width, header.height)
-// this.canvas = Canvas(this.canvasBitmap)
-// this.frames = ArrayList(animationControl.numFrames)
-//
-// }
-//
-// internal fun onParseComplete() {
-// val frames = this.frames
-//
-// if(frames != null && frames.size <= 1) {
-// mBitmapNonAnimation = toBitmap(scanlineProcessor.bitmap, mPixelSizeMax)
-// }
-//
-// canvasBitmap?.recycle()
-// canvasBitmap = null
-// }
-//
-// internal fun dispose() {
-// mBitmapNonAnimation?.recycle()
-// canvasBitmap?.recycle()
-//
-// val frames = this.frames
-// if(frames != null) {
-// for(f in frames) {
-// f.bitmap.recycle()
-// }
-// }
-// }
-//
-// // フレームが追加される
-// internal fun beginFrame(frameControl : PngFrameControl) : Argb8888ScanlineProcessor {
-// currentFrame = frameControl
-// return scanlineProcessor.cloneWithSharedBitmap(header.adjustFor(currentFrame))
-// }
-//
-// // フレームが追加される
-// internal fun completeFrame(frameImage : Argb8888Bitmap) {
-//
-// val frames = this.frames ?: return
-//
-// val canvasBitmap = this.canvasBitmap ?: return
-//
-// val currentFrame = this.currentFrame ?: return
-// this.currentFrame = null
-//
-// // APNGのフレーム画像をAndroidの形式に変換する
-// val frame = toBitmap(frameImage)
-//
-// var previous : Bitmap? = null
-// // Capture the current bitmap region IF it needs to be reverted after rendering
-// if(2 == currentFrame.disposeOp.toInt()) {
-// previous = Bitmap.createBitmap(canvasBitmap, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height) // or could use from frames?
-// //System.out.println(String.format("Captured previous %d x %d", previous.getWidth(), previous.getHeight()));
-// }
-//
-// var paint : Paint? = null // (for blend, leave paint null)
-// if(0 == currentFrame.blendOp.toInt()) { // SRC_OVER, not blend
-// paint = sSrcModePaint
-// }
-//
-// // boolean isFull = currentFrame.height == header.height && currentFrame.width == header.width;
-//
-// // Draw the new frame into place
-// canvas.drawBitmap(frame, currentFrame.xOffset.toFloat(), currentFrame.yOffset.toFloat(), paint)
-//
-// // Extract a drawable from the canvas. Have to copy the current bitmap.
-// // Store the drawable in the sequence of frames
-// val time_start = time_total
-// var time_width = currentFrame.delayMilliseconds.toLong()
-// if(time_width <= 0L) time_width = 1L
-// time_total = time_start + time_width
-//
-// val scaledBitmap = scaleBitmap(canvasBitmap.copy(Bitmap.Config.ARGB_8888, false), mPixelSizeMax)
-// if(scaledBitmap != null) {
-// frames.add(Frame(scaledBitmap, time_start, time_width))
-// }
-//
-// // Now "dispose" of the frame in preparation for the next.
-// // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
-//
-// when(currentFrame.disposeOp.toInt()) {
-//
-// 1 ->
-// // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
-// //System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
-// // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
-// //if (true || isFull) {
-// canvas.drawColor(0, PorterDuff.Mode.CLEAR) // Clear to fully transparent black
-//
-// 2 ->
-// // APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
-// //System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
-// // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
-// // Put the original section back
-// if(previous != null) {
-// canvas.drawBitmap(previous, currentFrame.xOffset.toFloat(), currentFrame.yOffset.toFloat(), sSrcModePaint)
-// previous.recycle()
-// }
-//
-// else -> {
-// // 0: Default should never happen
-//
-// // APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
-// //System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
-// // do nothing
-// // } else {
-// // Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
-// // paint = new Paint();
-// // paint.setColor(0);
-// // paint.setStyle(Paint.Style.FILL);
-// // canvas.drawRect(rt, paint);
-// // }
-//
-// }
-// }
-// }
-//
-// class FindFrameResult {
-// var bitmap : Bitmap? = null // may null
-// var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
-// }
-//
-// // シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する
-// fun findFrame(result : FindFrameResult, t : Long) {
-//
-// if(mBitmapNonAnimation != null) {
-// result.bitmap = mBitmapNonAnimation
-// result.delay = Long.MAX_VALUE
-// return
-// }
-//
-// val animationControl = this.animationControl
-// val frames = this.frames
-// if(animationControl == null || frames == null) {
-// // この場合は既に mBitmapNonAnimation が用意されてるはずだ
-// result.bitmap = null
-// result.delay = Long.MAX_VALUE
-// return
-// }
-//
-// val frame_count = frames.size
-//
-// val isFinite = ! animationControl.loopForever()
-// val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
-// val end_wait = if(isFinite) DELAY_AFTER_END else 0L
-// var loop_total = time_total * repeatSequenceCount + end_wait
-// if(loop_total <= 0) loop_total = 1
-//
-// val tf = (if(0.5f + t < 0f) 0f else t / durationScale).toLong()
-//
-// // 全体の繰り返し時刻で余りを計算
-// val tl = tf % loop_total
-// if(tl >= loop_total - end_wait) {
-// // 終端で待機状態
-// result.bitmap = frames[frame_count - 1].bitmap
-// result.delay = (0.5f + (loop_total - tl) * durationScale).toLong()
-// return
-// }
-// // 1ループの繰り返し時刻で余りを計算
-// val tt = tl % time_total
-//
-// // フレームリストを時刻で二分探索
-// var s = 0
-// var e = frame_count
-// 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.time_start,frame.time_start+frame.time_width );
-// if(tt < frame.time_start) {
-// e = mid
-// } else if(tt >= frame.time_start + frame.time_width) {
-// s = mid + 1
-// } else {
-// s = mid
-// break
-// }
-// }
-// s = if(s < 0) 0 else if(s >= frame_count - 1) frame_count - 1 else s
-// val frame = frames[s]
-// val delay = frame.time_start + frame.time_width - tt
-// result.bitmap = frames[s].bitmap
-// result.delay = (0.5f + durationScale * Math.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,time_total,s,frame.time_width,result.delay);
-// }
-//
-// /////////////////////////////////////////////////////////////////////
-//
-// // APNGのパース中に随時呼び出される
-// internal class APNGParseEventHandler(
-// private val size_max : Int
-// ) : BasicArgb8888Director() {
-//
-// private lateinit var header : PngHeader
-//
-// private var frames : APNGFrames? = null
-//
-// private val isAnimated : Boolean
-// get() = frames != null
-//
-// // ヘッダが分かった
-// @Throws(PngException::class)
-// override fun receiveHeader(header : PngHeader, buffer : PngScanlineBuffer) {
-// this.header = header
-//
-// // 親クラスのprotectedフィールドを更新する
-// val pngBitmap = Argb8888Bitmap(header.width, header.height)
-// this.scanlineProcessor = Argb8888Processors.from(header, buffer, pngBitmap)
-// }
-//
-// // デフォルト画像の手前で呼ばれる
-// override fun beforeDefaultImage() : Argb8888ScanlineProcessor {
-// return scanlineProcessor
-// }
-//
-// // デフォルト画像が分かった
-// // おそらく receiveAnimationControl より先に呼ばれる
-// override fun receiveDefaultImage(defaultImage : Argb8888Bitmap) {
-// // japng ライブラリの返すデフォルトイメージはあまり信用できないので使わない
-// }
-//
-// // アニメーション制御情報が分かった
-// override fun receiveAnimationControl(animationControl : PngAnimationControl) {
-// this.frames = APNGFrames(header , scanlineProcessor, animationControl, size_max)
-// }
-//
-// override fun wantDefaultImage() : Boolean {
-// return ! isAnimated
-// }
-//
-// override fun wantAnimationFrames() : Boolean {
-// return true // isAnimated;
-// }
-//
-// // フレーム制御情報が分かった
-// override fun receiveFrameControl(frameControl : PngFrameControl) : Argb8888ScanlineProcessor {
-// val frames = this.frames ?: throw RuntimeException("not animation image")
-// return frames.beginFrame(frameControl)
-// }
-//
-// // フレーム画像が分かった
-// override fun receiveFrameImage(frameImage : Argb8888Bitmap) {
-// val frames = this.frames ?: throw RuntimeException("not animation image")
-// frames.completeFrame(frameImage)
-// }
-//
-// // 結果を取得する
-// override fun getResult() : APNGFrames? {
-// val frames = this.frames
-// return if( frames?.hasMultipleFrame == true ){
-// frames
-// }else {
-// dispose()
-// return null
-// }
-// }
-//
-// // 処理中に例外が起きた場合、Bitmapリソースを解放する
-// fun dispose() {
-// frames?.dispose()
-// frames = null
-// }
-// }
-//
-//}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt
index d6c1aa27..2ce7506c 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt
@@ -14,7 +14,7 @@ import java.util.concurrent.ConcurrentHashMap
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
-
+import jp.juggler.apng.ApngFrames
class CustomEmojiCache(internal val context : Context) {
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt
index 21f28478..d680b4dc 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostHelper.kt
@@ -446,7 +446,7 @@ class PostHelper(
val part = src.substring(last_sharp + 1, end)
if(reCharsNotTag.matcher(part).find()) {
- // log.d( "checkTag: character not tag in string %s", part );
+ // warning.d( "checkTag: character not tag in string %s", part );
checkEmoji()
return
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt
index f5c2626b..4243335d 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/Utils.kt
@@ -54,7 +54,7 @@ object Utils {
// if( iv != 0 ) return iv;
// }catch(Throwable ex){
// }
- // log.e("missing resid for %s",name);
+ // warning.e("missing resid for %s",name);
// return R.string.Dialog_Cancel;
// }
@@ -547,7 +547,7 @@ fun String.digestSHA256() : String {
// sb.append( String.format( Locale.JAPAN, "%dk", n ) );
// t -= n * 1000L;
// }
-// // remain
+// // length
// if( sb.length() > 0 ){
// sb.append( String.format( Locale.JAPAN, "%03d", t ) );
// }else if( n > 0 ){
diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt b/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt
index 8e2fff53..ed1724c0 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt
@@ -51,7 +51,7 @@ class VersionString(src : String?) {
companion object {
- // private val log = new LogCategory( "VersionString" )
+ // private val warning = new LogCategory( "VersionString" )
private fun isDelimiter(c : Char) : Boolean {
return c == '.' || c == ' '
diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt b/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt
index 91c3e370..cf2979a8 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt
@@ -116,7 +116,7 @@ class MyNetworkImageView : AppCompatImageView {
val d = drawable
if(d is Animatable) {
if(d.isRunning) {
- //log.d("cancelLoading: Animatable.stop()")
+ //warning.d("cancelLoading: Animatable.stop()")
d.stop()
}
}
diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/NetworkEmojiView.kt b/app/src/main/java/jp/juggler/subwaytooter/view/NetworkEmojiView.kt
index 80e0234f..b880caf0 100644
--- a/app/src/main/java/jp/juggler/subwaytooter/view/NetworkEmojiView.kt
+++ b/app/src/main/java/jp/juggler/subwaytooter/view/NetworkEmojiView.kt
@@ -9,10 +9,11 @@ import android.os.SystemClock
import android.util.AttributeSet
import android.view.View
-import jp.juggler.subwaytooter.App1
-import jp.juggler.subwaytooter.util.ApngFrames
import java.lang.ref.WeakReference
+import jp.juggler.apng.ApngFrames
+import jp.juggler.subwaytooter.App1
+
class NetworkEmojiView : View {
constructor(context : Context) : super(context)
diff --git a/app/src/test/java/jp/juggler/subwaytooter/TestKotlinFeature.kt b/app/src/test/java/jp/juggler/subwaytooter/TestKotlinFeature.kt
index b7b88354..f4b94648 100644
--- a/app/src/test/java/jp/juggler/subwaytooter/TestKotlinFeature.kt
+++ b/app/src/test/java/jp/juggler/subwaytooter/TestKotlinFeature.kt
@@ -179,7 +179,7 @@ class TestKotlinFeature {
/*
蛇足だが、クラスファイルを読むのは
app/build/tmp/kotlin-classes/*UnitTest\**/TestKotlinFeature.class を
- javap.exe -c TestKotlinFeature.class > javap.log とすると逆アセンブルできる
+ javap.exe -c TestKotlinFeature.class > javap.warning とすると逆アセンブルできる
*/
}
diff --git a/settings.gradle b/settings.gradle
index 9f8d0af2..bb831a81 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':app', ':exif', ':colorpicker', ':emoji', ':apng'
+include ':app', ':exif', ':colorpicker', ':emoji', ':apng', ':apng_android'