Working on optimiser functionality to reduce APNG size.

Loader now static
Disassembler now static
Optimize imports
This commit is contained in:
oupson 2018-11-15 11:25:35 +01:00
parent 0150810541
commit 66055221f1
8 changed files with 219 additions and 528 deletions

View File

@ -1,9 +1,6 @@
package oupson.apng package oupson.apng
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.util.Log
import oupson.apng.Utils.Companion.isApng import oupson.apng.Utils.Companion.isApng
import oupson.apng.Utils.Companion.pngSignature import oupson.apng.Utils.Companion.pngSignature
import oupson.apng.Utils.Companion.to4Bytes import oupson.apng.Utils.Companion.to4Bytes
@ -12,240 +9,227 @@ import oupson.apng.chunks.fcTL
import oupson.apng.exceptions.NotApngException import oupson.apng.exceptions.NotApngException
import java.util.zip.CRC32 import java.util.zip.CRC32
class APNGDisassembler(val byteArray: ByteArray) { class APNGDisassembler() {
val pngList = ArrayList<Frame>() companion object {
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 blend_op : Utils.Companion.blend_op = Utils.Companion.blend_op.APNG_BLEND_OP_SOURCE
var dispose_op : Utils.Companion.dispose_op= Utils.Companion.dispose_op.APNG_DISPOSE_OP_NONE
var apng : Apng fun disassemble(byteArray: ByteArray) : Apng {
val pngList = ArrayList<Frame>()
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 blend_op: Utils.Companion.blend_op = Utils.Companion.blend_op.APNG_BLEND_OP_SOURCE
var dispose_op: Utils.Companion.dispose_op = Utils.Companion.dispose_op.APNG_DISPOSE_OP_NONE
val apng: Apng
if (isApng(byteArray)) {
apng = Apng()
val ihdr = IHDR()
ihdr.parseIHDR(byteArray)
maxWidth = ihdr.pngWidth
maxHeight = ihdr.pngHeight
for (i in 0 until byteArray.size) {
// find new Frame with fcTL
if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x63.toByte() && byteArray[i + 2] == 0x54.toByte() && byteArray[i + 3] == 0x4C.toByte()) {
if (png == null) {
if (cover != null) {
cover.addAll(to4Bytes(0).toList())
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND
val crC32 = CRC32()
crC32.update(iend, 0, iend.size)
cover.addAll(iend.toList())
cover.addAll(to4Bytes(crC32.value.toInt()).toList())
init { apng.cover = BitmapFactory.decodeByteArray(cover.toByteArray(), 0, cover.size)
if (isApng(byteArray)) { }
apng = Apng() png = ArrayList()
val ihdr = IHDR() val fcTL = fcTL(byteArray.copyOfRange(i - 4, i + 36))
ihdr.parseIHDR(byteArray) delay = fcTL.delay
maxWidth = ihdr.pngWidth yOffset = fcTL.y_offset
maxHeight = ihdr.pngHeight xOffset = fcTL.x_offset
for(i in 0 until byteArray.size) { blend_op = fcTL.blend_op
// find new Frame with fcTL dispose_op = fcTL.dispose_op
if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x63.toByte() && byteArray[ i + 2 ] == 0x54.toByte() && byteArray[ i + 3 ] == 0x4C.toByte()) { val width = fcTL.pngWidth
if (png == null) { val height = fcTL.pngHeight
if (cover != null) { png.addAll(pngSignature.toList())
cover!!.addAll(to4Bytes(0).toList()) png.addAll(generate_ihdr(ihdr, width, height).toList())
if (plte != null) {
png.addAll(plte.toList())
}
if (tnrs != null) {
png.addAll(tnrs.toList())
}
} else {
// Add IEND body length : 0
png.addAll(to4Bytes(0).toList())
// 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)
cover!!.addAll(iend.toList()) png.addAll(iend.toList())
cover!!.addAll(to4Bytes(crC32.value.toInt()).toList()) png.addAll(to4Bytes(crC32.value.toInt()).toList())
pngList.add(Frame(png.toByteArray(), delay, xOffset, yOffset, maxWidth, maxHeight, blend_op, dispose_op))
apng.cover = BitmapFactory.decodeByteArray(cover!!.toByteArray(), 0, cover!!.size) png = ArrayList()
}
png = ArrayList()
val fcTL = fcTL(byteArray.copyOfRange(i - 4, i + 36))
delay = fcTL.delay
yOffset = fcTL.y_offset
xOffset = fcTL.x_offset
blend_op = fcTL.blend_op
dispose_op = fcTL.dispose_op
val width = fcTL.pngWidth
val height = fcTL.pngHeight
png!!.addAll(pngSignature.toList())
png!!.addAll(generate_ihdr(ihdr, width, height).toList())
if (plte != null) { val bodySize = {
png!!.addAll(plte!!.toList()) var lengthString = ""
} byteArray.copyOfRange(i - 4, i).forEach {
lengthString += String.format("%02x", it)
}
lengthString.toLong(16).toInt()
}()
val newBytes = byteArray.copyOfRange(i - 4, i + 4 + bodySize)
val fcTL = fcTL(newBytes)
delay = fcTL.delay
if (tnrs != null) { yOffset = fcTL.y_offset
png!!.addAll(tnrs!!.toList()) xOffset = fcTL.x_offset
blend_op = fcTL.blend_op
dispose_op = fcTL.dispose_op
val width = fcTL.pngWidth
val height = fcTL.pngHeight
png.addAll(pngSignature.toList())
png.addAll(generate_ihdr(ihdr, width, height).toList())
if (plte != null) {
png.addAll(plte.toList())
}
if (tnrs != null) {
png.addAll(tnrs.toList())
}
} }
} else { } else if (i == byteArray.size - 1) {
// Add IEND body length : 0
png!!.addAll(to4Bytes(0).toList()) png!!.addAll(to4Bytes(0).toList())
// 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.toList()) png.addAll(iend.toList())
png!!.addAll(to4Bytes(crC32.value.toInt()).toList()) png.addAll(to4Bytes(crC32.value.toInt()).toList())
pngList.add(Frame(png!!.toByteArray(), delay, xOffset, yOffset, maxWidth, maxHeight, blend_op, dispose_op)) pngList.add(Frame(png.toByteArray(), delay, xOffset, yOffset, maxWidth, maxHeight, blend_op, dispose_op))
}
png = ArrayList() // Check if is IDAT
else if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x44.toByte() && byteArray[i + 2] == 0x41.toByte() && byteArray[i + 3] == 0x54.toByte()) {
val bodySize = { if (png == null) {
if (cover == null) {
cover = ArrayList()
cover.addAll(pngSignature.toList())
cover.addAll(generate_ihdr(ihdr, maxWidth, maxHeight).toList())
}
// Find the chunk length
var lengthString = "" var lengthString = ""
byteArray.copyOfRange(i - 4, i).forEach { byteArray.copyOfRange(i - 4, i).forEach {
lengthString += String.format("%02x", it) lengthString += String.format("%02x", it)
} }
lengthString.toLong(16).toInt() val bodySize = lengthString.toLong(16).toInt()
}() cover.addAll(byteArray.copyOfRange(i - 4, i).toList())
val newBytes = byteArray.copyOfRange(i - 4, i + 4 + bodySize) val body = ArrayList<Byte>()
val fcTL = fcTL(newBytes) body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
delay = fcTL.delay // Get image bytes
for (j in i + 4 until i + 4 + bodySize) {
yOffset = fcTL.y_offset body.add(byteArray[j])
xOffset = fcTL.x_offset }
val crC32 = CRC32()
blend_op = fcTL.blend_op crC32.update(body.toByteArray(), 0, body.size)
dispose_op = fcTL.dispose_op cover.addAll(body)
val width = fcTL.pngWidth cover.addAll(to4Bytes(crC32.value.toInt()).toList())
val height = fcTL.pngHeight } else {
png!!.addAll(pngSignature.toList()) // Find the chunk length
png!!.addAll(generate_ihdr(ihdr, width, height).toList()) var lengthString = ""
if (plte != null) { byteArray.copyOfRange(i - 4, i).forEach {
png!!.addAll(plte!!.toList()) lengthString += String.format("%02x", it)
} }
val bodySize = lengthString.toLong(16).toInt()
if (tnrs != null) { png.addAll(byteArray.copyOfRange(i - 4, i).toList())
png!!.addAll(tnrs!!.toList()) val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes
for (j in i + 4 until i + 4 + bodySize) {
body.add(byteArray[j])
}
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png.addAll(body)
png.addAll(to4Bytes(crC32.value.toInt()).toList())
} }
} }
} // Check if is fdAT
else if (i == byteArray.size - 1) { else if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x64.toByte() && byteArray[i + 2] == 0x41.toByte() && byteArray[i + 3] == 0x54.toByte()) {
png!!.addAll(to4Bytes(0).toList())
// 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.toList())
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
pngList.add(Frame(png!!.toByteArray(), delay, xOffset, yOffset, maxWidth, maxHeight, blend_op, dispose_op))
}
// Check if is IDAT
else if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x44.toByte() && byteArray[ i + 2 ] == 0x41.toByte() && byteArray[ i + 3 ] == 0x54.toByte()) {
if (png == null) {
if (cover == null) {
cover = ArrayList()
cover!!.addAll(pngSignature.toList())
cover!!.addAll(generate_ihdr(ihdr, maxWidth, maxHeight).toList())
}
// Find the chunk length // Find the chunk length
var lengthString = "" var lengthString = ""
byteArray.copyOfRange( i - 4, i).forEach { byteArray.copyOfRange(i - 4, i).forEach {
lengthString += String.format("%02x", it) lengthString += String.format("%02x", it)
} }
val bodySize = lengthString.toLong(16).toInt() val bodySize = lengthString.toLong(16).toInt()
cover!!.addAll(byteArray.copyOfRange(i-4, i).toList()) png!!.addAll(to4Bytes(bodySize - 4).toList())
val body = ArrayList<Byte>() val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList()) body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes // Get image bytes
for (j in i +4 until i + 4 + bodySize) { for (j in i + 8 until i + 4 + bodySize) {
body.add(byteArray[j]) body.add(byteArray[j])
} }
val crC32 = CRC32() val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size) crC32.update(body.toByteArray(), 0, body.size)
cover!!.addAll(body) png.addAll(body)
cover!!.addAll(to4Bytes(crC32.value.toInt()).toList()) png.addAll(to4Bytes(crC32.value.toInt()).toList())
} else { }
// Find the chunk length // Get plte chunks if exist. The PLTE chunk contains from 1 to 256 palette entries, each a three-byte series of the form:
else if (byteArray[i] == 0x50.toByte() && byteArray[i + 1] == 0x4C.toByte() && byteArray[i + 2] == 0x54.toByte() && byteArray[i + 3] == 0x45.toByte()) {
var lengthString = "" var lengthString = ""
byteArray.copyOfRange( i - 4, i).forEach { byteArray.copyOfRange(i - 4, i).forEach {
lengthString += String.format("%02x", it) lengthString += String.format("%02x", it)
} }
val bodySize = lengthString.toLong(16).toInt() val bodySize = lengthString.toLong(16).toInt()
png!!.addAll(byteArray.copyOfRange(i-4, i).toList()) plte = byteArray.copyOfRange(i - 4, i + 8 + bodySize)
val body = ArrayList<Byte>() }
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList()) // Get tnrs chunk if exist. Used for transparency
// Get image bytes else if (byteArray[i] == 0x74.toByte() && byteArray[i + 1] == 0x52.toByte() && byteArray[i + 2] == 0x4E.toByte() && byteArray[i + 3] == 0x53.toByte()) {
for (j in i +4 until i + 4 + bodySize) { var lengthString = ""
body.add(byteArray[j]) byteArray.copyOfRange(i - 4, i).forEach {
lengthString += String.format("%02x", it)
} }
val crC32 = CRC32() val bodySize = lengthString.toLong(16).toInt()
crC32.update(body.toByteArray(), 0, body.size) tnrs = byteArray.copyOfRange(i - 4, i + 8 + bodySize)
png!!.addAll(body)
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
} }
} }
// Check if is fdAT apng.frames = pngList
else if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x64.toByte() && byteArray[ i + 2 ] == 0x41.toByte() && byteArray[ i + 3 ] == 0x54.toByte()) { return apng
// Find the chunk length } else {
var lengthString = "" throw NotApngException()
byteArray.copyOfRange( i - 4, i).forEach {
lengthString += String.format("%02x", it)
}
val bodySize = lengthString.toLong(16).toInt()
png!!.addAll(to4Bytes(bodySize - 4).toList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes
for (j in i + 8 until i + 4 + bodySize) {
body.add(byteArray[j])
}
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png!!.addAll(body)
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
}
// Get plte chunks if exist. The PLTE chunk contains from 1 to 256 palette entries, each a three-byte series of the form:
else if (byteArray[i] == 0x50.toByte() && byteArray[i + 1] == 0x4C.toByte() && byteArray[ i + 2 ] == 0x54.toByte() && byteArray[ i + 3 ] == 0x45.toByte()) {
var lengthString = ""
byteArray.copyOfRange( i - 4, i).forEach {
lengthString += String.format("%02x", it)
}
val bodySize = lengthString.toLong(16).toInt()
plte = byteArray.copyOfRange( i -4, i + 8 + bodySize)
}
// Get tnrs chunk if exist. Used for transparency
else if (byteArray[i] == 0x74.toByte() && byteArray[i + 1] == 0x52.toByte() && byteArray[ i + 2 ] == 0x4E.toByte() && byteArray[ i + 3 ] == 0x53.toByte()) {
var lengthString = ""
byteArray.copyOfRange( i - 4, i).forEach {
lengthString += String.format("%02x", it)
}
val bodySize = lengthString.toLong(16).toInt()
tnrs = byteArray.copyOfRange( i -4, i + 8 + bodySize)
}
} }
apng.frames = pngList
} else {
throw NotApngException()
} }
} private fun generate_ihdr(ihdrOfApng: IHDR, width : Int, height : Int) : ByteArray {
val ihdr = ArrayList<Byte>()
private fun generate_ihdr(ihdrOfApng: IHDR, width : Int, height : Int) : ByteArray { // We need a body var to know body length and generate crc
val ihdr = ArrayList<Byte>() val ihdr_body = ArrayList<Byte>()
// We need a body var to know body length and generate crc // Add chunk body length
val ihdr_body = ArrayList<Byte>() ihdr.addAll(to4Bytes(ihdrOfApng.ihdrCorps.size).toList())
// Add chunk body length // Add IHDR
ihdr.addAll(to4Bytes(ihdrOfApng.ihdrCorps.size).toList()) ihdr_body.addAll(byteArrayOf(0x49.toByte(), 0x48.toByte(), 0x44.toByte(), 0x52.toByte()).toList())
// Add IHDR // Add the max width and height
ihdr_body.addAll(byteArrayOf(0x49.toByte(), 0x48.toByte(), 0x44.toByte(), 0x52.toByte()).toList()) ihdr_body.addAll(to4Bytes(width).toList())
// Add the max width and height ihdr_body.addAll(to4Bytes(height).toList())
ihdr_body.addAll(to4Bytes(width).toList()) // Add complicated stuff like depth color ...
ihdr_body.addAll(to4Bytes(height).toList()) // If you want correct png you need same parameters. Good solution is to create new png.
// Add complicated stuff like depth color ... ihdr_body.addAll(ihdrOfApng.ihdrCorps.copyOfRange(8, 13).toList())
// If you want correct png you need same parameters. Good solution is to create new png. // Generate CRC
ihdr_body.addAll(ihdrOfApng.ihdrCorps.copyOfRange(8, 13).toList()) val crC32 = CRC32()
// Generate CRC crC32.update(ihdr_body.toByteArray(), 0, ihdr_body.size)
val crC32 = CRC32() ihdr.addAll(ihdr_body)
crC32.update(ihdr_body.toByteArray(), 0, ihdr_body.size) ihdr.addAll(to4Bytes(crC32.value.toInt()).toList())
ihdr.addAll(ihdr_body) return ihdr.toByteArray()
ihdr.addAll(to4Bytes(crC32.value.toInt()).toList())
return ihdr.toByteArray()
}
fun genBitmap() : ArrayList<Bitmap> {
val generatedFrame = ArrayList<Bitmap>()
pngList.forEach {
val btm = Bitmap.createBitmap(it.maxWidth!!, it.maxHeight!!, Bitmap.Config.ARGB_8888)
val canvas = Canvas(btm)
canvas.drawBitmap(BitmapFactory.decodeByteArray(it.byteArray, 0, it.byteArray.size), it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), null)
generatedFrame.add(btm)
} }
return generatedFrame
} }
} }

View File

@ -3,7 +3,6 @@ package oupson.apng
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import oupson.apng.ImageUtils.PnnQuantizer import oupson.apng.ImageUtils.PnnQuantizer
import oupson.apng.Utils.Companion.convertImage
import oupson.apng.Utils.Companion.getBlend_op import oupson.apng.Utils.Companion.getBlend_op
import oupson.apng.Utils.Companion.getDispose_op import oupson.apng.Utils.Companion.getDispose_op
import oupson.apng.Utils.Companion.pngSignature import oupson.apng.Utils.Companion.pngSignature
@ -12,7 +11,6 @@ import oupson.apng.Utils.Companion.to4Bytes
import oupson.apng.Utils.Companion.toByteArray import oupson.apng.Utils.Companion.toByteArray
import oupson.apng.chunks.IDAT import oupson.apng.chunks.IDAT
import oupson.apng.exceptions.NoFrameException import oupson.apng.exceptions.NoFrameException
import java.io.ByteArrayOutputStream
import java.util.zip.CRC32 import java.util.zip.CRC32

View File

@ -2,7 +2,6 @@ package oupson.apng
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.*
import android.os.Handler
import android.widget.ImageView import android.widget.ImageView
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
@ -13,7 +12,7 @@ import java.net.URL
/** /**
* Class to play APNG * Class to play APNG
*/ */
class ApngAnimator(val context: Context) { class ApngAnimator(private val context: Context) {
var isPlaying = true var isPlaying = true
private set(value) { private set(value) {
field = value field = value
@ -54,7 +53,7 @@ class ApngAnimator(val context: Context) {
doAsync { doAsync {
this@ApngAnimator.speed = speed this@ApngAnimator.speed = speed
// Download PNG // Download PNG
APNGDisassembler(file.readBytes()).pngList.apply { APNGDisassembler.disassemble(file.readBytes()).frames.apply {
draw(this) draw(this)
} }
setupAnimationDrawableAndStart() setupAnimationDrawableAndStart()
@ -71,7 +70,7 @@ class ApngAnimator(val context: Context) {
doAsync(exceptionHandler = { e -> e.printStackTrace() }) { doAsync(exceptionHandler = { e -> e.printStackTrace() }) {
this@ApngAnimator.speed = speed this@ApngAnimator.speed = speed
// Download PNG // Download PNG
APNGDisassembler(Loader().load(context, url)).pngList.apply { APNGDisassembler.disassemble(Loader.load(context, url)).frames.apply {
draw(this) draw(this)
} }
setupAnimationDrawableAndStart() setupAnimationDrawableAndStart()
@ -87,7 +86,7 @@ class ApngAnimator(val context: Context) {
fun load(byteArray: ByteArray, speed: Float? = null) { fun load(byteArray: ByteArray, speed: Float? = null) {
doAsync { doAsync {
this@ApngAnimator.speed = speed this@ApngAnimator.speed = speed
APNGDisassembler(byteArray).pngList.apply { APNGDisassembler.disassemble(byteArray).frames.apply {
draw(this) draw(this)
} }
setupAnimationDrawableAndStart() setupAnimationDrawableAndStart()

View File

@ -1,287 +0,0 @@
package oupson.apng
import oupson.apng.Utils.Companion.pngSignature
import oupson.apng.exceptions.NoFrameException
import java.util.zip.CRC32
/**
* APNG is a class for create apng
*
* Call .addFrame() to add a Frame
* Call .create to get the generated file
*
* @author oupson
*
* @throws NotPngException
* @throws NoFrameException
*
*/
class ApngFactory {
private var seq = 0
var frames = ArrayList<Frame>()
/**
* @return a byte array of the generated png
*
* @throws NoFrameException
*/
fun create(): ByteArray {
if (frames.isNotEmpty()) {
val res = ArrayList<Byte>()
// Add PNG signature
res.addAll(pngSignature.toList())
// Add Image Header
res.addAll(generate_ihdr().toList())
// Add Animation Controller
res.addAll(generateACTL())
// Get max height and max width
val maxHeight = frames.sortedByDescending { it.height }[0].height
val maxWitdh = frames.sortedByDescending { it.width }[0].width
for (i in 0 until frames.size) {
// If it's the first frame
if (i == 0) {
val framesByte = ArrayList<Byte>()
// region fcTL
// Create the fcTL
val fcTL = ArrayList<Byte>()
// Add the length of the chunk body
framesByte.addAll(byteArrayOf(0x00, 0x00, 0x00, 0x1A).toList())
// Add acTL
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).toList())
// Add the frame number
fcTL.addAll(to4Bytes(seq).toList())
seq++
// Add width and height
fcTL.addAll(to4Bytes(frames[i].width).toList())
fcTL.addAll(to4Bytes(frames[i].height).toList())
// Calculate offset
if (frames[i].width < maxWitdh) {
val xOffset = (maxWitdh / 2) - (frames[i].width / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
if (frames[i].height < maxHeight) {
val xOffset = (maxHeight / 2) - (frames[i].height / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
// Set frame delay
fcTL.addAll(to2Bytes(frames[i].delay.toInt()).toList())
fcTL.addAll(to2Bytes(1000).toList())
fcTL.add(0x01)
fcTL.add(0x00)
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
framesByte.addAll(to4Bytes(crc.value.toInt()).toList())
// endregion
// region idat
frames[i].idat.IDATBody.forEach {
val fdat = ArrayList<Byte>()
// Add the chunk body length
framesByte.addAll(to4Bytes(it.size).toList())
// Add IDAT
fdat.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Add chunk body
fdat.addAll(it.toList())
// Generate CRC
val crc1 = CRC32()
crc1.update(fdat.toByteArray(), 0, fdat.size)
framesByte.addAll(fdat)
framesByte.addAll(to4Bytes(crc1.value.toInt()).toList())
}
// endregion
res.addAll(framesByte)
} else {
val framesByte = ArrayList<Byte>()
val fcTL = ArrayList<Byte>()
// region fcTL
framesByte.addAll(byteArrayOf(0x00, 0x00, 0x00, 0x1A).toList())
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).toList())
// Frame number
fcTL.addAll(to4Bytes(seq).toList())
seq++
// width and height
fcTL.addAll(to4Bytes(frames[i].width).toList())
fcTL.addAll(to4Bytes(frames[i].height).toList())
if (frames[i].width < maxWitdh) {
val xOffset = (maxWitdh / 2) - (frames[i].width / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
if (frames[i].height < maxHeight) {
val xOffset = (maxHeight / 2) - (frames[i].height / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
// Set frame delay
fcTL.addAll(to2Bytes(frames[i].delay.toInt()).toList())
fcTL.addAll(to2Bytes(1000).toList())
fcTL.add(0x01)
fcTL.add(0x00)
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
framesByte.addAll(to4Bytes(crc.value.toInt()).toList())
// endregion
// region fdAT
// Write fdAT
frames[i].idat.IDATBody.forEach {
val fdat = ArrayList<Byte>()
// Add IDAT size of frame + 4 byte of the seq
framesByte.addAll(to4Bytes(it.size + 4).toList())
// Add fdAT
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).toList())
// Add Sequence number
// ! THIS IS NOT FRAME NUMBER
fdat.addAll(to4Bytes(seq).toList())
// Increase seq
seq++
fdat.addAll(it.toList())
// Generate CRC
val crc1 = CRC32()
crc1.update(fdat.toByteArray(), 0, fdat.size)
framesByte.addAll(fdat)
framesByte.addAll(to4Bytes(crc1.value.toInt()).toList())
}
// endregion
res.addAll(framesByte)
}
}
// Add IEND body length : 0
res.addAll(to4Bytes(0).toList())
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
// Generate crc for IEND
val crC32 = CRC32()
crC32.update(iend, 0, iend.size)
res.addAll(iend.toList())
res.addAll(to4Bytes(crC32.value.toInt()).toList())
return res.toByteArray()
} else {
throw NoFrameException()
}
}
// Animation Control chunk
private fun generateACTL(): ArrayList<Byte> {
val res = ArrayList<Byte>()
val actl = ArrayList<Byte>()
// Add length bytes
res.addAll(to4Bytes(8).toList())
// Add acTL
actl.addAll(byteArrayOf(0x61, 0x63, 0x54, 0x4c).toList())
// Add number of frames
actl.addAll(to4Bytes(frames.size).toList())
// Number of repeat, 0 to infinite
actl.addAll(to4Bytes(0).toList())
res.addAll(actl)
// generate crc
val crc = CRC32()
crc.update(actl.toByteArray(), 0, actl.size)
res.addAll(to4Bytes(crc.value.toInt()).toList())
return res
}
/**
* Generate a 4 bytes array from an Int
* @param i The int
*/
fun to4Bytes(i: Int): ByteArray {
val result = ByteArray(4)
result[0] = (i shr 24).toByte()
result[1] = (i shr 16).toByte()
result[2] = (i shr 8).toByte()
result[3] = i /*>> 0*/.toByte()
return result
}
/**
* Generate a 2 bytes array from an Int
* @param i The int
*/
fun to2Bytes(i: Int): ByteArray {
val result = ByteArray(2)
result[0] = (i shr 8).toByte()
result[1] = i /*>> 0*/.toByte()
return result
}
/**
* Add a frame to the Animated PNG
*
* @param byteArray It's the byteArray of a png
* @param delay Delay in MS between this frame and the next
*/
fun addFrame(byteArray: ByteArray, delay: Float = 1000f) {
frames.add(Frame(byteArray, delay))
}
// Generate Image Header chunk
private fun generate_ihdr(): ByteArray {
val ihdr = ArrayList<Byte>()
// We need a body var to know body length and generate crc
val ihdr_body = ArrayList<Byte>()
// Get max height and max width of all the frames
val maxHeight = frames.sortedByDescending { it.height }[0].height
val maxWitdh = frames.sortedByDescending { it.width }[0].width
// Add chunk body length
ihdr.addAll(to4Bytes(frames[0].ihdr.ihdrCorps.size).toList())
// Add IHDR
ihdr_body.addAll(byteArrayOf(0x49.toByte(), 0x48.toByte(), 0x44.toByte(), 0x52.toByte()).toList())
// Add the max width and height
ihdr_body.addAll(to4Bytes(maxWitdh).toList())
ihdr_body.addAll(to4Bytes(maxHeight).toList())
// Add complicated stuff like depth color ...
// If you want correct png you need same parameters. Good solution is to create new png.
ihdr_body.addAll(frames[0].ihdr.ihdrCorps.copyOfRange(8, 13).toList())
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdr_body.toByteArray(), 0, ihdr_body.size)
ihdr.addAll(ihdr_body)
ihdr.addAll(to4Bytes(crC32.value.toInt()).toList())
return ihdr.toByteArray()
}
}

View File

@ -1,7 +1,6 @@
package oupson.apng package oupson.apng
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import oupson.apng.Utils.Companion.convertImage
import oupson.apng.Utils.Companion.isPng import oupson.apng.Utils.Companion.isPng
import oupson.apng.Utils.Companion.toByteArray import oupson.apng.Utils.Companion.toByteArray
import oupson.apng.chunks.IDAT import oupson.apng.chunks.IDAT

View File

@ -20,13 +20,13 @@ import java.util.Map;
import java.util.Random; import java.util.Random;
public class PnnQuantizer { public class PnnQuantizer {
protected final short SHORT_MAX = Short.MAX_VALUE; private final short SHORT_MAX = Short.MAX_VALUE;
protected final char BYTE_MAX = -Byte.MIN_VALUE + Byte.MAX_VALUE; private final char BYTE_MAX = -Byte.MIN_VALUE + Byte.MAX_VALUE;
protected boolean hasTransparency = false, hasSemiTransparency = false; private boolean hasTransparency = false, hasSemiTransparency = false;
protected int width, height; protected int width, height;
protected int pixels[] = null; protected int pixels[] = null;
protected Integer m_transparentColor; private Integer m_transparentColor;
protected Map<Integer, short[]> closestMap = new HashMap<>(); private Map<Integer, short[]> closestMap = new HashMap();
public PnnQuantizer(String fname) throws IOException { public PnnQuantizer(String fname) throws IOException {
fromBitmap(fname); fromBitmap(fname);
@ -54,7 +54,7 @@ public class PnnQuantizer {
int nn, fw, bk, tm, mtm; int nn, fw, bk, tm, mtm;
} }
protected int getColorIndex(final int c) private int getColorIndex(final int c)
{ {
if(hasSemiTransparency) if(hasSemiTransparency)
return (Color.alpha(c) & 0xF0) << 8 | (Color.red(c) & 0xF0) << 4 | (Color.green(c) & 0xF0) | (Color.blue(c) >> 4); return (Color.alpha(c) & 0xF0) << 8 | (Color.red(c) & 0xF0) << 4 | (Color.green(c) & 0xF0) | (Color.blue(c) >> 4);
@ -63,7 +63,7 @@ public class PnnQuantizer {
return (Color.red(c) & 0xF8) << 8 | (Color.green(c) & 0xFC) << 3 | (Color.blue(c) >> 3); return (Color.red(c) & 0xF8) << 8 | (Color.green(c) & 0xFC) << 3 | (Color.blue(c) >> 3);
} }
protected double sqr(double value) private double sqr(double value)
{ {
return value * value; return value * value;
} }
@ -412,7 +412,7 @@ public class PnnQuantizer {
return true; return true;
} }
protected Bitmap quantize_image(final int[] pixels, int[] qPixels) private Bitmap quantize_image(final int[] pixels, int[] qPixels)
{ {
int pixelIndex = 0; int pixelIndex = 0;
boolean odd_scanline = false; boolean odd_scanline = false;

View File

@ -8,31 +8,33 @@ import java.io.IOException
import java.net.URL import java.net.URL
class Loader { class Loader {
fun load( context: Context, url : URL) : ByteArray { companion object {
val currenDir = context.filesDir fun load(context: Context, url: URL): ByteArray {
val fileTXT = File(currenDir, "apngLoader.txt") val currenDir = context.filesDir
var filePNG = File(currenDir, "apngLoader.png") val fileTXT = File(currenDir, "apngLoader.txt")
if (fileTXT.exists() && url.toString() == fileTXT.readText()) { var filePNG = File(currenDir, "apngLoader.png")
return filePNG.readBytes() if (fileTXT.exists() && url.toString() == fileTXT.readText()) {
} else { return filePNG.readBytes()
try { } else {
val connection = url.openConnection() try {
connection.connect() val connection = url.openConnection()
val input = BufferedInputStream(connection.getInputStream()) connection.connect()
val output = ByteArrayOutputStream() val input = BufferedInputStream(connection.getInputStream())
val data = ByteArray(1024) val output = ByteArrayOutputStream()
var count = 0 val data = ByteArray(1024)
while ({ count = input.read(data); count }() != -1) { var count = 0
output.write(data, 0, count) while ({ count = input.read(data); count }() != -1) {
output.write(data, 0, count)
}
output.flush()
output.close()
input.close()
fileTXT.writeText(url.toString())
filePNG.writeBytes(output.toByteArray())
return output.toByteArray()
} catch (e: IOException) {
throw e
} }
output.flush()
output.close()
input.close()
fileTXT.writeText(url.toString())
filePNG.writeBytes(output.toByteArray())
return output.toByteArray()
} catch (e: IOException) {
throw e
} }
} }
} }

View File

@ -1,6 +1,5 @@
package oupson.apngcreator package oupson.apngcreator
import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
@ -8,14 +7,11 @@ import android.util.Log
import android.widget.SeekBar import android.widget.SeekBar
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import oupson.apng.ApngAnimator
import android.widget.Toast
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import oupson.apng.APNGDisassembler import oupson.apng.APNGDisassembler
import oupson.apng.Apng import oupson.apng.ApngAnimator
import oupson.apng.Loader import oupson.apng.Loader
import oupson.apngcreator.R.id.imageView
import java.io.File import java.io.File
import java.net.URL import java.net.URL
@ -40,9 +36,9 @@ class MainActivity : AppCompatActivity() {
doAsync { doAsync {
Loader().load(applicationContext, URL(imageUrl)).apply { Loader.load(applicationContext, URL(imageUrl)).apply {
val a = APNGDisassembler(this).apng val a = APNGDisassembler.disassemble(this)
a.reduceSize(100, false, 75) a.reduceSize(100, false, 60)
File(File(Environment.getExternalStorageDirectory(), "Documents"), "apng.png").writeBytes(a.toByteArray()) File(File(Environment.getExternalStorageDirectory(), "Documents"), "apng.png").writeBytes(a.toByteArray())
runOnUiThread { runOnUiThread {
toast("Converted ! ") toast("Converted ! ")