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

512 lines
13 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
import kotlin.math.max
2018-01-28 20:03:04 +01:00
class ApngFrames private constructor(
private val pixelSizeMax : Int = 0,
2018-01-30 14:30:50 +01:00
private val debug : Boolean = false
) : ApngDecoderCallback, GifDecoderCallback {
2018-01-28 20:03:04 +01:00
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
}
private val sPaintClear = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
color = 0
}
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
hDst = max(1, scale(size_max, hSrc, wSrc))
2018-01-28 20:03:04 +01:00
} else {
hDst = size_max
wDst = 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
private fun parseApng(
2018-01-30 14:30:50 +01:00
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
}
}
2019-08-12 01:39:19 +02:00
private fun parseGif(
inStream : InputStream,
pixelSizeMax : Int,
debug : Boolean = false
) : ApngFrames {
val result = ApngFrames(pixelSizeMax, debug)
try {
2019-08-12 03:12:39 +02:00
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
}
}
@Suppress("unused")
fun parse(
pixelSizeMax : Int,
debug : Boolean = false,
2019-08-12 01:39:19 +02:00
opener : () -> InputStream?
) : ApngFrames? {
2019-08-12 01:39:19 +02:00
val buf = ByteArray(8) { 0.toByte() }
opener()?.use { it.read(buf, 0, buf.size) }
if(buf.size >= 8
&& (buf[0].toInt() and 0xff) == 0x89
&& (buf[1].toInt() and 0xff) == 0x50
) {
2019-08-12 01:39:19 +02:00
return opener()?.use { parseApng(it, pixelSizeMax, debug) }
}
2019-08-12 01:39:19 +02:00
if(buf.size >= 6
&& buf[0].toChar() == 'G'
&& buf[1].toChar() == 'I'
&& buf[2].toChar() == 'F'
) {
2019-08-12 01:39:19 +02:00
return opener()?.use { parseGif(it, pixelSizeMax, debug) }
}
2019-08-12 01:39:19 +02:00
return null
}
2018-01-28 20:03:04 +01:00
}
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
class Frame(
val bitmap : Bitmap,
val timeStart : Long,
val timeWidth : Long
)
var frames : ArrayList<Frame>? = null
2018-01-28 20:03:04 +01:00
@Suppress("MemberVisibilityCanBePrivate")
val numFrames : Int
get() = frames?.size ?: 1
2018-01-28 20:03:04 +01:00
@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
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
val timeTotalLoop = max(1, timeTotal * repeatSequenceCount + endWait)
2018-01-28 20:03:04 +01:00
val tf = (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 * max(0f, delay.toFloat())).toLong()
2018-01-28 20:03:04 +01:00
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
) {
if(debug) {
Log.d(TAG, "onAnimationInfo")
}
2018-01-28 20:03:04 +01:00
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) {
if(debug) {
Log.d(TAG, "onDefaultImage")
}
2018-01-28 20:03:04 +01:00
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
) {
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}"
)
}
2018-01-28 20:03:04 +01:00
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) {
2018-01-30 14:30:50 +01:00
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.
2018-01-30 14:30:50 +01:00
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].
2018-01-30 14:30:50 +01:00
BlendOp.Over -> null
}
)
val frame = Frame(
bitmap = scaleBitmap(pixelSizeMax, canvasBitmap, recycleSrc = false),
timeStart = timeTotal,
timeWidth = max(1L, frameControl.delayMilliseconds)
2018-01-30 14:30:50 +01:00
)
frames.add(frame)
timeTotal += frame.timeWidth
2018-01-28 20:03:04 +01:00
when(disposeOp) {
// no disposal is done on this frame before rendering the next;
// the contents of the output buffer are left as is.
2018-01-30 14:30:50 +01:00
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.
2018-01-30 14:30:50 +01:00
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
}
}
///////////////////////////////////////////////////////////////////////
// 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
2019-08-12 01:39:19 +02:00
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()
}
}
2018-01-28 20:03:04 +01:00
}