package jp.juggler.apng 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 android.util.Log import java.io.InputStream import java.util.ArrayList class ApngFrames private constructor( private val pixelSizeMax : Int = 0, private val debug:Boolean =false ) : ApngDecoderCallback { companion object { private const val TAG = "ApngFrames" // ループしない画像の場合は3秒でまたループさせる private const val DELAY_AFTER_END = 3000L // アニメーションフレームの描画に使う private val sSrcModePaint : Paint by lazy { val paint = Paint() paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC) paint.isFilterBitmap = true paint } private 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. private fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? { if(src == null) return null val wSrc = src.width val hSrc = src.height if(size_max <= 0 || wSrc <= size_max && hSrc <= size_max) return src 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() ) } else { hDst = size_max wDst = Math.max( 1, (size_max.toFloat() * wSrc.toFloat() / hSrc.toFloat() + 0.5f).toInt() ) } Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst") val b2 = createBlankBitmap(wDst, hDst) val canvas = Canvas(b2) val rectSrc = Rect(0, 0, wSrc, hSrc) val rectDst = Rect(0, 0, wDst, hDst) canvas.drawBitmap(src, rectSrc, rectDst, sSrcModePaint ) src.recycle() return b2 } 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 ) } private fun toBitmap(src : ApngBitmap, size_max : Int) : Bitmap? { return scaleBitmap( toBitmap( src ), size_max ) } @Suppress("unused") fun parseApng(inStream : InputStream, pixelSizeMax : Int,debug:Boolean=false) : ApngFrames { val result = ApngFrames(pixelSizeMax,debug) try { ApngDecoder.parseStream(inStream, result) result.onParseComplete() return if( result.defaultImage != null || result.frames?.isNotEmpty() == true ){ result }else{ throw RuntimeException("APNG has no image") } } catch(ex : Throwable) { result.dispose() throw ex } } } private var header : ApngImageHeader? = null private var animationControl : ApngAnimationControl? = null val width : Int get() = Math.min( pixelSizeMax, header?.width ?: 1) val height : Int get() = Math.min( pixelSizeMax, header?.height ?: 1) @Suppress("MemberVisibilityCanBePrivate") val numFrames : Int get() = animationControl?.numFrames ?: 1 @Suppress("unused") val hasMultipleFrame : Boolean get() = numFrames > 1 private var timeTotal = 0L private lateinit var canvas : Canvas private var canvasBitmap : Bitmap? = null // 再生速度の調整 private var durationScale = 1f // APNGじゃなかった場合に使われる private var defaultImage : Bitmap? = null private class Frame( internal val bitmap : Bitmap, internal val time_start : Long, internal val time_width : Long ) private var frames : ArrayList? = null @Suppress("unused") constructor(bitmap : Bitmap) : this() { this.defaultImage = bitmap } private fun onParseComplete() { canvasBitmap?.recycle() canvasBitmap = null val frames = this.frames if( frames != null ){ if( frames.size > 1){ defaultImage?.recycle() defaultImage = null }else if( frames.size == 1){ defaultImage?.recycle() defaultImage = frames.first().bitmap frames.clear() } } } fun dispose() { defaultImage?.recycle() 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に格納する @Suppress("unused") fun findFrame(result : FindFrameResult, t : Long) { if(defaultImage != null) { result.bitmap = defaultImage 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 frameCount = frames.size val isFinite = ! animationControl.isPlayIndefinitely 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 tf = (if(0.5f + t < 0f) 0f else t / durationScale).toLong() // 全体の繰り返し時刻で余りを計算 val tl = tf % timeTotalLoop if(tl >= timeTotalLoop - endWait) { // 終端で待機状態 result.bitmap = frames[frameCount - 1].bitmap result.delay = (0.5f + (timeTotalLoop - tl) * durationScale).toLong() return } // 1ループの繰り返し時刻で余りを計算 val tt = tl % timeTotal // フレームリストを時刻で二分探索 var s = 0 var e = frameCount 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 >= frameCount - 1) frameCount - 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,timeTotal,s,frame.time_width,result.delay); } ///////////////////////////////////////////////////// // implements ApngDecoderCallback override fun onApngWarning(message : String) { Log.w(TAG, message) } override fun onApngDebug(message : String) { Log.d(TAG, message) } override fun canApngDebug():Boolean = debug override fun onHeader(apng : Apng, header : ApngImageHeader) { this.header = header } override fun onAnimationInfo( apng : Apng, header: ApngImageHeader, animationControl : ApngAnimationControl ) { this.animationControl = animationControl val canvasBitmap = createBlankBitmap(header.width, header.height) this.canvasBitmap = canvasBitmap this.canvas = Canvas(canvasBitmap) this.frames = ArrayList(animationControl.numFrames) } override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) { defaultImage?.recycle() defaultImage = toBitmap(bitmap, pixelSizeMax) } override fun onAnimationFrame( apng : Apng, frameControl : ApngFrameControl, bitmap : ApngBitmap ) { val frames = this.frames ?: return val canvasBitmap = this.canvasBitmap ?: return // APNGのフレーム画像をAndroidの形式に変換する。この段階ではリサイズしない val bitmapNative = 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 } val paint = if(frameControl.blendOp == BlendOp.Source) { sSrcModePaint // SRC_OVER, not blend } else { null // (for blend, leave paint null) } // Draw the new frame into place canvas.drawBitmap( bitmapNative, frameControl.xOffset.toFloat(), frameControl.yOffset.toFloat(), paint ) // 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 val scaledBitmap = scaleBitmap( canvasBitmap.copy( Bitmap.Config.ARGB_8888, false ), pixelSizeMax ) if(scaledBitmap != null) { frames.add(Frame(scaledBitmap, timeStart, timeWidth)) } // Now "dispose" of the frame in preparation for the next. // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk 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(), 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); // } } } } }