Skip to content

Commit

Permalink
Merge pull request #1931 from samunohito/hotfix/fix_apng_decode_error
Browse files Browse the repository at this point in the history
Fix: アプリ内から正常に表示できないAPNGを表示できるようにする
  • Loading branch information
pantasystem authored Oct 27, 2023
2 parents dfa5816 + beeb2e3 commit 4035b42
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 62 deletions.
2 changes: 1 addition & 1 deletion modules/common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ dependencies {
implementation libs.glide.glide
kapt libs.glide.compiler
implementation "com.google.accompanist:accompanist-glide:0.14.0"
implementation 'com.github.penfeizhou.android.animation:apng:2.23.0'
implementation 'com.github.penfeizhou.android.animation:apng:2.24.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation libs.okhttp3.logging.inspector

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ import java.nio.ByteBuffer
class MiGlideModule : AppGlideModule(){

override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val decoder = ByteBufferApngDecoder()
registry
.prepend(InputStream::class.java, FrameSeqDecoder::class.java, StreamApngDecoder(decoder))
.prepend(ByteBuffer::class.java, FrameSeqDecoder::class.java, decoder)
.prepend(InputStream::class.java, FrameSeqDecoder::class.java, StreamApngDecoder())
.prepend(ByteBuffer::class.java, FrameSeqDecoder::class.java, ByteBufferApngDecoder())
.register(FrameSeqDecoder::class.java, Drawable::class.java, FrameSeqDecoderDrawableTranscoder())
.register(FrameSeqDecoder::class.java, Bitmap::class.java, FrameSeqDecoderBitmapTranscoder(glide))
.register(SVG::class.java, BitmapDrawable::class.java, SvgBitmapTransCoder(context))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,132 @@ import com.github.penfeizhou.animation.loader.Loader
import java.nio.ByteBuffer


const val PNG: Long = 0x89504E47
class ByteBufferApngDecoder : ResourceDecoder<ByteBuffer, FrameSeqDecoder<*, *>> {
override fun decode(
source: ByteBuffer,
width: Int,
height: Int,
options: Options
): Resource<FrameSeqDecoder<*, *>> {
return ApngDecoderDelegate.decode(source, width, height, options)
}

override fun handles(source: ByteBuffer, options: Options): Boolean {
return ApngDecoderDelegate.handles(source, options)
}
}

class ByteBufferApngDecoder : ResourceDecoder<ByteBuffer, FrameSeqDecoder<*, *>> {
object ApngDecoderDelegate {
@JvmStatic
private val PNGHeaderBytes =
listOf(0x89, 0x50, 0x4E, 0x47)
.map { it.toByte() }
.toByteArray()

@JvmStatic
private val IENDBytes =
listOf(0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82)
.map { it.toByte() }
.toByteArray()

override fun decode(
@JvmStatic
fun decode(
source: ByteBuffer,
width: Int,
height: Int,
options: Options
): Resource<FrameSeqDecoder<*, *>> {
val sourceBytes = source.array()
val iEndChunkPos = findIEndChunkPosition(sourceBytes)

// IENDチャンクより後にバイト配列がある状態でライブラリに渡すと例外が発生するため、
// IENDチャンクの終わる位置が終端となるよう切り詰めておく
source.limit(iEndChunkPos + IENDBytes.size)

val loader: Loader = object : ByteBufferLoader() {
override fun getByteBuffer(): ByteBuffer {
source.position(0)
return source
}
}

val decoder = APNGDecoder(loader, null)
return FrameSeqDecoderResource(decoder, source.limit())
}

override fun handles(source: ByteBuffer, options: Options): Boolean {
val byteBufferArray = ByteArray(8)
source.get(byteBufferArray, 0, 4)
val header = ByteBuffer.wrap(byteBufferArray).long ushr 32
if (header != PNG) {
@JvmStatic
fun handles(source: ByteBuffer, options: Options): Boolean {
val sourceBytes = source.array()
if (!checkHeaderBytes(sourceBytes)) {
// PNGのヘッダではない
return false
}

if (findIEndChunkPosition(sourceBytes) < 0) {
// IENDチャンクを持たない
return false
}

return APNGParser.isAPNG(ByteBufferReader(source))
}

/**
* [source]の先頭がPNGヘッダかどうかを確認する。
* PNGヘッダであった場合はtrue、そうでない場合はfalseを返却する。
*/
@JvmStatic
private fun checkHeaderBytes(source: ByteArray): Boolean {
val headerBytes = source.sliceArray(0..PNGHeaderBytes.size)
return headerBytes.contentEquals(PNGHeaderBytes)
}

private class FrameSeqDecoderResource(
private val decoder: FrameSeqDecoder<*, *>,
private val size: Int
) : Resource<FrameSeqDecoder<*, *>> {
override fun getResourceClass(): Class<FrameSeqDecoder<*, *>> {
return FrameSeqDecoder::class.java
/**
* [source]からIENDチャンクを検索し、IENDチャンクの先頭のインデックスを返却する。
* IENDチャンクが見つからない場合は-1を返却する。
*/
@JvmStatic
private fun findIEndChunkPosition(source: ByteArray): Int {
if (source.size < IENDBytes.size) {
return -1
}

override fun get(): FrameSeqDecoder<*, *> {
return decoder
}
// 計算量をなるべく抑えたいので、IENDチャンクのサイズを除外した位置から検索する。
// IENDチャンクが終端にある正常なファイルの場合はループせず1回で抜けるはず…
val startPos = source.size - IENDBytes.size
for (idx in (0..startPos).reversed()) {
val curByte = source[idx]

override fun getSize(): Int {
return size
// 現在のインデックスから取れるバイト値がIENDチャンクの先頭と合致していた場合は、
// その位置からIENDチャンクと同じサイズ分だけ配列をスライスし、配列の中身が完全に一致するかを確認する
if (curByte == IENDBytes[0]) {
val hitTestTargetBytes = source.sliceArray(idx until (idx + IENDBytes.size))
if (hitTestTargetBytes.contentEquals(IENDBytes)) {
return idx
}
}
}

override fun recycle() {
decoder.stop()
}
return -1
}

}

private class FrameSeqDecoderResource(
private val decoder: FrameSeqDecoder<*, *>,
private val size: Int
) : Resource<FrameSeqDecoder<*, *>> {
override fun getResourceClass(): Class<FrameSeqDecoder<*, *>> {
return FrameSeqDecoder::class.java
}

override fun get(): FrameSeqDecoder<*, *> {
return decoder
}

override fun getSize(): Int {
return size
}

override fun recycle() {
decoder.stop()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,39 @@ package net.pantasystem.milktea.common.glide.apng
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import com.github.penfeizhou.animation.apng.decode.APNGParser
import com.github.penfeizhou.animation.decode.FrameSeqDecoder
import com.github.penfeizhou.animation.io.StreamReader
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer

class StreamApngDecoder(
val byteBufferApngDecoder: ByteBufferApngDecoder,
) : ResourceDecoder<InputStream, FrameSeqDecoder<*, *>> {
class StreamApngDecoder : ResourceDecoder<InputStream, FrameSeqDecoder<*, *>> {
companion object {
private const val BUFFER_SIZE = 16384
}

override fun decode(
source: InputStream,
width: Int,
height: Int,
options: Options,
): Resource<FrameSeqDecoder<*, *>>? {
val data = inputStreamToBytes(source) ?: return null
val byteBuffer = ByteBuffer.wrap(data)
return byteBufferApngDecoder.decode(byteBuffer, width, height, options)
val sourceBuffer = inputStreamToByteBuffer(source) ?: return null
return ApngDecoderDelegate.decode(sourceBuffer, width, height, options)
}

override fun handles(source: InputStream, options: Options): Boolean {
val headerBytes = ByteArray(8)
val bytesRead = source.read(headerBytes)
// ファイルが8バイト未満の場合、それは有効なPNGではない
if (bytesRead < 8) {
return false
}

val header = ByteBuffer.wrap(headerBytes).long ushr 32
if (header != PNG) {
return false
}
return APNGParser.isAPNG(StreamReader(source))
val sourceBuffer = inputStreamToByteBuffer(source) ?: return false
return ApngDecoderDelegate.handles(sourceBuffer, options)
}

private fun inputStreamToBytes(`is`: InputStream): ByteArray? {
val bufferSize = 16384
val buffer = ByteArrayOutputStream(bufferSize)
try {
var nRead: Int
val data = ByteArray(bufferSize)
while (`is`.read(data).also { nRead = it } != -1) {
buffer.write(data, 0, nRead)
private fun inputStreamToByteBuffer(inputStream: InputStream): ByteBuffer? {
val result = runCatching {
ByteArrayOutputStream(BUFFER_SIZE).use {
inputStream.copyTo(it)
it.toByteArray()
}
buffer.flush()
} catch (e: IOException) {
return null
}
return buffer.toByteArray()

return result.getOrNull()?.let { ByteBuffer.wrap(it) }
}
}
2 changes: 1 addition & 1 deletion modules/common_android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ dependencies {
//glide
kapt libs.glide.compiler
implementation "com.google.accompanist:accompanist-glide:0.14.0"
implementation 'com.github.penfeizhou.android.animation:apng:2.23.0'
implementation 'com.github.penfeizhou.android.animation:apng:2.24.0'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation libs.kotlin.datetime

Expand Down
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ dependencyResolutionManagement {
library('glide-glide', 'com.github.bumptech.glide:glide:4.14.2')
library('glide-compiler', 'com.github.bumptech.glide:compiler:4.14.2')
library('accompanist-glide', 'com.google.accompanist:accompanist-glide:0.14.0')
library('animation-apng', 'com.github.penfeizhou.android.animation:apng:2.23.0')
library('animation-apng', 'com.github.penfeizhou.android.animation:apng:2.24.0')

library('wada811-databinding', 'com.github.wada811:DataBinding-ktx:5.0.2')

Expand Down

0 comments on commit 4035b42

Please sign in to comment.