599 lines
15 KiB
Kotlin
599 lines
15 KiB
Kotlin
package jp.juggler.apng
|
|
|
|
import java.io.InputStream
|
|
import java.lang.StringBuilder
|
|
import kotlin.math.min
|
|
|
|
// https://raw.githubusercontent.com/rtyley/animated-gif-lib-for-java/master/src/main/java/com/madgag/gif/fmsware/GifDecoder.java
|
|
// original code author is Kevin Weiner, FM Software.
|
|
// LZW decoder adapted from John Cristy's ImageMagick.
|
|
|
|
// http://www.theimage.com/animation/pages/disposal3.html
|
|
// great sample images.
|
|
|
|
class GifDecoder(val callback : GifDecoderCallback) {
|
|
|
|
private class Rectangle(var x : Int = 0, var y : Int = 0, var w : Int = 0, var h : Int = 0) {
|
|
|
|
fun set(src : Rectangle) {
|
|
this.x = src.x
|
|
this.y = src.y
|
|
this.w = src.w
|
|
this.h = src.h
|
|
}
|
|
}
|
|
|
|
private class Reader(val bis : InputStream) {
|
|
|
|
var block = ByteArray(256) // current data block
|
|
var blockSize = 0 // block size
|
|
|
|
// Reads a single byte from the input stream.
|
|
fun byte() : Int = bis.read()
|
|
|
|
// Reads next 16-bit value, LSB first
|
|
fun UInt16() = byte() or (byte() shl 8)
|
|
|
|
fun array(ba : ByteArray, offset : Int = 0, length : Int = ba.size - offset) {
|
|
var nRead = 0
|
|
while(nRead < length) {
|
|
val delta = bis.read(ba, offset + nRead, length - nRead)
|
|
if(delta == - 1) throw error("unexpected End of Stream")
|
|
nRead += delta
|
|
}
|
|
}
|
|
|
|
// Reads specified bytes and compose it to ascii string
|
|
fun string(n : Int) : String {
|
|
return StringBuilder(n).apply{
|
|
ByteArray(n)
|
|
.also{ array(it)}
|
|
.forEach { append( Char( it.toInt() and 255)) }
|
|
}.toString()
|
|
}
|
|
|
|
// Reads next variable length block
|
|
fun block() : ByteArray {
|
|
blockSize = byte()
|
|
array(block, 0, blockSize)
|
|
return block
|
|
}
|
|
|
|
// Skips variable length blocks up to and including next zero length block.
|
|
fun skipBlock() {
|
|
do {
|
|
block()
|
|
} while(blockSize > 0)
|
|
}
|
|
}
|
|
|
|
// 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
|
|
enum class Dispose(val num : Int) {
|
|
|
|
Unspecified(0),
|
|
DontDispose(1),
|
|
RestoreBackground(2),
|
|
RestorePrevious(3)
|
|
}
|
|
|
|
companion object {
|
|
private const val MaxStackSize = 4096
|
|
private const val NullCode = - 1
|
|
private const val b0 = 0.toByte()
|
|
private const val OPAQUE = 255 shl 24
|
|
}
|
|
|
|
private var width = 0 // full image width
|
|
private var height = 0 // full image height
|
|
private var gctSize = 0 // size of global color table
|
|
private var loopCount = 1 // iterations; 0 = repeat forever
|
|
|
|
private var gct : IntArray? = null // global color table
|
|
|
|
private var bgIndex = 0 // background color index
|
|
private var bgColor = 0 // background color
|
|
private var lastBgColor = 0 // previous bg color
|
|
private var pixelAspect = 0 // pixel aspect ratio
|
|
|
|
private var interlace = false // interlace flag
|
|
|
|
private var lctFlag = false // local color table flag
|
|
private var lctSize = 0 // local color table size
|
|
|
|
private val srcRect = Rectangle() // current image position and size
|
|
private val lastRect = Rectangle() // last image rect
|
|
|
|
// last graphic control extension info
|
|
private var dispose = Dispose.Unspecified
|
|
private var lastDispose = Dispose.Unspecified
|
|
private var transparency = false // use transparent color
|
|
private var delay = 0 // delay in milliseconds
|
|
private var transIndex = 0 // transparent color index
|
|
|
|
// LZW decoder working arrays
|
|
private var prefix : ShortArray? = null
|
|
private var suffix : ByteArray? = null
|
|
private var pixelStack : ByteArray? = null
|
|
private var pixels : ByteArray? = null
|
|
|
|
private val frames = ArrayList<Pair<ApngFrameControl, ApngBitmap>>()
|
|
|
|
private var previousImage : ApngBitmap? = null
|
|
|
|
// 現在のdispose指定と描画結果を覚えておく
|
|
private fun memoryLastDispose(image : ApngBitmap) {
|
|
if(dispose != Dispose.RestorePrevious) previousImage = image
|
|
lastDispose = dispose
|
|
lastRect.set(srcRect)
|
|
lastBgColor = bgColor
|
|
}
|
|
|
|
// 前回のdispose指定を反映する
|
|
private fun applyLastDispose(destImage : ApngBitmap) {
|
|
|
|
if(lastDispose == Dispose.Unspecified) return
|
|
|
|
// restore previous image
|
|
val previousImage = this.previousImage
|
|
if(previousImage != null) {
|
|
System.arraycopy(previousImage.colors, 0, destImage.colors, 0, destImage.colors.size)
|
|
}
|
|
|
|
if(lastDispose == Dispose.RestoreBackground) {
|
|
|
|
// fill lastRect
|
|
|
|
val fillColor = if(transparency) {
|
|
0 // assume background is transparent
|
|
} else {
|
|
lastBgColor // use given background color
|
|
}
|
|
|
|
for(y in lastRect.y until lastRect.y + lastRect.h) {
|
|
val fillStart = y * destImage.width + lastRect.x
|
|
val fillWidth = lastRect.w
|
|
destImage.colors.fill(
|
|
fillColor,
|
|
fromIndex = fillStart,
|
|
toIndex = fillStart + fillWidth
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// render to ApngBitmap
|
|
// may use some previous frame.
|
|
private fun render(destImage : ApngBitmap, act : IntArray) {
|
|
// expose destination image's pixels as int array
|
|
val dest = destImage.colors
|
|
|
|
// copy each source line to the appropriate place in the destination
|
|
var pass = 1
|
|
var inc = 8
|
|
var iline = 0
|
|
for(i in 0 until srcRect.h) {
|
|
var line = i
|
|
if(interlace) {
|
|
if(iline >= srcRect.h) {
|
|
when(++ pass) {
|
|
2 -> {
|
|
iline = 4
|
|
}
|
|
|
|
3 -> {
|
|
iline = 2
|
|
inc = 4
|
|
}
|
|
|
|
4 -> {
|
|
iline = 1
|
|
inc = 2
|
|
}
|
|
}
|
|
}
|
|
line = iline
|
|
iline += inc
|
|
}
|
|
line += srcRect.y
|
|
if(line < height) {
|
|
|
|
// start of line in source
|
|
var sx = i * srcRect.w
|
|
|
|
//
|
|
val k = line * width
|
|
|
|
// loop for dest line.
|
|
for(dx in k + srcRect.x until min(k + width, k + srcRect.x + srcRect.w)) {
|
|
// map color and insert in destination
|
|
val index = pixels !![sx ++].toInt() and 0xff
|
|
val c = act[index]
|
|
if(c != 0) dest[dx] = c
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Decodes LZW image data into pixel array.
|
|
* Adapted from John Cristy's ImageMagick.
|
|
*/
|
|
private fun decodeImageData(reader : Reader) {
|
|
|
|
// allocate pixel array if need
|
|
val nPixels = srcRect.w * srcRect.h
|
|
if((pixels?.size ?: 0) < nPixels) pixels = ByteArray(nPixels)
|
|
val pixels = this.pixels !!
|
|
|
|
if(prefix == null) prefix = ShortArray(MaxStackSize)
|
|
if(suffix == null) suffix = ByteArray(MaxStackSize)
|
|
if(pixelStack == null) pixelStack = ByteArray(MaxStackSize + 1)
|
|
val prefix = this.prefix !!
|
|
val suffix = this.suffix !!
|
|
val pixelStack = this.pixelStack !!
|
|
|
|
// Initialize GIF data stream decoder.
|
|
val data_size = reader.byte()
|
|
val clear = 1 shl data_size
|
|
val end_of_information = clear + 1
|
|
|
|
var available = clear + 2
|
|
var old_code = NullCode
|
|
var code_size = data_size + 1
|
|
var code_mask = (1 shl code_size) - 1
|
|
|
|
for(code in 0 until clear) {
|
|
prefix[code] = 0
|
|
suffix[code] = code.toByte()
|
|
}
|
|
|
|
// Decode GIF pixel stream.
|
|
var datum = 0
|
|
var bits = 0
|
|
var count = 0
|
|
var first = 0
|
|
var top = 0
|
|
var bi = 0
|
|
var pi = 0
|
|
|
|
var i = 0
|
|
while(i < nPixels) {
|
|
if(top == 0) {
|
|
if(bits < code_size) {
|
|
// Load bytes until there are enough bits for a code.
|
|
if(count == 0) {
|
|
// Read a new data block.
|
|
reader.block()
|
|
count = reader.blockSize
|
|
if(count <= 0) break
|
|
bi = 0
|
|
}
|
|
datum += (reader.block[bi].toInt() and 0xff) shl bits
|
|
bits += 8
|
|
bi ++
|
|
count --
|
|
continue
|
|
}
|
|
|
|
// Get the next code.
|
|
var code = datum and code_mask
|
|
datum = datum shr code_size
|
|
bits -= code_size
|
|
|
|
// Interpret the code
|
|
if((code > available) || (code == end_of_information)) break
|
|
|
|
if(code == clear) {
|
|
// Reset decoder.
|
|
code_size = data_size + 1
|
|
code_mask = (1 shl code_size) - 1
|
|
available = clear + 2
|
|
old_code = NullCode
|
|
continue
|
|
}
|
|
if(old_code == NullCode) {
|
|
pixelStack[top ++] = suffix[code]
|
|
old_code = code
|
|
first = code
|
|
continue
|
|
}
|
|
val in_code = code
|
|
|
|
if(code == available) {
|
|
pixelStack[top ++] = first.toByte()
|
|
code = old_code
|
|
}
|
|
while(code > clear) {
|
|
pixelStack[top ++] = suffix[code]
|
|
code = prefix[code].toInt()
|
|
}
|
|
first = suffix[code].toInt() and 0xff
|
|
|
|
// Add a new string to the string table,
|
|
|
|
if(available >= MaxStackSize) {
|
|
pixelStack[top ++] = first.toByte()
|
|
continue
|
|
}
|
|
pixelStack[top ++] = first.toByte()
|
|
prefix[available] = old_code.toShort()
|
|
suffix[available] = first.toByte()
|
|
available ++
|
|
|
|
if((available and code_mask) == 0 && available < MaxStackSize) {
|
|
code_size ++
|
|
code_mask += available
|
|
}
|
|
|
|
old_code = in_code
|
|
}
|
|
|
|
// Pop a pixel off the pixel stack.
|
|
top --
|
|
pixels[pi ++] = pixelStack[top]
|
|
i ++
|
|
}
|
|
|
|
// clear missing pixels
|
|
for(n in pi until nPixels) {
|
|
pixels[n] = b0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads color table as 256 RGB integer values
|
|
*
|
|
* @param nColors int number of colors to read
|
|
* @return int array containing 256 colors (packed ARGB with full alpha)
|
|
*/
|
|
private fun parseColorTable(reader : Reader, nColors : Int) : IntArray {
|
|
val nBytes = 3 * nColors
|
|
val c = ByteArray(nBytes)
|
|
reader.array(c)
|
|
|
|
// max size to avoid bounds checks
|
|
val tab = IntArray(256)
|
|
var i = 0
|
|
var j = 0
|
|
while(i < nColors) {
|
|
val r = c[j].toInt() and 255
|
|
val g = c[j + 1].toInt() and 255
|
|
val b = c[j + 2].toInt() and 255
|
|
j += 3
|
|
tab[i ++] = (OPAQUE or (r shl 16) or (g shl 8) or b)
|
|
}
|
|
return tab
|
|
}
|
|
|
|
private fun parseDispose(num : Int) =
|
|
Dispose.values().find { it.num == num } ?: error("unknown dispose $num")
|
|
|
|
/**
|
|
* Reads Graphics Control Extension values
|
|
*/
|
|
private fun parseGraphicControlExt(reader : Reader) {
|
|
reader.byte() // block size
|
|
val packed = reader.byte() // packed fields
|
|
dispose = parseDispose((packed and 0x1c) shr 2) // disposal method
|
|
if(callback.canGifDebug()) callback.onGifDebug("parseGraphicControlExt: frame=${frames.size} dispose=$dispose")
|
|
// elect to keep old image if discretionary
|
|
if(dispose == Dispose.Unspecified) dispose = Dispose.DontDispose
|
|
|
|
transparency = (packed and 1) != 0
|
|
// delay in milliseconds
|
|
delay = reader.UInt16() * 10
|
|
// transparent color index
|
|
transIndex = reader.byte()
|
|
// block terminator
|
|
reader.byte()
|
|
}
|
|
|
|
// Reads Netscape extension to obtain iteration count
|
|
private fun readNetscapeExt(reader : Reader) {
|
|
do {
|
|
val block = reader.block()
|
|
if(block[0].toInt() == 1) {
|
|
// loop count sub-block
|
|
val b1 = block[1].toInt() and 255
|
|
val b2 = block[2].toInt() and 255
|
|
loopCount = ((b2 shl 8) and b1)
|
|
}
|
|
} while(reader.blockSize > 0)
|
|
}
|
|
|
|
// Reads next frame image
|
|
private fun parseFrame(reader : Reader) {
|
|
// (sub)image position & size
|
|
srcRect.x = reader.UInt16()
|
|
srcRect.y = reader.UInt16()
|
|
srcRect.w = reader.UInt16()
|
|
srcRect.h = reader.UInt16()
|
|
|
|
val packed = reader.byte()
|
|
lctFlag = (packed and 0x80) != 0 // 1 - local color table flag
|
|
interlace = (packed and 0x40) != 0 // 2 - interlace flag
|
|
// 3 - sort flag
|
|
// 4-5 - reserved
|
|
lctSize = 2 shl (packed and 7) // 6-8 - local color table size
|
|
|
|
val act = if(lctFlag) {
|
|
// make local table active
|
|
parseColorTable(reader, lctSize)
|
|
} else {
|
|
// make global table active
|
|
if(bgIndex == transIndex) bgColor = 0
|
|
gct !!
|
|
}
|
|
|
|
var save = 0
|
|
if(transparency) {
|
|
save = act[transIndex]
|
|
act[transIndex] = 0 // set transparent color if specified
|
|
}
|
|
|
|
decodeImageData(reader) // decode pixel data
|
|
reader.skipBlock()
|
|
|
|
// add image to frame list
|
|
frames.add(
|
|
Pair(
|
|
ApngFrameControl(
|
|
width = width,
|
|
height = height,
|
|
xOffset = 0,
|
|
yOffset = 0,
|
|
disposeOp = DisposeOp.None,
|
|
blendOp = BlendOp.Source,
|
|
sequenceNumber = frames.size,
|
|
delayMilliseconds = delay.toLong()
|
|
),
|
|
ApngBitmap(width, height).also {
|
|
applyLastDispose(it)
|
|
render(it, act) // transfer pixel data to image
|
|
memoryLastDispose(it)
|
|
}
|
|
)
|
|
)
|
|
|
|
if(transparency) {
|
|
act[transIndex] = save
|
|
}
|
|
|
|
/**
|
|
* Resets frame state for reading next image.
|
|
*/
|
|
dispose = Dispose.Unspecified
|
|
transparency = false
|
|
delay = 0
|
|
}
|
|
|
|
// read GIF content blocks
|
|
private fun readContents(reader : Reader) : ApngAnimationControl {
|
|
loopBlocks@ while(true) {
|
|
when(val blockCode = reader.byte()) {
|
|
// image separator
|
|
0x2C -> parseFrame(reader)
|
|
// extension
|
|
0x21 -> when(reader.byte()) {
|
|
// graphics control extension
|
|
0xf9 -> parseGraphicControlExt(reader)
|
|
// application extension
|
|
0xff -> {
|
|
val block = reader.block()
|
|
val app = StringBuilder(12)
|
|
for(i in 0 until 11) {
|
|
app.append( Char( block[i].toInt() and 255 ))
|
|
}
|
|
if(app.toString() == "NETSCAPE2.0") {
|
|
readNetscapeExt(reader)
|
|
} else {
|
|
reader.skipBlock() // don't care
|
|
}
|
|
}
|
|
|
|
else -> {
|
|
// uninteresting extension
|
|
reader.skipBlock()
|
|
}
|
|
}
|
|
|
|
// terminator
|
|
0x3b -> break@loopBlocks
|
|
|
|
// bad byte, but keep going and see what happens
|
|
0x00 -> {
|
|
}
|
|
|
|
else -> error("unknown block code $blockCode")
|
|
}
|
|
}
|
|
|
|
return ApngAnimationControl(numFrames = frames.size, numPlays = loopCount)
|
|
}
|
|
|
|
/**
|
|
* Initializes or re-initializes reader
|
|
*/
|
|
private fun reset() {
|
|
frames.clear()
|
|
loopCount = ApngAnimationControl.PLAY_INDEFINITELY
|
|
gct = null
|
|
prefix = null
|
|
suffix = null
|
|
pixelStack = null
|
|
pixels = null
|
|
previousImage = null
|
|
}
|
|
|
|
/**
|
|
* Reads GIF file header information.
|
|
*/
|
|
private fun parseImageHeader(reader : Reader) : ApngImageHeader {
|
|
|
|
val id = reader.string(6)
|
|
if(! id.startsWith("GIF"))
|
|
error("file header not match to GIF.")
|
|
|
|
/**
|
|
* Reads Logical Screen Descriptor
|
|
*/
|
|
|
|
// logical screen size
|
|
width = reader.UInt16()
|
|
height = reader.UInt16()
|
|
if(width < 1 || height < 1) error("too small size. ${width}*${height}")
|
|
|
|
// packed fields
|
|
val packed = reader.byte()
|
|
|
|
// global color table used
|
|
val gctFlag = (packed and 0x80) != 0 // 1 : global color table flag
|
|
// 2-4 : color resolution
|
|
// 5 : gct sort flag
|
|
gctSize = 2 shl (packed and 7) // 6-8 : gct size
|
|
|
|
bgIndex = reader.byte() // background color index
|
|
pixelAspect = reader.byte() // pixel aspect ratio
|
|
|
|
gct = if(gctFlag) {
|
|
val table = parseColorTable(reader, gctSize)
|
|
bgColor = table[bgIndex]
|
|
table
|
|
} else {
|
|
bgColor = 0
|
|
null
|
|
}
|
|
|
|
return ApngImageHeader(
|
|
width = this.width,
|
|
height = this.height,
|
|
bitDepth = 8,
|
|
colorType = ColorType.INDEX,
|
|
compressionMethod = CompressionMethod.Standard,
|
|
filterMethod = FilterMethod.Standard,
|
|
interlaceMethod = InterlaceMethod.None
|
|
)
|
|
}
|
|
|
|
fun parse(src : InputStream) {
|
|
|
|
reset()
|
|
|
|
val reader = Reader(src)
|
|
val header = parseImageHeader(reader)
|
|
val animationControl = readContents(reader)
|
|
|
|
// GIFは最後まで読まないとフレーム数が分からない
|
|
|
|
if(frames.isEmpty()) throw error("there is no frame.")
|
|
callback.onGifHeader(header)
|
|
callback.onGifAnimationInfo(header, animationControl)
|
|
for(frame in frames) {
|
|
callback.onGifAnimationFrame(frame.first, frame.second)
|
|
}
|
|
|
|
reset()
|
|
}
|
|
}
|