148 lines
6.1 KiB
Kotlin
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|