Prepare to first release

This commit is contained in:
oupson 2018-10-24 12:20:16 +02:00
parent efbe651377
commit 13a6a7266e
17 changed files with 68 additions and 145 deletions

View File

@ -5,28 +5,25 @@ import android.graphics.BitmapFactory
import android.graphics.Canvas
import oupson.apng.ApngFactory.Companion.pngSignature
import oupson.apng.Utils.Companion.to4Bytes
import oupson.apng.chunks.IHDR
import oupson.apng.chunks.fcTL
import oupson.apng.exceptions.NotApngException
import java.util.zip.CRC32
class APNGDisassembler(val byteArray: ByteArray) {
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
init {
if (ApngFactory.isApng(byteArray)) {
val ihdr = IHDR()
@ -48,8 +45,7 @@ class APNGDisassembler(val byteArray: ByteArray) {
cover!!.addAll(to4Bytes(crC32.value.toInt()).toList())
}
png = ArrayList()
val fcTL = fcTL(byteArray.copyOfRange(i-4, i + 32))
val fcTL = fcTL(byteArray.copyOfRange(i - 4, i + 32))
delay = fcTL.delay
yOffset = fcTL.y_offset
xOffset = fcTL.x_offset
@ -81,7 +77,7 @@ class APNGDisassembler(val byteArray: ByteArray) {
png = ArrayList()
val fcTL = fcTL(byteArray.copyOfRange(i-4, i + 32))
val fcTL = fcTL(byteArray.copyOfRange(i - 4, i + 32))
delay = fcTL.delay
yOffset = fcTL.y_offset
@ -154,8 +150,6 @@ class APNGDisassembler(val byteArray: ByteArray) {
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>()
@ -168,22 +162,23 @@ class APNGDisassembler(val byteArray: ByteArray) {
crC32.update(body.toByteArray(), 0, body.size)
png!!.addAll(body)
png!!.addAll(to4Bytes(crC32.value.toInt()).toList())
} else if (byteArray[i] == 0x50.toByte() && byteArray[i + 1] == 0x4C.toByte() && byteArray[ i + 2 ] == 0x54.toByte() && byteArray[ i + 3 ] == 0x45.toByte()) {
}
// 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()
val body = ArrayList<Byte>()
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()
val body = ArrayList<Byte>()
tnrs = byteArray.copyOfRange( i -4, i + 8 + bodySize)
}
}
@ -194,25 +189,18 @@ class APNGDisassembler(val byteArray: ByteArray) {
private fun generate_ihdr(ihdrOfApng: IHDR, width : Int, height : Int) : 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
// 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)
@ -232,7 +220,4 @@ class APNGDisassembler(val byteArray: ByteArray) {
}
return generatedFrame
}
}

View File

@ -2,13 +2,14 @@ package oupson.apng
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
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.to2Bytes
import oupson.apng.Utils.Companion.to4Bytes
import oupson.apng.Utils.Companion.toByteArray
import oupson.apng.chunks.IDAT
import oupson.apng.exceptions.NoFrameException
import java.util.zip.CRC32
@ -20,6 +21,11 @@ class Apng {
var maxWidth : Int? = null
var maxHeight : Int? = null
/**
* Image that will display in non compatible reader
* It's not necessary if the first frame is the biggest image.
* If it's null the library generate a cover with the first frame
*/
var cover : Bitmap? = null
set(value) {
field = convertImage(value!!)
@ -141,6 +147,7 @@ class Apng {
// region fcTL
// Create the fcTL
val fcTL = ArrayList<Byte>()
// Add the length of the chunk body
framesByte.addAll(byteArrayOf(0x00, 0x00, 0x00, 0x1A).toList())
@ -149,6 +156,7 @@ class Apng {
// Add the frame number
fcTL.addAll(to4Bytes(seq).toList())
// foreach fcTL or fdAT we must increment seq
seq++
// Add width and height
@ -156,7 +164,6 @@ class Apng {
fcTL.addAll(to4Bytes(frames[0].height).toList())
// Calculate offset
if (frames[0].x_offsets == null) {
if (frames[0].width < maxWitdh) {
val xOffset = (maxWitdh / 2) - (frames[0].width / 2)
@ -179,9 +186,11 @@ class Apng {
fcTL.addAll(to2Bytes(frames[0].delay.toInt()).toList())
fcTL.addAll(to2Bytes(1000).toList())
// Add dispose_op and blend_op
fcTL.add(getDispose_op(frames[0].dispose_op).toByte())
fcTL.add(getBlend_op(frames[0].blend_op).toByte())
// Create CRC
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
@ -190,25 +199,25 @@ class Apng {
// region idat
frames[0].idat.IDATBody.forEach {
val fdat = ArrayList<Byte>()
val idat = ArrayList<Byte>()
// Add the chunk body length
framesByte.addAll(to4Bytes(it.size).toList())
// Add IDAT
fdat.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
idat.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
// Add chunk body
fdat.addAll(it.toList())
idat.addAll(it.toList())
// Generate CRC
val crc1 = CRC32()
crc1.update(fdat.toByteArray(), 0, fdat.size)
framesByte.addAll(fdat)
crc1.update(idat.toByteArray(), 0, idat.size)
framesByte.addAll(idat)
framesByte.addAll(to4Bytes(crc1.value.toInt()).toList())
}
// endregion
res.addAll(framesByte)
} else {
val framesByte = ArrayList<Byte>()
// Add cover image : Not part of annimation
// region idat
// Add cover image : Not part of animation
// region IDAT
val idat = IDAT()
idat.parseIDAT(toByteArray(cover!!))
idat.IDATBody.forEach {
@ -223,10 +232,10 @@ class Apng {
}
// endregion
//region fcTL
val fcTL = ArrayList<Byte>()
// Add the length of the chunk body
framesByte.addAll(byteArrayOf(0x00, 0x00, 0x00, 0x1A).toList())
// Add fcTL
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).toList())
@ -261,9 +270,11 @@ class Apng {
fcTL.addAll(to2Bytes(frames[0].delay.toInt()).toList())
fcTL.addAll(to2Bytes(1000).toList())
// Add dispose_op and blend_op
fcTL.add(getDispose_op(frames[0].dispose_op).toByte())
fcTL.add(getBlend_op(frames[0].blend_op).toByte())
// Generate CRC
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
@ -292,7 +303,6 @@ class Apng {
}
for (i in 1 until frames.size) {
Log.e("Seq", seq.toString())
// If it's the first frame
val framesByte = ArrayList<Byte>()
val fcTL = ArrayList<Byte>()
@ -362,8 +372,6 @@ class Apng {
// endregion
res.addAll(framesByte)
}
if (frames.isNotEmpty()) {
// Add IEND body length : 0
@ -381,7 +389,14 @@ class Apng {
}
}
/**
* Generate a cover image that have the max width and height.
* You could also set yours
* @param bitmap The bitmap of the cover
* @param maxWidth Max width of APNG
* @param maxHeight Max height of the APNG
* @return An image cover
*/
fun generateCover(bitmap: Bitmap, maxWidth : Int, maxHeight : Int) : Bitmap {
return Bitmap.createScaledBitmap(bitmap, maxWidth, maxHeight, false)
}

View File

@ -4,11 +4,9 @@ import android.graphics.*
import android.graphics.drawable.AnimationDrawable
import android.os.Environment
import android.os.Handler
import android.util.Log
import android.widget.ImageView
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import oupson.apng.Utils.Companion.toByteArray
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
@ -17,12 +15,11 @@ import java.net.URL
class ApngAnimator {
var isPlaying = true
get() = field
private set(value) {field = value}
var Frames = ArrayList<Frame>()
var myHandler: Handler
var myHandler: Handler = Handler()
var counter = 0
@ -35,14 +32,8 @@ class ApngAnimator {
var background : Bitmap? = null
var isDebug = false
val imageView : ImageView?
init {
myHandler = Handler()
}
constructor() {
imageView = null
}
@ -50,7 +41,11 @@ class ApngAnimator {
this.imageView = imageView
}
/**
* Load an APNG file
* @param file The file to load
* @throws NotApngException
*/
fun load(file: File) {
// Download PNG
val extractedFrame = APNGDisassembler(file.readBytes()).pngList
@ -65,28 +60,20 @@ class ApngAnimator {
for (i in 1 until Frames.size) {
// Iterator
val it = Frames.get(i)
// Current bitmap for the frame
val btm = Bitmap.createBitmap(Frames[0].maxWidth!!, Frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
val canvas = Canvas(btm)
val current = BitmapFactory.decodeByteArray(it.byteArray, 0, it.byteArray.size).copy(Bitmap.Config.ARGB_8888, true)
// Write buffer to canvas
canvas.drawBitmap(bitmapBuffer, 0f, 0f, null)
// Clear current frame rect
// If `blend_op` is APNG_BLEND_OP_SOURCE all color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
if (it.blend_op == Utils.Companion.blend_op.APNG_BLEND_OP_SOURCE) {
canvas.drawRect(it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), it.x_offsets!! + current.width.toFloat(), it.y_offsets!! + current.height.toFloat(), { val paint = Paint(); paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR); paint }())
}
// Draw the bitmap
canvas.drawBitmap(current, it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), null)
generatedFrame.add(btm)
// Don't add current frame to bitmap buffer
if (Frames[i].dispose_op == Utils.Companion.dispose_op.APNG_DISPOSE_OP_PREVIOUS) {
//Do nothings
@ -104,76 +91,42 @@ class ApngAnimator {
bitmapBuffer = btm
}
}
// DEBUG FUNCTION : WRITE RENDERED FRAME TO EXTERNAL STORAGE
if (isDebug) {
for (i in 0 until generatedFrame.size) {
try {
FileOutputStream(File(File(Environment.getExternalStorageDirectory(), "Documents"), "image_$i.png")).use { out ->
generatedFrame[i].compress(Bitmap.CompressFormat.PNG, 100, out) // bmp is your Bitmap instance
// PNG is a lossless format, the compression factor (100) is ignored
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
nextFrame()
}
/**
* Load an APNG file
* @param url URL to load
* @throws NotApngException
*/
private fun loadUrl(url : URL) {
doAsync(exceptionHandler = {e -> e.printStackTrace()}) {
doAsync(exceptionHandler = {e -> throw e}) {
// Download PNG
val extractedFrame = APNGDisassembler(Loader().load(url)).pngList
// DEBUG FUNCTION : WRITE RENDERED FRAME TO EXTERNAL STORAGE
if (isDebug) {
for (i in 0 until extractedFrame.size) {
try {
FileOutputStream(File(File(Environment.getExternalStorageDirectory(), "Documents"), "image_$i.png")).use { out ->
out.write(extractedFrame[i].byteArray)
// PNG is a lossless format, the compression factor (100) is ignored
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
// Set last frame
lastFrame = extractedFrame[0]
// Init image buffer
bitmapBuffer = BitmapFactory.decodeByteArray(lastFrame?.byteArray!!, 0, lastFrame?.byteArray!!.size)
Log.e("ApngAnimator", "bitmapBuffer is null : ${bitmapBuffer == null}")
generatedFrame.add(BitmapFactory.decodeByteArray(lastFrame?.byteArray, 0, lastFrame?.byteArray!!.size))
Frames = extractedFrame
for (i in 1 until Frames.size) {
Log.e("ApngAnimator", "Render $i frame")
// Iterator
val it = Frames.get(i)
// Current bitmap for the frame
val btm = Bitmap.createBitmap(Frames[0].maxWidth!!, Frames[0].maxHeight!!, Bitmap.Config.ARGB_8888)
val canvas = Canvas(btm)
val current = BitmapFactory.decodeByteArray(it.byteArray, 0, it.byteArray.size).copy(Bitmap.Config.ARGB_8888, true)
// Write buffer to canvas
canvas.drawBitmap(bitmapBuffer, 0f, 0f, null)
// Clear current frame rect
// If `blend_op` is APNG_BLEND_OP_SOURCE all color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
if (it.blend_op == Utils.Companion.blend_op.APNG_BLEND_OP_SOURCE) {
canvas.drawRect(it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), it.x_offsets!! + current.width.toFloat(), it.y_offsets!! + current.height.toFloat(), { val paint = Paint(); paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR); paint }())
}
// Draw the bitmap
canvas.drawBitmap(current, it.x_offsets!!.toFloat(), it.y_offsets!!.toFloat(), null)
generatedFrame.add(btm)
// Don't add current frame to bitmap buffer
if (Frames[i].dispose_op == Utils.Companion.dispose_op.APNG_DISPOSE_OP_PREVIOUS) {
//Do nothings
@ -191,19 +144,6 @@ class ApngAnimator {
bitmapBuffer = btm
}
}
// DEBUG FUNCTION : WRITE RENDERED FRAME TO EXTERNAL STORAGE
if (isDebug) {
for (i in 0 until generatedFrame.size) {
try {
FileOutputStream(File(File(Environment.getExternalStorageDirectory(), "Documents"), "image_$i.png")).use { out ->
generatedFrame[i].compress(Bitmap.CompressFormat.PNG, 100, out) // bmp is your Bitmap instance
// PNG is a lossless format, the compression factor (100) is ignored
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
uiThread {
nextFrame()
}
@ -263,19 +203,6 @@ class ApngAnimator {
bitmapBuffer = btm
}
}
// DEBUG FUNCTION : WRITE RENDERED FRAME TO EXTERNAL STORAGE
if (isDebug) {
for (i in 0 until generatedFrame.size) {
try {
FileOutputStream(File(File(Environment.getExternalStorageDirectory(), "Documents"), "image_$i.png")).use { out ->
generatedFrame[i].compress(Bitmap.CompressFormat.PNG, 100, out) // bmp is your Bitmap instance
// PNG is a lossless format, the compression factor (100) is ignored
}
} catch (e: IOException) {
e.printStackTrace()
}
}
}
nextFrame()
}

View File

@ -1,5 +1,6 @@
package oupson.apng
import oupson.apng.exceptions.NoFrameException
import java.util.zip.CRC32

View File

@ -1,6 +1,9 @@
package oupson.apng
import oupson.apng.ApngFactory.Companion.isPng
import oupson.apng.chunks.IDAT
import oupson.apng.chunks.IHDR
import oupson.apng.exceptions.NotPngException
/**
* A frame for an animated png

View File

@ -2,7 +2,6 @@ package oupson.apng
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.net.URL
@ -11,27 +10,19 @@ class Loader {
try {
val connection = url.openConnection()
connection.connect()
// this will be useful so that you can show a typical 0-100% progress bar
val fileLength = connection.contentLength
// download the file
val input = BufferedInputStream(connection.getInputStream())
val output = ByteArrayOutputStream()
val data = ByteArray(1024)
var count: Int = 0
var count = 0
while ({count = input.read(data); count}() != -1) {
output.write(data, 0, count)
}
output.flush()
output.close()
input.close()
return output.toByteArray()
} catch (e: IOException) {
throw e
}
}
fun load(file : File) : ByteArray {
return file.readBytes()
}
}

View File

@ -1,4 +1,4 @@
package oupson.apng
package oupson.apng.chunks
class IDAT {
private var bodySize = -1

View File

@ -1,4 +1,4 @@
package oupson.apng
package oupson.apng.chunks
class IHDR {
private var corpsSize = -1

View File

@ -1,5 +1,6 @@
package oupson.apng
package oupson.apng.chunks
import oupson.apng.Utils
import oupson.apng.Utils.Companion.getBlend_op
import oupson.apng.Utils.Companion.getDispose_op
@ -20,7 +21,7 @@ class fcTL(byteArray: ByteArray) {
var y_offset : Int = 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 dispose_op : Utils.Companion.dispose_op = Utils.Companion.dispose_op.APNG_DISPOSE_OP_NONE
init {
for (i in 0 until byteArray.size) {
// Find fcTL chunk

View File

@ -1,4 +1,4 @@
package oupson.apng
package oupson.apng.chunks
class fdAT {
private var bodySize = -1

View File

@ -1,4 +1,4 @@
package oupson.apng
package oupson.apng.exceptions
class NoFrameException() : Exception()
class NotPngException() : Exception()

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">apng</string>
<string name="app_name">KAPNG</string>
</resources>

View File

@ -32,6 +32,7 @@ android {
}
ext.anko_version = '0.10.7'
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//noinspection GradleCompatible
implementation 'com.android.support:appcompat-v7:27.1.1'
@ -39,9 +40,10 @@ dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation project(':apng_library')
implementation "org.jetbrains.anko:anko:$anko_version"
implementation 'com.squareup.picasso:picasso:2.71828'
implementation project(":apng_library")
// implementation fileTree(include: ['*.aar'], dir: 'libs')
}
kotlin {
experimental {

Binary file not shown.

View File

@ -38,7 +38,6 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main)
animator = ApngAnimator(imageView)
animator.isDebug = true
animator.load(imageUrl)
Picasso.get().load(imageUrl).into(imageView2);

View File

@ -1,8 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.2.61'
ext.kotlin_version = '1.2.60'
ext.kotlin_version = '1.2.71'
repositories {
google()
jcenter()