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
|
// http://www.theimage.com/animation/pages/disposal3.html
|
||||||
// great sample images.
|
// 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) {
|
private class Rectangle(var x: Int = 0, var y: Int = 0, var w: Int = 0, var h: Int = 0) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package jp.juggler.apng
|
package jp.juggler.apng
|
||||||
|
|
||||||
interface GifDecoderCallback {
|
interface MyGifDecoderCallback {
|
||||||
fun onGifWarning(message: String)
|
fun onGifWarning(message: String)
|
||||||
fun onGifDebug(message: String)
|
fun onGifDebug(message: String)
|
||||||
fun canGifDebug(): Boolean
|
fun canGifDebug(): Boolean
|
|
@ -43,4 +43,6 @@ repositories {
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":apng")
|
api project(":apng")
|
||||||
implementation project(":base")
|
implementation project(":base")
|
||||||
|
|
||||||
|
implementation "com.github.zjupure:webpdecoder:2.3.$glideVersion"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package jp.juggler.apng
|
||||||
|
|
||||||
import android.graphics.*
|
import android.graphics.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import jp.juggler.util.data.encodeUTF8
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
@ -9,7 +10,7 @@ import kotlin.math.min
|
||||||
class ApngFrames private constructor(
|
class ApngFrames private constructor(
|
||||||
private val pixelSizeMax: Int = 0,
|
private val pixelSizeMax: Int = 0,
|
||||||
private val debug: Boolean = false,
|
private val debug: Boolean = false,
|
||||||
) : ApngDecoderCallback, GifDecoderCallback {
|
) : ApngDecoderCallback, MyGifDecoderCallback {
|
||||||
|
|
||||||
companion object {
|
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(
|
private fun parseGif(
|
||||||
inStream: InputStream,
|
inStream: InputStream,
|
||||||
pixelSizeMax: Int,
|
pixelSizeMax: Int,
|
||||||
|
@ -109,7 +127,7 @@ class ApngFrames private constructor(
|
||||||
): ApngFrames {
|
): ApngFrames {
|
||||||
val result = ApngFrames(pixelSizeMax, debug)
|
val result = ApngFrames(pixelSizeMax, debug)
|
||||||
try {
|
try {
|
||||||
GifDecoder(result).parse(inStream)
|
MyGifDecoder(result).parse(inStream)
|
||||||
result.onParseComplete()
|
result.onParseComplete()
|
||||||
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
|
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
|
||||||
?: error("GIF has no image")
|
?: error("GIF has no image")
|
||||||
|
@ -120,6 +138,8 @@ class ApngFrames private constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val apngHeadKey = byteArrayOf(0x89.toByte(), 0x50)
|
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 val gifHeadKey = "GIF".toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
private fun matchBytes(
|
private fun matchBytes(
|
||||||
|
@ -133,19 +153,36 @@ class ApngFrames private constructor(
|
||||||
return true
|
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(
|
fun parse(
|
||||||
pixelSizeMax: Int,
|
pixelSizeMax: Int,
|
||||||
debug: Boolean = false,
|
debug: Boolean = false,
|
||||||
opener: () -> InputStream?,
|
opener: () -> InputStream?,
|
||||||
): ApngFrames? {
|
): ApngFrames? {
|
||||||
|
|
||||||
val buf = ByteArray(8) { 0.toByte() }
|
val buf = ByteArray(12) { 0.toByte() }
|
||||||
opener()?.use { it.read(buf, 0, buf.size) }
|
opener()?.use { it.read(buf, 0, buf.size) }
|
||||||
|
|
||||||
if (buf.size >= 8 && matchBytes(buf, apngHeadKey)) {
|
if (buf.size >= 8 && matchBytes(buf, apngHeadKey)) {
|
||||||
return opener()?.use { parseApng(it, pixelSizeMax, debug) }
|
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)) {
|
if (buf.size >= 6 && matchBytes(buf, gifHeadKey)) {
|
||||||
return opener()?.use { parseGif(it, pixelSizeMax, debug) }
|
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 project(':apng_android')
|
||||||
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
|
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
|
||||||
|
|
||||||
|
|
||||||
// App1 とサーバ情報カラムで使う
|
// App1 とサーバ情報カラムで使う
|
||||||
api "org.conscrypt:conscrypt-android:2.5.2"
|
api "org.conscrypt:conscrypt-android:2.5.2"
|
||||||
|
|
||||||
|
|
|
@ -464,7 +464,7 @@ class CustomEmojiCache(
|
||||||
val errors = ArrayList<Throwable>()
|
val errors = ArrayList<Throwable>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// APNGをデコード
|
// APNGをデコード AWebPも
|
||||||
val x = ApngFrames.parse(64) { ByteArrayInputStream(data) }
|
val x = ApngFrames.parse(64) { ByteArrayInputStream(data) }
|
||||||
if (x != null) return x
|
if (x != null) return x
|
||||||
error("ApngFrames.parse returns null.")
|
error("ApngFrames.parse returns null.")
|
||||||
|
|
Loading…
Reference in New Issue