Partial fix of a bug with ApngAnimator and system style change
Working on documentation
This commit is contained in:
parent
323623b5fa
commit
f52b9e4563
|
@ -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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue