From 66055221f1c235453056868a2f9f1fc2b91f418b Mon Sep 17 00:00:00 2001 From: oupson Date: Thu, 15 Nov 2018 11:25:35 +0100 Subject: [PATCH] Working on optimiser functionality to reduce APNG size. Loader now static Disassembler now static Optimize imports --- .../main/java/oupson/apng/APNGDisassembler.kt | 370 +++++++++--------- .../src/main/java/oupson/apng/Apng.kt | 2 - .../src/main/java/oupson/apng/ApngAnimator.kt | 9 +- .../src/main/java/oupson/apng/ApngFactory.kt | 287 -------------- .../src/main/java/oupson/apng/Frame.kt | 1 - .../oupson/apng/ImageUtils/PnnQuantizer.java | 16 +- .../src/main/java/oupson/apng/Loader.kt | 50 +-- .../java/oupson/apngcreator/MainActivity.kt | 12 +- 8 files changed, 219 insertions(+), 528 deletions(-) delete mode 100644 apng_library/src/main/java/oupson/apng/ApngFactory.kt diff --git a/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt index f3e720d..ccb8f48 100644 --- a/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt +++ b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt @@ -1,9 +1,6 @@ package oupson.apng -import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.util.Log import oupson.apng.Utils.Companion.isApng import oupson.apng.Utils.Companion.pngSignature import oupson.apng.Utils.Companion.to4Bytes @@ -12,240 +9,227 @@ import oupson.apng.chunks.fcTL import oupson.apng.exceptions.NotApngException import java.util.zip.CRC32 -class APNGDisassembler(val byteArray: ByteArray) { - val pngList = ArrayList() - var png : ArrayList? = null - var cover : ArrayList? = 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 +class APNGDisassembler() { + companion object { - var apng : Apng + fun disassemble(byteArray: ByteArray) : Apng { + val pngList = ArrayList() + var png: ArrayList? = null + var cover: ArrayList? = 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 { - 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()) + apng.cover = BitmapFactory.decodeByteArray(cover.toByteArray(), 0, cover.size) + } + 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) { + png.addAll(plte.toList()) + } + + if (tnrs != null) { + png.addAll(tnrs.toList()) + } + } else { + // Add IEND body length : 0 + 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) - cover!!.addAll(iend.toList()) - cover!!.addAll(to4Bytes(crC32.value.toInt()).toList()) + 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)) - apng.cover = BitmapFactory.decodeByteArray(cover!!.toByteArray(), 0, cover!!.size) - } - 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()) + png = ArrayList() - if (plte != null) { - png!!.addAll(plte!!.toList()) - } + val bodySize = { + 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) { - png!!.addAll(tnrs!!.toList()) + 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) { + png.addAll(plte.toList()) + } + + if (tnrs != null) { + png.addAll(tnrs.toList()) + } } - } else { - // Add IEND body length : 0 + } else if (i == byteArray.size - 1) { 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)) - - png = ArrayList() - - val bodySize = { + 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 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 - - 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) { - png!!.addAll(plte!!.toList()) - } - - if (tnrs != null) { - png!!.addAll(tnrs!!.toList()) + val bodySize = lengthString.toLong(16).toInt() + cover.addAll(byteArray.copyOfRange(i - 4, i).toList()) + val body = ArrayList() + 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) + cover.addAll(body) + cover.addAll(to4Bytes(crC32.value.toInt()).toList()) + } else { + // Find the chunk length + var lengthString = "" + byteArray.copyOfRange(i - 4, i).forEach { + lengthString += String.format("%02x", it) + } + val bodySize = lengthString.toLong(16).toInt() + png.addAll(byteArray.copyOfRange(i - 4, i).toList()) + val body = ArrayList() + 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()) } } - } - else if (i == byteArray.size - 1) { - 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()) - } + // Check if is fdAT + else if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x64.toByte() && byteArray[i + 2] == 0x41.toByte() && byteArray[i + 3] == 0x54.toByte()) { // Find the chunk length var lengthString = "" - byteArray.copyOfRange( i - 4, i).forEach { + byteArray.copyOfRange(i - 4, i).forEach { lengthString += String.format("%02x", it) } val bodySize = lengthString.toLong(16).toInt() - cover!!.addAll(byteArray.copyOfRange(i-4, i).toList()) + png!!.addAll(to4Bytes(bodySize - 4).toList()) val body = ArrayList() body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList()) // 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]) } val crC32 = CRC32() crC32.update(body.toByteArray(), 0, body.size) - cover!!.addAll(body) - cover!!.addAll(to4Bytes(crC32.value.toInt()).toList()) - } else { - // Find the chunk length + 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 { + byteArray.copyOfRange(i - 4, i).forEach { lengthString += String.format("%02x", it) } val bodySize = lengthString.toLong(16).toInt() - png!!.addAll(byteArray.copyOfRange(i-4, i).toList()) - val body = ArrayList() - body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList()) - // Get image bytes - for (j in i +4 until i + 4 + bodySize) { - body.add(byteArray[j]) + 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 crC32 = CRC32() - crC32.update(body.toByteArray(), 0, body.size) - png!!.addAll(body) - png!!.addAll(to4Bytes(crC32.value.toInt()).toList()) + val bodySize = lengthString.toLong(16).toInt() + tnrs = byteArray.copyOfRange(i - 4, i + 8 + bodySize) } } - // Check if is fdAT - else if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x64.toByte() && byteArray[ i + 2 ] == 0x41.toByte() && byteArray[ i + 3 ] == 0x54.toByte()) { - // Find the chunk length - var lengthString = "" - 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() - 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 + return apng + } else { + throw NotApngException() } - apng.frames = pngList - - } else { - throw NotApngException() } - } - - private fun generate_ihdr(ihdrOfApng: IHDR, width : Int, height : Int) : ByteArray { - val ihdr = ArrayList() - // We need a body var to know body length and generate crc - val ihdr_body = ArrayList() - // Add chunk body length - ihdr.addAll(to4Bytes(ihdrOfApng.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(width).toList()) - ihdr_body.addAll(to4Bytes(height).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(ihdrOfApng.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() - } - - - fun genBitmap() : ArrayList { - val generatedFrame = ArrayList() - 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) + private fun generate_ihdr(ihdrOfApng: IHDR, width : Int, height : Int) : ByteArray { + val ihdr = ArrayList() + // We need a body var to know body length and generate crc + val ihdr_body = ArrayList() + // Add chunk body length + ihdr.addAll(to4Bytes(ihdrOfApng.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(width).toList()) + ihdr_body.addAll(to4Bytes(height).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(ihdrOfApng.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() } - return generatedFrame } } \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/Apng.kt b/apng_library/src/main/java/oupson/apng/Apng.kt index dc70eb8..f3b7c52 100644 --- a/apng_library/src/main/java/oupson/apng/Apng.kt +++ b/apng_library/src/main/java/oupson/apng/Apng.kt @@ -3,7 +3,6 @@ package oupson.apng import android.graphics.Bitmap import android.graphics.BitmapFactory import oupson.apng.ImageUtils.PnnQuantizer -import oupson.apng.Utils.Companion.convertImage import oupson.apng.Utils.Companion.getBlend_op import oupson.apng.Utils.Companion.getDispose_op 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.chunks.IDAT import oupson.apng.exceptions.NoFrameException -import java.io.ByteArrayOutputStream import java.util.zip.CRC32 diff --git a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt index aa86780..d76c60b 100644 --- a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt +++ b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt @@ -2,7 +2,6 @@ package oupson.apng import android.content.Context import android.graphics.* -import android.os.Handler import android.widget.ImageView import org.jetbrains.anko.doAsync import org.jetbrains.anko.uiThread @@ -13,7 +12,7 @@ import java.net.URL /** * Class to play APNG */ -class ApngAnimator(val context: Context) { +class ApngAnimator(private val context: Context) { var isPlaying = true private set(value) { field = value @@ -54,7 +53,7 @@ class ApngAnimator(val context: Context) { doAsync { this@ApngAnimator.speed = speed // Download PNG - APNGDisassembler(file.readBytes()).pngList.apply { + APNGDisassembler.disassemble(file.readBytes()).frames.apply { draw(this) } setupAnimationDrawableAndStart() @@ -71,7 +70,7 @@ class ApngAnimator(val context: Context) { doAsync(exceptionHandler = { e -> e.printStackTrace() }) { this@ApngAnimator.speed = speed // Download PNG - APNGDisassembler(Loader().load(context, url)).pngList.apply { + APNGDisassembler.disassemble(Loader.load(context, url)).frames.apply { draw(this) } setupAnimationDrawableAndStart() @@ -87,7 +86,7 @@ class ApngAnimator(val context: Context) { fun load(byteArray: ByteArray, speed: Float? = null) { doAsync { this@ApngAnimator.speed = speed - APNGDisassembler(byteArray).pngList.apply { + APNGDisassembler.disassemble(byteArray).frames.apply { draw(this) } setupAnimationDrawableAndStart() diff --git a/apng_library/src/main/java/oupson/apng/ApngFactory.kt b/apng_library/src/main/java/oupson/apng/ApngFactory.kt deleted file mode 100644 index 1ddb1e0..0000000 --- a/apng_library/src/main/java/oupson/apng/ApngFactory.kt +++ /dev/null @@ -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() - - /** - * @return a byte array of the generated png - * - * @throws NoFrameException - */ - fun create(): ByteArray { - - if (frames.isNotEmpty()) { - val res = ArrayList() - - // 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() - // region fcTL - // Create the fcTL - val fcTL = ArrayList() - // 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() - // 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() - val fcTL = ArrayList() - // 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() - // 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 { - val res = ArrayList() - val actl = ArrayList() - - // 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() - - // We need a body var to know body length and generate crc - val ihdr_body = ArrayList() - - // 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() - } -} \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/Frame.kt b/apng_library/src/main/java/oupson/apng/Frame.kt index 47991d2..d551c4b 100644 --- a/apng_library/src/main/java/oupson/apng/Frame.kt +++ b/apng_library/src/main/java/oupson/apng/Frame.kt @@ -1,7 +1,6 @@ package oupson.apng import android.graphics.BitmapFactory -import oupson.apng.Utils.Companion.convertImage import oupson.apng.Utils.Companion.isPng import oupson.apng.Utils.Companion.toByteArray import oupson.apng.chunks.IDAT diff --git a/apng_library/src/main/java/oupson/apng/ImageUtils/PnnQuantizer.java b/apng_library/src/main/java/oupson/apng/ImageUtils/PnnQuantizer.java index 0e51e6f..5aff13e 100644 --- a/apng_library/src/main/java/oupson/apng/ImageUtils/PnnQuantizer.java +++ b/apng_library/src/main/java/oupson/apng/ImageUtils/PnnQuantizer.java @@ -20,13 +20,13 @@ import java.util.Map; import java.util.Random; public class PnnQuantizer { - protected final short SHORT_MAX = Short.MAX_VALUE; - protected final char BYTE_MAX = -Byte.MIN_VALUE + Byte.MAX_VALUE; - protected boolean hasTransparency = false, hasSemiTransparency = false; + private final short SHORT_MAX = Short.MAX_VALUE; + private final char BYTE_MAX = -Byte.MIN_VALUE + Byte.MAX_VALUE; + private boolean hasTransparency = false, hasSemiTransparency = false; protected int width, height; protected int pixels[] = null; - protected Integer m_transparentColor; - protected Map closestMap = new HashMap<>(); + private Integer m_transparentColor; + private Map closestMap = new HashMap(); public PnnQuantizer(String fname) throws IOException { fromBitmap(fname); @@ -54,7 +54,7 @@ public class PnnQuantizer { int nn, fw, bk, tm, mtm; } - protected int getColorIndex(final int c) + private int getColorIndex(final int c) { if(hasSemiTransparency) 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); } - protected double sqr(double value) + private double sqr(double value) { return value * value; } @@ -412,7 +412,7 @@ public class PnnQuantizer { return true; } - protected Bitmap quantize_image(final int[] pixels, int[] qPixels) + private Bitmap quantize_image(final int[] pixels, int[] qPixels) { int pixelIndex = 0; boolean odd_scanline = false; diff --git a/apng_library/src/main/java/oupson/apng/Loader.kt b/apng_library/src/main/java/oupson/apng/Loader.kt index a0ffb33..efc8a9b 100644 --- a/apng_library/src/main/java/oupson/apng/Loader.kt +++ b/apng_library/src/main/java/oupson/apng/Loader.kt @@ -8,31 +8,33 @@ import java.io.IOException import java.net.URL class Loader { - fun load( context: Context, url : URL) : ByteArray { - val currenDir = context.filesDir - val fileTXT = File(currenDir, "apngLoader.txt") - var filePNG = File(currenDir, "apngLoader.png") - if (fileTXT.exists() && url.toString() == fileTXT.readText()) { - return filePNG.readBytes() - } else { - try { - val connection = url.openConnection() - connection.connect() - val input = BufferedInputStream(connection.getInputStream()) - val output = ByteArrayOutputStream() - val data = ByteArray(1024) - var count = 0 - while ({ count = input.read(data); count }() != -1) { - output.write(data, 0, count) + companion object { + fun load(context: Context, url: URL): ByteArray { + val currenDir = context.filesDir + val fileTXT = File(currenDir, "apngLoader.txt") + var filePNG = File(currenDir, "apngLoader.png") + if (fileTXT.exists() && url.toString() == fileTXT.readText()) { + return filePNG.readBytes() + } else { + try { + val connection = url.openConnection() + connection.connect() + val input = BufferedInputStream(connection.getInputStream()) + val output = ByteArrayOutputStream() + val data = ByteArray(1024) + var count = 0 + 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 } } } diff --git a/app-test/src/main/java/oupson/apngcreator/MainActivity.kt b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt index 873c39c..268d522 100644 --- a/app-test/src/main/java/oupson/apngcreator/MainActivity.kt +++ b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt @@ -1,6 +1,5 @@ package oupson.apngcreator -import android.graphics.BitmapFactory import android.os.Bundle import android.os.Environment import android.support.v7.app.AppCompatActivity @@ -8,14 +7,11 @@ import android.util.Log import android.widget.SeekBar import com.squareup.picasso.Picasso import kotlinx.android.synthetic.main.activity_main.* -import oupson.apng.ApngAnimator -import android.widget.Toast import org.jetbrains.anko.doAsync import org.jetbrains.anko.toast import oupson.apng.APNGDisassembler -import oupson.apng.Apng +import oupson.apng.ApngAnimator import oupson.apng.Loader -import oupson.apngcreator.R.id.imageView import java.io.File import java.net.URL @@ -40,9 +36,9 @@ class MainActivity : AppCompatActivity() { doAsync { - Loader().load(applicationContext, URL(imageUrl)).apply { - val a = APNGDisassembler(this).apng - a.reduceSize(100, false, 75) + Loader.load(applicationContext, URL(imageUrl)).apply { + val a = APNGDisassembler.disassemble(this) + a.reduceSize(100, false, 60) File(File(Environment.getExternalStorageDirectory(), "Documents"), "apng.png").writeBytes(a.toByteArray()) runOnUiThread { toast("Converted ! ")