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-04-20 15:22:21 +02: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
|
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
|
|
|
|
2018-04-20 15:22:21 +02: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
|
2018-04-20 15:22:21 +02:00
|
|
|
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
|
|
|
|
2018-04-20 15:22:21 +02:00
|
|
|
|
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
|
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
|
|
|
|
) {
|
2018-04-20 15:22:21 +02:00
|
|
|
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) {
|
2018-04-20 15:22:21 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-04-20 15:22:21 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
) {
|
2018-04-20 15:22:21 +02: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
|
|
|
|
|
2018-04-20 15:22:21 +02:00
|
|
|
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.
|
|
|
|
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-04-20 15:22:21 +02:00
|
|
|
when(disposeOp) {
|
2018-01-30 14:30:50 +01:00
|
|
|
|
|
|
|
// no disposal is done on this frame before rendering the next;
|
|
|
|
// the contents of the output buffer are left as is.
|
|
|
|
DisposeOp.None -> {
|
|
|
|
}
|
|
|
|
|
2018-04-20 15:22:21 +02:00
|
|
|
// the frame's region of the output buffer is
|
|
|
|
// to be cleared to fully transparent black
|
2018-01-30 14:30:50 +01:00
|
|
|
// before rendering the next frame.
|
2018-04-20 15:22:21 +02:00
|
|
|
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)
|
|
|
|
}
|
2018-01-30 14:30:50 +01:00
|
|
|
|
2018-04-20 15:22:21 +02:00
|
|
|
// the frame's region of the output buffer is
|
|
|
|
// to be reverted to the previous contents
|
2018-01-30 14:30:50 +01:00
|
|
|
// 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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|