リファクタ

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
class Apng {
var header: ApngImageHeader? = null
var background: ApngBackground? = null
var animationControl: ApngAnimationControl? = null
internal var palette: ApngPalette? = null
internal var transparentColor: ApngTransparentColor? = null
var header : ApngImageHeader? = null
var background : ApngBackground? = null
var animationControl : ApngAnimationControl? = null
internal var palette : ApngPalette? = null
internal var transparentColor : ApngTransparentColor? = null
}

View File

@ -4,29 +4,28 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence
class ApngAnimationControl internal constructor(src: ByteSequence) {
companion object {
const val PLAY_INDEFINITELY =0
}
// This must equal the number of `fcTL` chunks.
// 0 is not a valid value.
// 1 is a valid value for a single-frame APNG.
val numFrames: Int
// 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.
val numPlays: Int
init {
numFrames = src.readInt32()
numPlays = src.readInt32()
}
override fun toString() ="ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
val isFinite :Boolean
get() = numPlays > PLAY_INDEFINITELY
class ApngAnimationControl internal constructor(src : ByteSequence) {
companion object {
const val PLAY_INDEFINITELY = 0
}
// This must equal the number of `fcTL` chunks.
// 0 is not a valid value.
// 1 is a valid value for a single-frame APNG.
val numFrames : Int
// 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.
val numPlays : Int
init {
numFrames = src.readInt32()
numPlays = src.readInt32()
}
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
val isFinite : Boolean
get() = numPlays > PLAY_INDEFINITELY
}

View File

@ -4,34 +4,36 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence
class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) {
val red: Int
val green: Int
val blue: Int
val index: Int
init {
when (colorType) {
ColorType.GREY, ColorType.GREY_ALPHA -> {
val v = src.readUInt16()
red = v
green = v
blue = v
index = -1
}
ColorType.RGB, ColorType.RGBA -> {
red = src.readUInt16()
green = src.readUInt16()
blue = src.readUInt16()
index = -1
}
ColorType.INDEX -> {
red = -1
green = -1
blue = -1
index = src.readUInt8()
}
}
}
class ApngBackground internal constructor(colorType : ColorType, src : ByteSequence) {
val red : Int
val green : Int
val blue : Int
val index : Int
init {
when(colorType) {
ColorType.GREY, ColorType.GREY_ALPHA -> {
val v = src.readUInt16()
red = v
green = v
blue = v
index = - 1
}
ColorType.RGB, ColorType.RGBA -> {
red = src.readUInt16()
green = src.readUInt16()
blue = src.readUInt16()
index = - 1
}
ColorType.INDEX -> {
red = - 1
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) {
val newSize = width * height
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.height = height
// 透明な黒で初期化する
@ -24,11 +24,7 @@ class ApngBitmap(var width : Int, var height : Int) {
private var pos : Int = 0
var step : Int = 1
fun setPixel(argb : Int) : Pointer {
// if( pos == width) println("setPixel 0x%x".format(argb))
colors[pos] = argb
return this
}
fun setPixel(argb : Int) = apply { colors[pos] = argb }
fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel(
((a and 255) shl 24) or
@ -37,18 +33,14 @@ class ApngBitmap(var width : Int, var height : Int) {
(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.step = step
return this
}
fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step)
fun plus(x : Int) : Pointer {
pos += x
return this
}
fun plus(x : Int) = apply { pos += x }
fun next() = plus(step)

View File

@ -5,35 +5,35 @@ package jp.juggler.apng
import jp.juggler.apng.util.StreamTokenizer
import java.util.zip.CRC32
internal class ApngChunk(crc32:CRC32,tokenizer: StreamTokenizer) {
val size: Int
val type: String
init {
size = tokenizer.readInt32()
val typeBytes = tokenizer.readBytes(4)
type = typeBytes.toString(Charsets.UTF_8)
crc32.update(typeBytes)
}
fun readBody(crc32:CRC32,tokenizer: StreamTokenizer): ByteArray {
val bytes = tokenizer.readBytes(size)
val crcExpect = tokenizer.readUInt32()
crc32.update(bytes, 0, size)
val crcActual = crc32.value
if (crcActual != crcExpect) throw ParseError("CRC not match.")
return bytes
}
fun skipBody(tokenizer: StreamTokenizer) {
tokenizer.skipBytes((size + 4).toLong())
}
fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) {
val crcExpect = tokenizer.readUInt32()
if (crcActual != crcExpect) throw ParseError("CRC not match.")
}
internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) {
val size : Int
val type : String
init {
size = tokenizer.readInt32()
val typeBytes = tokenizer.readBytes(4)
type = typeBytes.toString(Charsets.UTF_8)
crc32.update(typeBytes)
}
fun readBody(crc32 : CRC32, tokenizer : StreamTokenizer) : ByteArray {
val bytes = tokenizer.readBytes(size)
val crcExpect = tokenizer.readUInt32()
crc32.update(bytes, 0, size)
val crcActual = crc32.value
if(crcActual != crcExpect) throw ApngParseError("CRC not match.")
return bytes
}
fun skipBody(tokenizer : StreamTokenizer) =
tokenizer.skipBytes((size + 4).toLong())
fun checkCRC(tokenizer : StreamTokenizer, crcActual : Long) {
val crcExpect = tokenizer.readUInt32()
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)
fun parseStream(
_inStream : InputStream,
inStream : InputStream,
callback : ApngDecoderCallback
) {
val apng = Apng()
val tokenizer = StreamTokenizer(_inStream)
val tokenizer = StreamTokenizer(inStream)
val pngHeader = tokenizer.readBytes(8)
if(! pngHeader.contentEquals(PNG_SIGNATURE)) {
throw ParseError("header not match")
throw ApngParseError("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")
throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
}
lastSequenceNumber = n
}
@ -56,7 +56,7 @@ object ApngDecoder {
"PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer))
"bKGD" -> {
val header = apng.header ?: throw ParseError("missing IHDR")
val header = apng.header ?: throw ApngParseError("missing IHDR")
apng.background = ApngBackground(
header.colorType,
ByteSequence(chunk.readBody(crc32, tokenizer))
@ -64,7 +64,7 @@ object ApngDecoder {
}
"tRNS" -> {
val header = apng.header ?: throw ParseError("missing IHDR")
val header = apng.header ?: throw ApngParseError("missing IHDR")
val body = chunk.readBody(crc32, tokenizer)
when(header.colorType) {
ColorType.GREY -> apng.transparentColor =
@ -72,15 +72,15 @@ object ApngDecoder {
ColorType.RGB -> apng.transparentColor =
ApngTransparentColor(false, ByteSequence(body))
ColorType.INDEX -> apng.palette?.parseTRNS(body)
?: throw ParseError("missing palette")
?: throw ApngParseError("missing palette")
else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}")
}
}
"IDAT" -> {
val header = apng.header ?: throw ParseError("missing IHDR")
val header = apng.header ?: throw ApngParseError("missing IHDR")
if(idatDecoder == null) {
bitmap ?: throw ParseError("missing bitmap")
bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(header.width, header.height)
idatDecoder = IdatDecoder(
apng,
@ -106,11 +106,11 @@ object ApngDecoder {
}
"acTL" -> {
val header = apng.header ?: throw ParseError("missing IHDR")
val header = apng.header ?: throw ApngParseError("missing IHDR")
val animationControl =
ApngAnimationControl(ByteSequence(chunk.readBody(crc32, tokenizer)))
apng.animationControl = animationControl
callback.onAnimationInfo(apng, header,animationControl)
callback.onAnimationInfo(apng, header, animationControl)
}
"fcTL" -> {
@ -121,9 +121,9 @@ object ApngDecoder {
}
"fdAT" -> {
val fctl = lastFctl ?: throw ParseError("missing fCTL before fdAT")
val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT")
if(fdatDecoder == null) {
bitmap ?: throw ParseError("missing bitmap")
bitmap ?: throw ApngParseError("missing bitmap")
bitmap.reset(fctl.width, fctl.height)
fdatDecoder = IdatDecoder(
apng,

View File

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

View File

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

View File

@ -4,40 +4,40 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence
class ApngFrameControl internal constructor(src: ByteSequence) {
val width: Int
val height: Int
val xOffset: Int
val yOffset: Int
val delayNum: Int
val delayDen: Int
val disposeOp: DisposeOp
val blendOp: BlendOp
init {
width = src.readInt32()
height = src.readInt32()
xOffset = src.readInt32()
yOffset = src.readInt32()
delayNum = src.readUInt16()
delayDen = src.readUInt16().let{ if(it==0) 100 else it}
var num:Int
num = src.readUInt8()
disposeOp = DisposeOp.values().first{it.num==num}
num = src.readUInt8()
blendOp = BlendOp.values().first{it.num==num}
}
override fun toString() ="ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayNum=$delayNum,delayDen=$delayDen,disposeOp=$disposeOp,blendOp=$blendOp)"
val delayMilliseconds : Long
get() = when(delayDen) {
1000 -> delayNum.toLong()
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
}
class ApngFrameControl internal constructor(src : ByteSequence) {
val width : Int
val height : Int
val xOffset : Int
val yOffset : Int
val delayNum : Int
val delayDen : Int
val disposeOp : DisposeOp
val blendOp : BlendOp
init {
width = src.readInt32()
height = src.readInt32()
xOffset = src.readInt32()
yOffset = src.readInt32()
delayNum = src.readUInt16()
delayDen = src.readUInt16().let { if(it == 0) 100 else it }
var num : Int
num = src.readUInt8()
disposeOp = DisposeOp.values().first { it.num == num }
num = src.readUInt8()
blendOp = BlendOp.values().first { it.num == num }
}
override fun toString() =
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayNum=$delayNum,delayDen=$delayDen,disposeOp=$disposeOp,blendOp=$blendOp)"
val delayMilliseconds : Long
get() = when(delayDen) {
1000 -> delayNum.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
// information from IHDR chunk.
class ApngImageHeader internal constructor(src: ByteSequence) {
val width: Int
val height: Int
val bitDepth: Int
val colorType: ColorType
val compressionMethod: CompressionMethod
val filterMethod: FilterMethod
val interlaceMethod: InterlaceMethod
init {
width = src.readInt32()
height = src.readInt32()
if(width <=0 || height <=0 ) throw ParseError("w=$width,h=$height is too small")
bitDepth = src.readUInt8()
var num:Int
//
num =src.readUInt8()
colorType = ColorType.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()
interlaceMethod = InterlaceMethod.values().first { it.num==num }
}
override fun toString() = "ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)"
class ApngImageHeader internal constructor(src : ByteSequence) {
val width : Int
val height : Int
val bitDepth : Int
val colorType : ColorType
val compressionMethod : CompressionMethod
val filterMethod : FilterMethod
val interlaceMethod : InterlaceMethod
init {
width = src.readInt32()
height = src.readInt32()
if(width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
bitDepth = src.readUInt8()
var num : Int
//
num = src.readUInt8()
colorType = ColorType.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()
interlaceMethod = InterlaceMethod.values().first { it.num == num }
}
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(
src : ByteArray // repeat of R,G,B
) {
companion object {
// full opaque black
const val OPAQUE = 255 shl 24
@ -18,14 +19,14 @@ class ApngPalette(
init {
val entryCount = src.size / 3
list = IntArray( entryCount)
list = IntArray(entryCount)
var pos = 0
for(i in 0 until entryCount) {
list[i] = OPAQUE or
(src.getUInt8(pos) shl 16) or
(src.getUInt8(pos+1) shl 8) or
src.getUInt8(pos+2)
pos+=3
(src.getUInt8(pos + 1) shl 8) or
src.getUInt8(pos + 2)
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
class ApngTransparentColor internal constructor(isGreyScale:Boolean, src: ByteSequence) {
val red:Int
val green:Int
val blue:Int
init{
if( isGreyScale){
val v = src.readUInt16()
red =v
green =v
blue =v
}else{
red =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)
class ApngTransparentColor internal constructor(isGreyScale : Boolean, src : ByteSequence) {
val red : Int
val green : Int
val blue : Int
init {
if(isGreyScale) {
val v = src.readUInt16()
red = v
green = v
blue = v
} else {
red = 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)
}

View File

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

View File

@ -1,6 +1,6 @@
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
@ -16,10 +16,11 @@ internal class ByteSequence(
var offset : Int,
var length : Int
) {
constructor(ba : ByteArray) : this(ba, 0, ba.size)
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()
offset += dataSize
length -= dataSize

View File

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

View File

@ -1,66 +1,66 @@
package jp.juggler.apng.util
import jp.juggler.apng.ParseError
import jp.juggler.apng.ApngParseError
import java.io.InputStream
import java.util.zip.CRC32
internal class StreamTokenizer(val inStream: InputStream) {
fun skipBytes(size: Long) {
var nRead = 0L
while (true) {
val remain = size - nRead
if (remain <= 0) break
val delta = inStream.skip(size - nRead)
if (delta <= 0) throw ParseError("skipBytes: unexpected EoS")
nRead += delta
}
}
fun readBytes(size: Int): ByteArray {
val dst = ByteArray(size)
var nRead = 0
while (true) {
val remain = size - nRead
if (remain <= 0) break
val delta = inStream.read(dst, nRead, size - nRead)
if (delta < 0) throw ParseError("readBytes: unexpected EoS")
nRead += delta
}
return dst
}
private fun readByte(): Int {
val b = inStream.read()
if( b == -1 ) throw ParseError("readBytes: unexpected EoS")
return b and 0xff
}
fun readInt32(): Int {
val b0 = readByte()
val b1 = readByte()
val b2 = readByte()
val b3 = readByte()
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
}
fun readInt32(crc32: CRC32): Int {
val ba = readBytes(4)
crc32.update(ba)
val b0 = ba[0].toInt() and 255
val b1 = ba[1].toInt() and 255
val b2 = ba[2].toInt() and 255
val b3 = ba[3].toInt() and 255
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
}
fun readUInt32(): Long {
val b0 = readByte()
val b1 = readByte()
val b2 = readByte()
val b3 = readByte()
return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong()
}
internal class StreamTokenizer(val inStream : InputStream) {
fun skipBytes(size : Long) {
var nRead = 0L
while(true) {
val remain = size - nRead
if(remain <= 0) break
val delta = inStream.skip(size - nRead)
if(delta <= 0) throw ApngParseError("skipBytes: unexpected EoS")
nRead += delta
}
}
fun readBytes(size : Int) : ByteArray {
val dst = ByteArray(size)
var nRead = 0
while(true) {
val remain = size - nRead
if(remain <= 0) break
val delta = inStream.read(dst, nRead, size - nRead)
if(delta < 0) throw ApngParseError("readBytes: unexpected EoS")
nRead += delta
}
return dst
}
private fun readByte() : Int {
val b = inStream.read()
if(b == - 1) throw ApngParseError("readByte: unexpected EoS")
return b and 0xff
}
fun readInt32() : Int {
val b0 = readByte()
val b1 = readByte()
val b2 = readByte()
val b3 = readByte()
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
}
fun readInt32(crc32 : CRC32) : Int {
val ba = readBytes(4)
crc32.update(ba)
val b0 = ba[0].toInt() and 255
val b1 = ba[1].toInt() and 255
val b2 = ba[2].toInt() and 255
val b3 = ba[3].toInt() and 255
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
}
fun readUInt32() : Long {
val b0 = readByte()
val b1 = readByte()
val b2 = readByte()
val b3 = readByte()
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 val sPaintDontBlend : Paint by lazy {
val paint = Paint()
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
paint.isFilterBitmap = true
paint
private val sPaintDontBlend = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
isFilterBitmap = true
}
private fun createBlankBitmap(w : Int, h : Int) =
@ -45,7 +43,7 @@ class ApngFrames private constructor(
val wSrc = src.width
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) {
src
} else {
@ -75,8 +73,8 @@ class ApngFrames private constructor(
return b2
}
private fun toAndroidBitmap(src : ApngBitmap) : Bitmap {
return Bitmap.createBitmap(
private fun toAndroidBitmap(src : ApngBitmap) =
Bitmap.createBitmap(
src.colors, // int[] 配列
0, // offset
src.width, //stride
@ -84,7 +82,6 @@ class ApngFrames private constructor(
src.height, //height
Bitmap.Config.ARGB_8888
)
}
private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) =
scaleBitmap(size_max, toAndroidBitmap(src))
@ -99,11 +96,8 @@ class ApngFrames private constructor(
try {
ApngDecoder.parseStream(inStream, result)
result.onParseComplete()
return if(result.defaultImage != null || result.frames?.isNotEmpty() == true) {
result
} else {
throw RuntimeException("APNG has no image")
}
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("APNG has no image")
} catch(ex : Throwable) {
result.dispose()
throw ex