2018-01-04 19:52:25 +01:00
package jp.juggler.subwaytooter.util
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
2018-01-27 12:00:44 +01:00
import android.util.Log
import jp.juggler.apng.*
2018-01-04 19:52:25 +01:00
import java.io.InputStream
import java.util.ArrayList
2018-01-27 12:00:44 +01:00
class ApngFrames ( private val pixelSizeMax : Int = 0 ) : ApngDecoderCallback {
2018-01-04 19:52:25 +01:00
companion object {
2018-01-27 12:00:44 +01:00
private const val TAG = " ApngFrames "
2018-01-04 19:52:25 +01:00
// ループしない画像の場合は3秒でまたループさせる
private const val DELAY _AFTER _END = 3000L
2018-01-27 12:00:44 +01:00
// アニメーションフレームの描画に使う
private val sSrcModePaint : Paint by lazy {
2018-01-10 16:47:35 +01:00
val paint = Paint ( )
paint . xfermode = PorterDuffXfermode ( PorterDuff . Mode . SRC )
paint . isFilterBitmap = true
paint
}
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
private fun createBlankBitmap ( w : Int , h : Int ) : Bitmap {
2018-01-04 19:52:25 +01:00
return Bitmap . createBitmap ( w , h , Bitmap . Config . ARGB _8888 )
}
// WARNING: ownership of "src" will be moved or recycled.
2018-01-27 12:00:44 +01:00
private fun scaleBitmap ( src : Bitmap ? , size _max : Int ) : Bitmap ? {
2018-01-04 19:52:25 +01:00
if ( src == null ) return null
2018-01-27 12:00:44 +01:00
if ( size _max <= 0 ) return src
val wSrc = src . width
val hSrc = src . height
if ( wSrc <= size _max && hSrc <= size _max ) return src
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
val wDst : Int
val hDst : Int
if ( wSrc >= hSrc ) {
wDst = size _max
hDst = Math . max (
1 ,
( size _max . toFloat ( ) * hSrc . toFloat ( ) / wSrc . toFloat ( ) + 0.5f ) . toInt ( )
)
2018-01-04 19:52:25 +01:00
} else {
2018-01-27 12:00:44 +01:00
hDst = size _max
wDst = Math . max (
1 ,
( size _max . toFloat ( ) * wSrc . toFloat ( ) / hSrc . toFloat ( ) + 0.5f ) . toInt ( )
)
2018-01-04 19:52:25 +01:00
}
2018-01-27 12:00:44 +01:00
val b2 = createBlankBitmap ( wDst , hDst )
2018-01-04 19:52:25 +01:00
val canvas = Canvas ( b2 )
2018-01-27 12:00:44 +01:00
val rectSrc = Rect ( 0 , 0 , wSrc , hSrc )
val rectDst = Rect ( 0 , 0 , wDst , hDst )
canvas . drawBitmap ( src , rectSrc , rectDst , sSrcModePaint )
2018-01-04 19:52:25 +01:00
src . recycle ( )
return b2
}
2018-01-27 12:00:44 +01:00
private fun toBitmap ( src : ApngBitmap ) : Bitmap {
return Bitmap . createBitmap (
src . colors , // int[] 配列
0 , // offset
src . width , //stride
src . width , // width
src . height , //height
Bitmap . Config . ARGB _8888
)
2018-01-04 19:52:25 +01:00
}
2018-01-27 12:00:44 +01:00
private fun toBitmap ( src : ApngBitmap , size _max : Int ) : Bitmap ? {
2018-01-04 19:52:25 +01:00
return scaleBitmap ( toBitmap ( src ) , size _max )
}
2018-01-27 12:00:44 +01:00
@Suppress ( " unused " )
fun parseApng ( inStream : InputStream , size _max : Int ) : ApngFrames {
val result = ApngFrames ( size _max )
2018-01-04 19:52:25 +01:00
try {
2018-01-27 12:00:44 +01:00
ApngDecoder . parseStream ( inStream , result )
result . onParseComplete ( )
return if ( result . defaultImage != null || result . frames ?. isNotEmpty ( ) == true ) {
result
} else {
throw RuntimeException ( " APNG has no image " )
}
2018-01-04 19:52:25 +01:00
} catch ( ex : Throwable ) {
2018-01-27 12:00:44 +01:00
result . dispose ( )
2018-01-04 19:52:25 +01:00
throw ex
}
}
}
2018-01-27 12:00:44 +01:00
private var header : ApngImageHeader ? = null
private var animationControl : ApngAnimationControl ? = null
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
val width : Int
get ( ) = header ?. width ?: 0
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
val height : Int
get ( ) = header ?. height ?: 0
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
@Suppress ( " MemberVisibilityCanBePrivate " )
val numFrames : Int
get ( ) = animationControl ?. numFrames ?: 1
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
@Suppress ( " unused " )
val hasMultipleFrame : Boolean
get ( ) = numFrames > 1
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
private var timeTotal = 0L
private lateinit var canvas : Canvas
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
private var canvasBitmap : Bitmap ? = null
2018-01-04 19:52:25 +01:00
// 再生速度の調整
2018-01-10 16:47:35 +01:00
private var durationScale = 1f
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
// APNGじゃなかった場合に使われる
private var defaultImage : Bitmap ? = null
2018-01-04 19:52:25 +01:00
private class Frame (
internal val bitmap : Bitmap ,
internal val time _start : Long ,
internal val time _width : Long
)
2018-01-27 12:00:44 +01:00
private var frames : ArrayList < Frame > ? = null
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
@Suppress ( " unused " )
constructor ( bitmap : Bitmap ) : this ( ) {
this . defaultImage = bitmap
2018-01-04 19:52:25 +01:00
}
2018-01-27 12:00:44 +01:00
private fun onParseComplete ( ) {
canvasBitmap ?. recycle ( )
canvasBitmap = null
2018-01-04 19:52:25 +01:00
val frames = this . frames
2018-01-27 12:00:44 +01:00
if ( frames != null ) {
if ( frames . size > 1 ) {
defaultImage ?. recycle ( )
defaultImage = null
} else if ( frames . size == 1 ) {
defaultImage ?. recycle ( )
defaultImage = frames . first ( ) . bitmap
frames . clear ( )
}
2018-01-04 19:52:25 +01:00
}
}
2018-01-27 12:00:44 +01:00
fun dispose ( ) {
defaultImage ?. recycle ( )
2018-01-04 19:52:25 +01:00
canvasBitmap ?. recycle ( )
val frames = this . frames
if ( frames != null ) {
for ( f in frames ) {
f . bitmap . recycle ( )
}
}
}
class FindFrameResult {
var bitmap : Bitmap ? = null // may null
var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
}
// シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する
2018-01-27 12:00:44 +01:00
@Suppress ( " unused " )
2018-01-04 19:52:25 +01:00
fun findFrame ( result : FindFrameResult , t : Long ) {
2018-01-27 12:00:44 +01:00
if ( defaultImage != null ) {
result . bitmap = defaultImage
2018-01-04 19:52:25 +01:00
result . delay = Long . MAX _VALUE
return
}
val animationControl = this . animationControl
val frames = this . frames
if ( animationControl == null || frames == null ) {
// この場合は既に mBitmapNonAnimation が用意されてるはずだ
result . bitmap = null
result . delay = Long . MAX _VALUE
return
}
2018-01-27 12:00:44 +01:00
val frameCount = frames . size
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
val isFinite = ! animationControl . isPlayIndefinitely
2018-01-04 19:52:25 +01:00
val repeatSequenceCount = if ( isFinite ) animationControl . numPlays else 1
2018-01-27 12:00:44 +01:00
val endWait = if ( isFinite ) DELAY _AFTER _END else 0L
val timeTotalLoop = Math . max ( 1 , timeTotal * repeatSequenceCount + endWait )
2018-01-04 19:52:25 +01:00
val tf = ( if ( 0.5f + t < 0f ) 0f else t / durationScale ) . toLong ( )
// 全体の繰り返し時刻で余りを計算
2018-01-27 12:00:44 +01:00
val tl = tf % timeTotalLoop
if ( tl >= timeTotalLoop - endWait ) {
2018-01-04 19:52:25 +01:00
// 終端で待機状態
2018-01-27 12:00:44 +01:00
result . bitmap = frames [ frameCount - 1 ] . bitmap
result . delay = ( 0.5f + ( timeTotalLoop - tl ) * durationScale ) . toLong ( )
2018-01-04 19:52:25 +01:00
return
}
// 1ループの繰り返し時刻で余りを計算
2018-01-27 12:00:44 +01:00
val tt = tl % timeTotal
2018-01-04 19:52:25 +01:00
// フレームリストを時刻で二分探索
var s = 0
2018-01-27 12:00:44 +01:00
var e = frameCount
2018-01-04 19:52:25 +01:00
while ( e - s > 1 ) {
val mid = s + e shr 1
val frame = frames [ mid ]
// log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.time_start,frame.time_start+frame.time_width );
if ( tt < frame . time _start ) {
e = mid
} else if ( tt >= frame . time _start + frame . time _width ) {
s = mid + 1
} else {
s = mid
break
}
}
2018-01-27 12:00:44 +01:00
s = if ( s < 0 ) 0 else if ( s >= frameCount - 1 ) frameCount - 1 else s
2018-01-04 19:52:25 +01:00
val frame = frames [ s ]
val delay = frame . time _start + frame . time _width - tt
result . bitmap = frames [ s ] . bitmap
result . delay = ( 0.5f + durationScale * Math . max ( 0f , delay . toFloat ( ) ) ) . toLong ( )
2018-01-27 12:00:44 +01:00
// 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.time_width,result.delay);
2018-01-04 19:52:25 +01:00
}
2018-01-27 12:00:44 +01:00
/////////////////////////////////////////////////////
// implements ApngDecoderCallback
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
override fun log ( message : String ) {
Log . d ( ApngFrames . TAG , message )
}
override fun onHeader ( apng : Apng , header : ApngImageHeader ) {
this . header = header
}
override fun onAnimationInfo (
apng : Apng ,
animationControl : ApngAnimationControl
) {
this . animationControl = animationControl
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
val canvasBitmap = ApngFrames . createBlankBitmap ( width , height )
this . canvasBitmap = canvasBitmap
this . canvas = Canvas ( canvasBitmap )
this . frames = ArrayList ( animationControl . numFrames )
}
override fun onDefaultImage ( apng : Apng , bitmap : ApngBitmap ) {
defaultImage ?. recycle ( )
defaultImage = ApngFrames . toBitmap ( bitmap , pixelSizeMax )
}
override fun onAnimationFrame (
apng : Apng ,
frameControl : ApngFrameControl ,
bitmap : ApngBitmap
) {
val frames = this . frames ?: return
val canvasBitmap = this . canvasBitmap ?: return
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
// APNGのフレーム画像をAndroidの形式に変換する。この段階ではリサイズしない
val bitmapNative = ApngFrames . toBitmap ( bitmap )
val previous : Bitmap ? = if ( frameControl . disposeOp == DisposeOp . Previous ) {
// Capture the current bitmap region IF it needs to be reverted after rendering
Bitmap . createBitmap (
canvasBitmap ,
frameControl . xOffset ,
frameControl . yOffset ,
frameControl . width ,
frameControl . height
)
} else {
null
2018-01-04 19:52:25 +01:00
}
2018-01-27 12:00:44 +01:00
val paint = if ( frameControl . blendOp == BlendOp . Source ) {
ApngFrames . sSrcModePaint // SRC_OVER, not blend
} else {
null // (for blend, leave paint null)
2018-01-04 19:52:25 +01:00
}
2018-01-27 12:00:44 +01:00
// Draw the new frame into place
canvas . drawBitmap (
bitmapNative ,
frameControl . xOffset . toFloat ( ) ,
frameControl . yOffset . toFloat ( ) ,
paint
)
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
// Extract a drawable from the canvas. Have to copy the current bitmap.
// Store the drawable in the sequence of frames
val timeStart = timeTotal
val timeWidth = Math . max ( 1L , frameControl . delayMilliseconds )
timeTotal += timeWidth
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
val scaledBitmap =
scaleBitmap ( canvasBitmap . copy ( Bitmap . Config . ARGB _8888 , false ) , pixelSizeMax )
if ( scaledBitmap != null ) {
frames . add ( Frame ( scaledBitmap , timeStart , timeWidth ) )
2018-01-04 19:52:25 +01:00
}
2018-01-27 12:00:44 +01:00
// Now "dispose" of the frame in preparation for the next.
// https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
2018-01-04 19:52:25 +01:00
2018-01-27 12:00:44 +01:00
when ( frameControl . disposeOp ) {
DisposeOp . None -> {
}
DisposeOp . Background ->
// APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
//System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
//if (true || isFull) {
canvas . drawColor ( 0 , PorterDuff . Mode . CLEAR ) // Clear to fully transparent black
DisposeOp . Previous ->
// APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
//System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
// Put the original section back
if ( previous != null ) {
canvas . drawBitmap (
previous , frameControl . xOffset . toFloat ( ) , frameControl . yOffset . toFloat ( ) ,
ApngFrames . sSrcModePaint
)
previous . recycle ( )
}
else -> {
// 0: Default should never happen
// APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
//System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
// do nothing
// } else {
// Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
// paint = new Paint();
// paint.setColor(0);
// paint.setStyle(Paint.Style.FILL);
// canvas.drawRect(rt, paint);
// }
2018-01-04 19:52:25 +01:00
}
}
}
}
2018-01-27 12:00:44 +01:00
//package jp.juggler.subwaytooter.util
//
//import android.graphics.Bitmap
//import android.graphics.Canvas
//import android.graphics.Paint
//import android.graphics.PorterDuff
//import android.graphics.PorterDuffXfermode
//import android.graphics.Rect
//
//import net.ellerton.japng.PngScanlineBuffer
//import net.ellerton.japng.argb8888.Argb8888Bitmap
//import net.ellerton.japng.argb8888.Argb8888Processor
//import net.ellerton.japng.argb8888.Argb8888Processors
//import net.ellerton.japng.argb8888.Argb8888ScanlineProcessor
//import net.ellerton.japng.argb8888.BasicArgb8888Director
//import net.ellerton.japng.chunks.PngAnimationControl
//import net.ellerton.japng.chunks.PngFrameControl
//import net.ellerton.japng.chunks.PngHeader
//import net.ellerton.japng.error.PngException
//import net.ellerton.japng.reader.DefaultPngChunkReader
//import net.ellerton.japng.reader.PngReadHelper
//
//import java.io.InputStream
//import java.util.ArrayList
//
//// APNGを解釈した結果を保持する
//// (フレーム数分のbitmapと時間情報)
//
//class APNGFrames {
//
// companion object {
//
// internal val log = LogCategory("APNGFrames")
//
// // ループしない画像の場合は3秒でまたループさせる
// private const val DELAY_AFTER_END = 3000L
//
// /**
// * Keep a 1x1 transparent image around as reference for creating a scaled starting bitmap.
// * Considering this because of some reported OutOfMemory errors, and this post:
// *
// *
// * http://stackoverflow.com/a/8527745/963195
// *
// *
// * Specifically: "NEVER use Bitmap.createBitmap(width, height, Config.ARGB_8888). I mean NEVER!"
// *
// *
// * Instead the 1x1 image (68 bytes of resources) is scaled up to the needed size.
// * Whether or not this fixes the OOM problems is TBD...
// */
// //static Bitmap sOnePxTransparent;
// internal val sSrcModePaint : Paint by lazy{
// val paint = Paint()
// paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
// paint.isFilterBitmap = true
// paint
// }
//
// internal fun createBlankBitmap(w : Int, h : Int) : Bitmap {
// return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
// }
//
// // WARNING: ownership of "src" will be moved or recycled.
// internal fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? {
// if(src == null) return null
//
// val src_w = src.width
// val src_h = src.height
// if(src_w <= size_max && src_h <= size_max) return src
//
// var dst_w : Int
// var dst_h : Int
// if(src_w >= src_h) {
// dst_w = size_max
// dst_h = (0.5f + src_h * size_max / src_w.toFloat()).toInt()
// if(dst_h < 1) dst_h = 1
// } else {
// dst_h = size_max
// dst_w = (0.5f + src_w * size_max / src_h.toFloat()).toInt()
// if(dst_w < 1) dst_w = 1
// }
//
// // この方法だとリークがあるらしい???
// // http://stackoverflow.com/a/8527745/963195
// // return Bitmap.createScaledBitmap( src, dst_w , dst_h , true );
//
// val b2 = createBlankBitmap(dst_w, dst_h)
// val canvas = Canvas(b2)
// val rect_src = Rect(0, 0, src_w, src_h)
// val rect_dst = Rect(0, 0, dst_w, dst_h)
// canvas.drawBitmap(src, rect_src, rect_dst, sSrcModePaint)
// src.recycle()
// return b2
// }
//
// internal fun toBitmap(src : Argb8888Bitmap) : Bitmap {
// val offset = 0
// val stride = src.width
// return Bitmap.createBitmap(src.pixelArray, offset, stride, src.width, src.height, Bitmap.Config.ARGB_8888)
// }
//
// internal fun toBitmap(src : Argb8888Bitmap, size_max : Int) : Bitmap? {
// return scaleBitmap(toBitmap(src), size_max)
// }
//
// /////////////////////////////////////////////////////////////////////
//
// // entry point is here
// @Throws(PngException::class)
// internal fun parseAPNG( inStream : InputStream, size_max : Int) : APNGFrames? {
// val handler = APNGParseEventHandler(size_max)
// try {
// val processor = Argb8888Processor(handler)
// val reader = DefaultPngChunkReader(processor)
// val result = PngReadHelper.read(inStream, reader)
// result?.onParseComplete()
// return result
// } catch(ex : Throwable) {
// handler.dispose()
// throw ex
// }
//
// }
// }
//
// // ピクセルサイズ制限
// private var mPixelSizeMax : Int = 0
//
// // APNGじゃなかった場合に使われる
// private var mBitmapNonAnimation : Bitmap? = null
//
// private lateinit var header : PngHeader
// private lateinit var scanlineProcessor : Argb8888ScanlineProcessor
// private lateinit var canvas : Canvas
//
// private var canvasBitmap : Bitmap? = null
// private var currentFrame : PngFrameControl? = null
// private var animationControl : PngAnimationControl? = null
//
// private var time_total = 0L
//
// private var frames : ArrayList<Frame>? = null
//
// ///////////////////////////////////////////////////////////////
//
// // 再生速度の調整
// private var durationScale = 1f
//
// private val numFrames : Int
// get() = animationControl?.numFrames ?: 1
//
// val hasMultipleFrame : Boolean
// get() = numFrames > 1
//
// private class Frame(
// internal val bitmap : Bitmap,
// internal val time_start : Long,
// internal val time_width : Long
// )
//
// ///////////////////////////////////////////////////////////////
//
// internal constructor(bitmap : Bitmap) {
// this.mBitmapNonAnimation = bitmap
// }
//
// internal constructor(
// header : PngHeader, scanlineProcessor : Argb8888ScanlineProcessor, animationControl : PngAnimationControl, size_max : Int
// ) {
// this.header = header
// this.scanlineProcessor = scanlineProcessor
// this.animationControl = animationControl
// this.mPixelSizeMax = size_max
//
// this.canvasBitmap = createBlankBitmap(header.width, header.height)
// this.canvas = Canvas(this.canvasBitmap)
// this.frames = ArrayList(animationControl.numFrames)
//
// }
//
// internal fun onParseComplete() {
// val frames = this.frames
//
// if(frames != null && frames.size <= 1) {
// mBitmapNonAnimation = toBitmap(scanlineProcessor.bitmap, mPixelSizeMax)
// }
//
// canvasBitmap?.recycle()
// canvasBitmap = null
// }
//
// internal fun dispose() {
// mBitmapNonAnimation?.recycle()
// canvasBitmap?.recycle()
//
// val frames = this.frames
// if(frames != null) {
// for(f in frames) {
// f.bitmap.recycle()
// }
// }
// }
//
// // フレームが追加される
// internal fun beginFrame(frameControl : PngFrameControl) : Argb8888ScanlineProcessor {
// currentFrame = frameControl
// return scanlineProcessor.cloneWithSharedBitmap(header.adjustFor(currentFrame))
// }
//
// // フレームが追加される
// internal fun completeFrame(frameImage : Argb8888Bitmap) {
//
// val frames = this.frames ?: return
//
// val canvasBitmap = this.canvasBitmap ?: return
//
// val currentFrame = this.currentFrame ?: return
// this.currentFrame = null
//
// // APNGのフレーム画像をAndroidの形式に変換する
// val frame = toBitmap(frameImage)
//
// var previous : Bitmap? = null
// // Capture the current bitmap region IF it needs to be reverted after rendering
// if(2 == currentFrame.disposeOp.toInt()) {
// previous = Bitmap.createBitmap(canvasBitmap, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height) // or could use from frames?
// //System.out.println(String.format("Captured previous %d x %d", previous.getWidth(), previous.getHeight()));
// }
//
// var paint : Paint? = null // (for blend, leave paint null)
// if(0 == currentFrame.blendOp.toInt()) { // SRC_OVER, not blend
// paint = sSrcModePaint
// }
//
// // boolean isFull = currentFrame.height == header.height && currentFrame.width == header.width;
//
// // Draw the new frame into place
// canvas.drawBitmap(frame, currentFrame.xOffset.toFloat(), currentFrame.yOffset.toFloat(), paint)
//
// // Extract a drawable from the canvas. Have to copy the current bitmap.
// // Store the drawable in the sequence of frames
// val time_start = time_total
// var time_width = currentFrame.delayMilliseconds.toLong()
// if(time_width <= 0L) time_width = 1L
// time_total = time_start + time_width
//
// val scaledBitmap = scaleBitmap(canvasBitmap.copy(Bitmap.Config.ARGB_8888, false), mPixelSizeMax)
// if(scaledBitmap != null) {
// frames.add(Frame(scaledBitmap, time_start, time_width))
// }
//
// // Now "dispose" of the frame in preparation for the next.
// // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
//
// when(currentFrame.disposeOp.toInt()) {
//
// 1 ->
// // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
// //System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
// //if (true || isFull) {
// canvas.drawColor(0, PorterDuff.Mode.CLEAR) // Clear to fully transparent black
//
// 2 ->
// // APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
// //System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
// // Put the original section back
// if(previous != null) {
// canvas.drawBitmap(previous, currentFrame.xOffset.toFloat(), currentFrame.yOffset.toFloat(), sSrcModePaint)
// previous.recycle()
// }
//
// else -> {
// // 0: Default should never happen
//
// // APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
// //System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
// // do nothing
// // } else {
// // Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
// // paint = new Paint();
// // paint.setColor(0);
// // paint.setStyle(Paint.Style.FILL);
// // canvas.drawRect(rt, paint);
// // }
//
// }
// }
// }
//
// class FindFrameResult {
// var bitmap : Bitmap? = null // may null
// var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
// }
//
// // シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する
// fun findFrame(result : FindFrameResult, t : Long) {
//
// if(mBitmapNonAnimation != null) {
// result.bitmap = mBitmapNonAnimation
// result.delay = Long.MAX_VALUE
// return
// }
//
// val animationControl = this.animationControl
// val frames = this.frames
// if(animationControl == null || frames == null) {
// // この場合は既に mBitmapNonAnimation が用意されてるはずだ
// result.bitmap = null
// result.delay = Long.MAX_VALUE
// return
// }
//
// val frame_count = frames.size
//
// val isFinite = ! animationControl.loopForever()
// val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
// val end_wait = if(isFinite) DELAY_AFTER_END else 0L
// var loop_total = time_total * repeatSequenceCount + end_wait
// if(loop_total <= 0) loop_total = 1
//
// val tf = (if(0.5f + t < 0f) 0f else t / durationScale).toLong()
//
// // 全体の繰り返し時刻で余りを計算
// val tl = tf % loop_total
// if(tl >= loop_total - end_wait) {
// // 終端で待機状態
// result.bitmap = frames[frame_count - 1].bitmap
// result.delay = (0.5f + (loop_total - tl) * durationScale).toLong()
// return
// }
// // 1ループの繰り返し時刻で余りを計算
// val tt = tl % time_total
//
// // フレームリストを時刻で二分探索
// var s = 0
// var e = frame_count
// while(e - s > 1) {
// val mid = s + e shr 1
// val frame = frames[mid]
// // log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.time_start,frame.time_start+frame.time_width );
// if(tt < frame.time_start) {
// e = mid
// } else if(tt >= frame.time_start + frame.time_width) {
// s = mid + 1
// } else {
// s = mid
// break
// }
// }
// s = if(s < 0) 0 else if(s >= frame_count - 1) frame_count - 1 else s
// val frame = frames[s]
// val delay = frame.time_start + frame.time_width - tt
// result.bitmap = frames[s].bitmap
// result.delay = (0.5f + durationScale * Math.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,time_total,s,frame.time_width,result.delay);
// }
//
// /////////////////////////////////////////////////////////////////////
//
// // APNGのパース中に随時呼び出される
// internal class APNGParseEventHandler(
// private val size_max : Int
// ) : BasicArgb8888Director<APNGFrames>() {
//
// private lateinit var header : PngHeader
//
// private var frames : APNGFrames? = null
//
// private val isAnimated : Boolean
// get() = frames != null
//
// // ヘッダが分かった
// @Throws(PngException::class)
// override fun receiveHeader(header : PngHeader, buffer : PngScanlineBuffer) {
// this.header = header
//
// // 親クラスのprotectedフィールドを更新する
// val pngBitmap = Argb8888Bitmap(header.width, header.height)
// this.scanlineProcessor = Argb8888Processors.from(header, buffer, pngBitmap)
// }
//
// // デフォルト画像の手前で呼ばれる
// override fun beforeDefaultImage() : Argb8888ScanlineProcessor {
// return scanlineProcessor
// }
//
// // デフォルト画像が分かった
// // おそらく receiveAnimationControl より先に呼ばれる
// override fun receiveDefaultImage(defaultImage : Argb8888Bitmap) {
// // japng ライブラリの返すデフォルトイメージはあまり信用できないので使わない
// }
//
// // アニメーション制御情報が分かった
// override fun receiveAnimationControl(animationControl : PngAnimationControl) {
// this.frames = APNGFrames(header , scanlineProcessor, animationControl, size_max)
// }
//
// override fun wantDefaultImage() : Boolean {
// return ! isAnimated
// }
//
// override fun wantAnimationFrames() : Boolean {
// return true // isAnimated;
// }
//
// // フレーム制御情報が分かった
// override fun receiveFrameControl(frameControl : PngFrameControl) : Argb8888ScanlineProcessor {
// val frames = this.frames ?: throw RuntimeException("not animation image")
// return frames.beginFrame(frameControl)
// }
//
// // フレーム画像が分かった
// override fun receiveFrameImage(frameImage : Argb8888Bitmap) {
// val frames = this.frames ?: throw RuntimeException("not animation image")
// frames.completeFrame(frameImage)
// }
//
// // 結果を取得する
// override fun getResult() : APNGFrames? {
// val frames = this.frames
// return if( frames?.hasMultipleFrame == true ){
// frames
// }else {
// dispose()
// return null
// }
// }
//
// // 処理中に例外が起きた場合、Bitmapリソースを解放する
// fun dispose() {
// frames?.dispose()
// frames = null
// }
// }
//
//}