1
0
mirror of https://github.com/tateisu/SubwayTooter synced 2025-01-26 16:56:28 +01:00

fix gif decode error

This commit is contained in:
tateisu 2019-08-12 10:12:39 +09:00
parent 13b2633b84
commit 1213ffba07
3 changed files with 74 additions and 77 deletions

View File

@ -14,17 +14,17 @@ class ApngAnimationControl(
// 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 = PLAY_INDEFINITELY
){
) {
companion object {
const val PLAY_INDEFINITELY = 0
internal fun parse(src : ByteSequence):ApngAnimationControl{
internal fun parse(src : ByteSequence) : ApngAnimationControl {
val numFrames = src.readInt32()
val numPlays = src.readInt32()
return ApngAnimationControl(
numFrames = numFrames,
numPlays = numPlays
numPlays = numPlays
)
}

View File

@ -3,11 +3,14 @@ package jp.juggler.apng
import java.io.InputStream
import kotlin.math.min
// thanks to https://raw.githubusercontent.com/rtyley/animated-gif-lib-for-java/master/src/main/java/com/madgag/gif/fmsware/GifDecoder.java
// 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.
class GifDecoder {
// 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) {
@ -34,7 +37,7 @@ class GifDecoder {
var nRead = 0
while(nRead < length) {
val delta = bis.read(ba, offset + nRead, length - nRead)
if(delta == - 1) throw RuntimeException("unexpected End of Stream")
if(delta == - 1) throw error("unexpected End of Stream")
nRead += delta
}
}
@ -62,24 +65,19 @@ class GifDecoder {
}
// 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
enum class Dispose(val num:Int){
NoAction(0),
LeaveInPlace(1),
RestoreToBg(2),
RestoreToPrev(3)
enum class Dispose(val num : Int) {
Unspecified(0),
DontDispose(1),
RestoreBackground(2),
RestorePrevious(3)
}
companion object {
// max decoder pixel stack size
private const val MaxStackSize = 4096
private const val NullCode = - 1
private const val b0 = 0.toByte()
private const val OPAQUE = 0xff shl 24
private const val OPAQUE = 255 shl 24
}
private var width = 0 // full image width
@ -103,8 +101,8 @@ class GifDecoder {
private val lastRect = Rectangle() // last image rect
// last graphic control extension info
private var dispose = Dispose.NoAction
private var lastDispose = Dispose.NoAction
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
@ -117,52 +115,54 @@ class GifDecoder {
private val frames = ArrayList<Pair<ApngFrameControl, ApngBitmap>>()
private var lastImage : ApngBitmap? = null
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
val dest_w = destImage.width
// fill in starting image contents based on last image's dispose code
if(lastDispose != Dispose.NoAction ) {
if(lastDispose == Dispose.RestoreToPrev ) {
// 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 == Dispose.RestoreToBg ) {
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
@ -209,6 +209,7 @@ class GifDecoder {
}
}
}
}
/**
@ -362,7 +363,7 @@ class GifDecoder {
return tab
}
private fun parseDispose(num:Int) =
private fun parseDispose(num : Int) =
Dispose.values().find { it.num == num } ?: error("unknown dispose $num")
/**
@ -371,9 +372,10 @@ class GifDecoder {
private fun parseGraphicControlExt(reader : Reader) {
reader.byte() // block size
val packed = reader.byte() // packed fields
dispose = parseDispose( (packed and 0x1c) shr 2 )// disposal method
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.NoAction ) dispose = Dispose.LeaveInPlace
if(dispose == Dispose.Unspecified) dispose = Dispose.DontDispose
transparency = (packed and 1) != 0
// delay in milliseconds
@ -430,10 +432,6 @@ class GifDecoder {
decodeImageData(reader) // decode pixel data
reader.skipBlock()
val image = ApngBitmap(width, height).also {
render(it, act) // transfer pixel data to image
}
// add image to frame list
frames.add(
Pair(
@ -447,7 +445,11 @@ class GifDecoder {
sequenceNumber = frames.size,
delayMilliseconds = delay.toLong()
),
image
ApngBitmap(width, height).also {
applyLastDispose(it)
render(it, act) // transfer pixel data to image
memoryLastDispose(it)
}
)
)
@ -458,12 +460,7 @@ class GifDecoder {
/**
* Resets frame state for reading next image.
*/
lastDispose = dispose
lastImage = image
lastRect.set(srcRect)
lastBgColor = bgColor
dispose = Dispose.NoAction
dispose = Dispose.Unspecified
transparency = false
delay = 0
}
@ -517,13 +514,13 @@ class GifDecoder {
*/
private fun reset() {
frames.clear()
lastImage = null
loopCount = ApngAnimationControl.PLAY_INDEFINITELY
gct = null
prefix = null
suffix = null
pixelStack = null
pixels = null
previousImage = null
}
/**
@ -576,7 +573,7 @@ class GifDecoder {
)
}
fun parse(src : InputStream, callback : GifDecoderCallback) {
fun parse(src : InputStream) {
reset()

View File

@ -116,7 +116,7 @@ class ApngFrames private constructor(
) : ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
GifDecoder().parse(inStream, result)
GifDecoder(result).parse(inStream)
result.onParseComplete()
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("GIF has no image")