SubwayTooter-Android-App/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt

148 lines
6.1 KiB
Kotlin

@file:Suppress("JoinDeclarationAndAssignment")
package jp.juggler.apng
import jp.juggler.apng.util.BufferPool
import jp.juggler.apng.util.ByteArrayTokenizer
import jp.juggler.apng.util.StreamTokenizer
import java.io.InputStream
import java.util.zip.CRC32
object ApngDecoder {
private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a)
fun parseStream(
_inStream: InputStream,
callback: ApngDecoderCallback
) {
val apng = Apng()
val tokenizer = StreamTokenizer(_inStream)
val pngHeader = tokenizer.readBytes(8)
if (!pngHeader.contentEquals(PNG_SIGNATURE)) {
throw ParseError("header not match")
}
var lastSequenceNumber: Int? = null
fun checkSequenceNumber(n: Int) {
val last = lastSequenceNumber
if (last != null && n <= last) {
throw ParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
}
lastSequenceNumber = n
}
val inBuffer = ByteArray(4096)
val inflateBufferPool = BufferPool(8192)
var idatDecoder: IdatDecoder? = null
var fdatDecoder: IdatDecoder? = null
val crc32 = CRC32()
var lastFctl: ApngFrameControl? = null
var bitmap: ApngBitmap? = null
loop@ while (true) {
crc32.reset()
val chunk = ApngChunk(crc32, tokenizer)
when (chunk.type) {
"IEND" -> break@loop
"IHDR" -> {
val header = ApngImageHeader(ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
bitmap = ApngBitmap(header.width, header.height)
apng.header = header
callback.onHeader(apng, header)
}
"PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer))
"bKGD" -> {
val header = apng.header ?: throw ParseError("missing IHDR")
apng.background = ApngBackground(header.colorType, ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
}
"tRNS" -> {
val header = apng.header ?: throw ParseError("missing IHDR")
val body = chunk.readBody(crc32, tokenizer)
when (header.colorType) {
ColorType.GREY -> apng.transparentColor = ApngTransparentColor(true, ByteArrayTokenizer(body))
ColorType.RGB -> apng.transparentColor = ApngTransparentColor(false, ByteArrayTokenizer(body))
ColorType.INDEX -> apng.palette?.parseTRNS(body) ?: throw ParseError("missing palette")
else -> callback.log("tRNS ignored. colorType =${header.colorType}")
}
}
"IDAT" -> {
val header = apng.header ?: throw ParseError("missing IHDR")
if (idatDecoder == null) {
bitmap ?: throw ParseError("missing bitmap")
bitmap.reset(header.width, header.height)
idatDecoder = IdatDecoder(apng, bitmap, inflateBufferPool) {
callback.onDefaultImage(apng, bitmap)
val fctl = lastFctl
if (fctl != null) {
// IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ
callback.onAnimationFrame(apng, fctl, bitmap)
}
}
}
idatDecoder.addData(
tokenizer.inStream,
chunk.size,
inBuffer,
crc32
)
chunk.checkCRC(tokenizer, crc32.value)
}
"acTL" -> {
val animationControl = ApngAnimationControl(ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
apng.animationControl = animationControl
callback.onAnimationInfo(apng, animationControl)
}
"fcTL" -> {
val bat = ByteArrayTokenizer(chunk.readBody(crc32, tokenizer))
checkSequenceNumber(bat.readInt32())
lastFctl = ApngFrameControl(bat)
fdatDecoder = null
}
"fdAT" -> {
val fctl = lastFctl ?: throw ParseError("missing fCTL before fdAT")
if (fdatDecoder == null) {
bitmap ?: throw ParseError("missing bitmap")
bitmap.reset(fctl.width, fctl.height)
fdatDecoder = IdatDecoder(apng, bitmap, inflateBufferPool) {
callback.onAnimationFrame(apng, fctl, bitmap)
}
}
checkSequenceNumber(tokenizer.readInt32(crc32))
fdatDecoder.addData(
tokenizer.inStream,
chunk.size - 4,
inBuffer,
crc32
)
chunk.checkCRC(tokenizer, crc32.value)
}
// 無視するチャンク
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information
"tEXt", "zTXt", "iTXt", // text information
"tIME", // timestamp
"hIST", // histogram
"pHYs", // Physical pixel dimensions
"sPLT" // Suggested palette (おそらく減色用?)
-> chunk.skipBody(tokenizer)
else -> {
callback.log("unknown chunk: type=%s,size=0x%x".format(chunk.type, chunk.size))
chunk.skipBody(tokenizer)
}
}
}
}
}