リファクタ

This commit is contained in:
tateisu 2018-02-01 18:30:46 +09:00
parent 84383661d8
commit 8c46295631
20 changed files with 414 additions and 435 deletions

View File

@ -1,9 +1,9 @@
package jp.juggler.apng package jp.juggler.apng
class Apng { class Apng {
var header: ApngImageHeader? = null var header : ApngImageHeader? = null
var background: ApngBackground? = null var background : ApngBackground? = null
var animationControl: ApngAnimationControl? = null var animationControl : ApngAnimationControl? = null
internal var palette: ApngPalette? = null internal var palette : ApngPalette? = null
internal var transparentColor: ApngTransparentColor? = null internal var transparentColor : ApngTransparentColor? = null
} }

View File

@ -4,29 +4,28 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngAnimationControl internal constructor(src : ByteSequence) {
class ApngAnimationControl internal constructor(src: ByteSequence) {
companion object {
companion object { const val PLAY_INDEFINITELY = 0
const val PLAY_INDEFINITELY =0 }
}
// This must equal the number of `fcTL` chunks.
// This must equal the number of `fcTL` chunks. // 0 is not a valid value.
// 0 is not a valid value. // 1 is a valid value for a single-frame APNG.
// 1 is a valid value for a single-frame APNG. val numFrames : Int
val numFrames: Int
// if it is 0, the animation should play indefinitely.
// if it is 0, the animation should play indefinitely. // If nonzero, the animation should come to rest on the final frame at the end of the last play.
// If nonzero, the animation should come to rest on the final frame at the end of the last play. val numPlays : Int
val numPlays: Int
init {
init { numFrames = src.readInt32()
numFrames = src.readInt32() numPlays = src.readInt32()
numPlays = src.readInt32() }
}
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
override fun toString() ="ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
val isFinite : Boolean
val isFinite :Boolean get() = numPlays > PLAY_INDEFINITELY
get() = numPlays > PLAY_INDEFINITELY
} }

View File

@ -4,34 +4,36 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) { class ApngBackground internal constructor(colorType : ColorType, src : ByteSequence) {
val red: Int val red : Int
val green: Int val green : Int
val blue: Int val blue : Int
val index: Int val index : Int
init { init {
when (colorType) { when(colorType) {
ColorType.GREY, ColorType.GREY_ALPHA -> { ColorType.GREY, ColorType.GREY_ALPHA -> {
val v = src.readUInt16() val v = src.readUInt16()
red = v red = v
green = v green = v
blue = v blue = v
index = -1 index = - 1
} }
ColorType.RGB, ColorType.RGBA -> {
red = src.readUInt16() ColorType.RGB, ColorType.RGBA -> {
green = src.readUInt16() red = src.readUInt16()
blue = src.readUInt16() green = src.readUInt16()
index = -1 blue = src.readUInt16()
} index = - 1
ColorType.INDEX -> { }
red = -1
green = -1 ColorType.INDEX -> {
blue = -1 red = - 1
index = src.readUInt8() green = - 1
} blue = - 1
} index = src.readUInt8()
} }
}
}
} }

View File

@ -11,7 +11,7 @@ class ApngBitmap(var width : Int, var height : Int) {
fun reset(width : Int, height : Int) { fun reset(width : Int, height : Int) {
val newSize = width * height val newSize = width * height
if(newSize > colors.size) if(newSize > colors.size)
throw ParseError("can't resize to $width x $height , it's greater than initial size") throw ApngParseError("can't resize to $width x $height , it's greater than initial size")
this.width = width this.width = width
this.height = height this.height = height
// 透明な黒で初期化する // 透明な黒で初期化する
@ -24,11 +24,7 @@ class ApngBitmap(var width : Int, var height : Int) {
private var pos : Int = 0 private var pos : Int = 0
var step : Int = 1 var step : Int = 1
fun setPixel(argb : Int) : Pointer { fun setPixel(argb : Int) = apply { colors[pos] = argb }
// if( pos == width) println("setPixel 0x%x".format(argb))
colors[pos] = argb
return this
}
fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel( fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel(
((a and 255) shl 24) or ((a and 255) shl 24) or
@ -37,18 +33,14 @@ class ApngBitmap(var width : Int, var height : Int) {
(b and 255) (b and 255)
) )
fun setOffset(pos : Int = 0, step : Int = 1) : Pointer { fun setOffset(pos : Int = 0, step : Int = 1) = apply {
this.pos = pos this.pos = pos
this.step = step this.step = step
return this
} }
fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step) fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step)
fun plus(x : Int) : Pointer { fun plus(x : Int) = apply { pos += x }
pos += x
return this
}
fun next() = plus(step) fun next() = plus(step)

View File

@ -5,35 +5,35 @@ package jp.juggler.apng
import jp.juggler.apng.util.StreamTokenizer import jp.juggler.apng.util.StreamTokenizer
import java.util.zip.CRC32 import java.util.zip.CRC32
internal class ApngChunk(crc32:CRC32,tokenizer: StreamTokenizer) { internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) {
val size: Int val size : Int
val type: String val type : String
init { init {
size = tokenizer.readInt32() size = tokenizer.readInt32()
val typeBytes = tokenizer.readBytes(4) val typeBytes = tokenizer.readBytes(4)
type = typeBytes.toString(Charsets.UTF_8) type = typeBytes.toString(Charsets.UTF_8)
crc32.update(typeBytes) crc32.update(typeBytes)
} }
fun readBody(crc32:CRC32,tokenizer: StreamTokenizer): ByteArray { fun readBody(crc32 : CRC32, tokenizer : StreamTokenizer) : ByteArray {
val bytes = tokenizer.readBytes(size) val bytes = tokenizer.readBytes(size)
val crcExpect = tokenizer.readUInt32() val crcExpect = tokenizer.readUInt32()
crc32.update(bytes, 0, size) crc32.update(bytes, 0, size)
val crcActual = crc32.value val crcActual = crc32.value
if (crcActual != crcExpect) throw ParseError("CRC not match.") if(crcActual != crcExpect) throw ApngParseError("CRC not match.")
return bytes return bytes
} }
fun skipBody(tokenizer: StreamTokenizer) { fun skipBody(tokenizer : StreamTokenizer) =
tokenizer.skipBytes((size + 4).toLong()) tokenizer.skipBytes((size + 4).toLong())
}
fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) { fun checkCRC(tokenizer : StreamTokenizer, crcActual : Long) {
val crcExpect = tokenizer.readUInt32() val crcExpect = tokenizer.readUInt32()
if (crcActual != crcExpect) throw ParseError("CRC not match.") if(crcActual != crcExpect) throw ApngParseError("CRC not match.")
} }
} }

View File

@ -11,22 +11,22 @@ object ApngDecoder {
private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a) private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a)
fun parseStream( fun parseStream(
_inStream : InputStream, inStream : InputStream,
callback : ApngDecoderCallback callback : ApngDecoderCallback
) { ) {
val apng = Apng() val apng = Apng()
val tokenizer = StreamTokenizer(_inStream) val tokenizer = StreamTokenizer(inStream)
val pngHeader = tokenizer.readBytes(8) val pngHeader = tokenizer.readBytes(8)
if(! pngHeader.contentEquals(PNG_SIGNATURE)) { if(! pngHeader.contentEquals(PNG_SIGNATURE)) {
throw ParseError("header not match") throw ApngParseError("header not match")
} }
var lastSequenceNumber : Int? = null var lastSequenceNumber : Int? = null
fun checkSequenceNumber(n : Int) { fun checkSequenceNumber(n : Int) {
val last = lastSequenceNumber val last = lastSequenceNumber
if(last != null && n <= last) { if(last != null && n <= last) {
throw ParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n") throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
} }
lastSequenceNumber = n lastSequenceNumber = n
} }
@ -56,7 +56,7 @@ object ApngDecoder {
"PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer)) "PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer))
"bKGD" -> { "bKGD" -> {
val header = apng.header ?: throw ParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
apng.background = ApngBackground( apng.background = ApngBackground(
header.colorType, header.colorType,
ByteSequence(chunk.readBody(crc32, tokenizer)) ByteSequence(chunk.readBody(crc32, tokenizer))
@ -64,7 +64,7 @@ object ApngDecoder {
} }
"tRNS" -> { "tRNS" -> {
val header = apng.header ?: throw ParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
val body = chunk.readBody(crc32, tokenizer) val body = chunk.readBody(crc32, tokenizer)
when(header.colorType) { when(header.colorType) {
ColorType.GREY -> apng.transparentColor = ColorType.GREY -> apng.transparentColor =
@ -72,15 +72,15 @@ object ApngDecoder {
ColorType.RGB -> apng.transparentColor = ColorType.RGB -> apng.transparentColor =
ApngTransparentColor(false, ByteSequence(body)) ApngTransparentColor(false, ByteSequence(body))
ColorType.INDEX -> apng.palette?.parseTRNS(body) ColorType.INDEX -> apng.palette?.parseTRNS(body)
?: throw ParseError("missing palette") ?: throw ApngParseError("missing palette")
else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}") else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}")
} }
} }
"IDAT" -> { "IDAT" -> {
val header = apng.header ?: throw ParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
if(idatDecoder == null) { if(idatDecoder == null) {
bitmap ?: throw ParseError("missing bitmap") bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(header.width, header.height) bitmap.reset(header.width, header.height)
idatDecoder = IdatDecoder( idatDecoder = IdatDecoder(
apng, apng,
@ -106,11 +106,11 @@ object ApngDecoder {
} }
"acTL" -> { "acTL" -> {
val header = apng.header ?: throw ParseError("missing IHDR") val header = apng.header ?: throw ApngParseError("missing IHDR")
val animationControl = val animationControl =
ApngAnimationControl(ByteSequence(chunk.readBody(crc32, tokenizer))) ApngAnimationControl(ByteSequence(chunk.readBody(crc32, tokenizer)))
apng.animationControl = animationControl apng.animationControl = animationControl
callback.onAnimationInfo(apng, header,animationControl) callback.onAnimationInfo(apng, header, animationControl)
} }
"fcTL" -> { "fcTL" -> {
@ -121,9 +121,9 @@ object ApngDecoder {
} }
"fdAT" -> { "fdAT" -> {
val fctl = lastFctl ?: throw ParseError("missing fCTL before fdAT") val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT")
if(fdatDecoder == null) { if(fdatDecoder == null) {
bitmap ?: throw ParseError("missing bitmap") bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(fctl.width, fctl.height) bitmap.reset(fctl.width, fctl.height)
fdatDecoder = IdatDecoder( fdatDecoder = IdatDecoder(
apng, apng,

View File

@ -8,7 +8,7 @@ interface ApngDecoderCallback {
// called for debug message // called for debug message
fun onApngDebug(message : String) {} fun onApngDebug(message : String) {}
fun canApngDebug():Boolean = false fun canApngDebug() : Boolean = false
// called when PNG image header is detected. // called when PNG image header is detected.
fun onHeader(apng : Apng, header : ApngImageHeader) fun onHeader(apng : Apng, header : ApngImageHeader)

View File

@ -1,41 +1,41 @@
package jp.juggler.apng package jp.juggler.apng
enum class ColorType(val num:Int ){ enum class ColorType(val num : Int) {
GREY(0), GREY(0),
RGB ( 2), RGB(2),
INDEX( 3), INDEX(3),
GREY_ALPHA ( 4), GREY_ALPHA(4),
RGBA ( 6), RGBA(6),
} }
enum class CompressionMethod(val num:Int ){ enum class CompressionMethod(val num : Int) {
Standard(0) Standard(0)
} }
enum class FilterMethod(val num:Int ){ enum class FilterMethod(val num : Int) {
Standard(0) Standard(0)
} }
enum class InterlaceMethod(val num:Int ){ enum class InterlaceMethod(val num : Int) {
None(0), None(0),
Standard(1) Standard(1)
} }
enum class FilterType(val num:Int ){ enum class FilterType(val num : Int) {
None(0), None(0),
Sub(1), Sub(1),
Up(2), Up(2),
Average(3), Average(3),
Paeth(4) Paeth(4)
} }
enum class DisposeOp(val num :Int){ enum class DisposeOp(val num : Int) {
None(0), None(0),
Background(1), Background(1),
Previous(2) Previous(2)
} }
enum class BlendOp(val num :Int){ enum class BlendOp(val num : Int) {
Source(0), Source(0),
Over(1) Over(1)
} }

View File

@ -4,40 +4,40 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngFrameControl internal constructor(src : ByteSequence) {
class ApngFrameControl internal constructor(src: ByteSequence) {
val width : Int
val width: Int val height : Int
val height: Int val xOffset : Int
val xOffset: Int val yOffset : Int
val yOffset: Int val delayNum : Int
val delayNum: Int val delayDen : Int
val delayDen: Int val disposeOp : DisposeOp
val disposeOp: DisposeOp val blendOp : BlendOp
val blendOp: BlendOp
init {
init { width = src.readInt32()
width = src.readInt32() height = src.readInt32()
height = src.readInt32() xOffset = src.readInt32()
xOffset = src.readInt32() yOffset = src.readInt32()
yOffset = src.readInt32() delayNum = src.readUInt16()
delayNum = src.readUInt16() delayDen = src.readUInt16().let { if(it == 0) 100 else it }
delayDen = src.readUInt16().let{ if(it==0) 100 else it}
var num : Int
var num:Int
num = src.readUInt8()
num = src.readUInt8() disposeOp = DisposeOp.values().first { it.num == num }
disposeOp = DisposeOp.values().first{it.num==num}
num = src.readUInt8()
num = src.readUInt8() blendOp = BlendOp.values().first { it.num == num }
blendOp = BlendOp.values().first{it.num==num} }
}
override fun toString() =
override fun toString() ="ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayNum=$delayNum,delayDen=$delayDen,disposeOp=$disposeOp,blendOp=$blendOp)" "ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayNum=$delayNum,delayDen=$delayDen,disposeOp=$disposeOp,blendOp=$blendOp)"
val delayMilliseconds : Long val delayMilliseconds : Long
get() = when(delayDen) { get() = when(delayDen) {
1000 -> delayNum.toLong() 1000 -> delayNum.toLong()
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong() else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
} }
} }

View File

@ -5,37 +5,39 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
// information from IHDR chunk. // information from IHDR chunk.
class ApngImageHeader internal constructor(src: ByteSequence) { class ApngImageHeader internal constructor(src : ByteSequence) {
val width: Int
val height: Int val width : Int
val bitDepth: Int val height : Int
val colorType: ColorType val bitDepth : Int
val compressionMethod: CompressionMethod val colorType : ColorType
val filterMethod: FilterMethod val compressionMethod : CompressionMethod
val interlaceMethod: InterlaceMethod val filterMethod : FilterMethod
val interlaceMethod : InterlaceMethod
init {
init {
width = src.readInt32()
height = src.readInt32() width = src.readInt32()
if(width <=0 || height <=0 ) throw ParseError("w=$width,h=$height is too small") height = src.readInt32()
if(width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
bitDepth = src.readUInt8()
bitDepth = src.readUInt8()
var num:Int
// var num : Int
num =src.readUInt8() //
colorType = ColorType.values().first { it.num==num } num = src.readUInt8()
// colorType = ColorType.values().first { it.num == num }
num =src.readUInt8() //
compressionMethod = CompressionMethod.values().first { it.num==num } num = src.readUInt8()
// compressionMethod = CompressionMethod.values().first { it.num == num }
num =src.readUInt8() //
filterMethod = FilterMethod.values().first { it.num==num } num = src.readUInt8()
// filterMethod = FilterMethod.values().first { it.num == num }
num =src.readUInt8() //
interlaceMethod = InterlaceMethod.values().first { it.num==num } num = src.readUInt8()
} interlaceMethod = InterlaceMethod.values().first { it.num == num }
}
override fun toString() = "ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)"
override fun toString() =
"ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)"
} }

View File

@ -7,6 +7,7 @@ import jp.juggler.apng.util.getUInt8
class ApngPalette( class ApngPalette(
src : ByteArray // repeat of R,G,B src : ByteArray // repeat of R,G,B
) { ) {
companion object { companion object {
// full opaque black // full opaque black
const val OPAQUE = 255 shl 24 const val OPAQUE = 255 shl 24
@ -18,14 +19,14 @@ class ApngPalette(
init { init {
val entryCount = src.size / 3 val entryCount = src.size / 3
list = IntArray( entryCount) list = IntArray(entryCount)
var pos = 0 var pos = 0
for(i in 0 until entryCount) { for(i in 0 until entryCount) {
list[i] = OPAQUE or list[i] = OPAQUE or
(src.getUInt8(pos) shl 16) or (src.getUInt8(pos) shl 16) or
(src.getUInt8(pos+1) shl 8) or (src.getUInt8(pos + 1) shl 8) or
src.getUInt8(pos+2) src.getUInt8(pos + 2)
pos+=3 pos += 3
} }
} }

View File

@ -0,0 +1,3 @@
package jp.juggler.apng
class ApngParseError(message: String) : IllegalArgumentException(message)

View File

@ -4,23 +4,24 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence import jp.juggler.apng.util.ByteSequence
class ApngTransparentColor internal constructor(isGreyScale:Boolean, src: ByteSequence) { class ApngTransparentColor internal constructor(isGreyScale : Boolean, src : ByteSequence) {
val red:Int val red : Int
val green:Int val green : Int
val blue:Int val blue : Int
init{
if( isGreyScale){ init {
val v = src.readUInt16() if(isGreyScale) {
red =v val v = src.readUInt16()
green =v red = v
blue =v green = v
}else{ blue = v
red =src.readUInt16() } else {
green =src.readUInt16() red = src.readUInt16()
blue =src.readUInt16() green = src.readUInt16()
} blue = src.readUInt16()
} }
}
fun match(grey:Int) = red == grey
fun match(r:Int,g:Int,b:Int) = (r==red && g == green && b == blue) fun match(grey : Int) = red == grey
fun match(r : Int, g : Int, b : Int) = (r == red && g == green && b == blue)
} }

View File

@ -4,7 +4,6 @@ package jp.juggler.apng
import jp.juggler.apng.util.* import jp.juggler.apng.util.*
import java.io.InputStream import java.io.InputStream
import java.util.*
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.Inflater import java.util.zip.Inflater
@ -33,7 +32,6 @@ internal class IdatDecoder(
private val dummyPaletteData = IntArray(0) private val dummyPaletteData = IntArray(0)
private fun abs(v : Int) = if(v >= 0) v else - v private fun abs(v : Int) = if(v >= 0) v else - v
// a = left, b = above, c = upper left // a = left, b = above, c = upper left
@ -51,8 +49,9 @@ internal class IdatDecoder(
private inline fun scanLine1(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { private inline fun scanLine1(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
var pos = 1 var pos = 1
var x = 0 var remain = pass_w
while(pass_w - x >= 8) { while(remain >= 8) {
remain -= 8
val v = baLine[pos ++].toInt() val v = baLine[pos ++].toInt()
block((v shr 7) and 1) block((v shr 7) and 1)
block((v shr 6) and 1) block((v shr 6) and 1)
@ -62,9 +61,7 @@ internal class IdatDecoder(
block((v shr 2) and 1) block((v shr 2) and 1)
block((v shr 1) and 1) block((v shr 1) and 1)
block(v and 1) block(v and 1)
x += 8
} }
val remain = pass_w - x
if(remain > 0) { if(remain > 0) {
val v = baLine[pos].toInt() val v = baLine[pos].toInt()
block((v shr 7) and 1) block((v shr 7) and 1)
@ -79,16 +76,15 @@ internal class IdatDecoder(
private inline fun scanLine2(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { private inline fun scanLine2(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
var pos = 1 var pos = 1
var x = 0 var remain = pass_w
while(pass_w - x >= 4) { while(remain >= 4) {
remain -= 4
val v = baLine[pos ++].toInt() val v = baLine[pos ++].toInt()
block((v shr 6) and 3) block((v shr 6) and 3)
block((v shr 4) and 3) block((v shr 4) and 3)
block((v shr 2) and 3) block((v shr 2) and 3)
block(v and 3) block(v and 3)
x += 4
} }
val remain = pass_w - x
if(remain > 0) { if(remain > 0) {
val v = baLine[pos].toInt() val v = baLine[pos].toInt()
block((v shr 6) and 3) block((v shr 6) and 3)
@ -99,14 +95,13 @@ internal class IdatDecoder(
private inline fun scanLine4(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { private inline fun scanLine4(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
var pos = 1 var pos = 1
var x = 0 var remain = pass_w
while(pass_w - x >= 2) { while(remain >= 2) {
remain -= 2
val v = baLine[pos ++].toInt() val v = baLine[pos ++].toInt()
block((v shr 4) and 15) block((v shr 4) and 15)
block(v and 15) block(v and 15)
x += 2
} }
val remain = pass_w - x
if(remain > 0) { if(remain > 0) {
val v = baLine[pos].toInt() val v = baLine[pos].toInt()
block((v shr 4) and 15) block((v shr 4) and 15)
@ -115,14 +110,16 @@ internal class IdatDecoder(
private inline fun scanLine8(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { private inline fun scanLine8(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block(baLine.getUInt8(pos ++)) block(baLine.getUInt8(pos ++))
} }
} }
private inline fun scanLine16(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { private inline fun scanLine16(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block(baLine.getUInt16(pos)) block(baLine.getUInt16(pos))
pos += 2 pos += 2
} }
@ -134,7 +131,8 @@ internal class IdatDecoder(
block : (r : Int, g : Int, b : Int) -> Unit block : (r : Int, g : Int, b : Int) -> Unit
) { ) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block( block(
baLine.getUInt8(pos), baLine.getUInt8(pos),
baLine.getUInt8(pos + 1), baLine.getUInt8(pos + 1),
@ -150,7 +148,8 @@ internal class IdatDecoder(
block : (r : Int, g : Int, b : Int) -> Unit block : (r : Int, g : Int, b : Int) -> Unit
) { ) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block( block(
baLine.getUInt16(pos), baLine.getUInt16(pos),
baLine.getUInt16(pos + 2), baLine.getUInt16(pos + 2),
@ -166,7 +165,8 @@ internal class IdatDecoder(
block : (r : Int, g : Int, b : Int, a : Int) -> Unit block : (r : Int, g : Int, b : Int, a : Int) -> Unit
) { ) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block( block(
baLine.getUInt8(pos), baLine.getUInt8(pos),
baLine.getUInt8(pos + 1), baLine.getUInt8(pos + 1),
@ -183,7 +183,8 @@ internal class IdatDecoder(
block : (r : Int, g : Int, b : Int, a : Int) -> Unit block : (r : Int, g : Int, b : Int, a : Int) -> Unit
) { ) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block( block(
baLine.getUInt16(pos), baLine.getUInt16(pos),
baLine.getUInt16(pos + 2), baLine.getUInt16(pos + 2),
@ -200,7 +201,8 @@ internal class IdatDecoder(
block : (g : Int, a : Int) -> Unit block : (g : Int, a : Int) -> Unit
) { ) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block( block(
baLine.getUInt8(pos), baLine.getUInt8(pos),
baLine.getUInt8(pos + 1) baLine.getUInt8(pos + 1)
@ -215,7 +217,8 @@ internal class IdatDecoder(
block : (g : Int, a : Int) -> Unit block : (g : Int, a : Int) -> Unit
) { ) {
var pos = 1 var pos = 1
for(x in 0 until pass_w) { var remain = pass_w
while(remain -- > 0) {
block( block(
baLine.getUInt16(pos), baLine.getUInt16(pos),
baLine.getUInt16(pos + 2) baLine.getUInt16(pos + 2)
@ -233,7 +236,7 @@ internal class IdatDecoder(
private val sampleBits : Int private val sampleBits : Int
private val sampleBytes : Int private val sampleBytes : Int
private val scanLineBytesMax : Int private val scanLineBytesMax : Int
private val linePool = LinkedList<ByteArray>() private val scanLinePool : BufferPool
private val transparentCheckerGrey : (v : Int) -> Int private val transparentCheckerGrey : (v : Int) -> Int
private val transparentCheckerRGB : (r : Int, g : Int, b : Int) -> Int private val transparentCheckerRGB : (r : Int, g : Int, b : Int) -> Int
private val renderScanLineFunc : (baLine : ByteArray) -> Unit private val renderScanLineFunc : (baLine : ByteArray) -> Unit
@ -250,15 +253,8 @@ internal class IdatDecoder(
init { init {
val header = requireNotNull(apng.header) val header = requireNotNull(apng.header)
this.colorType = header.colorType colorType = header.colorType
this.bitDepth = header.bitDepth bitDepth = header.bitDepth
this.paletteData = if(colorType == ColorType.INDEX) {
apng.palette?.list
?: throw ParseError("missing ApngPalette for index color")
} else {
dummyPaletteData
}
sampleBits = when(colorType) { sampleBits = when(colorType) {
ColorType.GREY, ColorType.INDEX -> bitDepth ColorType.GREY, ColorType.INDEX -> bitDepth
@ -269,9 +265,14 @@ internal class IdatDecoder(
sampleBytes = (sampleBits + 7) / 8 sampleBytes = (sampleBits + 7) / 8
scanLineBytesMax = 1 + (bitmap.width * sampleBits + 7) / 8 scanLineBytesMax = 1 + (bitmap.width * sampleBits + 7) / 8
scanLinePool = BufferPool(scanLineBytesMax)
linePool.add(ByteArray(scanLineBytesMax)) paletteData = if(colorType == ColorType.INDEX) {
linePool.add(ByteArray(scanLineBytesMax)) apng.palette?.list
?: throw ApngParseError("missing ApngPalette for index color")
} else {
dummyPaletteData
}
val transparentColor = apng.transparentColor val transparentColor = apng.transparentColor
@ -358,7 +359,7 @@ internal class IdatDecoder(
private fun renderIndex2(baLine : ByteArray) { private fun renderIndex2(baLine : ByteArray) {
scanLine2(baLine, passWidth) { v -> scanLine2(baLine, passWidth) { v ->
bitmapPointer.setPixel( paletteData[v] ).next() bitmapPointer.setPixel(paletteData[v]).next()
} }
} }
@ -400,7 +401,7 @@ internal class IdatDecoder(
} }
private fun colorBitsNotSupported() : Nothing { private fun colorBitsNotSupported() : Nothing {
throw ParseError("bitDepth $bitDepth is not supported for $colorType") throw ApngParseError("bitDepth $bitDepth is not supported for $colorType")
} }
private fun selectRenderFunc() = when(colorType) { private fun selectRenderFunc() = when(colorType) {
@ -443,11 +444,11 @@ internal class IdatDecoder(
passY = 0 passY = 0
scanLineBytes = 1 + (passWidth * sampleBits + 7) / 8 scanLineBytes = 1 + (passWidth * sampleBits + 7) / 8
baPreviousLine?.let { linePool.add(it) } scanLinePool.recycle(baPreviousLine)
baPreviousLine = null baPreviousLine = null
if(passWidth <= 0 || passHeight <= 0) { if(passWidth <= 0 || passHeight <= 0) {
if( callback.canApngDebug() ) callback.onApngDebug("pass $pass is empty. size=${passWidth}x${passHeight} ") if(callback.canApngDebug()) callback.onApngDebug("pass $pass is empty. size=${passWidth}x${passHeight} ")
incrementPassOrComplete() incrementPassOrComplete()
} }
} }
@ -464,20 +465,20 @@ internal class IdatDecoder(
// スキャンラインを読む。行を処理したらtrueを返す // スキャンラインを読む。行を処理したらtrueを返す
private fun readScanLine() : Boolean { private fun readScanLine() : Boolean {
if(inflateBufferQueue.remain < scanLineBytes){ if(inflateBufferQueue.remain < scanLineBytes) {
// not yet enough data to process scanline // not yet enough data to process scanline
return false return false
} }
val baLine = linePool.removeFirst() val baLine = scanLinePool.obtain()
inflateBufferQueue.readBytes(baLine, 0, scanLineBytes) inflateBufferQueue.readBytes(baLine, 0, scanLineBytes)
val filterNum = baLine.getUInt8(0) val filterNum = baLine.getUInt8(0)
val filterType = FilterType.values().first { it.num == filterNum } val filterType = FilterType.values().first { it.num == filterNum }
// if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType") // if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType")
when(filterType) { when(filterType) {
FilterType.None -> { FilterType.None -> {
} }
@ -485,23 +486,22 @@ internal class IdatDecoder(
FilterType.Sub -> { FilterType.Sub -> {
for(pos in 1 until scanLineBytes) { for(pos in 1 until scanLineBytes) {
val vCur = baLine.getUInt8(pos) val vCur = baLine.getUInt8(pos)
val leftPos = pos -sampleBytes val leftPos = pos - sampleBytes
val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos) val vLeft = if(leftPos <= 0) 0 else baLine.getUInt8(leftPos)
baLine[pos] = (vCur + vLeft).toByte() baLine[pos] = (vCur + vLeft).toByte()
// if( callback.canApngDebug() ){ // if( callback.canApngDebug() ){
// val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes) // val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes)
// val y = passInfo.yStart + passInfo.yStep * passY // val y = passInfo.yStart + passInfo.yStep * passY
// callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}") // callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}")
// } // }
} }
} }
FilterType.Up -> { FilterType.Up -> {
val baPreviousLine=this.baPreviousLine val baPreviousLine = this.baPreviousLine
for(pos in 1 until scanLineBytes) { for(pos in 1 until scanLineBytes) {
val vCur = baLine.getUInt8(pos) val vCur = baLine.getUInt8(pos)
val vUp = baPreviousLine?.getUInt8(pos) ?: 0 val vUp = baPreviousLine?.getUInt8(pos) ?: 0
@ -510,47 +510,47 @@ internal class IdatDecoder(
} }
FilterType.Average -> { FilterType.Average -> {
val baPreviousLine=this.baPreviousLine val baPreviousLine = this.baPreviousLine
for(pos in 1 until scanLineBytes) { for(pos in 1 until scanLineBytes) {
val vCur = baLine.getUInt8(pos) val vCur = baLine.getUInt8(pos)
val leftPos = pos -sampleBytes val leftPos = pos - sampleBytes
val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos) val vLeft = if(leftPos <= 0) 0 else baLine.getUInt8(leftPos)
val vUp = baPreviousLine?.getUInt8(pos) ?: 0 val vUp = baPreviousLine?.getUInt8(pos) ?: 0
baLine[pos] = (vCur + ((vLeft + vUp) shr 1)).toByte() baLine[pos] = (vCur + ((vLeft + vUp) shr 1)).toByte()
} }
} }
FilterType.Paeth -> { FilterType.Paeth -> {
val baPreviousLine=this.baPreviousLine val baPreviousLine = this.baPreviousLine
for(pos in 1 until scanLineBytes) { for(pos in 1 until scanLineBytes) {
val vCur = baLine.getUInt8(pos) val vCur = baLine.getUInt8(pos)
val leftPos = pos -sampleBytes val leftPos = pos - sampleBytes
val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos) val vLeft = if(leftPos <= 0) 0 else baLine.getUInt8(leftPos)
val vUp = baPreviousLine?.getUInt8(pos) ?: 0 val vUp = baPreviousLine?.getUInt8(pos) ?: 0
val vUpperLeft = if(leftPos <=0 ) 0 else baPreviousLine?.getUInt8(leftPos) ?: 0 val vUpperLeft = if(leftPos <= 0) 0 else baPreviousLine?.getUInt8(leftPos) ?: 0
baLine[pos] = (vCur + paeth(vLeft, vUp, vUpperLeft)).toByte() baLine[pos] = (vCur + paeth(vLeft, vUp, vUpperLeft)).toByte()
// if( callback.canApngDebug() ){ // if( callback.canApngDebug() ){
// val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes) // val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes)
// val y = passInfo.yStart + passInfo.yStep * passY // val y = passInfo.yStart + passInfo.yStep * passY
// callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}") // callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}")
// } // }
} }
} }
} }
// render scanline // render scanline
bitmapPointer.setXY( bitmapPointer.setXY(
x=passInfo.xStart, x = passInfo.xStart,
y=passInfo.yStart + passInfo.yStep * passY, y = passInfo.yStart + passInfo.yStep * passY,
step=passInfo.xStep step = passInfo.xStep
) )
renderScanLineFunc(baLine) renderScanLineFunc(baLine)
// save previous line // save previous line
baPreviousLine?.let { linePool.add(it) } scanLinePool.recycle(baPreviousLine)
baPreviousLine = baLine baPreviousLine = baLine
if(++ passY >= passHeight) { if(++ passY >= passHeight) {
@ -607,14 +607,14 @@ internal class IdatDecoder(
while(! inflater.needsInput()) { while(! inflater.needsInput()) {
val buffer = inflateBufferPool.obtain() val buffer = inflateBufferPool.obtain()
val nInflated = inflater.inflate(buffer) val nInflated = inflater.inflate(buffer)
if( nInflated <= 0 ){ if(nInflated <= 0) {
inflateBufferPool.recycle(buffer) inflateBufferPool.recycle(buffer)
}else{ } else {
inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated)) inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated))
// キューに追加したデータをScanLine単位で消費する // キューに追加したデータをScanLine単位で消費する
while(! isCompleted && readScanLine()) { while(! isCompleted && readScanLine()) {
} }
if(isCompleted){ if(isCompleted) {
inflateBufferQueue.clear() inflateBufferQueue.clear()
break break
} }

View File

@ -1,3 +0,0 @@
package jp.juggler.apng
class ParseError(message: String) : IllegalArgumentException(message)

View File

@ -2,14 +2,8 @@ package jp.juggler.apng.util
import java.util.* import java.util.*
internal class BufferPool(private val arraySize:Int){ internal class BufferPool(private val arraySize : Int) {
private val list =LinkedList<ByteArray>() private val list = LinkedList<ByteArray>()
fun obtain() : ByteArray = if(list.isEmpty()) ByteArray(arraySize) else list.removeFirst()
fun recycle(array: ByteArray) { fun recycle(array : ByteArray?) = array?.let { list.add(it) }
list.add( array) }
}
fun obtain(): ByteArray {
return if( list.isEmpty() ) ByteArray(arraySize) else list.removeFirst()
}
}

View File

@ -1,6 +1,6 @@
package jp.juggler.apng.util package jp.juggler.apng.util
import jp.juggler.apng.ParseError import jp.juggler.apng.ApngParseError
internal fun ByteArray.getUInt8(pos : Int) = get(pos).toInt() and 255 internal fun ByteArray.getUInt8(pos : Int) = get(pos).toInt() and 255
@ -16,10 +16,11 @@ internal class ByteSequence(
var offset : Int, var offset : Int,
var length : Int var length : Int
) { ) {
constructor(ba : ByteArray) : this(ba, 0, ba.size) constructor(ba : ByteArray) : this(ba, 0, ba.size)
private inline fun <T> readX(dataSize : Int, block : () -> T) : T { private inline fun <T> readX(dataSize : Int, block : () -> T) : T {
if(length < dataSize) throw ParseError("readX: unexpected end") if(length < dataSize) throw ApngParseError("readX: unexpected end")
val v = block() val v = block()
offset += dataSize offset += dataSize
length -= dataSize length -= dataSize

View File

@ -2,41 +2,34 @@ package jp.juggler.apng.util
import java.util.* import java.util.*
internal class ByteSequenceQueue(private val bufferRecycler :(ByteSequence)->Unit) { internal class ByteSequenceQueue(private val bufferRecycler : (ByteSequence) -> Unit) {
private val list = LinkedList<ByteSequence>() private val list = LinkedList<ByteSequence>()
val remain: Int val remain : Int
get() = list.sumBy { it.length } get() = list.sumBy { it.length }
fun add(range: ByteSequence) { fun add(range : ByteSequence) =list.add(range)
list.add(range)
} fun clear() = list.also{ it.forEach(bufferRecycler) }.clear()
fun clear() { fun readBytes(dst : ByteArray, offset : Int, length : Int) : Int {
for( item in list ){ var dstOffset = offset
bufferRecycler(item) var dstRemain = length
} while(dstRemain > 0 && list.isNotEmpty()) {
list.clear() val item = list.first()
} if(item.length <= 0) {
bufferRecycler(item)
fun readBytes(dst: ByteArray, offset: Int, length: Int): Int { list.removeFirst()
var dstOffset = offset } else {
var dstRemain = length val delta = Math.min(item.length, dstRemain)
while (dstRemain > 0 && list.isNotEmpty()) { System.arraycopy(item.array, item.offset, dst, dstOffset, delta)
val item = list.first() dstOffset += delta
if (item.length <= 0) { dstRemain -= delta
bufferRecycler(item) item.offset += delta
list.removeFirst() item.length -= delta
}else { }
val delta = Math.min(item.length, dstRemain) }
System.arraycopy(item.array, item.offset, dst, dstOffset, delta) return length - dstRemain
dstOffset += delta }
dstRemain -= delta
item.offset += delta
item.length -= delta
}
}
return length - dstRemain
}
} }

View File

@ -1,66 +1,66 @@
package jp.juggler.apng.util package jp.juggler.apng.util
import jp.juggler.apng.ParseError import jp.juggler.apng.ApngParseError
import java.io.InputStream import java.io.InputStream
import java.util.zip.CRC32 import java.util.zip.CRC32
internal class StreamTokenizer(val inStream: InputStream) { internal class StreamTokenizer(val inStream : InputStream) {
fun skipBytes(size: Long) { fun skipBytes(size : Long) {
var nRead = 0L var nRead = 0L
while (true) { while(true) {
val remain = size - nRead val remain = size - nRead
if (remain <= 0) break if(remain <= 0) break
val delta = inStream.skip(size - nRead) val delta = inStream.skip(size - nRead)
if (delta <= 0) throw ParseError("skipBytes: unexpected EoS") if(delta <= 0) throw ApngParseError("skipBytes: unexpected EoS")
nRead += delta nRead += delta
} }
} }
fun readBytes(size: Int): ByteArray { fun readBytes(size : Int) : ByteArray {
val dst = ByteArray(size) val dst = ByteArray(size)
var nRead = 0 var nRead = 0
while (true) { while(true) {
val remain = size - nRead val remain = size - nRead
if (remain <= 0) break if(remain <= 0) break
val delta = inStream.read(dst, nRead, size - nRead) val delta = inStream.read(dst, nRead, size - nRead)
if (delta < 0) throw ParseError("readBytes: unexpected EoS") if(delta < 0) throw ApngParseError("readBytes: unexpected EoS")
nRead += delta nRead += delta
} }
return dst return dst
} }
private fun readByte(): Int { private fun readByte() : Int {
val b = inStream.read() val b = inStream.read()
if( b == -1 ) throw ParseError("readBytes: unexpected EoS") if(b == - 1) throw ApngParseError("readByte: unexpected EoS")
return b and 0xff return b and 0xff
} }
fun readInt32(): Int { fun readInt32() : Int {
val b0 = readByte() val b0 = readByte()
val b1 = readByte() val b1 = readByte()
val b2 = readByte() val b2 = readByte()
val b3 = readByte() val b3 = readByte()
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
} }
fun readInt32(crc32: CRC32): Int { fun readInt32(crc32 : CRC32) : Int {
val ba = readBytes(4) val ba = readBytes(4)
crc32.update(ba) crc32.update(ba)
val b0 = ba[0].toInt() and 255 val b0 = ba[0].toInt() and 255
val b1 = ba[1].toInt() and 255 val b1 = ba[1].toInt() and 255
val b2 = ba[2].toInt() and 255 val b2 = ba[2].toInt() and 255
val b3 = ba[3].toInt() and 255 val b3 = ba[3].toInt() and 255
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
} }
fun readUInt32(): Long { fun readUInt32() : Long {
val b0 = readByte() val b0 = readByte()
val b1 = readByte() val b1 = readByte()
val b2 = readByte() val b2 = readByte()
val b3 = readByte() val b3 = readByte()
return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong() return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong()
} }
} }

View File

@ -24,11 +24,9 @@ class ApngFrames private constructor(
private const val DELAY_AFTER_END = 3000L private const val DELAY_AFTER_END = 3000L
// アニメーションフレームの描画に使う // アニメーションフレームの描画に使う
private val sPaintDontBlend : Paint by lazy { private val sPaintDontBlend = Paint().apply {
val paint = Paint() xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC) isFilterBitmap = true
paint.isFilterBitmap = true
paint
} }
private fun createBlankBitmap(w : Int, h : Int) = private fun createBlankBitmap(w : Int, h : Int) =
@ -45,7 +43,7 @@ class ApngFrames private constructor(
val wSrc = src.width val wSrc = src.width
val hSrc = src.height val hSrc = src.height
if(size_max <= 0 || wSrc <= size_max && hSrc <= size_max) { if(size_max <= 0 || (wSrc <= size_max && hSrc <= size_max)) {
return if(recycleSrc) { return if(recycleSrc) {
src src
} else { } else {
@ -75,8 +73,8 @@ class ApngFrames private constructor(
return b2 return b2
} }
private fun toAndroidBitmap(src : ApngBitmap) : Bitmap { private fun toAndroidBitmap(src : ApngBitmap) =
return Bitmap.createBitmap( Bitmap.createBitmap(
src.colors, // int[] 配列 src.colors, // int[] 配列
0, // offset 0, // offset
src.width, //stride src.width, //stride
@ -84,7 +82,6 @@ class ApngFrames private constructor(
src.height, //height src.height, //height
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
) )
}
private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) = private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) =
scaleBitmap(size_max, toAndroidBitmap(src)) scaleBitmap(size_max, toAndroidBitmap(src))
@ -99,11 +96,8 @@ class ApngFrames private constructor(
try { try {
ApngDecoder.parseStream(inStream, result) ApngDecoder.parseStream(inStream, result)
result.onParseComplete() result.onParseComplete()
return if(result.defaultImage != null || result.frames?.isNotEmpty() == true) { return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
result ?: throw RuntimeException("APNG has no image")
} else {
throw RuntimeException("APNG has no image")
}
} catch(ex : Throwable) { } catch(ex : Throwable) {
result.dispose() result.dispose()
throw ex throw ex