Work on result

This commit is contained in:
Oupson 2021-06-25 13:50:27 +02:00
parent 4ef644632a
commit 31c7529d45
2 changed files with 454 additions and 451 deletions

View File

@ -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<Drawable>? = null private var result: Result<Drawable>? = null
/** /**
@ -65,470 +65,474 @@ class ApngDecoder(input: InputStream, val config: Config) {
*/ */
suspend fun decodeApng( suspend fun decodeApng(
context: Context context: Context
): Drawable = withContext(Dispatchers.Default) { ): Result<Drawable> =
val inputStream = BufferedInputStream(inputStream) kotlin.runCatching {
val bytes = ByteArray(8) withContext(Dispatchers.Default) {
inputStream.mark(8) val inputStream = BufferedInputStream(inputStream)
withContext(Dispatchers.IO) { val bytes = ByteArray(8)
inputStream.read(bytes) 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
} }
val byteArray = lengthChunk.plus(chunk) if (Utils.isPng(bytes)) {
val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4) var png: ByteArrayOutputStream? = null
val crc = CRC32() var cover: ByteArrayOutputStream? = null
crc.update(byteArray, 4, byteArray.size - 8) var delay = -1f
if (chunkCRC == crc.value.toInt()) { var yOffset = -1
val name = byteArray.copyOfRange(4, 8) var xOffset = -1
when { var plte: ByteArray? = null
name.contentEquals(Utils.fcTL) -> { var tnrs: ByteArray? = null
if (png == null) { var maxWidth = 0
if (config.decodeCoverFrame) { var maxHeight = 0
drawable.coverFrame = cover?.let { var blendOp: Utils.Companion.BlendOp =
it.write(zeroLength) 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 // 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) png.write(Utils.IEND)
it.write(Utils.uIntToByteArray(crC32.value.toInt())) png.write(Utils.uIntToByteArray(crC32.value.toInt()))
val pngBytes = it.toByteArray() val btm = Bitmap.createBitmap(
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(
maxWidth, maxWidth,
maxHeight, maxHeight,
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
) )
val can = Canvas(res)
can.drawBitmap(btm, 0f, 0f, null) val pngBytes = png.toByteArray()
can.drawRect( 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(), xOffset.toFloat(),
yOffset.toFloat(), yOffset.toFloat(),
xOffset + decoded.width.toFloat(), null
yOffset + decoded.height.toFloat(),
clearPaint
) )
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. * 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. * 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 // Get delay numerator
val delayNum = Utils.uShortFromBytesBigEndian( val delayNum = Utils.uShortFromBytesBigEndian(
byteArray, 28 byteArray, 28
).toFloat() ).toFloat()
// Get delay denominator // Get delay denominator
var delayDen = Utils.uShortFromBytesBigEndian( var delayDen = Utils.uShortFromBytesBigEndian(
byteArray, 30 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).
if (delayDen == 0f) { if (delayDen == 0f) {
delayDen = 100f 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
} }
// 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. delay = (delayNum / delayDen * 1000)
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
val res = Bitmap.createBitmap( // 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, maxWidth,
maxHeight, maxHeight,
Bitmap.Config.ARGB_8888 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() val pngBytes = png.toByteArray()
return@withContext BitmapDrawable( val decoded = BitmapFactory.decodeByteArray(
context.resources,
BitmapFactory.decodeByteArray(
pngBytes, pngBytes,
0, 0,
pngBytes.size pngBytes.size
) )
) val canvas = Canvas(btm)
} canvas.drawBitmap(buffer!!, 0f, 0f, null)
}
} if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
name.contentEquals(Utils.IDAT) -> { canvas.drawRect(
val w = if (png == null) { xOffset.toFloat(),
if (isApng && !config.decodeCoverFrame) { yOffset.toFloat(),
if (BuildConfig.DEBUG) xOffset + decoded.width.toFloat(),
Log.d(TAG, "Ignoring cover frame") yOffset + decoded.height.toFloat(),
continue clearPaint
} )
if (cover == null) { }
cover = ByteArrayOutputStream()
cover.write(Utils.pngSignature) canvas.drawBitmap(
cover.write( decoded,
generateIhdr( xOffset.toFloat(),
ihdrOfApng, yOffset.toFloat(),
maxWidth, null
maxHeight
) )
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 name.contentEquals(Utils.acTL) -> { // TODO GET NBR REPETITIONS
} else { isApng = true
png }
} }
} else throw BadCRCException()
// Find the chunk length } while (byteRead != -1 && isActive)
val bodySize = withContext(Dispatchers.IO) {
Utils.uIntFromBytesBigEndian( inputStream.close()
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() drawable
} while (byteRead != -1 && isActive) } else {
withContext(Dispatchers.IO) { if (BuildConfig.DEBUG)
inputStream.close() Log.i(TAG, "Decoding non APNG stream")
} inputStream.reset()
return@withContext 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val bytesRead: ByteArray val bytesRead: ByteArray
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
bytesRead = inputStream.readBytes() bytesRead = inputStream.readBytes()
inputStream.close() 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<Drawable> { suspend fun getDecoded(context: Context): Result<Drawable> {
if (result == null) { if (result == null) {
result = kotlin.runCatching { result =
decodeApng(context) decodeApng(context)
}
kotlin.runCatching { kotlin.runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -537,6 +541,8 @@ class ApngDecoder(input: InputStream, val config: Config) {
}.onFailure { }.onFailure {
return Result.failure(it) return Result.failure(it)
} }
inputStream = null
} }
return result ?: Result.failure(NullPointerException("result is null")) return result ?: Result.failure(NullPointerException("result is null"))
@ -639,17 +645,19 @@ class ApngDecoder(input: InputStream, val config: Config) {
* @param config Decoder configuration * @param config Decoder configuration
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. * @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/ */
@Suppress("unused", "BlockingMethodInNonBlockingContext") @Suppress("unused")
@JvmStatic @JvmStatic
suspend fun constructFromUrl( suspend fun constructFromUrl(
url: URL, url: URL,
config: Config = Config() config: Config = Config()
) = ): Result<ApngDecoder> =
withContext(Dispatchers.IO) { kotlin.runCatching {
ApngDecoder( withContext(Dispatchers.IO) {
ByteArrayInputStream(Loader.load(url)), ApngDecoder(
config ByteArrayInputStream(Loader.load(url)),
) config
)
}
} }
} }
} }

View File

@ -153,7 +153,9 @@ class ApngLoader(parent: Job? = null) {
imageView: ImageView, imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config() config: ApngDecoder.Config = ApngDecoder.Config()
): Result<Drawable> { ): Result<Drawable> {
val result = ApngDecoder.constructFromUrl(url, config).getDecoded(context) val result =
ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) }
.getDecoded(context)
if (result.isSuccess) { if (result.isSuccess) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val drawable = result.getOrNull() val drawable = result.getOrNull()
@ -204,10 +206,10 @@ class ApngLoader(parent: Job? = null) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
context.assets.open(string.replace("file:///android_asset/", "")) context.assets.open(string.replace("file:///android_asset/", ""))
} }
}.onFailure { }.getOrElse {
return Result.failure(it) return Result.failure(it)
} }
val result = ApngDecoder(inputStream.getOrThrow(), config).getDecoded(context) val result = ApngDecoder(inputStream, config).getDecoded(context)
if (result.isSuccess) { if (result.isSuccess) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val drawable = result.getOrNull() val drawable = result.getOrNull()
@ -246,11 +248,9 @@ class ApngLoader(parent: Job? = null) {
coroutineScope.launch(Dispatchers.Default) { coroutineScope.launch(Dispatchers.Default) {
val drawable = decodeApngInto(context, file, imageView, config) val drawable = decodeApngInto(context, file, imageView, config)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (drawable.isSuccess) { drawable
callback?.onSuccess(drawable.getOrNull()!!) .onSuccess { callback?.onSuccess(it) }
} else { .onFailure { callback?.onError(it) }
callback?.onError(drawable.exceptionOrNull()!!)
}
} }
} }
@ -274,11 +274,9 @@ class ApngLoader(parent: Job? = null) {
) = coroutineScope.launch(Dispatchers.Default) { ) = coroutineScope.launch(Dispatchers.Default) {
val drawable = decodeApngInto(context, uri, imageView, config) val drawable = decodeApngInto(context, uri, imageView, config)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (drawable.isSuccess) { drawable
callback?.onSuccess(drawable.getOrNull()!!) .onSuccess { callback?.onSuccess(it) }
} else { .onFailure { callback?.onError(it) }
callback?.onError(drawable.exceptionOrNull()!!)
}
} }
} }
@ -300,11 +298,9 @@ class ApngLoader(parent: Job? = null) {
) = coroutineScope.launch(Dispatchers.Default) { ) = coroutineScope.launch(Dispatchers.Default) {
val drawable = decodeApngInto(context, res, imageView, config) val drawable = decodeApngInto(context, res, imageView, config)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (drawable.isSuccess) { drawable
callback?.onSuccess(drawable.getOrNull()!!) .onSuccess { callback?.onSuccess(it) }
} else { .onFailure { callback?.onError(it) }
callback?.onError(drawable.exceptionOrNull()!!)
}
} }
} }
@ -327,14 +323,13 @@ class ApngLoader(parent: Job? = null) {
) = coroutineScope.launch(Dispatchers.Default) { ) = coroutineScope.launch(Dispatchers.Default) {
val drawable = decodeApngInto(context, url, imageView, config) val drawable = decodeApngInto(context, url, imageView, config)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (drawable.isSuccess) { drawable
callback?.onSuccess(drawable.getOrNull()!!) .onSuccess { callback?.onSuccess(it) }
} else { .onFailure { callback?.onError(it) }
callback?.onError(drawable.exceptionOrNull()!!)
}
} }
} }
/** /**
* Load Apng into an imageView, asynchronously. * Load Apng into an imageView, asynchronously.
* @param context Context needed for decoding the image and creating the animation drawable. * @param context Context needed for decoding the image and creating the animation drawable.