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>() 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() } }