Partial fix of a bug with ApngAnimator and system style change

Working on documentation
This commit is contained in:
oupson 2020-05-09 20:14:22 +02:00
parent 323623b5fa
commit f52b9e4563
5 changed files with 356 additions and 289 deletions

View File

@ -17,304 +17,322 @@ import java.io.InputStream
import java.util.* import java.util.*
import java.util.zip.CRC32 import java.util.zip.CRC32
// TODO REWRITE
class APNGDisassembler { class APNGDisassembler {
companion object { private var png: ArrayList<Byte>? = null
private var png: ArrayList<Byte>? = null private var cover: ArrayList<Byte>? = null
private var cover: ArrayList<Byte>? = null private var delay = -1f
private var delay = -1f private var yOffset = -1
private var yOffset = -1 private var xOffset = -1
private var xOffset = -1 private var plte: ByteArray? = null
private var plte: ByteArray? = null private var tnrs: ByteArray? = null
private var tnrs: ByteArray? = null private var maxWidth = 0
private var maxWidth = 0 private var maxHeight = 0
private var maxHeight = 0 private var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
private var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE private var disposeOp: Utils.Companion.DisposeOp =
private var disposeOp: Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
private var ihdr = IHDR() private var ihdr = IHDR()
private var isApng = false private var isApng = false
var apng: Apng = Apng() var apng: Apng = Apng()
/** /**
* Disassemble an Apng file * Disassemble an Apng file
* @param byteArray The Byte Array of the file * @param byteArray The Byte Array of the file
* @return [Apng] The apng decoded * @return [Apng] The apng decoded
*/ */
fun disassemble(byteArray: ByteArray) : Apng { fun disassemble(byteArray: ByteArray): Apng {
reset() reset()
if (isApng(byteArray)) { if (isApng(byteArray)) {
var cursor = 8 var cursor = 8
while (cursor < byteArray.size) { while (cursor < byteArray.size) {
val length = parseLength(byteArray.copyOfRange(cursor, cursor + 4)) val length = parseLength(byteArray.copyOfRange(cursor, cursor + 4))
val chunk = byteArray.copyOfRange(cursor, cursor + length + 12) val chunk = byteArray.copyOfRange(cursor, cursor + length + 12)
parseChunk(chunk) parseChunk(chunk)
cursor += length + 12 cursor += length + 12
}
return apng
} else {
throw NotApngException()
} }
}
/**
* Disassemble an Apng file
* @param input Input Stream
* @return [Apng] The apng decoded
*/
fun disassemble(input : InputStream) : Apng {
reset()
val buffer = ByteArray(8)
input.read(buffer)
if (!isPng(buffer))
throw NotPngException()
var byteRead: Int
val lengthChunk = ByteArray(4)
do {
byteRead = input.read(lengthChunk)
if (byteRead == -1)
break
val length = parseLength(lengthChunk)
val chunk = ByteArray(length + 8)
byteRead = input.read(chunk)
parseChunk(lengthChunk.plus(chunk))
} while (byteRead != -1)
return apng return apng
} else {
throw NotApngException()
} }
}
/** /**
* Generate a correct IHDR from the IHDR chunk of the APNG * Disassemble an Apng file
* @param ihdrOfApng The IHDR of the APNG * @param input Input Stream
* @param width The width of the frame * @return [Apng] The apng decoded
* @param height The height of the frame */
* @return [ByteArray] The generated IHDR fun disassemble(input: InputStream): Apng {
*/ reset()
private fun generateIhdr(ihdrOfApng: IHDR, width : Int, height : Int) : ByteArray { val buffer = ByteArray(8)
val ihdr = ArrayList<Byte>()
// We need a body var to know body length and generate crc
val ihdrBody = ArrayList<Byte>()
// Add chunk body length
ihdr.addAll(to4Bytes(ihdrOfApng.body.size).asList())
// Add IHDR
ihdrBody.addAll(byteArrayOf(0x49.toByte(), 0x48.toByte(), 0x44.toByte(), 0x52.toByte()).asList())
// Add the max width and height
ihdrBody.addAll(to4Bytes(width).asList())
ihdrBody.addAll(to4Bytes(height).asList())
// Add complicated stuff like depth color ...
// If you want correct png you need same parameters. Good solution is to create new png.
ihdrBody.addAll(ihdrOfApng.body.copyOfRange(8, 13).asList())
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdrBody.toByteArray(), 0, ihdrBody.size)
ihdr.addAll(ihdrBody)
ihdr.addAll(to4Bytes(crC32.value.toInt()).asList())
return ihdr.toByteArray()
}
/** input.read(buffer)
* Parse the chunk
* @param byteArray The chunk with length and crc
*/
private fun parseChunk(byteArray: ByteArray) {
val i = 4
val chunkCRC = parseLength(byteArray.copyOfRange(byteArray.size - 4, byteArray.size))
val crc = CRC32()
crc.update(byteArray.copyOfRange(i, byteArray.size - 4))
if (chunkCRC == crc.value.toInt()) {
val name= byteArray.copyOfRange(i, i + 4)
when {
name.contentEquals(Utils.fcTL) -> {
if (png == null) {
cover?.let {
it.addAll(to4Bytes(0).asList())
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND
val crC32 = CRC32()
crC32.update(iend, 0, iend.size)
it.addAll(iend.asList())
it.addAll(to4Bytes(crC32.value.toInt()).asList())
apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size)
}
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
delay = fcTL.delay
yOffset = fcTL.yOffset
xOffset = fcTL.xOffset
blendOp = fcTL.blendOp
disposeOp = fcTL.disposeOp
val width = fcTL.pngWidth
val height = fcTL.pngHeight
if (xOffset + width > maxWidth) { if (!isPng(buffer))
throw BadApng("`yOffset` + `height` must be <= `IHDR` height") throw NotPngException()
} else if (yOffset + height > maxHeight) {
throw BadApng("`yOffset` + `height` must be <= `IHDR` height")
}
png?.addAll(pngSignature.asList()) var byteRead: Int
png?.addAll(generateIhdr(ihdr, width, height).asList())
plte?.let { val lengthChunk = ByteArray(4)
png?.addAll(it.asList()) do {
} byteRead = input.read(lengthChunk)
tnrs?.let {
png?.addAll(it.asList()) if (byteRead == -1)
} break
} else { val length = parseLength(lengthChunk)
// Add IEND body length : 0
png?.addAll(to4Bytes(0).asList()) val chunk = ByteArray(length + 8)
byteRead = input.read(chunk)
parseChunk(lengthChunk.plus(chunk))
} while (byteRead != -1)
return apng
}
/**
* Generate a correct IHDR from the IHDR chunk of the APNG
* @param ihdrOfApng The IHDR of the APNG
* @param width The width of the frame
* @param height The height of the frame
* @return [ByteArray] The generated IHDR
*/
private fun generateIhdr(ihdrOfApng: IHDR, width: Int, height: Int): ByteArray {
val ihdr = ArrayList<Byte>()
// We need a body var to know body length and generate crc
val ihdrBody = ArrayList<Byte>()
// Add chunk body length
ihdr.addAll(to4Bytes(ihdrOfApng.body.size).asList())
// Add IHDR
ihdrBody.addAll(
byteArrayOf(
0x49.toByte(),
0x48.toByte(),
0x44.toByte(),
0x52.toByte()
).asList()
)
// Add the max width and height
ihdrBody.addAll(to4Bytes(width).asList())
ihdrBody.addAll(to4Bytes(height).asList())
// Add complicated stuff like depth color ...
// If you want correct png you need same parameters. Good solution is to create new png.
ihdrBody.addAll(ihdrOfApng.body.copyOfRange(8, 13).asList())
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdrBody.toByteArray(), 0, ihdrBody.size)
ihdr.addAll(ihdrBody)
ihdr.addAll(to4Bytes(crC32.value.toInt()).asList())
return ihdr.toByteArray()
}
/**
* Parse the chunk
* @param byteArray The chunk with length and crc
*/
private fun parseChunk(byteArray: ByteArray) {
val i = 4
val chunkCRC = parseLength(byteArray.copyOfRange(byteArray.size - 4, byteArray.size))
val crc = CRC32()
crc.update(byteArray.copyOfRange(i, byteArray.size - 4))
if (chunkCRC == crc.value.toInt()) {
val name = byteArray.copyOfRange(i, i + 4)
when {
name.contentEquals(Utils.fcTL) -> {
if (png == null) {
cover?.let {
it.addAll(to4Bytes(0).asList())
// Add IEND // Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44) val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND // Generate crc for IEND
val crC32 = CRC32() val crC32 = CRC32()
crC32.update(iend, 0, iend.size) crC32.update(iend, 0, iend.size)
png?.addAll(iend.asList()) it.addAll(iend.asList())
png?.addAll(to4Bytes(crC32.value.toInt()).asList()) it.addAll(to4Bytes(crC32.value.toInt()).asList())
apng.frames.add(Frame(png!!.toByteArray(), delay, xOffset, yOffset, blendOp, disposeOp, maxWidth, maxHeight)) apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size)
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
delay = fcTL.delay
yOffset = fcTL.yOffset
xOffset = fcTL.xOffset
blendOp = fcTL.blendOp
disposeOp = fcTL.disposeOp
val width = fcTL.pngWidth
val height = fcTL.pngHeight
png?.addAll(pngSignature.asList())
png?.addAll(generateIhdr(ihdr, width, height).asList())
plte?.let {
png?.addAll(it.asList())
}
tnrs?.let {
png?.addAll(it.asList())
}
} }
} png = ArrayList()
name.contentEquals(Utils.IEND) -> { val fcTL = fcTL()
if (isApng) { fcTL.parse(byteArray)
png?.addAll(to4Bytes(0).asList()) delay = fcTL.delay
// Add IEND yOffset = fcTL.yOffset
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44) xOffset = fcTL.xOffset
// Generate crc for IEND blendOp = fcTL.blendOp
val crC32 = CRC32() disposeOp = fcTL.disposeOp
crC32.update(iend, 0, iend.size) val width = fcTL.pngWidth
png?.addAll(iend.asList()) val height = fcTL.pngHeight
png?.addAll(to4Bytes(crC32.value.toInt()).asList())
apng.frames.add( if (xOffset + width > maxWidth) {
Frame( throw BadApng("`yOffset` + `height` must be <= `IHDR` height")
png!!.toByteArray(), } else if (yOffset + height > maxHeight) {
delay, throw BadApng("`yOffset` + `height` must be <= `IHDR` height")
xOffset, }
yOffset,
blendOp, png?.addAll(pngSignature.asList())
disposeOp, png?.addAll(generateIhdr(ihdr, width, height).asList())
maxWidth, plte?.let {
maxHeight png?.addAll(it.asList())
) }
tnrs?.let {
png?.addAll(it.asList())
}
} else {
// Add IEND body length : 0
png?.addAll(to4Bytes(0).asList())
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND
val crC32 = CRC32()
crC32.update(iend, 0, iend.size)
png?.addAll(iend.asList())
png?.addAll(to4Bytes(crC32.value.toInt()).asList())
apng.frames.add(
Frame(
png!!.toByteArray(),
delay,
xOffset,
yOffset,
blendOp,
disposeOp,
maxWidth,
maxHeight
) )
} else { )
cover?.let { png = ArrayList()
it.addAll(to4Bytes(0).asList()) val fcTL = fcTL()
// Add IEND fcTL.parse(byteArray)
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44) delay = fcTL.delay
// Generate crc for IEND yOffset = fcTL.yOffset
val crC32 = CRC32() xOffset = fcTL.xOffset
crC32.update(iend, 0, iend.size) blendOp = fcTL.blendOp
it.addAll(iend.asList()) disposeOp = fcTL.disposeOp
it.addAll(to4Bytes(crC32.value.toInt()).asList()) val width = fcTL.pngWidth
apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size) val height = fcTL.pngHeight
} png?.addAll(pngSignature.asList())
apng.isApng = false png?.addAll(generateIhdr(ihdr, width, height).asList())
plte?.let {
png?.addAll(it.asList())
}
tnrs?.let {
png?.addAll(it.asList())
} }
} }
name.contentEquals(Utils.IDAT) -> { }
if (png == null) { name.contentEquals(Utils.IEND) -> {
if (cover == null) { if (isApng) {
cover = ArrayList() png?.addAll(to4Bytes(0).asList())
cover?.addAll(pngSignature.asList()) // Add IEND
cover?.addAll(generateIhdr(ihdr, maxWidth, maxHeight).asList()) val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
} // Generate crc for IEND
// Find the chunk length val crC32 = CRC32()
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i)) crC32.update(iend, 0, iend.size)
cover?.addAll(byteArray.copyOfRange(i - 4, i).asList()) png?.addAll(iend.asList())
val body = ArrayList<Byte>() png?.addAll(to4Bytes(crC32.value.toInt()).asList())
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList()) apng.frames.add(
// Get image bytes Frame(
body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).asList()) png!!.toByteArray(),
delay,
xOffset,
yOffset,
blendOp,
disposeOp,
maxWidth,
maxHeight
)
)
} else {
cover?.let {
it.addAll(to4Bytes(0).asList())
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND
val crC32 = CRC32() val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size) crC32.update(iend, 0, iend.size)
cover?.addAll(body) it.addAll(iend.asList())
cover?.addAll(to4Bytes(crC32.value.toInt()).asList()) it.addAll(to4Bytes(crC32.value.toInt()).asList())
} else { apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size)
// Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
png?.addAll(byteArray.copyOfRange(i - 4, i).asList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).asList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png?.addAll(body)
png?.addAll(to4Bytes(crC32.value.toInt()).asList())
} }
apng.isApng = false
} }
name.contentEquals(Utils.fdAT) -> { }
name.contentEquals(Utils.IDAT) -> {
if (png == null) {
if (cover == null) {
cover = ArrayList()
cover?.addAll(pngSignature.asList())
cover?.addAll(generateIhdr(ihdr, maxWidth, maxHeight).asList())
}
// Find the chunk length // Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i)) val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
png?.addAll(to4Bytes(bodySize - 4).asList()) cover?.addAll(byteArray.copyOfRange(i - 4, i).asList())
val body = ArrayList<Byte>() val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList()) body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Get image bytes // Get image bytes
body.addAll(byteArray.copyOfRange(i + 8, i + 4 + bodySize).asList()) body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).asList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
cover?.addAll(body)
cover?.addAll(to4Bytes(crC32.value.toInt()).asList())
} else {
// Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
png?.addAll(byteArray.copyOfRange(i - 4, i).asList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).asList())
val crC32 = CRC32() val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size) crC32.update(body.toByteArray(), 0, body.size)
png?.addAll(body) png?.addAll(body)
png?.addAll(to4Bytes(crC32.value.toInt()).asList()) png?.addAll(to4Bytes(crC32.value.toInt()).asList())
} }
name.contentEquals(Utils.plte) -> {
plte = byteArray
}
name.contentEquals(Utils.tnrs) -> {
tnrs = byteArray
}
name.contentEquals(Utils.IHDR) -> {
ihdr.parse(byteArray)
maxWidth = ihdr.pngWidth
maxHeight = ihdr.pngHeight
}
name.contentEquals(Utils.acTL) -> {
isApng = true
}
} }
} else throw BadCRC() name.contentEquals(Utils.fdAT) -> {
} // Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
png?.addAll(to4Bytes(bodySize - 4).asList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 8, i + 4 + bodySize).asList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png?.addAll(body)
png?.addAll(to4Bytes(crC32.value.toInt()).asList())
}
name.contentEquals(Utils.plte) -> {
plte = byteArray
}
name.contentEquals(Utils.tnrs) -> {
tnrs = byteArray
}
name.contentEquals(Utils.IHDR) -> {
ihdr.parse(byteArray)
maxWidth = ihdr.pngWidth
maxHeight = ihdr.pngHeight
}
name.contentEquals(Utils.acTL) -> {
isApng = true
}
}
} else throw BadCRC()
}
/** /**
* Reset all var before parsing APNG * Reset all var before parsing APNG
*/ */
private fun reset() { private fun reset() {
png = null png = null
cover = null cover = null
delay = -1f delay = -1f
yOffset = -1 yOffset = -1
xOffset = -1 xOffset = -1
plte = null plte = null
tnrs = null tnrs = null
maxWidth = 0 maxWidth = 0
maxHeight = 0 maxHeight = 0
ihdr = IHDR() ihdr = IHDR()
apng = Apng() apng = Apng()
isApng = false isApng = false
}
} }
} }

View File

@ -183,7 +183,7 @@ class ApngAnimator(private val context: Context?) {
// Download PNG // Download PNG
val inputStream = file.inputStream() val inputStream = file.inputStream()
APNGDisassembler.disassemble(inputStream).also { APNGDisassembler().disassemble(inputStream).also {
inputStream.close() inputStream.close()
if (it.isApng) { if (it.isApng) {
it.frames.also {frames -> it.frames.also {frames ->
@ -231,7 +231,7 @@ class ApngAnimator(private val context: Context?) {
// Download PNG // Download PNG
val inputStream = context.contentResolver.openInputStream(uri)!! val inputStream = context.contentResolver.openInputStream(uri)!!
APNGDisassembler.disassemble(inputStream).also { APNGDisassembler().disassemble(inputStream).also {
inputStream.close() inputStream.close()
if (it.isApng) { if (it.isApng) {
isApng = true isApng = true
@ -319,7 +319,8 @@ class ApngAnimator(private val context: Context?) {
this@ApngAnimator.speed = speed this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType scaleType = apngAnimatorOptions?.scaleType
// Download PNG // Download PNG
APNGDisassembler.disassemble(byteArray).frames.also { frames -> println(byteArray.size)
APNGDisassembler().disassemble(byteArray).frames.also { frames ->
draw(frames).apply { draw(frames).apply {
setupAnimationDrawableAndStart(this) setupAnimationDrawableAndStart(this)
} }
@ -391,7 +392,7 @@ class ApngAnimator(private val context: Context?) {
this@ApngAnimator.speed = speed this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType scaleType = apngAnimatorOptions?.scaleType
// Download PNG // Download PNG
APNGDisassembler.disassemble(byteArray).frames.also { frames -> APNGDisassembler().disassemble(byteArray).frames.also { frames ->
draw(frames).apply { draw(frames).apply {
setupAnimationDrawableAndStart(this) setupAnimationDrawableAndStart(this)
} }

View File

@ -8,12 +8,15 @@ import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
// TODO DOCUMENTATION (MAYBE WIKI) FOR THE CACHE
class Loader { class Loader {
companion object { companion object {
/** /**
* Download file from given url. * Download file from given url on the [Dispatchers.IO] scope.
* @param url Url of the file to download. * @param url Url of the file to download.
* @return [ByteArray] of the file. * @return [ByteArray] of the file.
* @throws IOException thrown when retrieving the file.
* @throws Exception when returned code of the [HttpURLConnection] is not 200 (OK).
*/ */
@Throws(IOException::class, Exception::class) @Throws(IOException::class, Exception::class)
suspend fun load(url: URL): ByteArray = suspend fun load(url: URL): ByteArray =

View File

@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import oupson.apng.APNGDisassembler
import oupson.apng.BuildConfig import oupson.apng.BuildConfig
import oupson.apng.Loader import oupson.apng.Loader
import oupson.apng.chunks.IHDR import oupson.apng.chunks.IHDR
@ -31,13 +30,23 @@ import java.util.zip.CRC32
class ApngDecoder { class ApngDecoder {
interface Callback { interface Callback {
/**
* Function called when the file was successfully decoded.
* @param drawable Can be an [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].
*/
fun onSuccess(drawable: Drawable) fun onSuccess(drawable: Drawable)
/**
* Function called when something gone wrong.
* @param error The problem.
*/
fun onError(error: java.lang.Exception) fun onError(error: java.lang.Exception)
} }
companion object { companion object {
private const val TAG = "ApngDecoder" private const val TAG = "ApngDecoder"
// Paint used to clear the buffer
private val clearPaint: Paint by lazy { private val clearPaint: Paint by lazy {
Paint().apply { Paint().apply {
xfermode = PorterDuffXfermode( xfermode = PorterDuffXfermode(
@ -52,11 +61,17 @@ class ApngDecoder {
* @param inStream Input Stream to decode. Will be close at the end. * @param inStream Input Stream to decode. Will be close at the end.
* @param speed Optional parameter. * @param speed Optional parameter.
* @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].
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
@JvmStatic @JvmStatic
@JvmOverloads @JvmOverloads
fun decodeApng(context: Context, inStream: InputStream, speed: Float = 1f, config : Bitmap.Config = Bitmap.Config.ARGB_8888): Drawable { fun decodeApng(
context: Context,
inStream: InputStream,
speed: Float = 1f,
config: Bitmap.Config = Bitmap.Config.ARGB_8888
): Drawable {
val inputStream = BufferedInputStream(inStream) val inputStream = BufferedInputStream(inStream)
val bytes = ByteArray(8) val bytes = ByteArray(8)
inputStream.mark(8) inputStream.mark(8)
@ -115,11 +130,11 @@ class ApngDecoder {
crC32.update(iend, 0, iend.size) crC32.update(iend, 0, iend.size)
it.addAll(iend.asList()) it.addAll(iend.asList())
it.addAll(Utils.to4Bytes(crC32.value.toInt()).asList()) it.addAll(Utils.to4Bytes(crC32.value.toInt()).asList())
APNGDisassembler.apng.cover = BitmapFactory.decodeByteArray( /**APNGDisassembler.apng.cover = BitmapFactory.decodeByteArray(
it.toByteArray(), it.toByteArray(),
0, 0,
it.size it.size
) )*/ // TODO
} }
png = ArrayList() png = ArrayList()
val fcTL = fcTL() val fcTL = fcTL()
@ -198,7 +213,10 @@ class ApngDecoder {
context.resources, context.resources,
if (btm.config != config) { if (btm.config != config) {
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
Log.v(TAG, "Bitmap Config : ${btm.config}, Config : $config") Log.v(
TAG,
"Bitmap Config : ${btm.config}, Config : $config"
)
btm.copy(config, btm.isMutable) btm.copy(config, btm.isMutable)
} else { } else {
btm btm
@ -306,7 +324,10 @@ class ApngDecoder {
context.resources, context.resources,
if (btm.config != config) { if (btm.config != config) {
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
Log.v(TAG, "Bitmap Config : ${btm.config}, Config : $config") Log.v(
TAG,
"Bitmap Config : ${btm.config}, Config : $config"
)
btm.copy(config, btm.isMutable) btm.copy(config, btm.isMutable)
} else { } else {
btm btm
@ -478,10 +499,16 @@ class ApngDecoder {
* @param file File to decode. * @param file File to decode.
* @param speed Optional parameter. * @param speed Optional parameter.
* @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].
*/ */
@Suppress("unused") @Suppress("unused")
@JvmStatic @JvmStatic
fun decodeApng(context: Context, file: File, speed: Float = 1f, config : Bitmap.Config = Bitmap.Config.ARGB_8888): Drawable = fun decodeApng(
context: Context,
file: File,
speed: Float = 1f,
config: Bitmap.Config = Bitmap.Config.ARGB_8888
): Drawable =
decodeApng( decodeApng(
context, context,
FileInputStream(file), speed, config FileInputStream(file), speed, config
@ -493,12 +520,17 @@ class ApngDecoder {
* @param uri Uri to open. * @param uri Uri to open.
* @param speed Optional parameter. * @param speed Optional parameter.
* @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.
*/ */
@Suppress("unused") @Suppress("unused")
@JvmStatic @JvmStatic
fun decodeApng(context: Context, uri: Uri, speed: Float = 1f, config : Bitmap.Config = Bitmap.Config.ARGB_8888): Drawable { fun decodeApng(
val inputStream = context.contentResolver.openInputStream(uri) context: Context,
?: throw Exception("Failed to open InputStream, InputStream is null") uri: Uri,
speed: Float = 1f,
config: Bitmap.Config = Bitmap.Config.ARGB_8888
): Drawable {
val inputStream = context.contentResolver.openInputStream(uri)!!
return decodeApng( return decodeApng(
context, context,
inputStream, inputStream,
@ -513,10 +545,16 @@ class ApngDecoder {
* @param res Resource to decode. * @param res Resource to decode.
* @param speed Optional parameter. * @param speed Optional parameter.
* @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.
*/ */
@Suppress("unused") @Suppress("unused")
@JvmStatic @JvmStatic
fun decodeApng(context: Context, @RawRes res: Int, speed: Float = 1f, config : Bitmap.Config = Bitmap.Config.ARGB_8888): Drawable = fun decodeApng(
context: Context,
@RawRes res: Int,
speed: Float = 1f,
config: Bitmap.Config = Bitmap.Config.ARGB_8888
): Drawable =
decodeApng( decodeApng(
context, context,
context.resources.openRawResource(res), context.resources.openRawResource(res),
@ -530,10 +568,16 @@ class ApngDecoder {
* @param url URL to decode. * @param url URL to decode.
* @param speed Optional parameter. * @param speed Optional parameter.
* @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.
*/ */
@Suppress("unused") @Suppress("unused")
@JvmStatic @JvmStatic
suspend fun decodeApng(context: Context, url: URL, speed: Float = 1f, config : Bitmap.Config = Bitmap.Config.ARGB_8888) = suspend fun decodeApng(
context: Context,
url: URL,
speed: Float = 1f,
config: Bitmap.Config = Bitmap.Config.ARGB_8888
) =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
decodeApng( decodeApng(
context, context,
@ -561,7 +605,7 @@ class ApngDecoder {
imageView: ImageView, imageView: ImageView,
speed: Float = 1f, speed: Float = 1f,
callback: Callback? = null, callback: Callback? = null,
config : Bitmap.Config = Bitmap.Config.ARGB_8888 config: Bitmap.Config = Bitmap.Config.ARGB_8888
) { ) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {
@ -606,10 +650,9 @@ class ApngDecoder {
imageView: ImageView, imageView: ImageView,
speed: Float = 1f, speed: Float = 1f,
callback: Callback? = null, callback: Callback? = null,
config : Bitmap.Config = Bitmap.Config.ARGB_8888 config: Bitmap.Config = Bitmap.Config.ARGB_8888
) { ) {
val inputStream = context.contentResolver.openInputStream(uri) val inputStream = context.contentResolver.openInputStream(uri)!!
?: throw Exception("Failed to open InputStream, InputStream is null")
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {
val drawable = val drawable =
@ -652,7 +695,7 @@ class ApngDecoder {
imageView: ImageView, imageView: ImageView,
speed: Float = 1f, speed: Float = 1f,
callback: Callback? = null, callback: Callback? = null,
config : Bitmap.Config = Bitmap.Config.ARGB_8888 config: Bitmap.Config = Bitmap.Config.ARGB_8888
) { ) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {
@ -698,7 +741,7 @@ class ApngDecoder {
imageView: ImageView, imageView: ImageView,
speed: Float = 1f, speed: Float = 1f,
callback: Callback? = null, callback: Callback? = null,
config : Bitmap.Config = Bitmap.Config.ARGB_8888 config: Bitmap.Config = Bitmap.Config.ARGB_8888
) { ) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {
@ -747,7 +790,7 @@ class ApngDecoder {
imageView: ImageView, imageView: ImageView,
speed: Float = 1f, speed: Float = 1f,
callback: Callback? = null, callback: Callback? = null,
config : Bitmap.Config = Bitmap.Config.ARGB_8888 config: Bitmap.Config = Bitmap.Config.ARGB_8888
) { ) {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {

View File

@ -11,6 +11,8 @@ import java.io.OutputStream
import java.util.zip.CRC32 import java.util.zip.CRC32
// TODO DOCUMENTATION // TODO DOCUMENTATION
// TODO BITMAP ENCODING
// TODO BUFFER AND BUFFER DEACTIVATION WHEN BITMAP CONFIG DOES NOT CONTAIN AN ALPHA CHANNEL
class ApngEncoder( class ApngEncoder(
private val outputStream: OutputStream, private val outputStream: OutputStream,
private val width : Int, private val width : Int,