Documentation
This commit is contained in:
parent
a20d9ee7d9
commit
6b68edb273
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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) }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue