SubwayTooter-Android-App/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt

389 lines
11 KiB
Kotlin

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<Frame>? = 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);
// }
}
}
}
}