Working on better ApngDecoder
This commit is contained in:
parent
758d729e94
commit
907cb0f106
|
@ -8,3 +8,4 @@
|
|||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
/.idea
|
Binary file not shown.
|
@ -32,7 +32,6 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
implementation "org.jetbrains.anko:anko:$anko_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
|
||||
}
|
||||
repositories {
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
package oupson.apng
|
||||
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import oupson.apng.chunks.IHDR
|
||||
import oupson.apng.chunks.fcTL
|
||||
import oupson.apng.exceptions.BadApng
|
||||
import oupson.apng.exceptions.BadCRC
|
||||
import oupson.apng.utils.Utils
|
||||
import oupson.apng.utils.Utils.Companion.isPng
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.util.zip.CRC32
|
||||
|
||||
class ExperimentalApngDecoder {
|
||||
companion object {
|
||||
private val clearPaint : Paint by lazy {
|
||||
Paint().apply {
|
||||
xfermode = PorterDuffXfermode(
|
||||
PorterDuff.Mode.CLEAR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeApng(inStream: InputStream, speed : Float = 1f) : Drawable {
|
||||
val inputStream = BufferedInputStream(inStream)
|
||||
val bytes = ByteArray(8)
|
||||
inputStream.mark(0)
|
||||
inputStream.read(bytes)
|
||||
if (isPng(bytes)) {
|
||||
var png: ArrayList<Byte>? = null
|
||||
var cover: ArrayList<Byte>? = 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
|
||||
val ihdr = IHDR()
|
||||
var isApng = false
|
||||
|
||||
var drawable = CustomAnimationDrawable()
|
||||
|
||||
var buffer : Bitmap? = null
|
||||
|
||||
// TODO REMOVE
|
||||
|
||||
var byteRead: Int
|
||||
val lengthChunk = ByteArray(4)
|
||||
do {
|
||||
byteRead = inputStream.read(lengthChunk)
|
||||
|
||||
if (byteRead == -1)
|
||||
break
|
||||
val length = Utils.parseLength(lengthChunk)
|
||||
|
||||
val chunk = ByteArray(length + 8)
|
||||
byteRead = inputStream.read(chunk)
|
||||
|
||||
val byteArray = lengthChunk.plus(chunk)
|
||||
val i = 4
|
||||
val chunkCRC =
|
||||
Utils.parseLength(byteArray.copyOfRange(byteArray.size - 4, byteArray.size))
|
||||
val crc = CRC32()
|
||||
crc.update(byteArray.copyOfRange(i, byteArray.size - 4))
|
||||
if (chunkCRC == crc.value.toInt()) {
|
||||
when (byteArray.copyOfRange(i, i + 4).contentToString()) {
|
||||
Utils.fcTL -> {
|
||||
if (png == null) {
|
||||
cover?.let {
|
||||
it.addAll(Utils.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(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
APNGDisassembler.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) {
|
||||
throw BadApng("`yOffset` + `height` must be <= `IHDR` height")
|
||||
} else if (yOffset + height > maxHeight) {
|
||||
throw BadApng("`yOffset` + `height` must be <= `IHDR` height")
|
||||
}
|
||||
|
||||
png.addAll(Utils.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(Utils.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(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
|
||||
val btm = Bitmap.createBitmap(maxWidth, maxHeight, Bitmap.Config.ARGB_8888)
|
||||
val decoded = BitmapFactory.decodeByteArray(png.toByteArray(), 0, png.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(btm), (delay / 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
|
||||
}
|
||||
|
||||
|
||||
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(Utils.pngSignature.asList())
|
||||
png.addAll(generateIhdr(ihdr, width, height).asList())
|
||||
plte?.let {
|
||||
png.addAll(it.asList())
|
||||
}
|
||||
tnrs?.let {
|
||||
png.addAll(it.asList())
|
||||
}
|
||||
}
|
||||
}
|
||||
Utils.IEND -> {
|
||||
if (isApng && png != null) {
|
||||
png.addAll(Utils.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(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
|
||||
|
||||
val btm = Bitmap.createBitmap(maxWidth, maxHeight, Bitmap.Config.ARGB_8888)
|
||||
val decoded = BitmapFactory.decodeByteArray(png.toByteArray(), 0, png.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(btm), (delay / 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 {
|
||||
// TODO Check before the end
|
||||
cover?.let {
|
||||
it.addAll(Utils.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(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
return BitmapDrawable(BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
Utils.IDAT -> {
|
||||
if (png == null) {
|
||||
if (cover == null) {
|
||||
cover = ArrayList()
|
||||
cover.addAll(Utils.pngSignature.asList())
|
||||
cover.addAll(generateIhdr(
|
||||
ihdr,
|
||||
maxWidth,
|
||||
maxHeight
|
||||
).asList())
|
||||
}
|
||||
// Find the chunk length
|
||||
val bodySize = Utils.parseLength(byteArray.copyOfRange(i - 4, i))
|
||||
cover.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)
|
||||
cover.addAll(body)
|
||||
cover.addAll(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
} else {
|
||||
// Find the chunk length
|
||||
val bodySize = Utils.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(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
}
|
||||
}
|
||||
Utils.fdAT -> {
|
||||
// Find the chunk length
|
||||
val bodySize = Utils.parseLength(byteArray.copyOfRange(i - 4, i))
|
||||
png?.addAll(Utils.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(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
}
|
||||
Utils.plte -> {
|
||||
plte = byteArray
|
||||
}
|
||||
Utils.tnrs -> {
|
||||
tnrs = byteArray
|
||||
}
|
||||
Utils.IHDR -> {
|
||||
ihdr.parse(byteArray)
|
||||
maxWidth = ihdr.pngWidth
|
||||
maxHeight =ihdr.pngHeight
|
||||
buffer = Bitmap.createBitmap(maxWidth, maxHeight, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
Utils.acTL -> {
|
||||
isApng = true
|
||||
}
|
||||
}
|
||||
} else throw BadCRC()
|
||||
} while (byteRead != -1)
|
||||
return drawable
|
||||
} else {
|
||||
inputStream.reset()
|
||||
return Drawable.createFromStream(
|
||||
inputStream,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(Utils.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(Utils.to4Bytes(width).asList())
|
||||
ihdrBody.addAll(Utils.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(Utils.to4Bytes(crC32.value.toInt()).asList())
|
||||
return ihdr.toByteArray()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ buildscript {
|
|||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.1'
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
Loading…
Reference in New Issue