improve gif decoder

This commit is contained in:
tateisu 2019-08-12 08:39:19 +09:00
parent aed1bde1bc
commit 898521d0d1
11 changed files with 260 additions and 331 deletions

View File

@ -3,104 +3,70 @@ package jp.juggler.apng
import java.io.InputStream
import kotlin.math.min
/**
* Class GifDecoder - Decodes a GIF file into one or more frames.
*
* Example:
*
* <pre>
* {@code
* GifDecoder d = new GifDecoder();
* d.read("sample.gif");
* int n = d.getFrameCount();
* for (int i = 0; i < n; i++) {
* BufferedImage frame = d.getFrame(i); // frame i
* int t = d.getDelay(i); // display duration of frame in milliseconds
* // do something with frame
* }
* }
* </pre>
* No copyright asserted on the source code of this class. May be used for
* any purpose, however, refer to the Unisys LZW patent for any additional
* restrictions. Please forward any corrections to questions at fmsware.com.
*
* @author Kevin Weiner, FM Software; LZW decoder adapted from John Cristy's ImageMagick.
* @version 1.03 November 2003
*
*/
class Rectangle(val x : Int, val y : Int, val w : Int, val h : Int)
class GifFrame(val image : ApngBitmap, val delay : Int)
private class Reader(val bis : InputStream) {
class GifDecoder {
var block = ByteArray(256) // current data block
var blockSize = 0 // block size
// Reads a single byte from the input stream.
fun read() : Int = bis.read()
fun readArray(ba:ByteArray,offset:Int=0,length:Int=ba.size-offset) =
bis.read(ba,offset,length)
/**
* Reads next 16-bit value, LSB first
*/
// read 16-bit value, LSB first
fun readShort() = read() or (read() shl 8)
/**
* Reads next variable length block from input.
*
* @return number of bytes stored in "buffer"
*/
fun readBlock() : ByteArray {
val blockSize = read()
this.blockSize = blockSize
var n = 0
while(n < blockSize) {
val delta = bis .read(block, n, blockSize - n)
if(delta == - 1) throw RuntimeException("unexpected EOS")
n += delta
private class Rectangle(var x : Int = 0, var y : Int = 0, var w : Int = 0, var h : Int = 0) {
fun set(x : Int, y : Int, w : Int, h : Int) {
this.x = x
this.y = y
this.w = w
this.h = h
}
return block
}
/**
* Skips variable length blocks up to and including
* next zero length block.
*/
fun skip() {
do {
readBlock()
} while((blockSize > 0))
}
// read n byte and compose it to ascii string
fun string(n : Int) =
StringBuilder()
.apply {
for(i in 0 until n) {
append(read().toChar())
}
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 RuntimeException("unexpected End of Stream")
nRead += delta
}
.toString()
}
@Suppress("MemberVisibilityCanBePrivate", "unused")
class GifDecoder(val callback: GifDecoderCallback) {
}
// Reads specified bytes and compose it to ascii string
fun string(n : Int) : String {
val ba = ByteArray(n)
array(ba)
return ba.map { it.toChar() }.joinToString(separator = "")
}
// 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)
}
}
companion object {
// max decoder pixel stack size
const val MaxStackSize = 4096
private const val MaxStackSize = 4096
const val NullCode = - 1
private const val NullCode = - 1
private const val b0 = 0.toByte()
private const val OPAQUE = 0xff shl 24
}
private var width = 0 // full image width
@ -109,8 +75,6 @@ class GifDecoder(val callback: GifDecoderCallback) {
private var loopCount = 1 // iterations; 0 = repeat forever
private var gct : IntArray? = null // global color table
private var lct : IntArray? = null // local color table
private var act : IntArray? = null // active color table
private var bgIndex = 0 // background color index
private var bgColor = 0 // background color
@ -127,8 +91,7 @@ class GifDecoder(val callback: GifDecoderCallback) {
private var iw = 0 // current image rectangle
private var ih = 0 // current image rectangle
private var lastRect : Rectangle? = null // last image rect
private var image : ApngBitmap? = null // current frame
private val lastRect : Rectangle = Rectangle() // last image rect
// last graphic control extension info
private var dispose = 0
@ -144,37 +107,109 @@ class GifDecoder(val callback: GifDecoderCallback) {
private var pixelStack : ByteArray? = null
private var pixels : ByteArray? = null
private var frames = ArrayList<GifFrame>() // frames read from current file
private var frameCount = 0
private val frames = ArrayList<Pair<ApngFrameControl, ApngBitmap>>()
/////////////////////////////////////////////////////////////
// get decode result.
/**
* Gets the image contents of frame n.
*
* @return BufferedImage representation of frame, or null if n is invalid.
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
fun getFrame(n : Int) : ApngBitmap? {
frames?.let {
if(n in 0 until it.size) return it[n].image
}
return null
}
/////////////////////////////////////////////////////////////
// private functions.
private var lastImage : ApngBitmap? = null
// 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
val dest_w = destImage.width
// fill in starting image contents based on last image's dispose code
if(lastDispose > 0) {
if(lastDispose == 3) {
// use image before last
val idx = frames.size - 2
lastImage = if(idx > 0) {
frames[idx-1].second
} else {
null
}
} else {
// lastDisposeが3以外でもlastImageはnullではない場合がある
}
lastImage?.let{ lastImage ->
// copy pixels
System.arraycopy(lastImage.colors, 0, dest, 0, dest.size)
if(lastDispose == 2) {
val fillColor = if(transparency) {
0 // assume background is transparent
} else {
lastBgColor // use given background color
}
// fill lastRect
for(y in lastRect.y until lastRect.y + lastRect.h) {
val fillStart = y * dest_w + lastRect.x
val fillWidth = lastRect.w
dest.fill(fillColor, fromIndex = fillStart, toIndex = fillStart + fillWidth)
}
}
}
}
// 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 ih) {
var line = i
if(interlace) {
if(iline >= ih) {
when(++ pass) {
2 -> {
iline = 4
}
3 -> {
iline = 2
inc = 4
}
4 -> {
iline = 1
inc = 2
}
}
}
line = iline
iline += inc
}
line += iy
if(line < height) {
// start of line in source
var sx = i * iw
//
val k = line * width
// loop for dest line.
for(dx in k + ix until min(k + width, k + ix + iw)) {
// 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) {
private fun decodeImageData(reader : Reader) {
// Reallocate pizel array if need
// allocate pixel array if need
val npix = iw * ih
if((pixels?.size ?: 0) < npix) pixels = ByteArray(npix)
val pixels = this.pixels !!
@ -187,9 +222,15 @@ class GifDecoder(val callback: GifDecoderCallback) {
val pixelStack = this.pixelStack !!
// Initialize GIF data stream decoder.
val data_size = reader.read()
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()
@ -203,10 +244,6 @@ class GifDecoder(val callback: GifDecoderCallback) {
var top = 0
var bi = 0
var pi = 0
var available = clear + 2
var old_code = NullCode
var code_size = data_size + 1
var code_mask = (1 shl code_size) - 1
var i = 0
while(i < npix) {
@ -215,9 +252,9 @@ class GifDecoder(val callback: GifDecoderCallback) {
// Load bytes until there are enough bits for a code.
if(count == 0) {
// Read a new data block.
reader.readBlock()
reader.block()
count = reader.blockSize
if( count <= 0) break
if(count <= 0) break
bi = 0
}
datum += (reader.block[bi].toInt() and 0xff) shl bits
@ -292,123 +329,27 @@ class GifDecoder(val callback: GifDecoderCallback) {
}
}
/**
* Creates new frame image from current data (and previous
* frames as specified by their disposition codes).
*/
private fun setPixels(destImage : ApngBitmap) {
// expose destination image's pixels as int array
val dest = destImage.colors
val dest_w = destImage.width
// fill in starting image contents based on last image's dispose code
var lastImage : ApngBitmap? = null // previous frame
if(lastDispose > 0) {
if(lastDispose == 3) {
// use image before last
val n = frameCount - 2
if(n > 0) {
lastImage = getFrame(n - 1)
} else {
lastImage = null
}
}
}
if(lastImage != null) {
// copy pixels
System.arraycopy(lastImage.colors, 0, dest, 0, dest.size)
val lastRect = this.lastRect
if(lastDispose == 2 && lastRect != null) {
// 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 * dest_w + lastRect.x
val fillWidth = lastRect.w
dest.fill(fillColor, fromIndex = fillStart, toIndex = fillStart + fillWidth)
}
}
}
// 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 ih) {
var line = i
if(interlace) {
if(iline >= ih) {
pass ++
when(++ pass) {
2 -> {
iline = 4
inc = 8
}
3 -> {
iline = 2
inc = 4
}
4 -> {
iline = 1
inc = 2
}
}
}
line = iline
iline += inc
}
line += iy
if(line < height) {
// start of line in source
var sx = i * iw
//
val k = line * width
// loop for dest line.
for(dx in k + ix until min(k + width, k + ix + iw)) {
// map color and insert in destination
val index = pixels !![sx ++].toInt() and 0xff
val c = act !![index]
if(c != 0) dest[dx] = c
}
}
}
}
/**
* 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 readColorTable(reader:Reader,nColors : Int) : IntArray {
private fun parseColorTable(reader : Reader, nColors : Int) : IntArray {
val nBytes = 3 * nColors
val c = ByteArray(nBytes)
val n = reader.readArray(c)
if(n < nBytes) throw RuntimeException("unexpected EOS")
reader.array(c)
// max size to avoid bounds checks
val tab = IntArray(256)
var i = 0
var j = 0
val opaque = 0xff shl 24
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)
tab[i ++] = (OPAQUE or (r shl 16) or (g shl 8) or b)
}
return tab
}
@ -416,48 +357,43 @@ class GifDecoder(val callback: GifDecoderCallback) {
/**
* Reads Graphics Control Extension values
*/
private fun readGraphicControlExt(reader:Reader) {
reader.read() // block size
val packed = reader.read() // packed fields
private fun parseGraphicControlExt(reader : Reader) {
reader.byte() // block size
val packed = reader.byte() // packed fields
dispose = (packed and 0x1c) shr 2 // disposal method
// elect to keep old image if discretionary
if(dispose == 0) dispose = 1
transparency = (packed and 1) != 0
// delay in milliseconds
delay = reader.readShort() * 10
delay = reader.UInt16() * 10
// transparent color index
transIndex = reader.read()
transIndex = reader.byte()
// block terminator
reader.read()
reader.byte()
}
// Reads Netscape extension to obtain iteration count
private fun readNetscapeExt(reader:Reader) {
private fun readNetscapeExt(reader : Reader) {
do {
val block = reader.readBlock()
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 )
} while(reader.blockSize > 0)
}
// Reads next frame image
private fun readImage(reader:Reader) {
ix = reader.readShort() // (sub)image position & size
iy = reader.readShort()
iw = reader.readShort()
ih = reader.readShort()
private fun parseFrame(reader : Reader) {
ix = reader.UInt16() // (sub)image position & size
iy = reader.UInt16()
iw = reader.UInt16()
ih = reader.UInt16()
val packed = reader.read()
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
@ -465,63 +401,72 @@ class GifDecoder(val callback: GifDecoderCallback) {
lctSize = 2 shl (packed and 7) // 6-8 - local color table size
val act = if(lctFlag) {
lct = readColorTable(reader,lctSize) // read table
lct !! // make local table active
// make local table active
parseColorTable(reader, lctSize)
} else {
if(bgIndex == transIndex) bgColor = 0
gct !! // make global table active
}
this.act = act
var save = 0
if(transparency) {
save = act[transIndex]
act[transIndex] = 0 // set transparent color if specified
}
decodeImageData(reader) // decode pixel data
reader.skip()
reader.skipBlock()
++ frameCount
// create new image to receive frame data
val image = ApngBitmap(width, height).apply {
setPixels(this) // transfer pixel data to image
val image = ApngBitmap(width, height).also {
render(it,act) // transfer pixel data to image
}
this.image = image
frames.add(GifFrame(image, delay)) // add image to frame list
// 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()
),
image
)
)
if(transparency) {
act[transIndex] = save
}
/**
* Resets frame state for reading next image.
*/
lastDispose = dispose
lastRect = Rectangle(ix, iy, iw, ih)
lastRect.set(ix, iy, iw, ih)
lastBgColor = bgColor
dispose = 0
transparency = false
delay = 0
lct = null
lastImage = image
}
private fun readContents(reader : Reader) {
// read GIF file content blocks
// read GIF content blocks
private fun readContents(reader : Reader) : ApngAnimationControl {
loopBlocks@ while(true) {
when(val blockCode = reader.read()) {
when(val blockCode = reader.byte()) {
// image separator
0x2C -> readImage(reader)
0x2C -> parseFrame(reader)
// extension
0x21 -> when(reader.read()) {
0x21 -> when(reader.byte()) {
// graphics control extension
0xf9 -> readGraphicControlExt(reader)
0xf9 -> parseGraphicControlExt(reader)
// application extension
0xff -> {
val block = reader.readBlock()
val block = reader.block()
var app = ""
for(i in 0 until 11) {
app += block[i].toChar()
@ -529,13 +474,13 @@ class GifDecoder(val callback: GifDecoderCallback) {
if(app == "NETSCAPE2.0") {
readNetscapeExt(reader)
} else {
reader.skip() // don't care
reader.skipBlock() // don't care
}
}
else -> {
// uninteresting extension
reader.skip()
reader.skipBlock()
}
}
@ -549,23 +494,25 @@ class GifDecoder(val callback: GifDecoderCallback) {
else -> error("unknown block code $blockCode")
}
}
return ApngAnimationControl(numFrames = frames.size, numPlays = loopCount)
}
var header : ApngImageHeader? = null
var animationControl : ApngAnimationControl? = null
/**
* Initializes or re-initializes reader
*/
private fun reset(){
frames.clear()
lastImage = null
loopCount = ApngAnimationControl.PLAY_INDEFINITELY
}
/**
* Reads GIF file header information.
*/
private fun readHeader(reader : Reader) {
private fun parseImageHeader(reader : Reader) : ApngImageHeader {
/**
* Initializes or re-initializes reader
*/
frameCount = 0
frames.clear()
lct = null
loopCount = ApngAnimationControl.PLAY_INDEFINITELY
reset()
val id = reader.string(6)
if(! id.startsWith("GIF"))
@ -574,14 +521,14 @@ class GifDecoder(val callback: GifDecoderCallback) {
/**
* Reads Logical Screen Descriptor
*/
// logical screen size
width = reader.readShort()
height = reader.readShort()
if( width < 1 || height < 1) error("too small size. ${width}*${height}")
width = reader.UInt16()
height = reader.UInt16()
if(width < 1 || height < 1) error("too small size. ${width}*${height}")
// packed fields
val packed = reader.read()
val packed = reader.byte()
// global color table used
val gctFlag = (packed and 0x80) != 0 // 1 : global color table flag
@ -589,19 +536,19 @@ class GifDecoder(val callback: GifDecoderCallback) {
// 5 : gct sort flag
gctSize = 2 shl (packed and 7) // 6-8 : gct size
bgIndex = reader.read() // background color index
pixelAspect = reader.read() // pixel aspect ratio
bgIndex = reader.byte() // background color index
pixelAspect = reader.byte() // pixel aspect ratio
gct = if(gctFlag) {
val table = readColorTable(reader,gctSize)
val table = parseColorTable(reader, gctSize)
bgColor = table[bgIndex]
table
}else{
} else {
bgColor = 0
null
}
val header = ApngImageHeader(
return ApngImageHeader(
width = this.width,
height = this.height,
bitDepth = 8,
@ -610,43 +557,22 @@ class GifDecoder(val callback: GifDecoderCallback) {
filterMethod = FilterMethod.Standard,
interlaceMethod = InterlaceMethod.None
)
this.header = header
}
fun parse(src : InputStream, callback : GifDecoderCallback) {
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)
}
@Suppress("MemberVisibilityCanBePrivate", "unused")
fun read(src : InputStream) {
val reader = Reader(src)
readHeader(reader)
readContents(reader)
val header = this.header!!
if(frameCount < 0) throw error("frameCount < 0")
// GIFは最後まで読まないとフレーム数が分からない
// 最後まで読んでからフレーム数をコールバック
val animationControl = ApngAnimationControl(numFrames = frameCount,numPlays = loopCount)
this.animationControl = animationControl
callback.onGifAnimationInfo( header, animationControl)
// 各フレームを送る
var i=0
for(frame in frames){
val frameControl = ApngFrameControl(
width = header.width,
height = header.height,
xOffset = 0,
yOffset = 0,
disposeOp = DisposeOp.None,
blendOp = BlendOp.Source,
sequenceNumber=i++,
delayMilliseconds = frame.delay.toLong()
)
callback.onGifAnimationFrame(frameControl,frame.image)
callback.onGifAnimationInfo(header, animationControl)
for(frame in frames) {
callback.onGifAnimationFrame(frame.first, frame.second)
}
frames.clear()
reset()
}
}
}

View File

@ -108,6 +108,7 @@ class ApngFrames private constructor(
}
}
private fun parseGif(
inStream : InputStream,
pixelSizeMax : Int,
@ -115,7 +116,7 @@ class ApngFrames private constructor(
) : ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
GifDecoder(result).read(inStream)
GifDecoder().parse(inStream, result)
result.onParseComplete()
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("GIF has no image")
@ -125,31 +126,31 @@ class ApngFrames private constructor(
}
}
// PNGヘッダを確認
@Suppress("unused")
fun parse(
pixelSizeMax : Int,
debug : Boolean = false,
opener: ()->InputStream?
opener : () -> InputStream?
) : ApngFrames? {
val buf = ByteArray(8){ 0.toByte() }
opener()?.use{ it.read(buf,0,buf.size) }
val buf = ByteArray(8) { 0.toByte() }
opener()?.use { it.read(buf, 0, buf.size) }
if(buf.size >= 8
&& (buf[0].toInt() and 0xff) == 0x89
&& (buf[1].toInt() and 0xff) == 0x50
) {
return opener()?.use{ parseApng(it,pixelSizeMax,debug) }
return opener()?.use { parseApng(it, pixelSizeMax, debug) }
}
if(buf.size >= 6
&& buf[0].toChar() == 'G'
&& buf[1].toChar() == 'I'
&& buf[2].toChar() == 'F'
) {
return opener()?.use{ parseGif(it,pixelSizeMax,debug) }
return opener()?.use { parseGif(it, pixelSizeMax, debug) }
}
return null
}
}
@ -460,7 +461,6 @@ class ApngFrames private constructor(
}
override fun onGifAnimationInfo(
header : ApngImageHeader,
animationControl : ApngAnimationControl
) {
@ -469,14 +469,12 @@ class ApngFrames private constructor(
}
this.animationControl = animationControl
this.frames = ArrayList(animationControl.numFrames)
val canvasBitmap = createBlankBitmap(header.width, header.height)
this.canvasBitmap = canvasBitmap
this.canvas = Canvas(canvasBitmap)
}
override fun onGifAnimationFrame(
frameControl : ApngFrameControl,
frameBitmap : ApngBitmap
) {
@ -488,9 +486,14 @@ class ApngFrames private constructor(
}
val frames = this.frames ?: return
if(frames.isEmpty()) {
defaultImage?.recycle()
defaultImage = toAndroidBitmap(frameBitmap, pixelSizeMax)
// ここでwidth,heightがセットされる
}
val frameBitmapAndroid = toAndroidBitmap(frameBitmap)
try {
val frame = Frame(
bitmap = scaleBitmap(pixelSizeMax, frameBitmapAndroid, recycleSrc = false),
timeStart = timeTotal,

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB