From f52b9e4563ed1abf7b903da8dc133893e6dd1666 Mon Sep 17 00:00:00 2001 From: oupson Date: Sat, 9 May 2020 20:14:22 +0200 Subject: [PATCH] Partial fix of a bug with ApngAnimator and system style change Working on documentation --- .../main/java/oupson/apng/APNGDisassembler.kt | 548 +++++++++--------- .../src/main/java/oupson/apng/ApngAnimator.kt | 9 +- .../src/main/java/oupson/apng/Loader.kt | 5 +- .../java/oupson/apng/decoder/ApngDecoder.kt | 81 ++- .../java/oupson/apng/encoder/ApngEncoder.kt | 2 + 5 files changed, 356 insertions(+), 289 deletions(-) diff --git a/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt index 2404c77..59974e0 100644 --- a/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt +++ b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt @@ -17,304 +17,322 @@ import java.io.InputStream import java.util.* import java.util.zip.CRC32 +// TODO REWRITE class APNGDisassembler { - companion object { - private var png: ArrayList? = null - private var cover: ArrayList? = null - private var delay = -1f - private var yOffset = -1 - private var xOffset = -1 - private var plte: ByteArray? = null - private var tnrs: ByteArray? = null - private var maxWidth = 0 - private var maxHeight = 0 - private var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE - private var disposeOp: Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE - private var ihdr = IHDR() - private var isApng = false + private var png: ArrayList? = null + private var cover: ArrayList? = null + private var delay = -1f + private var yOffset = -1 + private var xOffset = -1 + private var plte: ByteArray? = null + private var tnrs: ByteArray? = null + private var maxWidth = 0 + private var maxHeight = 0 + private var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE + private var disposeOp: Utils.Companion.DisposeOp = + Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE + private var ihdr = IHDR() + private var isApng = false - var apng: Apng = Apng() + var apng: Apng = Apng() - /** - * Disassemble an Apng file - * @param byteArray The Byte Array of the file - * @return [Apng] The apng decoded - */ - fun disassemble(byteArray: ByteArray) : Apng { - reset() - if (isApng(byteArray)) { - var cursor = 8 - while (cursor < byteArray.size) { - val length = parseLength(byteArray.copyOfRange(cursor, cursor + 4)) - val chunk = byteArray.copyOfRange(cursor, cursor + length + 12) - parseChunk(chunk) - cursor += length + 12 - } - return apng - } else { - throw NotApngException() + /** + * Disassemble an Apng file + * @param byteArray The Byte Array of the file + * @return [Apng] The apng decoded + */ + fun disassemble(byteArray: ByteArray): Apng { + reset() + if (isApng(byteArray)) { + var cursor = 8 + while (cursor < byteArray.size) { + val length = parseLength(byteArray.copyOfRange(cursor, cursor + 4)) + val chunk = byteArray.copyOfRange(cursor, cursor + length + 12) + parseChunk(chunk) + cursor += length + 12 } - } - - /** - * 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 + } else { + throw NotApngException() } + } - /** - * 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() - // We need a body var to know body length and generate crc - val ihdrBody = ArrayList() - // 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() - } + /** + * Disassemble an Apng file + * @param input Input Stream + * @return [Apng] The apng decoded + */ + fun disassemble(input: InputStream): Apng { + reset() + val buffer = ByteArray(8) - /** - * 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 + input.read(buffer) - if (xOffset + width > maxWidth) { - throw BadApng("`yOffset` + `height` must be <= `IHDR` height") - } else if (yOffset + height > maxHeight) { - throw BadApng("`yOffset` + `height` must be <= `IHDR` height") - } + if (!isPng(buffer)) + throw NotPngException() - png?.addAll(pngSignature.asList()) - png?.addAll(generateIhdr(ihdr, width, height).asList()) - plte?.let { - png?.addAll(it.asList()) - } - tnrs?.let { - png?.addAll(it.asList()) - } - } else { - // Add IEND body length : 0 - png?.addAll(to4Bytes(0).asList()) + 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 + } + + /** + * 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() + // We need a body var to know body length and generate crc + val ihdrBody = ArrayList() + // 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 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)) - 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()) - } + it.addAll(iend.asList()) + it.addAll(to4Bytes(crC32.value.toInt()).asList()) + apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size) } - } - name.contentEquals(Utils.IEND) -> { - if (isApng) { - 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 - ) + 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) { + throw BadApng("`yOffset` + `height` must be <= `IHDR` height") + } else if (yOffset + height > maxHeight) { + throw BadApng("`yOffset` + `height` must be <= `IHDR` height") + } + + png?.addAll(pngSignature.asList()) + png?.addAll(generateIhdr(ihdr, width, height).asList()) + plte?.let { + 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 { - 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) - } - apng.isApng = false + ) + 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()) } } - 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 - val bodySize = parseLength(byteArray.copyOfRange(i - 4, i)) - cover?.addAll(byteArray.copyOfRange(i - 4, i).asList()) - val body = ArrayList() - body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList()) - // Get image bytes - body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).asList()) + } + name.contentEquals(Utils.IEND) -> { + if (isApng) { + 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 { + it.addAll(to4Bytes(0).asList()) + // Add IEND + val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44) + // Generate crc for IEND 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() - 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()) + 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) } + 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 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() body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList()) // 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() + 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()) } - 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() + 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 - */ - private fun reset() { - png = null - cover = null - delay = -1f - yOffset = -1 - xOffset = -1 - plte = null - tnrs = null - maxWidth = 0 - maxHeight = 0 - ihdr = IHDR() - apng = Apng() - isApng = false - } + /** + * Reset all var before parsing APNG + */ + private fun reset() { + png = null + cover = null + delay = -1f + yOffset = -1 + xOffset = -1 + plte = null + tnrs = null + maxWidth = 0 + maxHeight = 0 + ihdr = IHDR() + apng = Apng() + isApng = false } } \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt index 34d8497..7d51e0f 100644 --- a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt +++ b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt @@ -183,7 +183,7 @@ class ApngAnimator(private val context: Context?) { // Download PNG val inputStream = file.inputStream() - APNGDisassembler.disassemble(inputStream).also { + APNGDisassembler().disassemble(inputStream).also { inputStream.close() if (it.isApng) { it.frames.also {frames -> @@ -231,7 +231,7 @@ class ApngAnimator(private val context: Context?) { // Download PNG val inputStream = context.contentResolver.openInputStream(uri)!! - APNGDisassembler.disassemble(inputStream).also { + APNGDisassembler().disassemble(inputStream).also { inputStream.close() if (it.isApng) { isApng = true @@ -319,7 +319,8 @@ class ApngAnimator(private val context: Context?) { this@ApngAnimator.speed = speed scaleType = apngAnimatorOptions?.scaleType // Download PNG - APNGDisassembler.disassemble(byteArray).frames.also { frames -> + println(byteArray.size) + APNGDisassembler().disassemble(byteArray).frames.also { frames -> draw(frames).apply { setupAnimationDrawableAndStart(this) } @@ -391,7 +392,7 @@ class ApngAnimator(private val context: Context?) { this@ApngAnimator.speed = speed scaleType = apngAnimatorOptions?.scaleType // Download PNG - APNGDisassembler.disassemble(byteArray).frames.also { frames -> + APNGDisassembler().disassemble(byteArray).frames.also { frames -> draw(frames).apply { setupAnimationDrawableAndStart(this) } diff --git a/apng_library/src/main/java/oupson/apng/Loader.kt b/apng_library/src/main/java/oupson/apng/Loader.kt index e259ec7..0f2d25d 100644 --- a/apng_library/src/main/java/oupson/apng/Loader.kt +++ b/apng_library/src/main/java/oupson/apng/Loader.kt @@ -8,12 +8,15 @@ import java.io.IOException import java.net.HttpURLConnection import java.net.URL +// TODO DOCUMENTATION (MAYBE WIKI) FOR THE CACHE class Loader { 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. * @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) suspend fun load(url: URL): ByteArray = diff --git a/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt b/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt index 10f6739..8d00c0a 100644 --- a/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt +++ b/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import oupson.apng.APNGDisassembler import oupson.apng.BuildConfig import oupson.apng.Loader import oupson.apng.chunks.IHDR @@ -31,13 +30,23 @@ import java.util.zip.CRC32 class ApngDecoder { 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) + + /** + * Function called when something gone wrong. + * @param error The problem. + */ fun onError(error: java.lang.Exception) } companion object { private const val TAG = "ApngDecoder" + // Paint used to clear the buffer private val clearPaint: Paint by lazy { Paint().apply { xfermode = PorterDuffXfermode( @@ -52,11 +61,17 @@ class ApngDecoder { * @param inStream Input Stream to decode. Will be close at the end. * @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. + * @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") @JvmStatic @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 bytes = ByteArray(8) inputStream.mark(8) @@ -115,11 +130,11 @@ class ApngDecoder { crC32.update(iend, 0, iend.size) it.addAll(iend.asList()) it.addAll(Utils.to4Bytes(crC32.value.toInt()).asList()) - APNGDisassembler.apng.cover = BitmapFactory.decodeByteArray( + /**APNGDisassembler.apng.cover = BitmapFactory.decodeByteArray( it.toByteArray(), 0, it.size - ) + )*/ // TODO } png = ArrayList() val fcTL = fcTL() @@ -198,7 +213,10 @@ class ApngDecoder { context.resources, if (btm.config != config) { 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) } else { btm @@ -306,7 +324,10 @@ class ApngDecoder { context.resources, if (btm.config != config) { 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) } else { btm @@ -478,10 +499,16 @@ class ApngDecoder { * @param file File to decode. * @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. + * @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") @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( context, FileInputStream(file), speed, config @@ -493,12 +520,17 @@ class ApngDecoder { * @param uri Uri to open. * @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. + * @return [AnimationDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. */ @Suppress("unused") @JvmStatic - fun decodeApng(context: Context, uri: Uri, speed: Float = 1f, config : Bitmap.Config = Bitmap.Config.ARGB_8888): Drawable { - val inputStream = context.contentResolver.openInputStream(uri) - ?: throw Exception("Failed to open InputStream, InputStream is null") + fun decodeApng( + context: Context, + uri: Uri, + speed: Float = 1f, + config: Bitmap.Config = Bitmap.Config.ARGB_8888 + ): Drawable { + val inputStream = context.contentResolver.openInputStream(uri)!! return decodeApng( context, inputStream, @@ -513,10 +545,16 @@ class ApngDecoder { * @param res Resource to decode. * @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. + * @return [AnimationDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. */ @Suppress("unused") @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( context, context.resources.openRawResource(res), @@ -530,10 +568,16 @@ class ApngDecoder { * @param url URL to decode. * @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. + * @return [AnimationDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. */ @Suppress("unused") @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) { decodeApng( context, @@ -561,7 +605,7 @@ class ApngDecoder { imageView: ImageView, speed: Float = 1f, callback: Callback? = null, - config : Bitmap.Config = Bitmap.Config.ARGB_8888 + config: Bitmap.Config = Bitmap.Config.ARGB_8888 ) { GlobalScope.launch(Dispatchers.IO) { try { @@ -606,10 +650,9 @@ class ApngDecoder { imageView: ImageView, speed: Float = 1f, callback: Callback? = null, - config : Bitmap.Config = Bitmap.Config.ARGB_8888 + config: Bitmap.Config = Bitmap.Config.ARGB_8888 ) { - val inputStream = context.contentResolver.openInputStream(uri) - ?: throw Exception("Failed to open InputStream, InputStream is null") + val inputStream = context.contentResolver.openInputStream(uri)!! GlobalScope.launch(Dispatchers.IO) { try { val drawable = @@ -652,7 +695,7 @@ class ApngDecoder { imageView: ImageView, speed: Float = 1f, callback: Callback? = null, - config : Bitmap.Config = Bitmap.Config.ARGB_8888 + config: Bitmap.Config = Bitmap.Config.ARGB_8888 ) { GlobalScope.launch(Dispatchers.IO) { try { @@ -698,7 +741,7 @@ class ApngDecoder { imageView: ImageView, speed: Float = 1f, callback: Callback? = null, - config : Bitmap.Config = Bitmap.Config.ARGB_8888 + config: Bitmap.Config = Bitmap.Config.ARGB_8888 ) { GlobalScope.launch(Dispatchers.IO) { try { @@ -747,7 +790,7 @@ class ApngDecoder { imageView: ImageView, speed: Float = 1f, callback: Callback? = null, - config : Bitmap.Config = Bitmap.Config.ARGB_8888 + config: Bitmap.Config = Bitmap.Config.ARGB_8888 ) { GlobalScope.launch(Dispatchers.IO) { try { diff --git a/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt b/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt index 4eea78f..f478b62 100644 --- a/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt +++ b/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt @@ -11,6 +11,8 @@ import java.io.OutputStream import java.util.zip.CRC32 // TODO DOCUMENTATION +// TODO BITMAP ENCODING +// TODO BUFFER AND BUFFER DEACTIVATION WHEN BITMAP CONFIG DOES NOT CONTAIN AN ALPHA CHANNEL class ApngEncoder( private val outputStream: OutputStream, private val width : Int,