Animated WebP 絵文字のデコードに対応
This commit is contained in:
parent
84c9cfcc22
commit
29f68216cf
|
@ -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) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -43,4 +43,6 @@ repositories {
|
|||
dependencies {
|
||||
api project(":apng")
|
||||
implementation project(":base")
|
||||
|
||||
implementation "com.github.zjupure:webpdecoder:2.3.$glideVersion"
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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する
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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.")
|
||||
|
|
Loading…
Reference in New Issue