GIF形式のカスタム絵文字に対応

This commit is contained in:
tateisu 2019-08-12 01:52:08 +09:00
parent 49ee5696d4
commit 73b53ba9c4
10 changed files with 957 additions and 158 deletions

View File

@ -4,24 +4,30 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence
class ApngAnimationControl internal constructor(src : ByteSequence) {
companion object {
const val PLAY_INDEFINITELY = 0
}
class ApngAnimationControl(
// 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
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
val numPlays : Int = PLAY_INDEFINITELY
){
init {
numFrames = src.readInt32()
numPlays = src.readInt32()
companion object {
const val PLAY_INDEFINITELY = 0
internal fun parse(src : ByteSequence):ApngAnimationControl{
val numFrames = src.readInt32()
val numPlays = src.readInt32()
return ApngAnimationControl(
numFrames = numFrames,
numPlays = numPlays
)
}
}
override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"

View File

@ -47,7 +47,8 @@ object ApngDecoder {
"IEND" -> break@loop
"IHDR" -> {
val header = ApngImageHeader(ByteSequence(chunk.readBody(crc32, tokenizer)))
val header =
ApngImageHeader.parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
bitmap = ApngBitmap(header.width, header.height)
apng.header = header
callback.onHeader(apng, header)
@ -107,17 +108,17 @@ object ApngDecoder {
"acTL" -> {
val header = apng.header ?: throw ApngParseError("missing IHDR")
val animationControl =
ApngAnimationControl(ByteSequence(chunk.readBody(crc32, tokenizer)))
val animationControl = ApngAnimationControl
.parse(ByteSequence(chunk.readBody(crc32, tokenizer)))
apng.animationControl = animationControl
callback.onAnimationInfo(apng, header, animationControl)
}
"fcTL" -> {
val bat = ByteSequence(chunk.readBody(crc32, tokenizer))
val sequenceNumber =bat.readInt32()
val sequenceNumber = bat.readInt32()
checkSequenceNumber(sequenceNumber)
lastFctl = ApngFrameControl(bat,sequenceNumber)
lastFctl = ApngFrameControl.parse(bat, sequenceNumber)
fdatDecoder = null
}
@ -135,7 +136,7 @@ object ApngDecoder {
callback.onAnimationFrame(apng, fctl, bitmap)
}
}
val sequenceNumber =tokenizer.readInt32(crc32)
val sequenceNumber = tokenizer.readInt32(crc32)
checkSequenceNumber(sequenceNumber)
fdatDecoder.addData(
tokenizer.inStream,
@ -145,8 +146,8 @@ object ApngDecoder {
)
chunk.checkCRC(tokenizer, crc32.value)
}
// 無視するチャンク
// 無視するチャンク
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information
"tEXt", "zTXt", "iTXt", // text information
"tIME", // timestamp

View File

@ -4,40 +4,51 @@ package jp.juggler.apng
import jp.juggler.apng.util.ByteSequence
class ApngFrameControl internal constructor(src : ByteSequence,var sequenceNumber:Int) {
class ApngFrameControl (
val width : Int,
val height : Int,
val xOffset : Int,
val yOffset : Int,
val disposeOp : DisposeOp,
val blendOp : BlendOp,
val sequenceNumber:Int,
val delayMilliseconds: Long
) {
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 }
companion object{
internal fun parse(src : ByteSequence, sequenceNumber:Int) :ApngFrameControl{
val width = src.readInt32()
val height = src.readInt32()
val xOffset = src.readInt32()
val yOffset = src.readInt32()
val delayNum = src.readUInt16()
val delayDen = src.readUInt16().let { if(it == 0) 100 else it }
var num : Int
num = src.readUInt8()
val disposeOp = DisposeOp.values().first { it.num == num }
num = src.readUInt8()
val blendOp = BlendOp.values().first { it.num == num }
return ApngFrameControl(
width =width,
height = height,
xOffset = xOffset,
yOffset = yOffset,
disposeOp = disposeOp,
blendOp = blendOp,
sequenceNumber = sequenceNumber,
delayMilliseconds = when(delayDen) {
0,1000 -> delayNum.toLong()
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
}
)
}
}
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()
}
"ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayMilliseconds=$delayMilliseconds,disposeOp=$disposeOp,blendOp=$blendOp)"
}

View File

@ -5,37 +5,49 @@ 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
class ApngImageHeader(
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 }
) {
companion object{
internal fun parse (src : ByteSequence) :ApngImageHeader{
val width = src.readInt32()
val height = src.readInt32()
if(width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small")
val bitDepth = src.readUInt8()
var num : Int
//
num = src.readUInt8()
val colorType = ColorType.values().first { it.num == num }
//
num = src.readUInt8()
val compressionMethod = CompressionMethod.values().first { it.num == num }
//
num = src.readUInt8()
val filterMethod = FilterMethod.values().first { it.num == num }
//
num = src.readUInt8()
val interlaceMethod = InterlaceMethod.values().first { it.num == num }
return ApngImageHeader(
width =width,
height = height,
bitDepth = bitDepth,
colorType = colorType,
compressionMethod = compressionMethod,
filterMethod = filterMethod,
interlaceMethod = interlaceMethod
)
}
}
override fun toString() =

View File

@ -0,0 +1,652 @@
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) {
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
}
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())
}
}
.toString()
}
@Suppress("MemberVisibilityCanBePrivate", "unused")
class GifDecoder(val callback: GifDecoderCallback) {
companion object {
// max decoder pixel stack size
const val MaxStackSize = 4096
const val NullCode = - 1
private const val b0 = 0.toByte()
}
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 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
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 var ix = 0 // current image rectangle
private var iy = 0 // current image rectangle
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
// last graphic control extension info
private var dispose = 0
// 0=no action; 1=leave in place; 2=restore to bg; 3=restore to prev
private var lastDispose = 0
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 var frames = ArrayList<GifFrame>() // frames read from current file
private var frameCount = 0
/////////////////////////////////////////////////////////////
// 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.
/**
* Decodes LZW image data into pixel array.
* Adapted from John Cristy's ImageMagick.
*/
private fun decodeImageData(reader:Reader) {
// Reallocate pizel array if need
val npix = iw * ih
if((pixels?.size ?: 0) < npix) pixels = ByteArray(npix)
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.read()
val clear = 1 shl data_size
val end_of_information = clear + 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 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) {
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.readBlock()
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 npix) {
pixels[n] = b0
}
}
/**
* 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 {
val nBytes = 3 * nColors
val c = ByteArray(nBytes)
val n = reader.readArray(c)
if(n < nBytes) throw RuntimeException("unexpected EOS")
// 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)
}
return tab
}
/**
* Reads Graphics Control Extension values
*/
private fun readGraphicControlExt(reader:Reader) {
reader.read() // block size
val packed = reader.read() // 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
// transparent color index
transIndex = reader.read()
// block terminator
reader.read()
}
// Reads Netscape extension to obtain iteration count
private fun readNetscapeExt(reader:Reader) {
do {
val block = reader.readBlock()
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 readImage(reader:Reader) {
ix = reader.readShort() // (sub)image position & size
iy = reader.readShort()
iw = reader.readShort()
ih = reader.readShort()
val packed = reader.read()
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) {
lct = readColorTable(reader,lctSize) // read table
lct !! // make local table active
} 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()
++ frameCount
// create new image to receive frame data
val image = ApngBitmap(width, height).apply {
setPixels(this) // transfer pixel data to image
}
this.image = image
frames.add(GifFrame(image, delay)) // add image to frame list
if(transparency) {
act[transIndex] = save
}
/**
* Resets frame state for reading next image.
*/
lastDispose = dispose
lastRect = Rectangle(ix, iy, iw, ih)
lastBgColor = bgColor
dispose = 0
transparency = false
delay = 0
lct = null
}
private fun readContents(reader : Reader) {
// read GIF file content blocks
loopBlocks@ while(true) {
when(val blockCode = reader.read()) {
// image separator
0x2C -> readImage(reader)
// extension
0x21 -> when(reader.read()) {
// graphics control extension
0xf9 -> readGraphicControlExt(reader)
// application extension
0xff -> {
val block = reader.readBlock()
var app = ""
for(i in 0 until 11) {
app += block[i].toChar()
}
if(app == "NETSCAPE2.0") {
readNetscapeExt(reader)
} else {
reader.skip() // don't care
}
}
else -> {
// uninteresting extension
reader.skip()
}
}
// terminator
0x3b -> break@loopBlocks
// bad byte, but keep going and see what happens
0x00 -> {
}
else -> error("unknown block code $blockCode")
}
}
}
var header : ApngImageHeader? = null
var animationControl : ApngAnimationControl? = null
/**
* Reads GIF file header information.
*/
private fun readHeader(reader : Reader) {
/**
* Initializes or re-initializes reader
*/
frameCount = 0
frames.clear()
lct = null
loopCount = ApngAnimationControl.PLAY_INDEFINITELY
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.readShort()
height = reader.readShort()
if( width < 1 || height < 1) error("too small size. ${width}*${height}")
// packed fields
val packed = reader.read()
// 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.read() // background color index
pixelAspect = reader.read() // pixel aspect ratio
gct = if(gctFlag) {
val table = readColorTable(reader,gctSize)
bgColor = table[bgIndex]
table
}else{
bgColor = 0
null
}
val header = ApngImageHeader(
width = this.width,
height = this.height,
bitDepth = 8,
colorType = ColorType.INDEX,
compressionMethod = CompressionMethod.Standard,
filterMethod = FilterMethod.Standard,
interlaceMethod = InterlaceMethod.None
)
this.header = header
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)
}
frames.clear()
}
}

View File

@ -0,0 +1,11 @@
package jp.juggler.apng
interface GifDecoderCallback {
fun onGifWarning(message : String)
fun onGifDebug(message : String)
fun canGifDebug() : Boolean
fun onGifHeader(header : ApngImageHeader)
fun onGifAnimationInfo( header : ApngImageHeader, animationControl : ApngAnimationControl )
fun onGifAnimationFrame( frameControl : ApngFrameControl, frameBitmap : ApngBitmap )
}

View File

@ -10,11 +10,12 @@ import android.util.Log
import java.io.InputStream
import java.util.ArrayList
import kotlin.math.max
class ApngFrames private constructor(
private val pixelSizeMax : Int = 0,
private val debug : Boolean = false
) : ApngDecoderCallback {
) : ApngDecoderCallback, GifDecoderCallback {
companion object {
@ -59,10 +60,10 @@ class ApngFrames private constructor(
val hDst : Int
if(wSrc >= hSrc) {
wDst = size_max
hDst = Math.max(1, scale(size_max, hSrc, wSrc))
hDst = max(1, scale(size_max, hSrc, wSrc))
} else {
hDst = size_max
wDst = Math.max(1, scale(size_max, wSrc, hSrc))
wDst = max(1, scale(size_max, wSrc, hSrc))
}
//Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst")
@ -90,8 +91,7 @@ class ApngFrames private constructor(
private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) =
scaleBitmap(size_max, toAndroidBitmap(src))
@Suppress("unused")
fun parseApng(
private fun parseApng(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
@ -108,6 +108,50 @@ class ApngFrames private constructor(
}
}
private fun parseGif(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
) : ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
GifDecoder(result).read(inStream)
result.onParseComplete()
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("GIF has no image")
} catch(ex : Throwable) {
result.dispose()
throw ex
}
}
// PNGヘッダを確認
@Suppress("unused")
fun parse(
pixelSizeMax : Int,
debug : Boolean = false,
opener: ()->InputStream?
) : ApngFrames? {
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) }
}
if(buf.size >= 6
&& buf[0].toChar() == 'G'
&& buf[1].toChar() == 'I'
&& buf[2].toChar() == 'F'
) {
return opener()?.use{ parseGif(it,pixelSizeMax,debug) }
}
return null
}
}
private var header : ApngImageHeader? = null
@ -120,7 +164,6 @@ class ApngFrames private constructor(
var height : Int = 1
private set
class Frame(
val bitmap : Bitmap,
val timeStart : Long,
@ -156,7 +199,6 @@ class ApngFrames private constructor(
}
}
constructor(bitmap : Bitmap) : this() {
defaultImage = bitmap
}
@ -214,9 +256,9 @@ class ApngFrames private constructor(
val isFinite = animationControl.isFinite
val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
val endWait = if(isFinite) DELAY_AFTER_END else 0L
val timeTotalLoop = Math.max(1, timeTotal * repeatSequenceCount + endWait)
val timeTotalLoop = max(1, timeTotal * repeatSequenceCount + endWait)
val tf = (Math.max(0, t) / durationScale).toLong()
val tf = (max(0, t) / durationScale).toLong()
// 全体の繰り返し時刻で余りを計算
val tl = tf % timeTotalLoop
@ -251,7 +293,7 @@ class ApngFrames private constructor(
val frame = frames[s]
val delay = frame.timeStart + frame.timeWidth - tt
result.bitmap = frames[s].bitmap
result.delay = (0.5f + durationScale * Math.max(0f, delay.toFloat())).toLong()
result.delay = (0.5f + durationScale * max(0f, delay.toFloat())).toLong()
// log.d("findFrame tf=%d,tl=%d/%d,tt=%d/%d,s=%d,w=%d,delay=%d",tf,tl,loop_total,tt,timeTotal,s,frame.timeWidth,result.delay);
}
@ -278,8 +320,8 @@ class ApngFrames private constructor(
header : ApngImageHeader,
animationControl : ApngAnimationControl
) {
if(debug){
Log.d(TAG,"onAnimationInfo")
if(debug) {
Log.d(TAG, "onAnimationInfo")
}
this.animationControl = animationControl
this.frames = ArrayList(animationControl.numFrames)
@ -290,32 +332,33 @@ class ApngFrames private constructor(
}
override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
if(debug){
Log.d(TAG,"onDefaultImage")
if(debug) {
Log.d(TAG, "onDefaultImage")
}
defaultImage?.recycle()
defaultImage = toAndroidBitmap(bitmap, pixelSizeMax)
}
override fun onAnimationFrame(
apng : Apng,
frameControl : ApngFrameControl,
frameBitmap : ApngBitmap
) {
if(debug){
Log.d(TAG,"onAnimationFrame seq=${frameControl.sequenceNumber }, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}")
if(debug) {
Log.d(
TAG,
"onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}"
)
}
val frames = this.frames ?: return
val canvasBitmap = this.canvasBitmap ?: return
val disposeOp = when{
val disposeOp = when {
// If the first `fcTL` chunk uses a `dispose_op` of APNG_DISPOSE_OP_PREVIOUS it should be treated as APNG_DISPOSE_OP_BACKGROUND.
frameControl.disposeOp == DisposeOp.Previous && frames.isEmpty() -> DisposeOp.Background
else-> frameControl.disposeOp
else -> frameControl.disposeOp
}
val previous : Bitmap? = when(disposeOp) {
@ -340,11 +383,11 @@ class ApngFrames private constructor(
frameControl.xOffset.toFloat(),
frameControl.yOffset.toFloat(),
when(frameControl.blendOp) {
// all color components of the frame, including alpha,
// overwrite the current contents of the frame's output buffer region.
// all color components of the frame, including alpha,
// overwrite the current contents of the frame's output buffer region.
BlendOp.Source -> sPaintDontBlend
// the frame should be composited onto the output buffer based on its alpha,
// using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2].
// the frame should be composited onto the output buffer based on its alpha,
// using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2].
BlendOp.Over -> null
}
)
@ -352,34 +395,34 @@ class ApngFrames private constructor(
val frame = Frame(
bitmap = scaleBitmap(pixelSizeMax, canvasBitmap, recycleSrc = false),
timeStart = timeTotal,
timeWidth = Math.max(1L, frameControl.delayMilliseconds)
timeWidth = max(1L, frameControl.delayMilliseconds)
)
frames.add(frame)
timeTotal += frame.timeWidth
when(disposeOp) {
// no disposal is done on this frame before rendering the next;
// the contents of the output buffer are left as is.
// no disposal is done on this frame before rendering the next;
// the contents of the output buffer are left as is.
DisposeOp.None -> {
}
// the frame's region of the output buffer is
// to be cleared to fully transparent black
// before rendering the next frame.
// the frame's region of the output buffer is
// to be cleared to fully transparent black
// before rendering the next frame.
DisposeOp.Background -> {
val rect = Rect()
rect.left = frameControl.xOffset
rect.top = frameControl.yOffset
rect.right = frameControl.xOffset + frameControl.width
rect.bottom = frameControl.yOffset + frameControl.height
canvas.drawRect(rect,sPaintClear)
canvas.drawRect(rect, sPaintClear)
// canvas.drawColor(0, PorterDuff.Mode.CLEAR)
}
// the frame's region of the output buffer is
// to be reverted to the previous contents
// before rendering the next frame.
// the frame's region of the output buffer is
// to be reverted to the previous contents
// before rendering the next frame.
DisposeOp.Previous -> if(previous != null) {
canvas.drawBitmap(
previous,
@ -398,4 +441,68 @@ class ApngFrames private constructor(
previous?.recycle()
}
}
///////////////////////////////////////////////////////////////////////
// Gif support
override fun onGifWarning(message : String) {
Log.w(TAG, message)
}
override fun onGifDebug(message : String) {
Log.d(TAG, message)
}
override fun canGifDebug() : Boolean = debug
override fun onGifHeader(header : ApngImageHeader) {
this.header = header
}
override fun onGifAnimationInfo(
header : ApngImageHeader,
animationControl : ApngAnimationControl
) {
if(debug) {
Log.d(TAG, "onAnimationInfo")
}
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
) {
if(debug) {
Log.d(
TAG,
"onAnimationFrame seq=${frameControl.sequenceNumber}, xywh=${frameControl.xOffset},${frameControl.yOffset},${frameControl.width},${frameControl.height} blendOp=${frameControl.blendOp}, disposeOp=${frameControl.disposeOp},delay=${frameControl.delayMilliseconds}"
)
}
val frames = this.frames ?: return
val frameBitmapAndroid = toAndroidBitmap(frameBitmap)
try {
val frame = Frame(
bitmap = scaleBitmap(pixelSizeMax, frameBitmapAndroid, recycleSrc = false),
timeStart = timeTotal,
timeWidth = max(1L, frameControl.delayMilliseconds)
)
frames.add(frame)
timeTotal += frame.timeWidth
} finally {
frameBitmapAndroid.recycle()
}
}
}

View File

@ -253,17 +253,12 @@ class CustomEmojiCache(internal val context : Context) {
}
}
private fun decodeAPNG(data : ByteArray, url : String) : ApngFrames? {
try {
// PNGヘッダを確認
if(data.size >= 8
&& (data[0].toInt() and 0xff) == 0x89
&& (data[1].toInt() and 0xff) == 0x50
) {
// APNGをデコード
return ApngFrames.parseApng(ByteArrayInputStream(data), 64)
}
// APNGをデコード
val x = ApngFrames.parse(64){ ByteArrayInputStream(data) }
if(x != null) return x
// fall thru
} catch(ex : Throwable) {
if(DEBUG) log.trace(ex)

View File

@ -33,9 +33,9 @@ class ActList : AppCompatActivity(), CoroutineScope {
private lateinit var listAdapter : MyAdapter
private var timeAnimationStart : Long = 0L
private lateinit var activityJob: Job
private lateinit var activityJob : Job
override val coroutineContext: CoroutineContext
override val coroutineContext : CoroutineContext
get() = Dispatchers.Main + activityJob
override fun onCreate(savedInstanceState : Bundle?) {
@ -50,15 +50,15 @@ class ActList : AppCompatActivity(), CoroutineScope {
listView.onItemClickListener = listAdapter
timeAnimationStart = SystemClock.elapsedRealtime()
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Assume thisActivity is the current activity
if( PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)){
if(PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)) {
ActivityCompat.requestPermissions(
this,
arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE),
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE_STORAGE
)
@ -71,7 +71,7 @@ class ActList : AppCompatActivity(), CoroutineScope {
super.onDestroy()
activityJob.cancel()
}
override fun onRequestPermissionsResult(
requestCode : Int,
permissions : Array<String>,
@ -89,12 +89,12 @@ class ActList : AppCompatActivity(), CoroutineScope {
}
return
}
// other 'case' lines to check for other
// permissions this app might request
// other 'case' lines to check for other
// permissions this app might request
}
}
private fun load() = launch{
private fun load() = launch {
val list = async(Dispatchers.IO) {
// RawリソースのIDと名前の一覧
R.raw::class.java.fields
@ -185,29 +185,32 @@ class ActList : AppCompatActivity(), CoroutineScope {
lastId = resId
apngView.apngFrames?.dispose()
apngView.apngFrames = null
launch{
var apngFrames :ApngFrames? = null
launch {
var apngFrames : ApngFrames? = null
try {
lastJob?.cancelAndJoin()
val job = async(Dispatchers.IO) {
resources?.openRawResource(resId)?.use { inStream ->
ApngFrames.parseApng(inStream, 128)
try {
ApngFrames.parse(128){resources?.openRawResource(resId)}
} catch(ex : Throwable) {
ex.printStackTrace()
null
}
}
lastJob = job
apngFrames = job.await()
if(apngFrames != null && lastId == resId ){
if(apngFrames != null && lastId == resId) {
apngView.apngFrames = apngFrames
apngFrames = null
}
} catch(ex : Throwable) {
ex.printStackTrace()
Log.e(TAG, "load error: ${ex.javaClass.simpleName} ${ex.message}")
}finally{
} finally {
apngFrames?.dispose()
}
}

View File

@ -5,10 +5,10 @@ import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Environment
import androidx.appcompat.app.AppCompatActivity
import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import jp.juggler.apng.ApngFrames
import kotlinx.coroutines.*
import java.io.File
@ -63,16 +63,17 @@ class ActViewer : AppCompatActivity() , CoroutineScope {
launch{
var apngFrames : ApngFrames? = null
try {
apngFrames = async(Dispatchers.IO) {
resources.openRawResource(resId).use {
ApngFrames.parseApng(
it,
apngFrames = withContext(Dispatchers.IO) {
try {
ApngFrames.parse(
1024,
debug = true
)
){resources?.openRawResource(resId)}
} catch(ex : Throwable) {
ex.printStackTrace()
null
}
}.await()
}
apngView.visibility = View.VISIBLE
tvError.visibility = View.GONE