Adding function release as requested in issue #7

Working on an update of ApngEncoder, for now it is called Experimental Apng Encoder but will be merged in ApngEncoder when it is stable
This commit is contained in:
oupson 2020-05-14 08:45:34 +02:00
parent 80b0024ebb
commit d32f5f3ebf
4 changed files with 287 additions and 4 deletions

View File

@ -114,6 +114,9 @@ class ApngEncoder(
} }
} }
frameIndex++ frameIndex++
if (usePngEncoder) {
PngEncoder.release()
}
} }
fun writeEnd() { fun writeEnd() {

View File

@ -0,0 +1,268 @@
package oupson.apng.encoder
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import oupson.apng.chunks.IDAT
import oupson.apng.imageUtils.PngEncoder
import oupson.apng.utils.Utils
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.zip.CRC32
// TODO DOCUMENTATION
// TODO BITMAP ENCODING
// TODO BUFFER AND BUFFER DEACTIVATION WHEN BITMAP CONFIG DOES NOT CONTAIN AN ALPHA CHANNEL
// TODO JAVA OVERLOADS
class ExperimentalApngEncoder(
private val outputStream: OutputStream,
private val width : Int,
private val height : Int,
numberOfFrames : Int,
private val config : Bitmap.Config = Bitmap.Config.ARGB_8888) {
private var frameIndex = 0
private var seq = 0
private val idatName : List<Byte> by lazy {
listOf(0x49.toByte(), 0x44.toByte(), 0x41.toByte(), 0x54.toByte())
}
init {
outputStream.write(Utils.pngSignature)
outputStream.write(generateIhdr())
outputStream.write(generateACTL(numberOfFrames))
}
// TODO ADD SUPPORT FOR FIRST FRAME NOT IN ANIM
// TODO OPTIMISE APNG
@JvmOverloads
fun writeFrame(
inputStream: InputStream,
delay: Float = 1000f,
xOffsets: Int = 0,
yOffsets: Int = 0,
blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE,
disposeOp: Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE,
usePngEncoder: Boolean = false
) {
val btm = BitmapFactory.decodeStream(inputStream).let {
if (it.config != config)
it.copy(config, it.isMutable)
else
it
}
inputStream.close()
if (frameIndex == 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)")
}
generateFCTL(btm, delay, disposeOp, blendOp, xOffsets, yOffsets)
val idat = IDAT().apply {
val byteArray = if (usePngEncoder) {
PngEncoder.encode(btm, true)
} else {
val outputStream = ByteArrayOutputStream()
btm.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.toByteArray()
}
var cursor = 8
while (cursor < byteArray.size) {
val chunk = byteArray.copyOfRange(cursor, cursor + Utils.parseLength(byteArray.copyOfRange(cursor, cursor + 4)) + 12)
parse(chunk)
cursor += Utils.parseLength(byteArray.copyOfRange(cursor, cursor + 4)) + 12
}
}
idat.IDATBody.forEach { idatBody ->
if (frameIndex == 0) {
val idatChunk = ArrayList<Byte>().let { i ->
// Add IDAT
i.addAll(idatName)
// Add chunk body
i.addAll(idatBody.asList())
i.toByteArray()
}
// Add the chunk body length
outputStream.write(Utils.to4Bytes(idatBody.size))
// Generate CRC
val crc1 = CRC32()
crc1.update(idatChunk, 0, idatChunk.size)
outputStream.write(idatChunk)
outputStream.write(Utils.to4Bytes(crc1.value.toInt()))
} else {
val fdat = ArrayList<Byte>().let { fdat ->
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).asList())
// Add fdat
fdat.addAll(Utils.to4Bytes(seq++).asList())
// Add chunk body
fdat.addAll(idatBody.asList())
fdat.toByteArray()
}
// Add the chunk body length
outputStream.write(Utils.to4Bytes(idatBody.size + 4))
// Generate CRC
val crc1 = CRC32()
crc1.update(fdat, 0, fdat.size)
outputStream.write(fdat)
outputStream.write(Utils.to4Bytes(crc1.value.toInt()))
}
}
frameIndex++
if (usePngEncoder) {
PngEncoder.release()
}
}
fun writeEnd() {
// Add IEND body length : 0
outputStream.write(Utils.to4Bytes(0))
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND
val crC32 = CRC32()
crC32.update(iend, 0, iend.size)
outputStream.write(iend)
outputStream.write(Utils.to4Bytes(crC32.value.toInt()))
}
/**
* Generate the IHDR chunks.
* @return [ByteArray] The byteArray generated
*/
private fun generateIhdr(): ByteArray {
val ihdr = ArrayList<Byte>()
// We need a body var to know body length and generate crc
val ihdrBody = ArrayList<Byte>()
/**
if (((maxWidth != frames[0].width) && (maxHeight != frames[0].height)) && cover == null) {
cover = generateCover(BitmapFactory.decodeByteArray(frames[0].byteArray, 0, frames[0].byteArray.size), maxWidth!!, maxHeight!!)
}*/
// Add chunk body length
ihdr.addAll(arrayListOf(0x00, 0x00, 0x00, 0x0d))
// ADD IHDR
ihdrBody.addAll(arrayListOf(0x49, 0x48, 0x44, 0x52))
// Add the max width and height
ihdrBody.addAll(Utils.to4Bytes(width).asList())
ihdrBody.addAll(Utils.to4Bytes(height).asList())
// BIT DEPTH
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ihdrBody.add(when (config) {
Bitmap.Config.ARGB_8888 -> 8.toByte()
Bitmap.Config.RGBA_F16 -> 8.toByte()
Bitmap.Config.RGB_565 -> 8.toByte()
else -> throw Exception("CONFIG IS NOT SUPPORTED")
})
} else {
// SEEMS LIKE THERE IS A BUG WITH RGBA_F16 AND WHEN
ihdrBody.add(
if (config == Bitmap.Config.ARGB_8888)
8.toByte()
else if (config == Bitmap.Config.RGB_565)
8.toByte()
else
throw Exception("CONFIG IS NOT SUPPORTED")
)
}
// COLOR TYPE
ihdrBody.add(
if (config == Bitmap.Config.RGB_565) {
2.toByte()
} else {
6.toByte()
}
)
// COMPRESSION
ihdrBody.add(0.toByte())
// FILTER
ihdrBody.add(0.toByte())
// INTERLACE
ihdrBody.add(0.toByte())
// 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()
}
/**
* Generate the animation control chunk
* @return [ArrayList] The byteArray generated
*/
private fun generateACTL(num: Int): ByteArray {
val res = ArrayList<Byte>()
val actl = ArrayList<Byte>()
// Add length bytes
res.addAll(arrayListOf(0, 0, 0, 0x08))
// Add acTL
actl.addAll(byteArrayOf(0x61, 0x63, 0x54, 0x4c).asList())
// Add number of frames
actl.addAll(Utils.to4Bytes(num).asList())
// Number of repeat, 0 to infinite
actl.addAll(Utils.to4Bytes(0).asList())
res.addAll(actl)
// generate crc
val crc = CRC32()
crc.update(actl.toByteArray(), 0, actl.size)
res.addAll(Utils.to4Bytes(crc.value.toInt()).asList())
return res.toByteArray()
}
private fun generateFCTL(btm : Bitmap, delay: Float, disposeOp: Utils.Companion.DisposeOp, blendOp: Utils.Companion.BlendOp, xOffsets: Int, yOffsets: Int) {
val fcTL = ArrayList<Byte>()
// Add the length of the chunk body
outputStream.write(byteArrayOf(0x00, 0x00, 0x00, 0x1A))
// Add fcTL
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).asList())
// Add the frame number
fcTL.addAll(Utils.to4Bytes(seq++).asList())
// Add width and height
fcTL.addAll(Utils.to4Bytes(btm.width).asList())
fcTL.addAll(Utils.to4Bytes(btm.height).asList())
// Add offsets
fcTL.addAll(Utils.to4Bytes(xOffsets).asList())
fcTL.addAll(Utils.to4Bytes(yOffsets).asList())
// Set frame delay
fcTL.addAll(Utils.to2Bytes(delay.toInt()).asList())
fcTL.addAll(Utils.to2Bytes(1000).asList())
// Add DisposeOp and BlendOp
fcTL.add(Utils.getDisposeOp(disposeOp).toByte())
fcTL.add(Utils.getBlendOp(blendOp).toByte())
// Create CRC
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
outputStream.write(fcTL.toByteArray())
outputStream.write(Utils.to4Bytes(crc.value.toInt()))
}
}

View File

@ -10,6 +10,7 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
// TODO FIND A BETTER SOLUTION // TODO FIND A BETTER SOLUTION
// TODO ABSOLUTELY NOT THREAD SAFE, FIX THAT
/** /**
* Taken from http://catcode.com/pngencoder/com/keypoint/PngEncoder.java * Taken from http://catcode.com/pngencoder/com/keypoint/PngEncoder.java
*/ */
@ -137,6 +138,12 @@ class PngEncoder {
return newArray return newArray
} }
fun release() {
image?.recycle()
image = null
pngBytes = null
}
/** /**
* Write an array of bytes into the pngBytes array. * Write an array of bytes into the pngBytes array.
* Note: This routine has the side effect of updating * Note: This routine has the side effect of updating

View File

@ -5,6 +5,7 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.Menu import android.view.Menu
@ -20,7 +21,7 @@ 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.encoder.ApngEncoder import oupson.apng.encoder.ExperimentalApngEncoder
import oupson.apngcreator.BuildConfig import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R import oupson.apngcreator.R
import oupson.apngcreator.adapter.ImageAdapter import oupson.apngcreator.adapter.ImageAdapter
@ -123,7 +124,7 @@ class CreatorActivity : AppCompatActivity() {
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
Log.i(TAG, "MaxWidth : $maxWidth; MaxHeight : $maxHeight") Log.i(TAG, "MaxWidth : $maxWidth; MaxHeight : $maxHeight")
val encoder = ApngEncoder(out, maxWidth, maxHeight, items.size) val encoder = ExperimentalApngEncoder(out, maxWidth, maxHeight, items.size)
items.forEachIndexed { i, uri -> items.forEachIndexed { i, uri ->
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
Log.v(TAG, "Encoding frame $i") Log.v(TAG, "Encoding frame $i")
@ -203,7 +204,7 @@ class CreatorActivity : AppCompatActivity() {
str?.close() str?.close()
} }
val encoder = ApngEncoder(out, maxWidth, maxHeight, items.size) val encoder = ExperimentalApngEncoder(out, maxWidth, maxHeight, items.size)
items.forEach { uri -> items.forEach { uri ->
println("delay : ${uri.second.toFloat()}ms") println("delay : ${uri.second.toFloat()}ms")
val str = contentResolver.openInputStream(uri.first) ?: return@forEach val str = contentResolver.openInputStream(uri.first) ?: return@forEach
@ -318,7 +319,11 @@ class CreatorActivity : AppCompatActivity() {
if (BuildConfig.DEBUG) if (BuildConfig.DEBUG)
Log.i(TAG, "MaxWidth : $maxWidth; MaxHeight : $maxHeight") Log.i(TAG, "MaxWidth : $maxWidth; MaxHeight : $maxHeight")
val encoder = ApngEncoder(out, maxWidth, maxHeight, items.size) val encoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ExperimentalApngEncoder(out, maxWidth, maxHeight, items.size, Bitmap.Config.RGBA_F16)
} else {
TODO("VERSION.SDK_INT < O")
}
items.forEach { uri -> items.forEach { uri ->
// println("delay : ${adapter?.delay?.get(i)?.toFloat() ?: 1000f}ms") // println("delay : ${adapter?.delay?.get(i)?.toFloat() ?: 1000f}ms")
val str = val str =