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

515 lines
14 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
import kotlin.math.max
import kotlin.math.min
class ApngFrames private constructor(
private val pixelSizeMax : Int = 0,
private val debug : Boolean = false
) : ApngDecoderCallback, GifDecoderCallback {
companion object {
private const val TAG = "ApngFrames"
// ループしない画像の場合は3秒でまたループさせる
private const val DELAY_AFTER_END = 3000L
// アニメーションフレームの描画に使う
private val sPaintDontBlend = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
isFilterBitmap = true
}
private val sPaintClear = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
color = 0
}
private fun createBlankBitmap(w : Int, h : Int) =
Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
private fun scale(max : Int, num : Int, den : Int) =
(max.toFloat() * num.toFloat() / den.toFloat() + 0.5f).toInt()
private fun scaleBitmap(
size_max : Int,
src : Bitmap,
recycleSrc : Boolean = true // true: ownership of "src" will be moved or recycled.
) : Bitmap {
val wSrc = src.width
val hSrc = src.height
if(size_max <= 0 || (wSrc <= size_max && hSrc <= size_max)) {
return if(recycleSrc) {
src
} else {
src.copy(Bitmap.Config.ARGB_8888, false)
}
}
val wDst : Int
val hDst : Int
if(wSrc >= hSrc) {
wDst = size_max
hDst = max(1, scale(size_max, hSrc, wSrc))
} else {
hDst = size_max
wDst = max(1, scale(size_max, wSrc, hSrc))
}
//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, sPaintDontBlend)
if(recycleSrc) src.recycle()
return b2
}
private fun toAndroidBitmap(src : ApngBitmap) =
Bitmap.createBitmap(
src.colors, // int[] 配列
0, // offset
src.width, //stride
src.width, // width
src.height, //height
Bitmap.Config.ARGB_8888
)
private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) =
scaleBitmap(size_max, toAndroidBitmap(src))
private fun parseApng(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
) : ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
ApngDecoder.parseStream(inStream, result)
result.onParseComplete()
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("APNG has no image")
} catch(ex : Throwable) {
result.dispose()
throw ex
}
}
private fun parseGif(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
) : ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
GifDecoder(result).parse(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
}
}
private val apngHeadKey = byteArrayOf(0x89.toByte(),0x50)
private val gifHeadKey = "GIF".toByteArray(Charsets.UTF_8)
private fun matchBytes(ba1:ByteArray,ba2:ByteArray,length:Int=min(ba1.size,ba2.size)):Boolean{
for( i in 0 until length){
if( ba1[i] != ba2[i] ) return false
}
return true
}
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 && matchBytes(buf, apngHeadKey) ) {
return opener()?.use { parseApng(it, pixelSizeMax, debug) }
}
if(buf.size >= 6 && matchBytes(buf, gifHeadKey) ) {
return opener()?.use { parseGif(it, pixelSizeMax, debug) }
}
return null
}
}
private var header : ApngImageHeader? = null
private var animationControl : ApngAnimationControl? = null
// width,height (after resized)
var width : Int = 1
private set
var height : Int = 1
private set
class Frame(
val bitmap : Bitmap,
val timeStart : Long,
val timeWidth : Long
)
var frames : ArrayList<Frame>? = null
@Suppress("MemberVisibilityCanBePrivate")
val numFrames : Int
get() = frames?.size ?: 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
set(value) {
field = value
if(value != null) {
width = value.width
height = value.height
}
}
constructor(bitmap : Bitmap) : 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() {
canvasBitmap?.recycle()
defaultImage?.recycle()
frames?.forEach { it.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 || frames.isEmpty()) {
// ここは通らないはず…
result.bitmap = null
result.delay = Long.MAX_VALUE
return
}
val frameCount = frames.size
val isFinite = animationControl.isFinite
val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
val endWait = if(isFinite) DELAY_AFTER_END else 0L
val timeTotalLoop = max(1, timeTotal * repeatSequenceCount + endWait)
val tf = (max(0, 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.timeStart,frame.timeStart+frame.timeWidth );
if(tt < frame.timeStart) {
e = mid
} else if(tt >= frame.timeStart + frame.timeWidth) {
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.timeStart + frame.timeWidth - tt
result.bitmap = frames[s].bitmap
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);
}
/////////////////////////////////////////////////////
// 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
) {
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 onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
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}"
)
}
val frames = this.frames ?: return
val canvasBitmap = this.canvasBitmap ?: return
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
}
val previous : Bitmap? = when(disposeOp) {
DisposeOp.Previous -> Bitmap.createBitmap(
canvasBitmap,
frameControl.xOffset,
frameControl.yOffset,
frameControl.width,
frameControl.height
)
else -> null
}
try {
val frameBitmapAndroid = toAndroidBitmap(frameBitmap)
try {
canvas.drawBitmap(
frameBitmapAndroid,
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.
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].
BlendOp.Over -> null
}
)
val frame = Frame(
bitmap = scaleBitmap(pixelSizeMax, canvasBitmap, recycleSrc = false),
timeStart = timeTotal,
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.
DisposeOp.None -> {
}
// 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.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.
DisposeOp.Previous -> if(previous != null) {
canvas.drawBitmap(
previous,
frameControl.xOffset.toFloat(),
frameControl.yOffset.toFloat(),
sPaintDontBlend
)
}
}
} finally {
frameBitmapAndroid.recycle()
}
} finally {
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
if(frames.isEmpty()) {
defaultImage?.recycle()
defaultImage = toAndroidBitmap(frameBitmap, pixelSizeMax)
// ここでwidth,heightがセットされる
}
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()
}
}
}