Bug fix with disassembler.

Chunk optimisation
Working on apng optimiser ! Experimental, buggy
Add crc verification
This commit is contained in:
oupson 2018-12-19 08:28:49 +01:00
parent 6acbf227f2
commit 6d1f6af7c5
22 changed files with 496 additions and 191 deletions

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WizardSettings">
<option name="children">
<map>
<entry key="vectorWizard">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="vectorAssetStep">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="clipartAsset">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material_design_icons/content/ic_add_black_24dp.xml" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
<option name="values">
<map>
<entry key="color" value="ffffff" />
<entry key="outputName" value="ic_add_white_24dp" />
<entry key="sourceFile" value="C:\Users\oupso" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@ -3,6 +3,7 @@ package oupson.apng
import android.graphics.BitmapFactory
import oupson.apng.chunks.IHDR
import oupson.apng.chunks.fcTL
import oupson.apng.exceptions.BadCRC
import oupson.apng.exceptions.NotApngException
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.isApng
@ -25,19 +26,18 @@ class APNGDisassembler {
private var maxHeight = 0
private var blend_op: Utils.Companion.blend_op = Utils.Companion.blend_op.APNG_BLEND_OP_SOURCE
private var dispose_op: Utils.Companion.dispose_op = Utils.Companion.dispose_op.APNG_DISPOSE_OP_NONE
private val ihdr = IHDR()
private var ihdr = IHDR()
var apng: Apng = Apng()
/**
* Disassemble an Apng file
* @param byteArray The Byte Array of the file
* @return The apng decoded
*/
fun disassemble(byteArray: ByteArray) : Apng {
reset()
if (isApng(byteArray)) {
ihdr.parse(byteArray)
maxWidth = ihdr.pngWidth
maxHeight = ihdr.pngHeight
var cursor = 8
while (cursor < byteArray.size) {
val chunk = byteArray.copyOfRange(cursor, cursor + parseLength(byteArray.copyOfRange(cursor, cursor + 4)) + 12)
@ -74,41 +74,75 @@ class APNGDisassembler {
private fun parseChunk(byteArray: ByteArray) {
val i = 4
val name = Arrays.toString(byteArray.copyOfRange(i, i +4))
when (name) {
Utils.fcTL -> {
if (png == null) {
cover?.let {
it.addAll(to4Bytes(0).toList())
val chunkCRC = parseLength(byteArray.copyOfRange(byteArray.size - 4, byteArray.size))
val crc = CRC32();
crc.update(byteArray.copyOfRange(i, byteArray.size - 4))
if (chunkCRC == crc.value.toInt()) {
val name = Arrays.toString(byteArray.copyOfRange(i, i + 4))
when (name) {
Utils.fcTL -> {
if (png == null) {
cover?.let {
it.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)
it.addAll(iend.toList())
it.addAll(to4Bytes(crC32.value.toInt()).toList())
apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size)
}
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
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(generateIhdr(ihdr, width, height).toList())
plte?.let {
png!!.addAll(it.toList())
}
tnrs?.let {
png!!.addAll(it.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)
it.addAll(iend.toList())
it.addAll(to4Bytes(crC32.value.toInt()).toList())
apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size)
png!!.addAll(iend.toList())
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
apng.frames.add(Frame(png!!.toByteArray(), delay, xOffset, yOffset, maxWidth, maxHeight, blend_op, dispose_op))
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
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(generateIhdr(ihdr, width, height).toList())
plte?.let {
png!!.addAll(it.toList())
}
tnrs?.let {
png!!.addAll(it.toList())
}
}
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
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(generateIhdr(ihdr, width, height).toList())
plte?.let {
png!!.addAll(it.toList())
}
tnrs?.let {
png!!.addAll(it.toList())
}
} else {
// Add IEND body length : 0
}
Utils.IEND -> {
png!!.addAll(to4Bytes(0).toList())
// Add IEND
val iend = byteArrayOf(0x49, 0x45, 0x4E, 0x44)
@ -118,89 +152,79 @@ class APNGDisassembler {
png!!.addAll(iend.toList())
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
apng.frames.add(Frame(png!!.toByteArray(), delay, xOffset, yOffset, maxWidth, maxHeight, blend_op, dispose_op))
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
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(generateIhdr(ihdr, width, height).toList())
plte?.let {
png!!.addAll(it.toList())
}
tnrs?.let {
png!!.addAll(it.toList())
}
Utils.IDAT -> {
if (png == null) {
if (cover == null) {
cover = ArrayList()
cover!!.addAll(pngSignature.toList())
cover!!.addAll(generateIhdr(ihdr, maxWidth, maxHeight).toList())
}
// Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
cover!!.addAll(byteArray.copyOfRange(i - 4, i).toList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).toList())
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
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
png!!.addAll(byteArray.copyOfRange(i - 4, i).toList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).toList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png!!.addAll(body)
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
}
}
}
Utils.IEND -> {
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())
apng.frames.add(Frame(png!!.toByteArray(), delay, xOffset, yOffset, maxWidth, maxHeight, blend_op, dispose_op))
}
Utils.IDAT -> {
if (png == null) {
if (cover == null) {
cover = ArrayList()
cover!!.addAll(pngSignature.toList())
cover!!.addAll(generateIhdr(ihdr, maxWidth, maxHeight).toList())
}
Utils.fdAT -> {
// Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
cover!!.addAll(byteArray.copyOfRange(i - 4, i).toList())
png!!.addAll(to4Bytes(bodySize - 4).toList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4,i + 4 + bodySize).toList())
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
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
png!!.addAll(byteArray.copyOfRange(i - 4, i).toList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4,i + 4 + bodySize).toList())
body.addAll(byteArray.copyOfRange(i + 8, i + 4 + bodySize).toList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png!!.addAll(body)
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
}
Utils.plte -> {
plte = byteArray
}
Utils.tnrs -> {
tnrs = byteArray
}
Utils.IHDR -> {
ihdr.parse(byteArray)
maxWidth = ihdr.pngWidth
maxHeight = ihdr.pngHeight
}
}
Utils.fdAT -> {
// Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
png!!.addAll(to4Bytes(bodySize - 4).toList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 8,i + 4 + bodySize).toList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png!!.addAll(body)
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
}
Utils.plte -> {
plte = byteArray
}
Utils.tnrs -> {
tnrs = byteArray
}
}
} else throw BadCRC()
}
private fun reset() {
png = null
cover = null
delay = -1f
yOffset = -1
xOffset = -1
plte = null
tnrs = null
maxWidth = 0
maxHeight = 0
ihdr = IHDR()
apng = Apng()
}
}
}

View File

@ -2,6 +2,7 @@ package oupson.apng
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import oupson.apng.ImageUtils.BitmapDiffCalculator
import oupson.apng.ImageUtils.PngEncoder
import oupson.apng.ImageUtils.PnnQuantizer
import oupson.apng.chunks.IDAT
@ -37,7 +38,7 @@ class Apng {
* @param bitmap The bitamp to add
*/
fun addFrames(bitmap: Bitmap) {
frames.add(Frame(PngEncoder.encode(bitmap, true, 1)))
frames.add(Frame(PngEncoder.encode(bitmap, true)))
}
/**
@ -46,7 +47,7 @@ class Apng {
* @param delay Delay of the frame
*/
fun addFrames(bitmap: Bitmap, delay : Float) {
frames.add(Frame(PngEncoder.encode(bitmap, true, 1), delay))
frames.add(Frame(PngEncoder.encode(bitmap, true), delay))
}
/**
@ -57,7 +58,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(bitmap: Bitmap, delay: Float, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(Frame(PngEncoder.encode(bitmap, true, 1), delay, blend_op, dispose_op))
frames.add(Frame(PngEncoder.encode(bitmap, true), delay, blend_op, dispose_op))
}
/**
@ -70,7 +71,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(bitmap: Bitmap, delay: Float, xOffset : Int, yOffset : Int, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(Frame(PngEncoder.encode(bitmap, true, 1), delay, xOffset, yOffset, blend_op, dispose_op))
frames.add(Frame(PngEncoder.encode(bitmap, true), delay, xOffset, yOffset, blend_op, dispose_op))
}
/**
@ -79,7 +80,7 @@ class Apng {
* @param bitmap The bitamp to add
*/
fun addFrames(index : Int, bitmap: Bitmap) {
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1)))
frames.add(index, Frame(PngEncoder.encode(bitmap, true)))
}
/**
@ -89,7 +90,7 @@ class Apng {
* @param delay Delay of the frame
*/
fun addFrames(index : Int, bitmap: Bitmap, delay : Float) {
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1), delay))
frames.add(index, Frame(PngEncoder.encode(bitmap, true), delay))
}
/**
@ -101,7 +102,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(index: Int, bitmap: Bitmap, delay: Float, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1), delay, blend_op, dispose_op))
frames.add(index, Frame(PngEncoder.encode(bitmap, true), delay, blend_op, dispose_op))
}
/**
@ -115,7 +116,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(index: Int, bitmap: Bitmap, delay: Float, xOffset : Int, yOffset : Int, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1), delay, xOffset, yOffset, blend_op, dispose_op))
frames.add(index, Frame(PngEncoder.encode(bitmap, true), delay, xOffset, yOffset, blend_op, dispose_op))
}
fun addFrames(frame : Frame) {
@ -508,4 +509,20 @@ class Apng {
}
frames = apng.frames
}
fun optimiseFrame() {
maxHeight = frames.sortedByDescending { it.height }[0].height
maxWidth = frames.sortedByDescending { it.width }[0].width
frames.forEach {
it.maxWidth = maxWidth
it.maxHeight = maxHeight
}
val drawedFrame = ApngAnimator(null).draw(frames)
for (i in 1 until frames.size) {
val diffCalculator = BitmapDiffCalculator(drawedFrame[i - 1], drawedFrame[i])
frames[i].byteArray = PngEncoder.encode(diffCalculator.res)
frames[i].x_offsets = diffCalculator.xOffset
frames[i].y_offsets = diffCalculator.yOffset
}
}
}

View File

@ -18,7 +18,7 @@ import java.net.URL
/**
* Class to play APNG
*/
class ApngAnimator(private val context: Context) {
class ApngAnimator(private val context: Context?) {
var isPlaying = true
private set(value) {
field = value
@ -36,7 +36,7 @@ class ApngAnimator(private val context: Context) {
}
}
private var imageView: ImageView? = null
private var anim: CustomAnimationDrawable? = null
var anim: CustomAnimationDrawable? = null
private var activeAnimation: CustomAnimationDrawable? = null
private var doOnLoaded : (ApngAnimator) -> Unit = {}
@SuppressWarnings("WeakerAccess")
@ -47,10 +47,10 @@ class ApngAnimator(private val context: Context) {
@SuppressWarnings("WeakerAccess")
var loadNotApng = true
private val sharedPreferences : SharedPreferences = context.getSharedPreferences("apngAnimator", Context.MODE_PRIVATE)
private val sharedPreferences : SharedPreferences? = context?.getSharedPreferences("apngAnimator", Context.MODE_PRIVATE)
init {
loadNotApng = sharedPreferences.getBoolean("loadNotApng", true)
loadNotApng = sharedPreferences?.getBoolean("loadNotApng", true) ?: true
}
/**
@ -58,9 +58,9 @@ class ApngAnimator(private val context: Context) {
*/
@SuppressWarnings("WeakerAccess")
fun loadNotApng(boolean: Boolean) {
val editor = sharedPreferences.edit()
editor.putBoolean("loadNotApng", boolean)
editor.apply()
val editor = sharedPreferences?.edit()
editor?.putBoolean("loadNotApng", boolean)
editor?.apply()
}
/**
@ -94,7 +94,7 @@ class ApngAnimator(private val context: Context) {
}
} else {
if (loadNotApng) {
context.runOnUiThread {
context?.runOnUiThread {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.size))
}
@ -105,6 +105,8 @@ class ApngAnimator(private val context: Context) {
}
}
/**
* Load an APNG file and starts playing the animation.
* @param uri The uri to load
@ -113,25 +115,26 @@ class ApngAnimator(private val context: Context) {
*/
fun load(uri : Uri, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) {
doAsync {
val bytes = context.contentResolver.openInputStream(uri).readBytes()
if (isApng(bytes)) {
isApng = true
this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType
// Download PNG
APNGDisassembler.disassemble(bytes).frames.apply {
draw(this).apply {
setupAnimationDrawableAndStart(this)
}
}
} else {
if (loadNotApng) {
context.runOnUiThread {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.size))
context?.contentResolver?.openInputStream(uri)?.readBytes()?.let {
if (isApng(it)) {
isApng = true
this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType
// Download PNG
APNGDisassembler.disassemble(it).frames.apply {
draw(this).apply {
setupAnimationDrawableAndStart(this)
}
}
} else {
throw NotApngException()
if (loadNotApng) {
context.runOnUiThread {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(it, 0, it.size))
}
} else {
throw NotApngException()
}
}
}
}
@ -148,7 +151,7 @@ class ApngAnimator(private val context: Context) {
doAsync(exceptionHandler = { e -> e.printStackTrace() }) {
this@ApngAnimator.speed = speed
// Download PNG
Loader.load(context, url).apply {
Loader.load(context!!, url).apply {
if (isApng(this)) {
isApng = true
this@ApngAnimator.speed = speed
@ -197,7 +200,7 @@ class ApngAnimator(private val context: Context) {
}
} else {
if (loadNotApng) {
context.runOnUiThread {
context?.runOnUiThread {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size))
}
@ -223,7 +226,7 @@ class ApngAnimator(private val context: Context) {
} else if (File(string).exists()) {
var pathToLoad = if (string.startsWith("content://")) string else "file://$string"
pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
val bytes = context.contentResolver.openInputStream(Uri.parse(pathToLoad)).readBytes()
val bytes = context!!.contentResolver.openInputStream(Uri.parse(pathToLoad)).readBytes()
if (isApng(bytes)) {
load(bytes, speed, apngAnimatorOptions)
} else {
@ -261,7 +264,7 @@ class ApngAnimator(private val context: Context) {
/**
* Draw frames
*/
private fun draw(extractedFrame: ArrayList<Frame>) : ArrayList<Bitmap> {
fun draw(extractedFrame: ArrayList<Frame>) : ArrayList<Bitmap> {
val generatedFrame = ArrayList<Bitmap>()
// Set last frame
duration = ArrayList()

View File

@ -3,10 +3,10 @@ package oupson.apng
import android.graphics.drawable.AnimationDrawable
/**
* Extension of the [AnimationDrawable] that provides an [AnimationListener]. This will allow
* Extension of the [AnimationDrawable] that provides an animationListener This will allow
* for the caller to listen for specific animation related events.
*/
internal class CustomAnimationDrawable : AnimationDrawable() {
class CustomAnimationDrawable : AnimationDrawable() {
private var onAnimationLoop : () -> Unit = {}
fun setOnAnimationLoopListener( f : () -> Unit) {

View File

@ -4,11 +4,13 @@ import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import oupson.apng.utils.Utils
class BitmapDiffCalculator(val firstBitmap: Bitmap, val secondBitmap : Bitmap) {
class BitmapDiffCalculator(firstBitmap: Bitmap, secondBitmap : Bitmap) {
val res : Bitmap
var xOffset : Int = 0
var yOffset : Int = 0
var blend_op = Utils.Companion.blend_op.APNG_BLEND_OP_OVER
init {
val difBitmap = Bitmap.createBitmap(firstBitmap.width, firstBitmap.height, Bitmap.Config.ARGB_8888)
val difCanvas = Canvas(difBitmap)
@ -35,7 +37,9 @@ class BitmapDiffCalculator(val firstBitmap: Bitmap, val secondBitmap : Bitmap) {
}
bottomLoop@ while (true) {
for (x in 0 until difBitmap.width) {
if (difBitmap.getPixel(x, height - 1) != Color.TRANSPARENT) {
if (height - 1 < 0) {
break@bottomLoop
} else if (difBitmap.getPixel(x, height - 1) != Color.TRANSPARENT) {
break@bottomLoop
}
}

View File

@ -7,14 +7,13 @@ class IDAT : Chunk {
override var body = byteArrayOf()
override fun parse(byteArray: ByteArray) {
for (i in 0 until byteArray.size) {
// Find IDAT chunk
if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x44.toByte() && byteArray[ i + 2 ] == 0x41.toByte() && byteArray[ i + 3 ] == 0x54.toByte()) {
// Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
// Get image bytes
IDATBody.add(byteArray.copyOfRange(i + 4, i + 4 + bodySize))
}
val i = 4
// Find IDAT chunk
if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x44.toByte() && byteArray[i + 2] == 0x41.toByte() && byteArray[i + 3] == 0x54.toByte()) {
// Find the chunk length
val bodySize = parseLength(byteArray.copyOfRange(i - 4, i))
// Get image bytes
IDATBody.add(byteArray.copyOfRange(i + 4, i + 4 + bodySize))
}
}
}

View File

@ -23,36 +23,35 @@ class fcTL : Chunk {
var dispose_op : Utils.Companion.dispose_op = Utils.Companion.dispose_op.APNG_DISPOSE_OP_NONE
override fun parse(byteArray: ByteArray) {
for (i in 0 until byteArray.size) {
// Find fcTL chunk
if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x63.toByte() && byteArray[ i + 2 ] == 0x54.toByte() && byteArray[ i + 3 ] == 0x4C.toByte()) {
// Get length of the body of the chunk
var bodySize = parseLength(byteArray.copyOfRange(i - 4, 1))
// Get the width of the png
pngWidth = parseLength(byteArray.copyOfRange(i + 8, i + 12))
// Get the height of the png
pngHeight = parseLength(byteArray.copyOfRange(i + 12, i + 16))
/*
val i = 4
// Find fcTL chunk
if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x63.toByte() && byteArray[i + 2] == 0x54.toByte() && byteArray[i + 3] == 0x4C.toByte()) {
// Get length of the body of the chunk
val bodySize = parseLength(byteArray.copyOfRange(i - 4, 1))
// Get the width of the png
pngWidth = parseLength(byteArray.copyOfRange(i + 8, i + 12))
// Get the height of the png
pngHeight = parseLength(byteArray.copyOfRange(i + 12, i + 16))
/*
* The `delay_num` and `delay_den` parameters together specify a fraction indicating the time to display the current frame, in seconds.
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound.
*/
// Get delay numerator
val delay_num = parseLength(byteArray.copyOfRange(i + 24, i + 26)).toFloat()
// Get delay denominator
var delay_den = parseLength(byteArray.copyOfRange(i + 26, i + 28)).toFloat()
// Get delay numerator
val delay_num = parseLength(byteArray.copyOfRange(i + 24, i + 26)).toFloat()
// Get delay denominator
var delay_den = parseLength(byteArray.copyOfRange(i + 26, i + 28)).toFloat()
// If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second).
if (delay_den == 0f) {
delay_den = 100f
}
delay = (delay_num / delay_den * 1000)
// Get x and y offsets
x_offset = parseLength(byteArray.copyOfRange(i + 16, i+ 20))
y_offset = parseLength( byteArray.copyOfRange(i + 20, i+ 24))
body = byteArray.copyOfRange(i +4, i + bodySize + 4)
blend_op = getBlend_op(String.format("%02x", byteArray[33]).toLong(16).toInt())
dispose_op = getDispose_op(String.format("%02x", byteArray[32]).toLong(16).toInt())
// If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second).
if (delay_den == 0f) {
delay_den = 100f
}
delay = (delay_num / delay_den * 1000)
// Get x and y offsets
x_offset = parseLength(byteArray.copyOfRange(i + 16, i + 20))
y_offset = parseLength(byteArray.copyOfRange(i + 20, i + 24))
body = byteArray.copyOfRange(i + 4, i + bodySize + 4)
blend_op = getBlend_op(String.format("%02x", byteArray[33]).toLong(16).toInt())
dispose_op = getDispose_op(String.format("%02x", byteArray[32]).toLong(16).toInt())
}
}
}

View File

@ -3,4 +3,5 @@ package oupson.apng.exceptions
class NoFrameException : Exception()
class NotPngException : Exception()
class NotApngException : Exception()
class NoFcTL : Exception()
class NoFcTL : Exception()
class BadCRC : Exception()

View File

@ -143,5 +143,6 @@ class Utils {
val fdAT = Arrays.toString(byteArrayOf(0x66, 0x64, 0x41, 0x54))
val plte = Arrays.toString(byteArrayOf(0x50, 0x4c, 0x54, 0x45))
val tnrs = Arrays.toString(byteArrayOf(0x74, 0x52, 0x4e, 0x53))
val IHDR = Arrays.toString(byteArrayOf(0x49, 0x48, 0x44, 0x52))
}
}

View File

@ -35,7 +35,6 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//noinspection GradleCompatible
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
@ -48,6 +47,7 @@ dependencies {
// implementation fileTree(include: ['*.aar'], dir: 'libs')
//implementation 'com.github.oupson:Kapng-Android:1.0.0'
implementation 'com.android.support:design:27.1.1' // where X.X.X version
implementation 'asia.ivity.android:drag-sort-listview:1.0'
}
kotlin {
experimental {

View File

@ -8,13 +8,13 @@
<application
android:allowBackup="true"
android:hardwareAccelerated="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:hardwareAccelerated="false"
android:largeHeap="true">
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -22,17 +22,19 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".Main2Activity"
<activity
android:name=".Main2Activity"
android:label="APNG Viewer">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/png"/>
<data android:mimeType="image/png" />
</intent-filter>
</activity>
<activity android:name=".CreatorActivity"></activity>
</application>
</manifest>

View File

@ -0,0 +1,96 @@
package oupson.apngcreator
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.util.Log
import kotlinx.android.synthetic.main.activity_creator.*
import org.jetbrains.anko.alert
import org.jetbrains.anko.customView
import org.jetbrains.anko.imageView
import org.jetbrains.anko.sdk27.coroutines.onClick
import oupson.apng.APNGDisassembler
import oupson.apng.Apng
import oupson.apng.ApngAnimator
import oupson.apng.ImageUtils.PngEncoder
import oupson.apngcreator.adapter.frameListViewAdapter
import java.io.File
class CreatorActivity : AppCompatActivity() {
var items : ArrayList<Bitmap> = ArrayList()
var bitmapAdapter : frameListViewAdapter? = null
val PICK_IMAGE = 999
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_creator)
fab_add_frame.onClick {
val getIntent = Intent(Intent.ACTION_GET_CONTENT)
getIntent.type = "image/*"
val pickIntent = Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
pickIntent.type = "image/*"
val chooserIntent = Intent.createChooser(getIntent, "Select Image")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(pickIntent))
startActivityForResult(chooserIntent, PICK_IMAGE)
}
fab_create.onClick {
var apngCreated = Apng()
items.forEach {
apngCreated.addFrames(it)
}
Log.e("tag", apngCreated.frames.size.toString())
apngCreated = APNGDisassembler.disassemble(apngCreated.toByteArray())
apngCreated.optimiseFrame()
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "vtm").writeBytes(apngCreated.toByteArray())
val a = ApngAnimator(applicationContext)
a.load(apngCreated.toByteArray())
a.onLoaded {
alert {
customView {
imageView {
Log.e("tag", "${it.anim?.numberOfFrames.toString()} : ${items.size}")
it.anim?.let {
for (i in 0 until it.numberOfFrames) {
val vt = Bitmap.createBitmap(it.getFrame(i).intrinsicWidth, it.getFrame(i).intrinsicHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(vt)
it.getFrame(i).draw(canvas)
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "frame$i.png").writeBytes(PngEncoder.encode(vt))
}
}
this.setImageDrawable(it.anim)
}
}
}.show()
}
}
bitmapAdapter = frameListViewAdapter(this, items)
dragView.adapter = bitmapAdapter
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode) {
PICK_IMAGE -> {
if (resultCode == Activity.RESULT_OK) {
contentResolver.openInputStream(data?.data).readBytes().apply {
items.add(BitmapFactory.decodeByteArray(this, 0, this.size))
bitmapAdapter?.notifyDataSetChanged()
}
}
}
}
}
}

View File

@ -1,21 +1,24 @@
package oupson.apngcreator
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.SeekBar
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_main.*
import oupson.apng.ApngAnimator
import java.util.*
class MainActivity : AppCompatActivity() {
lateinit var animator: ApngAnimator
//val imageUrl = "https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"
//val imageUrl = "http://orig06.deviantart.net/7812/f/2012/233/7/5/twilight_rapidash_shaded_and_animated_by_tamalesyatole-d5bz7hd.png"
val imageUrl = "https://raw.githubusercontent.com/tinify/iMessage-Panda-sticker/master/StickerPackExtension/Stickers.xcstickers/Sticker%20Pack.stickerpack/panda.sticker/panda.png"
// val imageUrl = "http://oupson.oupsman.fr/apng/bigApng.png"
// val imageUrl = "https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"
val imageUrl = "http://orig06.deviantart.net/7812/f/2012/233/7/5/twilight_rapidash_shaded_and_animated_by_tamalesyatole-d5bz7hd.png"
// val imageUrl = "https://raw.githubusercontent.com/tinify/iMessage-Panda-sticker/master/StickerPackExtension/Stickers.xcstickers/Sticker%20Pack.stickerpack/panda.sticker/panda.png"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@ -53,4 +56,20 @@ class MainActivity : AppCompatActivity() {
animator.pause()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
when(item?.itemId) {
R.id.action_open_create_activity -> {
val intent = Intent(this, CreatorActivity::class.java)
startActivity(intent)
finish()
}
}
return super.onOptionsItemSelected(item)
}
}

View File

@ -0,0 +1,21 @@
package oupson.apngcreator.adapter
import android.content.Context
import android.graphics.Bitmap
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import oupson.apngcreator.R
class frameListViewAdapter (context: Context, val bitmaps: List<Bitmap>) : ArrayAdapter<Bitmap>(context, 0, bitmaps) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = LayoutInflater.from(context).inflate(R.layout.framelistviewadapterlayout, parent, false)
val imageView = view.findViewById<ImageView>(R.id.frameAdapterImageView)
imageView.setImageBitmap(bitmaps[position])
return view
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CreatorActivity">
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_create"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_play_arrow_white_24dp" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_add_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:layout_marginBottom="16dp"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_play_arrow_white_24dp" />
<ListView
android:id="@+id/dragView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:id="@+id/frameAdapterImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_open_create_activity"
android:title="Create apng"
app:showAsAction="never"/>
</menu>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="drag_handle" />
</resources>