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 9dc20fa..ff3a7a0 100644 --- a/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt +++ b/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt @@ -53,7 +53,7 @@ class ApngDecoder(input: InputStream, val config: Config) { } } - private val inputStream: InputStream? = input + private var inputStream: InputStream? = input private var result: Result? = null /** @@ -65,470 +65,474 @@ class ApngDecoder(input: InputStream, val config: Config) { */ suspend fun decodeApng( context: Context - ): Drawable = withContext(Dispatchers.Default) { - val inputStream = BufferedInputStream(inputStream) - val bytes = ByteArray(8) - inputStream.mark(8) - withContext(Dispatchers.IO) { - inputStream.read(bytes) - } - - if (Utils.isPng(bytes)) { - var png: ByteArrayOutputStream? = null - var cover: ByteArrayOutputStream? = null - var delay = -1f - var yOffset = -1 - var xOffset = -1 - var plte: ByteArray? = null - var tnrs: ByteArray? = null - var maxWidth = 0 - var maxHeight = 0 - var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE - var disposeOp: Utils.Companion.DisposeOp = - Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE - - var ihdrOfApng = ByteArray(0) - - var isApng = false - - val drawable = ApngDrawable().apply { - isOneShot = false - } - - var buffer: Bitmap? = null - - var byteRead: Int - val lengthChunk = ByteArray(4) - do { - val length: Int - val chunk: ByteArray - if (withContext(Dispatchers.IO) { - byteRead = inputStream.read(lengthChunk) - - - if (byteRead != -1) { - length = Utils.uIntFromBytesBigEndian(lengthChunk) - - chunk = ByteArray(length + 8) - byteRead = inputStream.read(chunk) - false - } else { - chunk = ByteArray(0) - true - } - }) { - break + ): Result = + kotlin.runCatching { + withContext(Dispatchers.Default) { + val inputStream = BufferedInputStream(inputStream) + val bytes = ByteArray(8) + inputStream.mark(8) + withContext(Dispatchers.IO) { + inputStream.read(bytes) } - val byteArray = lengthChunk.plus(chunk) - val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4) - val crc = CRC32() - crc.update(byteArray, 4, byteArray.size - 8) - if (chunkCRC == crc.value.toInt()) { - val name = byteArray.copyOfRange(4, 8) - when { - name.contentEquals(Utils.fcTL) -> { - if (png == null) { - if (config.decodeCoverFrame) { - drawable.coverFrame = cover?.let { - it.write(zeroLength) + if (Utils.isPng(bytes)) { + var png: ByteArrayOutputStream? = null + var cover: ByteArrayOutputStream? = null + var delay = -1f + var yOffset = -1 + var xOffset = -1 + var plte: ByteArray? = null + var tnrs: ByteArray? = null + var maxWidth = 0 + var maxHeight = 0 + var blendOp: Utils.Companion.BlendOp = + Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE + var disposeOp: Utils.Companion.DisposeOp = + Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE + + var ihdrOfApng = ByteArray(0) + + var isApng = false + + val drawable = ApngDrawable().apply { + isOneShot = false + } + + var buffer: Bitmap? = null + + var byteRead: Int + val lengthChunk = ByteArray(4) + do { + val length: Int + val chunk: ByteArray + if (withContext(Dispatchers.IO) { + byteRead = inputStream.read(lengthChunk) + + + if (byteRead != -1) { + length = Utils.uIntFromBytesBigEndian(lengthChunk) + + chunk = ByteArray(length + 8) + byteRead = inputStream.read(chunk) + false + } else { + chunk = ByteArray(0) + true + } + }) { + break + } + + val byteArray = lengthChunk.plus(chunk) + val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4) + val crc = CRC32() + crc.update(byteArray, 4, byteArray.size - 8) + if (chunkCRC == crc.value.toInt()) { + val name = byteArray.copyOfRange(4, 8) + when { + name.contentEquals(Utils.fcTL) -> { + if (png == null) { + if (config.decodeCoverFrame) { + drawable.coverFrame = cover?.let { + it.write(zeroLength) + // Generate crc for IEND + val crC32 = CRC32() + crC32.update(Utils.IEND, 0, Utils.IEND.size) + it.write(Utils.IEND) + it.write(Utils.uIntToByteArray(crC32.value.toInt())) + + val pngBytes = it.toByteArray() + BitmapFactory.decodeByteArray( + pngBytes, + 0, + pngBytes.size + ) + } + } + cover = null + } else { + // Add IEND body length : 0 + png.write(zeroLength) + // Add IEND // Generate crc for IEND val crC32 = CRC32() crC32.update(Utils.IEND, 0, Utils.IEND.size) - it.write(Utils.IEND) - it.write(Utils.uIntToByteArray(crC32.value.toInt())) + png.write(Utils.IEND) + png.write(Utils.uIntToByteArray(crC32.value.toInt())) - val pngBytes = it.toByteArray() - BitmapFactory.decodeByteArray( - pngBytes, - 0, - pngBytes.size - ) - } - } - cover = null - } else { - // Add IEND body length : 0 - png.write(zeroLength) - // Add IEND - // Generate crc for IEND - val crC32 = CRC32() - crC32.update(Utils.IEND, 0, Utils.IEND.size) - png.write(Utils.IEND) - png.write(Utils.uIntToByteArray(crC32.value.toInt())) - - val btm = Bitmap.createBitmap( - maxWidth, - maxHeight, - Bitmap.Config.ARGB_8888 - ) - - val pngBytes = png.toByteArray() - val decoded = BitmapFactory.decodeByteArray( - pngBytes, - 0, - pngBytes.size - ) - val canvas = Canvas(btm) - canvas.drawBitmap(buffer!!, 0f, 0f, null) - - if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) { - canvas.drawRect( - xOffset.toFloat(), - yOffset.toFloat(), - xOffset + decoded.width.toFloat(), - yOffset + decoded.height.toFloat(), - clearPaint - ) - } - - canvas.drawBitmap( - decoded, - xOffset.toFloat(), - yOffset.toFloat(), - null - ) - - drawable.addFrame( - BitmapDrawable( - context.resources, - if (btm.config != config.bitmapConfig) { - if (BuildConfig.DEBUG) - Log.v( - TAG, - "Bitmap Config : ${btm.config}, Config : $config" - ) - btm.copy(config.bitmapConfig, btm.isMutable) - } else { - btm - } - ), - (delay / config.speed).toInt() - ) - - when (disposeOp) { - Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> { - //Do nothings - } - // Add current frame to bitmap buffer - // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. - Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> { - val res = Bitmap.createBitmap( + val btm = Bitmap.createBitmap( maxWidth, maxHeight, Bitmap.Config.ARGB_8888 ) - val can = Canvas(res) - can.drawBitmap(btm, 0f, 0f, null) - can.drawRect( + + val pngBytes = png.toByteArray() + val decoded = BitmapFactory.decodeByteArray( + pngBytes, + 0, + pngBytes.size + ) + val canvas = Canvas(btm) + canvas.drawBitmap(buffer!!, 0f, 0f, null) + + if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) { + canvas.drawRect( + xOffset.toFloat(), + yOffset.toFloat(), + xOffset + decoded.width.toFloat(), + yOffset + decoded.height.toFloat(), + clearPaint + ) + } + + canvas.drawBitmap( + decoded, xOffset.toFloat(), yOffset.toFloat(), - xOffset + decoded.width.toFloat(), - yOffset + decoded.height.toFloat(), - clearPaint + null ) - buffer = res + + drawable.addFrame( + BitmapDrawable( + context.resources, + if (btm.config != config.bitmapConfig) { + if (BuildConfig.DEBUG) + Log.v( + TAG, + "Bitmap Config : ${btm.config}, Config : $config" + ) + btm.copy(config.bitmapConfig, btm.isMutable) + } else { + btm + } + ), + (delay / config.speed).toInt() + ) + + when (disposeOp) { + Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> { + //Do nothings + } + // Add current frame to bitmap buffer + // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. + Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> { + val res = Bitmap.createBitmap( + maxWidth, + maxHeight, + Bitmap.Config.ARGB_8888 + ) + val can = Canvas(res) + can.drawBitmap(btm, 0f, 0f, null) + can.drawRect( + xOffset.toFloat(), + yOffset.toFloat(), + xOffset + decoded.width.toFloat(), + yOffset + decoded.height.toFloat(), + clearPaint + ) + buffer = res + } + else -> buffer = btm + } + } - else -> buffer = btm - } - } + png = ByteArrayOutputStream(4096) - png = ByteArrayOutputStream(4096) + // Parse Frame ConTroL chunk + // Get the width of the png + val width = Utils.uIntFromBytesBigEndian( + byteArray, 12 + ) + // Get the height of the png + val height = Utils.uIntFromBytesBigEndian( + byteArray, 16 + ) - // Parse Frame ConTroL chunk - // Get the width of the png - val width = Utils.uIntFromBytesBigEndian( - byteArray, 12 - ) - // Get the height of the png - val height = Utils.uIntFromBytesBigEndian( - byteArray, 16 - ) - - /* + /* * The `delay_num` and `delay_den` parameters together specify a fraction indicating the time to display the current frame, in seconds. * If the the value of the numerator is 0 the decoder should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound. */ - // Get delay numerator - val delayNum = Utils.uShortFromBytesBigEndian( - byteArray, 28 - ).toFloat() - // Get delay denominator - var delayDen = Utils.uShortFromBytesBigEndian( - byteArray, 30 - ).toFloat() + // Get delay numerator + val delayNum = Utils.uShortFromBytesBigEndian( + byteArray, 28 + ).toFloat() + // Get delay denominator + var delayDen = Utils.uShortFromBytesBigEndian( + byteArray, 30 + ).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 (delayDen == 0f) { - delayDen = 100f - } - - delay = (delayNum / delayDen * 1000) - - // Get x and y offsets - xOffset = Utils.uIntFromBytesBigEndian( - byteArray, 20 - ) - yOffset = Utils.uIntFromBytesBigEndian( - byteArray, 24 - ) - blendOp = Utils.decodeBlendOp(byteArray[33].toInt()) - disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt()) - - if (xOffset + width > maxWidth) { - throw BadApngException("`xOffset` + `width` must be <= `IHDR` width") - } else if (yOffset + height > maxHeight) { - throw BadApngException("`yOffset` + `height` must be <= `IHDR` height") - } - - png.write(Utils.pngSignature) - png.write( - generateIhdr( - ihdrOfApng, - width, - height - ) - ) - plte?.let { - png.write(it) - } - tnrs?.let { - png.write(it) - } - - } - name.contentEquals(Utils.IEND) -> { - if (isApng && png != null) { - png.write(zeroLength) - // Add IEND - // Generate crc for IEND - val crC32 = CRC32() - crC32.update(Utils.IEND, 0, Utils.IEND.size) - png.write(Utils.IEND) - png.write(Utils.uIntToByteArray(crC32.value.toInt())) - - val btm = Bitmap.createBitmap( - maxWidth, - maxHeight, - Bitmap.Config.ARGB_8888 - ) - - val pngBytes = png.toByteArray() - val decoded = BitmapFactory.decodeByteArray( - pngBytes, - 0, - pngBytes.size - ) - val canvas = Canvas(btm) - canvas.drawBitmap(buffer!!, 0f, 0f, null) - - if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) { - canvas.drawRect( - xOffset.toFloat(), - yOffset.toFloat(), - xOffset + decoded.width.toFloat(), - yOffset + decoded.height.toFloat(), - clearPaint - ) - } - - canvas.drawBitmap( - decoded, - xOffset.toFloat(), - yOffset.toFloat(), - null - ) - drawable.addFrame( - BitmapDrawable( - context.resources, - if (btm.config != config.bitmapConfig) { - if (BuildConfig.DEBUG) - Log.v( - TAG, - "Bitmap Config : ${btm.config}, Config : $config" - ) - btm.copy(config.bitmapConfig, btm.isMutable) - } else { - btm - } - ), - (delay / config.speed).toInt() - ) - - when (disposeOp) { - Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> { - //Do nothings + // 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 (delayDen == 0f) { + delayDen = 100f } - // Add current frame to bitmap buffer - // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. - Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> { - val res = Bitmap.createBitmap( + + delay = (delayNum / delayDen * 1000) + + // Get x and y offsets + xOffset = Utils.uIntFromBytesBigEndian( + byteArray, 20 + ) + yOffset = Utils.uIntFromBytesBigEndian( + byteArray, 24 + ) + blendOp = Utils.decodeBlendOp(byteArray[33].toInt()) + disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt()) + + if (xOffset + width > maxWidth) { + throw BadApngException("`xOffset` + `width` must be <= `IHDR` width") + } else if (yOffset + height > maxHeight) { + throw BadApngException("`yOffset` + `height` must be <= `IHDR` height") + } + + png.write(Utils.pngSignature) + png.write( + generateIhdr( + ihdrOfApng, + width, + height + ) + ) + plte?.let { + png.write(it) + } + tnrs?.let { + png.write(it) + } + + } + name.contentEquals(Utils.IEND) -> { + if (isApng && png != null) { + png.write(zeroLength) + // Add IEND + // Generate crc for IEND + val crC32 = CRC32() + crC32.update(Utils.IEND, 0, Utils.IEND.size) + png.write(Utils.IEND) + png.write(Utils.uIntToByteArray(crC32.value.toInt())) + + val btm = Bitmap.createBitmap( maxWidth, maxHeight, Bitmap.Config.ARGB_8888 ) - val can = Canvas(res) - can.drawBitmap(btm, 0f, 0f, null) - can.drawRect( - xOffset.toFloat(), - yOffset.toFloat(), - xOffset + decoded.width.toFloat(), - yOffset + decoded.height.toFloat(), - clearPaint - ) - buffer = res - } - else -> buffer = btm - } - } else { - cover?.let { - it.write(zeroLength) - // Add IEND - // Generate crc for IEND - val crC32 = CRC32() - crC32.update(Utils.IEND, 0, Utils.IEND.size) - it.write(Utils.IEND) - it.write(Utils.uIntToByteArray(crC32.value.toInt())) - withContext(Dispatchers.IO) { - inputStream.close() - } - val pngBytes = it.toByteArray() - return@withContext BitmapDrawable( - context.resources, - BitmapFactory.decodeByteArray( + val pngBytes = png.toByteArray() + val decoded = BitmapFactory.decodeByteArray( pngBytes, 0, pngBytes.size ) - ) - } - } - } - name.contentEquals(Utils.IDAT) -> { - val w = if (png == null) { - if (isApng && !config.decodeCoverFrame) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignoring cover frame") - continue - } - if (cover == null) { - cover = ByteArrayOutputStream() - cover.write(Utils.pngSignature) - cover.write( - generateIhdr( - ihdrOfApng, - maxWidth, - maxHeight + val canvas = Canvas(btm) + canvas.drawBitmap(buffer!!, 0f, 0f, null) + + if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) { + canvas.drawRect( + xOffset.toFloat(), + yOffset.toFloat(), + xOffset + decoded.width.toFloat(), + yOffset + decoded.height.toFloat(), + clearPaint + ) + } + + canvas.drawBitmap( + decoded, + xOffset.toFloat(), + yOffset.toFloat(), + null ) + drawable.addFrame( + BitmapDrawable( + context.resources, + if (btm.config != config.bitmapConfig) { + if (BuildConfig.DEBUG) + Log.v( + TAG, + "Bitmap Config : ${btm.config}, Config : $config" + ) + btm.copy(config.bitmapConfig, btm.isMutable) + } else { + btm + } + ), + (delay / config.speed).toInt() + ) + + when (disposeOp) { + Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> { + //Do nothings + } + // Add current frame to bitmap buffer + // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame. + Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> { + val res = Bitmap.createBitmap( + maxWidth, + maxHeight, + Bitmap.Config.ARGB_8888 + ) + val can = Canvas(res) + can.drawBitmap(btm, 0f, 0f, null) + can.drawRect( + xOffset.toFloat(), + yOffset.toFloat(), + xOffset + decoded.width.toFloat(), + yOffset + decoded.height.toFloat(), + clearPaint + ) + buffer = res + } + else -> buffer = btm + } + } else { + cover?.let { + it.write(zeroLength) + // Add IEND + // Generate crc for IEND + val crC32 = CRC32() + crC32.update(Utils.IEND, 0, Utils.IEND.size) + it.write(Utils.IEND) + it.write(Utils.uIntToByteArray(crC32.value.toInt())) + withContext(Dispatchers.IO) { + inputStream.close() + } + + val pngBytes = it.toByteArray() + return@withContext BitmapDrawable( + context.resources, + BitmapFactory.decodeByteArray( + pngBytes, + 0, + pngBytes.size + ) + ) + } + } + } + name.contentEquals(Utils.IDAT) -> { + val w = if (png == null) { + if (isApng && !config.decodeCoverFrame) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Ignoring cover frame") + continue + } + if (cover == null) { + cover = ByteArrayOutputStream() + cover.write(Utils.pngSignature) + cover.write( + generateIhdr( + ihdrOfApng, + maxWidth, + maxHeight + ) + ) + } + cover + } else { + png + } + + // Find the chunk length + val bodySize = + Utils.uIntFromBytesBigEndian( + byteArray, 0 + ) + w.write(byteArray.copyOfRange(0, 4)) + + val body = ByteArray(4 + bodySize) + + System.arraycopy(Utils.IDAT, 0, body, 0, 4) + + // Get image bytes + System.arraycopy(byteArray, 8, body, 4, bodySize) + + val crC32 = CRC32() + crC32.update(body, 0, body.size) + w.write(body) + w.write(Utils.uIntToByteArray(crC32.value.toInt())) + } + name.contentEquals(Utils.fdAT) -> { + // Find the chunk length + val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0) + png?.write(Utils.uIntToByteArray(bodySize - 4)) + + val body = ByteArray(bodySize) + System.arraycopy(Utils.IDAT, 0, body, 0, 4) + + // Get image bytes + System.arraycopy(byteArray, 12, body, 4, bodySize - 4) + + val crC32 = CRC32() + crC32.update(body, 0, body.size) + png?.write(body) + png?.write(Utils.uIntToByteArray(crC32.value.toInt())) + } + name.contentEquals(Utils.plte) -> { + plte = byteArray + } + name.contentEquals(Utils.tnrs) -> { + tnrs = byteArray + } + name.contentEquals(Utils.IHDR) -> { + // Get length of the body of the chunk + val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0) + // Get the width of the png + maxWidth = Utils.uIntFromBytesBigEndian(byteArray, 8) + // Get the height of the png + maxHeight = Utils.uIntFromBytesBigEndian(byteArray, 12) + ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4) + + buffer = Bitmap.createBitmap( + maxWidth, + maxHeight, + Bitmap.Config.ARGB_8888 ) } - cover - } else { - png + name.contentEquals(Utils.acTL) -> { // TODO GET NBR REPETITIONS + isApng = true + } } - - // Find the chunk length - val bodySize = - Utils.uIntFromBytesBigEndian( - byteArray, 0 - ) - w.write(byteArray.copyOfRange(0, 4)) - - val body = ByteArray(4 + bodySize) - - System.arraycopy(Utils.IDAT, 0, body, 0, 4) - - // Get image bytes - System.arraycopy(byteArray, 8, body, 4, bodySize) - - val crC32 = CRC32() - crC32.update(body, 0, body.size) - w.write(body) - w.write(Utils.uIntToByteArray(crC32.value.toInt())) - } - name.contentEquals(Utils.fdAT) -> { - // Find the chunk length - val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0) - png?.write(Utils.uIntToByteArray(bodySize - 4)) - - val body = ByteArray(bodySize) - System.arraycopy(Utils.IDAT, 0, body, 0, 4) - - // Get image bytes - System.arraycopy(byteArray, 12, body, 4, bodySize - 4) - - val crC32 = CRC32() - crC32.update(body, 0, body.size) - png?.write(body) - png?.write(Utils.uIntToByteArray(crC32.value.toInt())) - } - name.contentEquals(Utils.plte) -> { - plte = byteArray - } - name.contentEquals(Utils.tnrs) -> { - tnrs = byteArray - } - name.contentEquals(Utils.IHDR) -> { - // Get length of the body of the chunk - val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0) - // Get the width of the png - maxWidth = Utils.uIntFromBytesBigEndian(byteArray, 8) - // Get the height of the png - maxHeight = Utils.uIntFromBytesBigEndian(byteArray, 12) - ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4) - - buffer = Bitmap.createBitmap( - maxWidth, - maxHeight, - Bitmap.Config.ARGB_8888 - ) - } - name.contentEquals(Utils.acTL) -> { // TODO GET NBR REPETITIONS - isApng = true - } + } else throw BadCRCException() + } while (byteRead != -1 && isActive) + withContext(Dispatchers.IO) { + inputStream.close() } - } else throw BadCRCException() - } while (byteRead != -1 && isActive) - withContext(Dispatchers.IO) { - inputStream.close() - } - return@withContext drawable - } else { - if (BuildConfig.DEBUG) - Log.i(TAG, "Decoding non APNG stream") - inputStream.reset() + drawable + } else { + if (BuildConfig.DEBUG) + Log.i(TAG, "Decoding non APNG stream") + inputStream.reset() - return@withContext if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - val bytesRead: ByteArray - withContext(Dispatchers.IO) { - bytesRead = inputStream.readBytes() - inputStream.close() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + val bytesRead: ByteArray + withContext(Dispatchers.IO) { + bytesRead = inputStream.readBytes() + inputStream.close() + } + val buf = ByteBuffer.wrap(bytesRead) + val source = ImageDecoder.createSource(buf) + withContext(Dispatchers.IO) { + ImageDecoder.decodeDrawable(source) + } + } else { + val drawable = Drawable.createFromStream( + inputStream, + null + ) + withContext(Dispatchers.IO) { + inputStream.close() + } + drawable!! + } } - val buf = ByteBuffer.wrap(bytesRead) - val source = ImageDecoder.createSource(buf) - withContext(Dispatchers.IO) { - ImageDecoder.decodeDrawable(source) - } - } else { - val drawable = Drawable.createFromStream( - inputStream, - null - ) - withContext(Dispatchers.IO) { - inputStream.close() - } - drawable } } - } suspend fun getDecoded(context: Context): Result { if (result == null) { - result = kotlin.runCatching { + result = decodeApng(context) - } + kotlin.runCatching { withContext(Dispatchers.IO) { @@ -537,6 +541,8 @@ class ApngDecoder(input: InputStream, val config: Config) { }.onFailure { return Result.failure(it) } + + inputStream = null } return result ?: Result.failure(NullPointerException("result is null")) @@ -639,17 +645,19 @@ class ApngDecoder(input: InputStream, val config: Config) { * @param config Decoder configuration * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. */ - @Suppress("unused", "BlockingMethodInNonBlockingContext") + @Suppress("unused") @JvmStatic suspend fun constructFromUrl( url: URL, config: Config = Config() - ) = - withContext(Dispatchers.IO) { - ApngDecoder( - ByteArrayInputStream(Loader.load(url)), - config - ) + ): Result = + kotlin.runCatching { + withContext(Dispatchers.IO) { + ApngDecoder( + ByteArrayInputStream(Loader.load(url)), + config + ) + } } } } \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt b/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt index a670415..20d46eb 100644 --- a/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt +++ b/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt @@ -153,7 +153,9 @@ class ApngLoader(parent: Job? = null) { imageView: ImageView, config: ApngDecoder.Config = ApngDecoder.Config() ): Result { - val result = ApngDecoder.constructFromUrl(url, config).getDecoded(context) + val result = + ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) } + .getDecoded(context) if (result.isSuccess) { withContext(Dispatchers.Main) { val drawable = result.getOrNull() @@ -204,10 +206,10 @@ class ApngLoader(parent: Job? = null) { withContext(Dispatchers.IO) { context.assets.open(string.replace("file:///android_asset/", "")) } - }.onFailure { + }.getOrElse { return Result.failure(it) } - val result = ApngDecoder(inputStream.getOrThrow(), config).getDecoded(context) + val result = ApngDecoder(inputStream, config).getDecoded(context) if (result.isSuccess) { withContext(Dispatchers.Main) { val drawable = result.getOrNull() @@ -246,11 +248,9 @@ class ApngLoader(parent: Job? = null) { coroutineScope.launch(Dispatchers.Default) { val drawable = decodeApngInto(context, file, imageView, config) withContext(Dispatchers.Main) { - if (drawable.isSuccess) { - callback?.onSuccess(drawable.getOrNull()!!) - } else { - callback?.onError(drawable.exceptionOrNull()!!) - } + drawable + .onSuccess { callback?.onSuccess(it) } + .onFailure { callback?.onError(it) } } } @@ -274,11 +274,9 @@ class ApngLoader(parent: Job? = null) { ) = coroutineScope.launch(Dispatchers.Default) { val drawable = decodeApngInto(context, uri, imageView, config) withContext(Dispatchers.Main) { - if (drawable.isSuccess) { - callback?.onSuccess(drawable.getOrNull()!!) - } else { - callback?.onError(drawable.exceptionOrNull()!!) - } + drawable + .onSuccess { callback?.onSuccess(it) } + .onFailure { callback?.onError(it) } } } @@ -300,11 +298,9 @@ class ApngLoader(parent: Job? = null) { ) = coroutineScope.launch(Dispatchers.Default) { val drawable = decodeApngInto(context, res, imageView, config) withContext(Dispatchers.Main) { - if (drawable.isSuccess) { - callback?.onSuccess(drawable.getOrNull()!!) - } else { - callback?.onError(drawable.exceptionOrNull()!!) - } + drawable + .onSuccess { callback?.onSuccess(it) } + .onFailure { callback?.onError(it) } } } @@ -327,14 +323,13 @@ class ApngLoader(parent: Job? = null) { ) = coroutineScope.launch(Dispatchers.Default) { val drawable = decodeApngInto(context, url, imageView, config) withContext(Dispatchers.Main) { - if (drawable.isSuccess) { - callback?.onSuccess(drawable.getOrNull()!!) - } else { - callback?.onError(drawable.exceptionOrNull()!!) - } + drawable + .onSuccess { callback?.onSuccess(it) } + .onFailure { callback?.onError(it) } } } + /** * Load Apng into an imageView, asynchronously. * @param context Context needed for decoding the image and creating the animation drawable.