Documentation

This commit is contained in:
Oupson 2020-09-20 01:06:43 +02:00
parent a20d9ee7d9
commit 6b68edb273
3 changed files with 95 additions and 73 deletions

View File

@ -2,6 +2,7 @@ package oupson.apng.encoder
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import oupson.apng.utils.Utils
import java.io.ByteArrayOutputStream
import java.io.IOException
@ -17,6 +18,7 @@ import kotlin.math.min
// TODO JAVA OVERLOADS
// TODO ADD SUPPORT FOR FIRST FRAME NOT IN ANIM
// TODO OPTIMISE APNG
// TODO OPTIONS SUCH AS NUMBER OF REPETITIONS
class ExperimentalApngEncoder(
private val outputStream: OutputStream,
private val width: Int,
@ -27,21 +29,29 @@ class ExperimentalApngEncoder(
compressionLevel: Int = 0
) {
companion object {
private const val TAG = "ExperimentalApngEncoder"
/** Constants for filter (NONE) */
private const val FILTER_NONE = 0
const val FILTER_NONE = 0
/** Constants for filter (SUB) */
private const val FILTER_SUB = 1
const val FILTER_SUB = 1
/** Constants for filter (UP) */
private const val FILTER_UP = 2
const val FILTER_UP = 2
/** Constants for filter (LAST) */
private const val FILTER_LAST = 2
const val FILTER_LAST = 2
}
private var frameIndex = 0
private var seq = 0
/** Current Frame. **/
private var currentFrame = 0
/**
* Current sequence of the animation.
* @see [https://developer.mozilla.org/en-US/docs/Mozilla/Tech/APNG#Chunk_sequence_numbers](Mozilla documentation.)
**/
private var currentSeq = 0
/** CRC. */
private var crc = CRC32()
@ -103,16 +113,18 @@ class ExperimentalApngEncoder(
blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE,
disposeOp: Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
) {
if (frameIndex == 0) {
if (currentFrame == 0) {
if (btm.width != width)
throw Exception("Width of first frame must be equal to width of APNG. (${btm.width} != $width)")
if (btm.height != height)
throw Exception("Height of first frame must be equal to height of APNG. (${btm.height} != $height)")
}
// TODO CHECK IF btm IS BIGGER THANT THE APNG
writeFCTL(btm, delay, disposeOp, blendOp, xOffsets, yOffsets)
writeImageData(btm)
frameIndex++
currentSeq++
}
fun writeEnd() {
@ -128,42 +140,31 @@ class ExperimentalApngEncoder(
}
/**
* Generate the IHDR chunks.
* @return [ByteArray] The byteArray generated
* Write the header into the outputStream.
*/
private fun writeHeader() {
writeInt4(13)
val arrayList = arrayListOf<Byte>()
arrayList.addAll(Utils.IHDR.asList())
arrayList.addAll(Utils.to4Bytes(width))
arrayList.addAll(Utils.to4Bytes(height))
arrayList.add(8) // bit depth
arrayList.add(if (encodeAlpha) 6 else 2) // direct model
arrayList.add(0) // compression method
arrayList.add(0) // filter method
arrayList.add(0) // no interlace
val header = arrayListOf<Byte>()
header.addAll(Utils.IHDR.asList())
header.addAll(Utils.to4Bytes(width))
header.addAll(Utils.to4Bytes(height))
header.add(8) // bit depth
header.add(if (encodeAlpha) 6 else 2) // direct model
header.add(0) // compression method
header.add(0) // filter method
header.add(0) // no interlace
outputStream.write(
arrayList.toByteArray()
header.toByteArray()
)
crc.reset()
crc.update(arrayList.toByteArray())
crc.update(header.toByteArray())
crcValue = crc.value
writeInt4(crcValue.toInt())
}
/**
* Write a two-byte integer into the outputStream.
*
*/
@Suppress("unused")
private fun writeInt2(n: Int) {
val temp = byteArrayOf((n shr 8 and 0xff).toByte(), (n and 0xff).toByte())
outputStream.write(temp)
}
/**
* Write a four-byte integer into the outputStream.
*
* @param n The four-byte integer to write.
*/
private fun writeInt4(n: Int) {
val temp = byteArrayOf(
@ -177,6 +178,7 @@ class ExperimentalApngEncoder(
/**
* Write the animation control chunk into the outputStream.
* @param num The number of frame the animation contain.
*/
private fun writeACTL(num: Int) {
val actl = ArrayList<Byte>()
@ -220,7 +222,7 @@ class ExperimentalApngEncoder(
fcTL.addAll(Utils.fcTL.asList())
// Add the frame number
fcTL.addAll(Utils.to4Bytes(seq++))
fcTL.addAll(Utils.to4Bytes(currentSeq++))
// Add width and height
fcTL.addAll(Utils.to4Bytes(btm.width))
@ -241,13 +243,15 @@ class ExperimentalApngEncoder(
// Create CRC
crc.reset()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
// Write all
outputStream.write(fcTL.toByteArray())
outputStream.write(Utils.to4BytesArray(crc.value.toInt()))
}
/**
* Write the image data into the pngBytes array.
* This will write one or more PNG "IDAT" chunks. In order
* Write the image data into the ouputStrea.
* This will write one or more PNG "IDAT"/"fdAT" chunks. In order
* to conserve memory, this method grabs as many rows as will
* fit into 32K bytes, or the whole image; whichever is less.
*
@ -337,19 +341,20 @@ class ExperimentalApngEncoder(
nCompressed = compressedLines.size
crc.reset()
writeInt4(nCompressed + if (frameIndex == 0) 0 else 4)
//bytePos = writeBytes(idat, bytePos)
//TODO
if (frameIndex == 0) {
// Add 4 bytes to the length, for the sequence number
writeInt4(nCompressed + if (currentFrame == 0) 0 else 4)
if (currentFrame == 0) {
outputStream.write(Utils.IDAT)
crc.update(Utils.IDAT)
} else {
val fdat = ArrayList<Byte>().also { fdat ->
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).asList())
// Add fdat
fdat.addAll(Utils.to4Bytes(seq++).asList())
// Add chunk body
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).asList())
// Add the sequence number
fdat.addAll(Utils.to4Bytes(currentSeq++).asList())
}.toByteArray()
outputStream.write(fdat)
crc.update(fdat)
}
@ -363,7 +368,7 @@ class ExperimentalApngEncoder(
scrunch.end()
return true
} catch (e: IOException) {
System.err.println(e.toString())
Log.e(TAG,"Error while writing IDAT/fdAT chunks", e)
return false
}
}

View File

@ -2,4 +2,4 @@ package oupson.apng.utils
import android.widget.ImageView
class ApngAnimatorOptions(val scaleType : ImageView.ScaleType? = ImageView.ScaleType.FIT_CENTER)
class ApngAnimatorOptions(val scaleType: ImageView.ScaleType? = ImageView.ScaleType.FIT_CENTER)

View File

@ -19,7 +19,7 @@ class Utils {
* @param byteArray APNG
* @return True if is an APNG
*/
fun isApng(byteArray: ByteArray) : Boolean {
fun isApng(byteArray: ByteArray): Boolean {
if (!isPng(byteArray)) return false
try {
val acTL = byteArrayOf(0x61, 0x63, 0x54, 0x4c)
@ -29,12 +29,12 @@ class Utils {
// if byteArray contain acTL
if (it.contentEquals(acTL)) {
return true
} else if (it.contentEquals(IDAT)){
} else if (it.contentEquals(IDAT)) {
return false
}
}
return false
} catch (e : Exception) {
} catch (e: Exception) {
return false
}
}
@ -45,17 +45,23 @@ class Utils {
val pngSignature: ByteArray by lazy {
byteArrayOf(
0x89.toByte(),
0x50.toByte(),
0x4E.toByte(),
0x47.toByte(),
0x0D.toByte(),
0x0A.toByte(),
0x1A.toByte(),
0x0A.toByte()
0x50,
0x4E,
0x47,
0x0D,
0x0A,
0x1A,
0x0A
)
}
// TODO DOC DISPOSE OP AND BLEND OP
/**
* DisposeOp specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
* Values :
* - [APNG_DISPOSE_OP_NONE] : No disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
* - [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.
* - [APNG_DISPOSE_OP_PREVIOUS] : The frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
*/
enum class DisposeOp {
APNG_DISPOSE_OP_NONE,
APNG_DISPOSE_OP_BACKGROUND,
@ -67,8 +73,8 @@ class Utils {
* @param disposeOp The DisposeOp
* @return [Int] An int equivalent to the DisposeOp
*/
fun getDisposeOp(disposeOp: DisposeOp) : Int {
return when(disposeOp) {
fun getDisposeOp(disposeOp: DisposeOp): Int {
return when (disposeOp) {
Companion.DisposeOp.APNG_DISPOSE_OP_NONE -> 0
Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> 1
Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> 2
@ -80,8 +86,8 @@ class Utils {
* @param int Int of the DisposeOp
* @return [DisposeOp] A DisposeOp
*/
fun getDisposeOp(int: Int) : DisposeOp {
return when(int) {
fun getDisposeOp(int: Int): DisposeOp {
return when (int) {
0 -> DisposeOp.APNG_DISPOSE_OP_NONE
1 -> DisposeOp.APNG_DISPOSE_OP_BACKGROUND
2 -> DisposeOp.APNG_DISPOSE_OP_PREVIOUS
@ -89,6 +95,12 @@ class Utils {
}
}
/**
* BlendOp specifies whether the frame is to be alpha blended into the current output buffer content, or whether it should completely replace its region in the output buffer.
* Values :
* - [APNG_BLEND_OP_SOURCE] : All color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
* - [APNG_BLEND_OP_OVER] : The frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the Alpha Channel Processing section of the Extensions to the PNG Specification, Version 1.2.0.
*/
enum class BlendOp {
APNG_BLEND_OP_SOURCE,
APNG_BLEND_OP_OVER
@ -99,8 +111,8 @@ class Utils {
* @param blendOp The BlendOp
* @return [Int] An int equivalent to the BlendOp
*/
fun getBlendOp(blendOp: BlendOp) : Int {
return when(blendOp) {
fun getBlendOp(blendOp: BlendOp): Int {
return when (blendOp) {
Companion.BlendOp.APNG_BLEND_OP_SOURCE -> 0
Companion.BlendOp.APNG_BLEND_OP_OVER -> 1
}
@ -111,8 +123,8 @@ class Utils {
* @param int Int of the BlendOp
* @return [BlendOp] A BlendOp
*/
fun getBlendOp(int : Int) : BlendOp {
return when(int) {
fun getBlendOp(int: Int): BlendOp {
return when (int) {
0 -> BlendOp.APNG_BLEND_OP_SOURCE
1 -> BlendOp.APNG_BLEND_OP_OVER
else -> BlendOp.APNG_BLEND_OP_SOURCE
@ -134,7 +146,12 @@ class Utils {
* @return [ByteArray] 4 Bytes
*/
fun to4BytesArray(i: Int): ByteArray {
return byteArrayOf((i shr 24).toByte(), (i shr 16).toByte(), (i shr 8).toByte(), i.toByte())
return byteArrayOf(
(i shr 24).toByte(),
(i shr 16).toByte(),
(i shr 8).toByte(),
i.toByte()
)
}
/**
@ -151,7 +168,7 @@ class Utils {
* [byteArray] The beginning of the chunk, containing the length
* [Int] The length of the chunk
*/
fun parseLength(byteArray: ByteArray) : Int {
fun parseLength(byteArray: ByteArray): Int {
var lengthString = ""
byteArray.forEach {
lengthString += String.format("%02x", it)
@ -160,13 +177,13 @@ class Utils {
return lengthString.toLong(16).toInt()
}
val fcTL : ByteArray by lazy { byteArrayOf(0x66, 0x63, 0x54, 0x4c) }
val IEND : ByteArray by lazy { byteArrayOf(0x49, 0x45, 0x4e, 0x44) }
val IDAT : ByteArray by lazy { byteArrayOf(0x49, 0x44, 0x41, 0x54) }
val fdAT : ByteArray by lazy { byteArrayOf(0x66, 0x64, 0x41, 0x54) }
val plte : ByteArray by lazy { byteArrayOf(0x50, 0x4c, 0x54, 0x45) }
val tnrs : ByteArray by lazy { byteArrayOf(0x74, 0x52, 0x4e, 0x53) }
val IHDR : ByteArray by lazy { byteArrayOf(0x49, 0x48, 0x44, 0x52) }
val acTL : ByteArray by lazy { byteArrayOf(0x61, 0x63, 0x54, 0x4c) }
val fcTL: ByteArray by lazy { byteArrayOf(0x66, 0x63, 0x54, 0x4c) }
val IEND: ByteArray by lazy { byteArrayOf(0x49, 0x45, 0x4e, 0x44) }
val IDAT: ByteArray by lazy { byteArrayOf(0x49, 0x44, 0x41, 0x54) }
val fdAT: ByteArray by lazy { byteArrayOf(0x66, 0x64, 0x41, 0x54) }
val plte: ByteArray by lazy { byteArrayOf(0x50, 0x4c, 0x54, 0x45) }
val tnrs: ByteArray by lazy { byteArrayOf(0x74, 0x52, 0x4e, 0x53) }
val IHDR: ByteArray by lazy { byteArrayOf(0x49, 0x48, 0x44, 0x52) }
val acTL: ByteArray by lazy { byteArrayOf(0x61, 0x63, 0x54, 0x4c) }
}
}