Animated WebP 絵文字のデコードに対応

This commit is contained in:
tateisu 2023-01-16 04:00:34 +09:00
parent 84c9cfcc22
commit 29f68216cf
7 changed files with 141 additions and 9 deletions

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -43,4 +43,6 @@ repositories {
dependencies {
api project(":apng")
implementation project(":base")
implementation "com.github.zjupure:webpdecoder:2.3.$glideVersion"
}

View File

@ -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) }
}

View File

@ -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する
}
}
}

View File

@ -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"

View File

@ -464,7 +464,7 @@ class CustomEmojiCache(
val errors = ArrayList<Throwable>()
try {
// APNGをデコード
// APNGをデコード AWebPも
val x = ApngFrames.parse(64) { ByteArrayInputStream(data) }
if (x != null) return x
error("ApngFrames.parse returns null.")