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

367 lines
9.5 KiB
Kotlin
Raw Normal View History

2018-01-28 20:03:04 +01:00
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,
2018-01-30 14:30:50 +01:00
private val debug : Boolean = false
2018-01-28 20:03:04 +01:00
) : ApngDecoderCallback {
companion object {
private const val TAG = "ApngFrames"
// ループしない画像の場合は3秒でまたループさせる
private const val DELAY_AFTER_END = 3000L
// アニメーションフレームの描画に使う
2018-02-01 10:30:46 +01:00
private val sPaintDontBlend = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
isFilterBitmap = true
2018-01-28 20:03:04 +01:00
}
2018-01-30 14:30:50 +01:00
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()
2018-01-28 20:03:04 +01:00
2018-01-30 14:30:50 +01:00
private fun scaleBitmap(
size_max : Int,
src : Bitmap,
recycleSrc : Boolean = true // true: ownership of "src" will be moved or recycled.
) : Bitmap {
2018-01-28 20:03:04 +01:00
val wSrc = src.width
val hSrc = src.height
2018-02-01 10:30:46 +01:00
if(size_max <= 0 || (wSrc <= size_max && hSrc <= size_max)) {
2018-01-30 14:30:50 +01:00
return if(recycleSrc) {
src
} else {
src.copy(Bitmap.Config.ARGB_8888, false)
}
}
2018-01-28 20:03:04 +01:00
val wDst : Int
val hDst : Int
if(wSrc >= hSrc) {
wDst = size_max
2018-01-30 14:30:50 +01:00
hDst = Math.max(1, scale(size_max, hSrc, wSrc))
2018-01-28 20:03:04 +01:00
} else {
hDst = size_max
2018-01-30 14:30:50 +01:00
wDst = Math.max(1, scale(size_max, wSrc, hSrc))
2018-01-28 20:03:04 +01:00
}
2018-01-29 01:50:37 +01:00
//Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst")
2018-01-28 20:03:04 +01:00
val b2 = createBlankBitmap(wDst, hDst)
val canvas = Canvas(b2)
val rectSrc = Rect(0, 0, wSrc, hSrc)
val rectDst = Rect(0, 0, wDst, hDst)
2018-01-30 14:30:50 +01:00
canvas.drawBitmap(src, rectSrc, rectDst, sPaintDontBlend)
if(recycleSrc) src.recycle()
2018-01-28 20:03:04 +01:00
return b2
}
2018-02-01 10:30:46 +01:00
private fun toAndroidBitmap(src : ApngBitmap) =
Bitmap.createBitmap(
2018-01-28 20:03:04 +01:00
src.colors, // int[] 配列
0, // offset
src.width, //stride
src.width, // width
src.height, //height
Bitmap.Config.ARGB_8888
)
2018-01-30 14:30:50 +01:00
private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) =
scaleBitmap(size_max, toAndroidBitmap(src))
2018-01-28 20:03:04 +01:00
@Suppress("unused")
2018-01-30 14:30:50 +01:00
fun parseApng(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
) : ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
2018-01-28 20:03:04 +01:00
try {
ApngDecoder.parseStream(inStream, result)
result.onParseComplete()
2018-02-01 10:30:46 +01:00
return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true }
?: throw RuntimeException("APNG has no image")
2018-01-28 20:03:04 +01:00
} catch(ex : Throwable) {
result.dispose()
throw ex
}
}
}
private var header : ApngImageHeader? = null
private var animationControl : ApngAnimationControl? = null
2018-01-30 14:30:50 +01:00
// width,height (after resized)
var width : Int = 1
2018-01-30 13:28:01 +01:00
private set
2018-01-28 20:03:04 +01:00
2018-01-30 13:28:01 +01:00
var height : Int = 1
private set
2018-01-28 20:03:04 +01:00
@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
2018-01-30 14:30:50 +01:00
set(value) {
field = value
if(value != null) {
width = value.width
height = value.height
}
}
2018-01-28 20:03:04 +01:00
private class Frame(
internal val bitmap : Bitmap,
2018-01-30 14:30:50 +01:00
internal val timeStart : Long,
internal val timeWidth : Long
2018-01-28 20:03:04 +01:00
)
private var frames : ArrayList<Frame>? = null
constructor(bitmap : Bitmap) : this() {
2018-01-30 14:30:50 +01:00
defaultImage = bitmap
2018-01-28 20:03:04 +01:00
}
private fun onParseComplete() {
canvasBitmap?.recycle()
canvasBitmap = null
val frames = this.frames
2018-01-30 14:30:50 +01:00
if(frames != null) {
if(frames.size > 1) {
2018-01-28 20:03:04 +01:00
defaultImage?.recycle()
defaultImage = null
2018-01-30 14:30:50 +01:00
} else if(frames.size == 1) {
2018-01-28 20:03:04 +01:00
defaultImage?.recycle()
defaultImage = frames.first().bitmap
frames.clear()
}
}
}
fun dispose() {
canvasBitmap?.recycle()
2018-01-30 14:30:50 +01:00
defaultImage?.recycle()
frames?.forEach { it.bitmap.recycle() }
2018-01-28 20:03:04 +01:00
}
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
2018-01-30 14:30:50 +01:00
if(animationControl == null || frames == null || frames.isEmpty()) {
// ここは通らないはず…
2018-01-28 20:03:04 +01:00
result.bitmap = null
result.delay = Long.MAX_VALUE
return
}
val frameCount = frames.size
2018-01-30 14:30:50 +01:00
val isFinite = animationControl.isFinite
2018-01-28 20:03:04 +01:00
val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
val endWait = if(isFinite) DELAY_AFTER_END else 0L
2018-01-30 14:30:50 +01:00
val timeTotalLoop = Math.max(1, timeTotal * repeatSequenceCount + endWait)
2018-01-28 20:03:04 +01:00
2018-01-30 14:30:50 +01:00
val tf = (Math.max(0, t) / durationScale).toLong()
2018-01-28 20:03:04 +01:00
// 全体の繰り返し時刻で余りを計算
val tl = tf % timeTotalLoop
2018-01-30 14:30:50 +01:00
2018-01-28 20:03:04 +01:00
if(tl >= timeTotalLoop - endWait) {
// 終端で待機状態
result.bitmap = frames[frameCount - 1].bitmap
result.delay = (0.5f + (timeTotalLoop - tl) * durationScale).toLong()
return
}
2018-01-30 14:30:50 +01:00
2018-01-28 20:03:04 +01:00
// 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]
2018-01-30 14:30:50 +01:00
// 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) {
2018-01-28 20:03:04 +01:00
e = mid
2018-01-30 14:30:50 +01:00
} else if(tt >= frame.timeStart + frame.timeWidth) {
2018-01-28 20:03:04 +01:00
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]
2018-01-30 14:30:50 +01:00
val delay = frame.timeStart + frame.timeWidth - tt
2018-01-28 20:03:04 +01:00
result.bitmap = frames[s].bitmap
result.delay = (0.5f + durationScale * Math.max(0f, delay.toFloat())).toLong()
2018-01-30 14:30:50 +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.timeWidth,result.delay);
2018-01-28 20:03:04 +01:00
}
/////////////////////////////////////////////////////
// implements ApngDecoderCallback
override fun onApngWarning(message : String) {
Log.w(TAG, message)
}
override fun onApngDebug(message : String) {
Log.d(TAG, message)
}
2018-01-30 14:30:50 +01:00
override fun canApngDebug() : Boolean = debug
2018-01-28 20:03:04 +01:00
override fun onHeader(apng : Apng, header : ApngImageHeader) {
this.header = header
}
override fun onAnimationInfo(
apng : Apng,
2018-01-30 14:30:50 +01:00
header : ApngImageHeader,
2018-01-28 20:03:04 +01:00
animationControl : ApngAnimationControl
) {
this.animationControl = animationControl
2018-01-30 14:30:50 +01:00
this.frames = ArrayList(animationControl.numFrames)
2018-01-28 20:03:04 +01:00
2018-01-30 14:30:50 +01:00
val canvasBitmap = createBlankBitmap(header.width, header.height)
2018-01-28 20:03:04 +01:00
this.canvasBitmap = canvasBitmap
this.canvas = Canvas(canvasBitmap)
}
override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
defaultImage?.recycle()
2018-01-30 14:30:50 +01:00
defaultImage = toAndroidBitmap(bitmap, pixelSizeMax)
2018-01-28 20:03:04 +01:00
}
override fun onAnimationFrame(
apng : Apng,
frameControl : ApngFrameControl,
2018-01-30 13:28:01 +01:00
frameBitmap : ApngBitmap
2018-01-28 20:03:04 +01:00
) {
val frames = this.frames ?: return
val canvasBitmap = this.canvasBitmap ?: return
2018-01-30 14:30:50 +01:00
val previous : Bitmap? = when(frameControl.disposeOp) {
DisposeOp.Previous -> Bitmap.createBitmap(
2018-01-28 20:03:04 +01:00
canvasBitmap,
frameControl.xOffset,
frameControl.yOffset,
frameControl.width,
frameControl.height
)
2018-01-30 14:30:50 +01:00
else -> null
2018-01-28 20:03:04 +01:00
}
2018-01-30 14:30:50 +01:00
try {
2018-01-28 20:03:04 +01:00
2018-01-30 14:30:50 +01:00
val frameBitmapAndroid = toAndroidBitmap(frameBitmap)
2018-01-28 20:03:04 +01:00
2018-01-30 14:30:50 +01:00
try {
2018-01-28 20:03:04 +01:00
2018-01-30 14:30:50 +01:00
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 = Math.max(1L, frameControl.delayMilliseconds)
)
frames.add(frame)
timeTotal += frame.timeWidth
2018-01-28 20:03:04 +01:00
2018-01-30 14:30:50 +01:00
when(frameControl.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 -> 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()
2018-01-28 20:03:04 +01:00
}
2018-01-30 14:30:50 +01:00
} finally {
previous?.recycle()
2018-01-28 20:03:04 +01:00
}
}
}