New class : ApngDrawable, to get the cover frame

Remove useless array copy
This commit is contained in:
Oupson 2021-02-24 11:41:46 +01:00
parent b584d6d013
commit d4924627cf
5 changed files with 54 additions and 29 deletions

View File

@ -17,6 +17,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import oupson.apng.BuildConfig import oupson.apng.BuildConfig
import oupson.apng.decoder.ApngDecoder.Companion.decodeApng import oupson.apng.decoder.ApngDecoder.Companion.decodeApng
import oupson.apng.drawable.ApngDrawable
import oupson.apng.exceptions.BadApngException import oupson.apng.exceptions.BadApngException
import oupson.apng.exceptions.BadCRCException import oupson.apng.exceptions.BadCRCException
import oupson.apng.utils.Loader import oupson.apng.utils.Loader
@ -67,6 +68,7 @@ class ApngDecoder {
* @param config Configuration applied to the bitmap added to the animation. Please note that the frame is decoded in ARGB_8888 and converted after, for the buffer. * @param config Configuration applied to the bitmap added to the animation. Please note that the frame is decoded in ARGB_8888 and converted after, for the buffer.
* @return [AnimationDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable]. * @return [AnimationDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
*/ */
// TODO BETTER CONFIG (Maybe data class with speed, config, and a settings to ignore cover frame ?)
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
@ -98,7 +100,7 @@ class ApngDecoder {
var isApng = false var isApng = false
val drawable = AnimationDrawable().apply { val drawable = ApngDrawable().apply {
isOneShot = false isOneShot = false
} }
@ -111,14 +113,13 @@ class ApngDecoder {
if (byteRead == -1) if (byteRead == -1)
break break
val length = Utils.uIntFromBytesBigEndian(lengthChunk.map(Byte::toInt)) val length = Utils.uIntFromBytesBigEndian(lengthChunk)
val chunk = ByteArray(length + 8) val chunk = ByteArray(length + 8)
byteRead = inputStream.read(chunk) byteRead = inputStream.read(chunk)
val byteArray = lengthChunk.plus(chunk) val byteArray = lengthChunk.plus(chunk)
val chunkCRC = val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4)
Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(byteArray.size - 4, byteArray.size).map(Byte::toInt))
val crc = CRC32() val crc = CRC32()
crc.update(byteArray, 4, byteArray.size - 8) crc.update(byteArray, 4, byteArray.size - 8)
if (chunkCRC == crc.value.toInt()) { if (chunkCRC == crc.value.toInt()) {
@ -126,20 +127,20 @@ class ApngDecoder {
when { when {
name.contentEquals(Utils.fcTL) -> { name.contentEquals(Utils.fcTL) -> {
if (png == null) { if (png == null) {
cover?.let { drawable.coverFrame = cover?.let {
it.write(zeroLength) it.write(zeroLength)
// Generate crc for IEND // Generate crc for IEND
val crC32 = CRC32() val crC32 = CRC32()
crC32.update(Utils.IEND, 0, Utils.IEND.size) crC32.update(Utils.IEND, 0, Utils.IEND.size)
it.write(Utils.IEND) it.write(Utils.IEND)
it.write(Utils.uIntToByteArray(crC32.value.toInt())) it.write(Utils.uIntToByteArray(crC32.value.toInt()))
/**APNGDisassembler.apng.cover = BitmapFactory.decodeByteArray(
it.toByteArray(), val pngBytes = it.toByteArray()
BitmapFactory.decodeByteArray(
pngBytes,
0, 0,
it.size pngBytes.size
)*/ // TODO )
// This will be ignored, has this is not a frame in the anim :/
// TODO CAN BE A DRAWING THAT INHERITS FROM DRAWING ANIMATION
} }
} else { } else {
// Add IEND body length : 0 // Add IEND body length : 0
@ -233,11 +234,11 @@ class ApngDecoder {
// Parse Frame ConTroL chunk // Parse Frame ConTroL chunk
// Get the width of the png // Get the width of the png
val width = Utils.uIntFromBytesBigEndian( val width = Utils.uIntFromBytesBigEndian(
byteArray.copyOfRange(12, 16).map(Byte::toInt) byteArray, 12
) )
// Get the height of the png // Get the height of the png
val height = Utils.uIntFromBytesBigEndian( val height = Utils.uIntFromBytesBigEndian(
byteArray.copyOfRange(16, 20).map(Byte::toInt) byteArray, 16
) )
/* /*
@ -246,11 +247,11 @@ class ApngDecoder {
*/ */
// Get delay numerator // Get delay numerator
val delayNum = Utils.uShortFromBytesBigEndian( val delayNum = Utils.uShortFromBytesBigEndian(
byteArray.copyOfRange(28, 30).map(Byte::toInt) byteArray, 28
).toFloat() ).toFloat()
// Get delay denominator // Get delay denominator
var delayDen = Utils.uShortFromBytesBigEndian( var delayDen = Utils.uShortFromBytesBigEndian(
byteArray.copyOfRange(30, 32).map(Byte::toInt) byteArray, 30
).toFloat() ).toFloat()
// If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second). // If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second).
@ -262,10 +263,10 @@ class ApngDecoder {
// Get x and y offsets // Get x and y offsets
xOffset = Utils.uIntFromBytesBigEndian( xOffset = Utils.uIntFromBytesBigEndian(
byteArray.copyOfRange(20, 24).map(Byte::toInt) byteArray, 20
) )
yOffset = Utils.uIntFromBytesBigEndian( yOffset = Utils.uIntFromBytesBigEndian(
byteArray.copyOfRange(24, 28).map(Byte::toInt) byteArray, 24
) )
blendOp = Utils.decodeBlendOp(byteArray[33].toInt()) blendOp = Utils.decodeBlendOp(byteArray[33].toInt())
disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt()) disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt())
@ -419,7 +420,7 @@ class ApngDecoder {
// Find the chunk length // Find the chunk length
val bodySize = val bodySize =
Utils.uIntFromBytesBigEndian( Utils.uIntFromBytesBigEndian(
byteArray.copyOfRange(0, 4).map(Byte::toInt) byteArray, 0
) )
w.write(byteArray.copyOfRange(0, 4)) w.write(byteArray.copyOfRange(0, 4))
@ -437,7 +438,7 @@ class ApngDecoder {
} }
name.contentEquals(Utils.fdAT) -> { name.contentEquals(Utils.fdAT) -> {
// Find the chunk length // Find the chunk length
val bodySize = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(0, 4).map(Byte::toInt)) val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
png?.write(Utils.uIntToByteArray(bodySize - 4)) png?.write(Utils.uIntToByteArray(bodySize - 4))
val body = ByteArray(bodySize) val body = ByteArray(bodySize)
@ -459,11 +460,11 @@ class ApngDecoder {
} }
name.contentEquals(Utils.IHDR) -> { name.contentEquals(Utils.IHDR) -> {
// Get length of the body of the chunk // Get length of the body of the chunk
val bodySize = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(4 - 4, 4).map(Byte::toInt)) val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
// Get the width of the png // Get the width of the png
maxWidth = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(4 +4, 4 + 8).map(Byte::toInt)) maxWidth = Utils.uIntFromBytesBigEndian(byteArray, 8)
// Get the height of the png // Get the height of the png
maxHeight = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(4 +8, 4 + 12).map(Byte::toInt)) maxHeight = Utils.uIntFromBytesBigEndian(byteArray, 12)
ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4) ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4)
buffer = Bitmap.createBitmap( buffer = Bitmap.createBitmap(

View File

@ -0,0 +1,9 @@
package oupson.apng.drawable
import android.graphics.Bitmap
import android.graphics.drawable.AnimationDrawable
// TODO DOCUMENT
class ApngDrawable : AnimationDrawable() {
var coverFrame : Bitmap? = null
}

View File

@ -190,6 +190,13 @@ class Utils {
((bytes[2] and 0xFF) shl 8) or ((bytes[2] and 0xFF) shl 8) or
(bytes[3] and 0xFF) (bytes[3] and 0xFF)
// TODO DOCUMENT AND TEST
fun uIntFromBytesBigEndian(bytes: ByteArray, offset: Int = 0): Int =
((bytes[offset + 0].toInt() and 0xFF) shl 24) or
((bytes[offset + 1].toInt() and 0xFF) shl 16) or
((bytes[offset + 2].toInt() and 0xFF) shl 8) or
(bytes[offset + 3].toInt() and 0xFF)
/** /**
* Parse an unsigned short * Parse an unsigned short
* [bytes] The bytes, in big endian order * [bytes] The bytes, in big endian order
@ -200,6 +207,11 @@ class Utils {
(((bytes[0] and 0xFF) shl 8) or (((bytes[0] and 0xFF) shl 8) or
(bytes[1] and 0xFF)) (bytes[1] and 0xFF))
// TODO DOCUMENT AND TEST
fun uShortFromBytesBigEndian(bytes: ByteArray, offset : Int = 0): Int =
(((bytes[offset].toInt() and 0xFF) shl 8) or
(bytes[offset + 1].toInt() and 0xFF))
val fcTL: ByteArray by lazy { byteArrayOf(0x66, 0x63, 0x54, 0x4c) } val fcTL: ByteArray by lazy { byteArrayOf(0x66, 0x63, 0x54, 0x4c) }
val IEND: ByteArray by lazy { byteArrayOf(0x49, 0x45, 0x4e, 0x44) } val IEND: ByteArray by lazy { byteArrayOf(0x49, 0x45, 0x4e, 0x44) }
val IDAT: ByteArray by lazy { byteArrayOf(0x49, 0x44, 0x41, 0x54) } val IDAT: ByteArray by lazy { byteArrayOf(0x49, 0x44, 0x41, 0x54) }

View File

@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import oupson.apng.decoder.ApngDecoder import oupson.apng.decoder.ApngDecoder
import oupson.apng.drawable.ApngDrawable
import oupson.apngcreator.BuildConfig import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R import oupson.apngcreator.R
import java.net.URL import java.net.URL
@ -40,7 +41,7 @@ class ApngDecoderFragment : Fragment() {
callback = object : ApngDecoder.Callback { callback = object : ApngDecoder.Callback {
override fun onSuccess(drawable: Drawable) { override fun onSuccess(drawable: Drawable) {
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
Log.i(TAG, "onSuccess()") Log.i(TAG, "onSuccess(), has cover frame : ${(drawable as? ApngDrawable)?.coverFrame != null}")
} }
override fun onError(error: Exception) { override fun onError(error: Exception) {

View File

@ -1,7 +1,6 @@
package oupson.apngcreator.fragments package oupson.apngcreator.fragments
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -15,6 +14,7 @@ import androidx.fragment.app.Fragment
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_creator.* import kotlinx.android.synthetic.main.activity_creator.*
import oupson.apng.decoder.ApngDecoder import oupson.apng.decoder.ApngDecoder
import oupson.apng.drawable.ApngDrawable
import oupson.apngcreator.BuildConfig import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R import oupson.apngcreator.R
@ -36,7 +36,7 @@ class KotlinFragment : Fragment() {
private var speedSeekBar : SeekBar? = null private var speedSeekBar : SeekBar? = null
//private var animator : ApngAnimator? = null //private var animator : ApngAnimator? = null
private var animation : AnimationDrawable? = null private var animation : ApngDrawable? = null
private var durations : IntArray? = null private var durations : IntArray? = null
private var frameIndex = 0 private var frameIndex = 0
@ -82,8 +82,9 @@ class KotlinFragment : Fragment() {
pauseButton?.setOnClickListener { pauseButton?.setOnClickListener {
animation = animation?.let { animation -> animation = animation?.let { animation ->
val res = AnimationDrawable() val res = ApngDrawable()
animation.stop() animation.stop()
res.coverFrame = animation.coverFrame
val currentFrame = animation.current val currentFrame = animation.current
frameLoop@ for (i in 0 until animation.numberOfFrames) { frameLoop@ for (i in 0 until animation.numberOfFrames) {
@ -120,8 +121,9 @@ class KotlinFragment : Fragment() {
if (seekBar != null && durations != null) { if (seekBar != null && durations != null) {
val speed = seekBar.progress.toFloat() / 100f val speed = seekBar.progress.toFloat() / 100f
animation = animation?.let { animation -> animation = animation?.let { animation ->
val res = AnimationDrawable() val res = ApngDrawable()
animation.stop() animation.stop()
res.coverFrame = animation.coverFrame
for (i in 0 until animation.numberOfFrames) { for (i in 0 until animation.numberOfFrames) {
res.addFrame(animation.getFrame(i), (durations!![i].toFloat() / speed).toInt()) res.addFrame(animation.getFrame(i), (durations!![i].toFloat() / speed).toInt())
@ -142,7 +144,7 @@ class KotlinFragment : Fragment() {
apngImageView!!, apngImageView!!,
callback = object : ApngDecoder.Callback { callback = object : ApngDecoder.Callback {
override fun onSuccess(drawable: Drawable) { override fun onSuccess(drawable: Drawable) {
animation = (drawable as? AnimationDrawable) animation = (drawable as? ApngDrawable)
durations = IntArray(animation?.numberOfFrames ?: 0) { i -> durations = IntArray(animation?.numberOfFrames ?: 0) { i ->
animation?.getDuration(i) ?: 0 animation?.getDuration(i) ?: 0
} }