diff --git a/apng/src/main/java/jp/juggler/apng/GifDecoder.kt b/apng/src/main/java/jp/juggler/apng/MyGifDecoder.kt similarity index 96% rename from apng/src/main/java/jp/juggler/apng/GifDecoder.kt rename to apng/src/main/java/jp/juggler/apng/MyGifDecoder.kt index 3fe754f1..0e5ab1aa 100644 --- a/apng/src/main/java/jp/juggler/apng/GifDecoder.kt +++ b/apng/src/main/java/jp/juggler/apng/MyGifDecoder.kt @@ -10,7 +10,7 @@ import kotlin.math.min // http://www.theimage.com/animation/pages/disposal3.html // great sample images. -class GifDecoder(val callback: GifDecoderCallback) { +class MyGifDecoder(val callback: MyGifDecoderCallback) { private class Rectangle(var x: Int = 0, var y: Int = 0, var w: Int = 0, var h: Int = 0) { diff --git a/apng/src/main/java/jp/juggler/apng/GifDecoderCallback.kt b/apng/src/main/java/jp/juggler/apng/MyGifDecoderCallback.kt similarity index 88% rename from apng/src/main/java/jp/juggler/apng/GifDecoderCallback.kt rename to apng/src/main/java/jp/juggler/apng/MyGifDecoderCallback.kt index db8fc7cf..83034fe8 100644 --- a/apng/src/main/java/jp/juggler/apng/GifDecoderCallback.kt +++ b/apng/src/main/java/jp/juggler/apng/MyGifDecoderCallback.kt @@ -1,10 +1,10 @@ package jp.juggler.apng -interface GifDecoderCallback { +interface MyGifDecoderCallback { fun onGifWarning(message: String) fun onGifDebug(message: String) fun canGifDebug(): Boolean fun onGifHeader(header: ApngImageHeader) fun onGifAnimationInfo(header: ApngImageHeader, animationControl: ApngAnimationControl) fun onGifAnimationFrame(frameControl: ApngFrameControl, frameBitmap: ApngBitmap) -} \ No newline at end of file +} diff --git a/apng_android/build.gradle b/apng_android/build.gradle index 2d8f6384..b07e3bfe 100644 --- a/apng_android/build.gradle +++ b/apng_android/build.gradle @@ -43,4 +43,6 @@ repositories { dependencies { api project(":apng") implementation project(":base") + + implementation "com.github.zjupure:webpdecoder:2.3.$glideVersion" } diff --git a/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt index ba4cc1b6..23125474 100644 --- a/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt +++ b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt @@ -2,6 +2,7 @@ package jp.juggler.apng import android.graphics.* import android.util.Log +import jp.juggler.util.data.encodeUTF8 import java.io.InputStream import kotlin.math.max import kotlin.math.min @@ -9,7 +10,7 @@ import kotlin.math.min class ApngFrames private constructor( private val pixelSizeMax: Int = 0, private val debug: Boolean = false, -) : ApngDecoderCallback, GifDecoderCallback { +) : ApngDecoderCallback, MyGifDecoderCallback { companion object { @@ -102,6 +103,23 @@ class ApngFrames private constructor( } } + private fun parseWebP( + inStream: InputStream, + pixelSizeMax: Int, + debug: Boolean = false, + ): ApngFrames { + val result = ApngFrames(pixelSizeMax, debug) + try { + MyWebPDecoder(result).parse(inStream) + result.onParseComplete() + return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true } + ?: error("WebP has no image") + } catch (ex: Throwable) { + result.dispose() + throw ex + } + } + private fun parseGif( inStream: InputStream, pixelSizeMax: Int, @@ -109,7 +127,7 @@ class ApngFrames private constructor( ): ApngFrames { val result = ApngFrames(pixelSizeMax, debug) try { - GifDecoder(result).parse(inStream) + MyGifDecoder(result).parse(inStream) result.onParseComplete() return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true } ?: error("GIF has no image") @@ -120,6 +138,8 @@ class ApngFrames private constructor( } private val apngHeadKey = byteArrayOf(0x89.toByte(), 0x50) + private val webpHeadKey1 = "RIFF".encodeUTF8() + private val webpHeadKey2 = "WEBP".encodeUTF8() private val gifHeadKey = "GIF".toByteArray(Charsets.UTF_8) private fun matchBytes( @@ -133,19 +153,36 @@ class ApngFrames private constructor( return true } + private fun matchBytesOffset( + ba1: ByteArray, + ba1Offset: Int, + ba2: ByteArray, + length: Int = ba2.size, + ): Boolean { + for (i in 0 until length) { + if (ba1[i + ba1Offset] != ba2[i]) return false + } + return true + } + fun parse( pixelSizeMax: Int, debug: Boolean = false, opener: () -> InputStream?, ): ApngFrames? { - val buf = ByteArray(8) { 0.toByte() } + val buf = ByteArray(12) { 0.toByte() } opener()?.use { it.read(buf, 0, buf.size) } if (buf.size >= 8 && matchBytes(buf, apngHeadKey)) { return opener()?.use { parseApng(it, pixelSizeMax, debug) } } - + if (buf.size >= 12 && + matchBytesOffset(buf, 0, webpHeadKey1) && + matchBytesOffset(buf, 8, webpHeadKey2) + ) { + return opener()?.use { parseWebP(it, pixelSizeMax, debug) } + } if (buf.size >= 6 && matchBytes(buf, gifHeadKey)) { return opener()?.use { parseGif(it, pixelSizeMax, debug) } } diff --git a/apng_android/src/main/java/jp/juggler/apng/MyWebPDecoder.kt b/apng_android/src/main/java/jp/juggler/apng/MyWebPDecoder.kt new file mode 100644 index 00000000..ca1ef417 --- /dev/null +++ b/apng_android/src/main/java/jp/juggler/apng/MyWebPDecoder.kt @@ -0,0 +1,94 @@ +package jp.juggler.apng + +import android.graphics.Bitmap +import com.bumptech.glide.gifdecoder.GifDecoder +import com.bumptech.glide.integration.webp.WebpImage +import com.bumptech.glide.integration.webp.decoder.WebpDecoder +import java.io.InputStream +import java.nio.ByteBuffer +import kotlin.math.max + +class MyWebPDecoder(val callback: MyGifDecoderCallback) { + + private val bitmapProvider = object : GifDecoder.BitmapProvider { + override fun obtainByteArray(size: Int) = ByteArray(size) + override fun obtainIntArray(size: Int) = IntArray(size) + override fun obtain(width: Int, height: Int, config: Bitmap.Config): Bitmap = + Bitmap.createBitmap(width, height, config) + + override fun release(bitmap: Bitmap) { + bitmap.recycle() + } + + override fun release(bytes: ByteArray) { + } + + override fun release(array: IntArray) { + } + } + + fun parse(src: InputStream) { + val decoder = WebpDecoder( + bitmapProvider, + WebpImage.create(src.readBytes()) ?: error("WebpImage.create returns null."), + ByteBuffer.allocate(0), + 1 // sampleSize。元データのサイズをこの数字で割る。 + ) + try { + val frameCount = decoder.frameCount + if (frameCount < 1) error("webp has no frames.") + + if (decoder.width < 1 || decoder.height < 1) error("too small size.") + // logical screen size + + // list of pair of ApngFrameControl, ApngBitmap + repeat(frameCount) { frameNumber -> + decoder.advance() + val frameDelay = decoder.nextDelay + val bitmap = decoder.nextFrame!! + try { + val srcWidth = bitmap.width + val srcHeight = bitmap.height + if (frameNumber == 0) { + val header = ApngImageHeader( + width = srcWidth, + height = srcHeight, + bitDepth = 8, + colorType = ColorType.INDEX, + compressionMethod = CompressionMethod.Standard, + filterMethod = FilterMethod.Standard, + interlaceMethod = InterlaceMethod.None + ) + val animationControl = ApngAnimationControl( + numFrames = frameCount, + numPlays = max(0, decoder.netscapeLoopCount) // 0 means infinite + ) + + callback.onGifHeader(header) + callback.onGifAnimationInfo(header, animationControl) + } + callback.onGifAnimationFrame( + ApngFrameControl( + width = srcWidth, + height = srcHeight, + xOffset = 0, + yOffset = 0, + disposeOp = DisposeOp.None, + blendOp = BlendOp.Source, + sequenceNumber = frameNumber, + delayMilliseconds = frameDelay.toLong(), + ), + ApngBitmap(srcWidth, srcHeight).also { dst -> + bitmap.getPixels(dst.colors, 0, dst.width, 0, 0, srcWidth, srcHeight) + } + ) + } finally { + bitmap.recycle() + } + } + } finally { + decoder.clear() + // WebPImageはdecoderがdisposeする + } + } +} diff --git a/app/build.gradle b/app/build.gradle index 7e8f2955..9094354a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -135,7 +135,6 @@ dependencies { implementation project(':apng_android') implementation fileTree(include: ['*.aar'], dir: 'src/main/libs') - // App1 とサーバ情報カラムで使う api "org.conscrypt:conscrypt-android:2.5.2" 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 6aca4bef..396aee99 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt @@ -464,7 +464,7 @@ class CustomEmojiCache( val errors = ArrayList() try { - // APNGをデコード + // APNGをデコード AWebPも val x = ApngFrames.parse(64) { ByteArrayInputStream(data) } if (x != null) return x error("ApngFrames.parse returns null.")