commit 498f3364665088f7e7e25d93e9338753cf6c4ece Author: oupson Date: Thu Sep 27 22:05:08 2018 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5edb4ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000..56ffea8 Binary files /dev/null and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..fb5508e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..199f869 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apng_library/.gitignore b/apng_library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/apng_library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/apng_library/build.gradle b/apng_library/build.gradle new file mode 100644 index 0000000..f535f5d --- /dev/null +++ b/apng_library/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 27 + defaultConfig { + minSdkVersion 21 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + productFlavors { + } +} + +dependencies { + implementation 'com.android.support:appcompat-v7:27.1.1' + 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 "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} +repositories { + mavenCentral() +} diff --git a/apng_library/proguard-rules.pro b/apng_library/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/apng_library/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/apng_library/src/androidTest/java/oupson/apng/ExampleInstrumentedTest.java b/apng_library/src/androidTest/java/oupson/apng/ExampleInstrumentedTest.java new file mode 100644 index 0000000..e5f0cb8 --- /dev/null +++ b/apng_library/src/androidTest/java/oupson/apng/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package oupson.apng; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("oupson.apng.test", appContext.getPackageName()); + } +} diff --git a/apng_library/src/main/AndroidManifest.xml b/apng_library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b2ea72a --- /dev/null +++ b/apng_library/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/apng_library/src/main/java/oupson/apng/APNG.kt b/apng_library/src/main/java/oupson/apng/APNG.kt new file mode 100644 index 0000000..70d4e91 --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/APNG.kt @@ -0,0 +1,306 @@ +package oupson.apng + +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 APNG { + + 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 + } + + companion object { + // Return true if png + fun isPng(byteArray: ByteArray) : Boolean { + return byteArray.copyOfRange(0, 8).contentToString() == pngSignature.contentToString() + } + + fun isApng(byteArray: ByteArray) : Boolean { + for (i in 0 until byteArray.size) { + // if byteArray contain acTL + if (byteArray[i] == 0x66.toByte() && byteArray[i + 1] == 0x63.toByte() && byteArray[ i + 2] == 0x54.toByte() && byteArray[ i + 3] == 0x4c.toByte()) { + // It's an apng + return true + } + } + // Else it's not an apng + return false + } + + // Signature for png + val pngSignature : ByteArray = byteArrayOf(0x89.toByte(), 0x50.toByte(), 0x4E.toByte(), 0x47.toByte(), 0x0D.toByte(), 0x0A.toByte(), 0x1A.toByte(), 0x0A.toByte()) + } + + /** + * 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/APNGDisassembler.kt b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt new file mode 100644 index 0000000..5225b7d --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/APNGDisassembler.kt @@ -0,0 +1,184 @@ +package oupson.apng + +import android.graphics.Bitmap +import oupson.apng.APNG.Companion.isApng +import java.util.zip.CRC32 +import android.graphics.BitmapFactory +import android.util.Log + + +class APNGDisassembler(val byteArray: ByteArray) { + val pngList = ArrayList() + var png : ArrayList? = null + + var delayList = ArrayList() + + var yOffset= ArrayList() + + var xOffset = ArrayList() + + var maxWidth = 0 + var maxHeight = 0 + init { + if (isApng(byteArray)) { + 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() || i == byteArray.size - 1) { + if (png == null) { + png = ArrayList() + + val fcTL = fcTL(byteArray.copyOfRange(i-4, i + 28)) + delayList.add(fcTL.delay) + + yOffset.add(fcTL.y_offset) + xOffset.add(fcTL.x_offset) + Log.e("APNG", "delay : + ${fcTL.delay}") + val width = fcTL.pngWidth + val height = fcTL.pngHeight + png!!.addAll(APNG.pngSignature.toList()) + png!!.addAll(generate_ihdr(ihdr, width, height).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) + png!!.addAll(iend.toList()) + png!!.addAll(to4Bytes(crC32.value.toInt()).toList()) + + pngList.add(png!!.toByteArray()) + + png = ArrayList() + + val fcTL = fcTL(byteArray.copyOfRange(i-4, i + 28)) + delayList.add(fcTL.delay) + + yOffset.add(fcTL.y_offset) + xOffset.add(fcTL.x_offset) + + val width = fcTL.pngWidth + val height = fcTL.pngHeight + png!!.addAll(APNG.pngSignature.toList()) + png!!.addAll(generate_ihdr(ihdr, width, height).toList()) + } + } else if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x44.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) + } + + + var 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 (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) + } + + + var 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()) + } + } + } else { + throw NotApngException() + } + } + + fun getbitmapList() : ArrayList + { + var res = ArrayList() + Log.e("APNG", "pngList : ${pngList.size}, delayList : ${delayList.size}") + for (i in 0 until pngList.size) { + res.add(Frame(pngList[i], delayList[i], xOffset[i], yOffset[i], maxWidth, maxHeight)) + } + return res + } + + 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() + + // 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) + ihdr.addAll(ihdr_body) + ihdr.addAll(to4Bytes(crC32.value.toInt()).toList()) + return ihdr.toByteArray() + } + + 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 + } + + + +} +class extractedFrame(val bitmap: Bitmap, val delay : Float, val xoffset : Int, val yoffset : Int, val maxWidth : Int, val maxHeight : Int) { +} \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/ApngAnimator.kt b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt new file mode 100644 index 0000000..ab26808 --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/ApngAnimator.kt @@ -0,0 +1,67 @@ +package oupson.apng + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.os.Environment +import android.os.Handler +import android.widget.ImageView +import java.io.File + +class ApngAnimator(val imageView : ImageView) { + var play = true + var Frames = ArrayList() + var myHandler: Handler + var counter = 0 + + val generatedFrame = ArrayList() + + init { + myHandler = Handler() + + + } + + fun load(file: File) { + val extractedFrame = APNGDisassembler(file.readBytes()).getbitmapList() + Frames = extractedFrame + + Frames.forEach { + val btm = Bitmap.createBitmap(Frames[0].maxWidth, Frames[0].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) + } + + nextFrame() + } + + fun nextFrame() { + if (counter == Frames.size) { + counter = 0 + } + val delay = Frames[counter].delay + imageView.setImageBitmap(generatedFrame[counter]) + counter++ + myHandler.postDelayed({ + mustPlay() + }, delay.toLong()) + } + + fun pause() { + play = false + + } + fun play() { + play = true + mustPlay() + } + + private fun mustPlay() { + if (play) { + nextFrame() + } + } + + +} \ 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 new file mode 100644 index 0000000..8730ceb --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/Frame.kt @@ -0,0 +1,108 @@ +package oupson.apng + +import oupson.apng.APNG.Companion.isPng + +/** + * A frame for an animated png + * @author oupson + * @param byteArray The byte Array of the png + * @param delay The delay in ms, default is 1000 + * @throws NotPngException + */ + +class Frame { + + val byteArray : ByteArray + + val width : Int + val height : Int + + val ihdr : IHDR + + var idat : IDAT + + val delay : Float + + val x_offsets : Int + val y_offsets : Int + + val maxWidth : Int + val maxHeight : Int + + constructor(byteArray: ByteArray) { + if (isPng(byteArray)) { + this.byteArray = byteArray + // Get width and height for image + ihdr = IHDR() + ihdr.parseIHDR(byteArray) + + width = ihdr.pngWidth + height = ihdr.pngHeight + + // Get image bytes + idat = IDAT() + idat.parseIDAT(byteArray) + + delay = 1000f + + x_offsets = 0 + y_offsets = 0 + + maxHeight = -1 + maxWidth = -1 + } else { + throw NotPngException() + } + } + constructor(byteArray: ByteArray, delay : Float) { + if (isPng(byteArray)) { + this.byteArray = byteArray + // Get width and height for image + ihdr = IHDR() + ihdr.parseIHDR(byteArray) + + width = ihdr.pngWidth + height = ihdr.pngHeight + + // Get image bytes + idat = IDAT() + idat.parseIDAT(byteArray) + + this.delay = delay + + x_offsets = 0 + y_offsets = 0 + + maxHeight = -1 + maxWidth = -1 + } else { + throw NotPngException() + } + } + + constructor(byteArray: ByteArray, delay : Float, xOffsets : Int, yOffsets : Int, maxWidth : Int, maxHeight : Int) { + if (isPng(byteArray)) { + this.byteArray = byteArray + // Get width and height for image + ihdr = IHDR() + ihdr.parseIHDR(byteArray) + + width = ihdr.pngWidth + height = ihdr.pngHeight + + // Get image bytes + idat = IDAT() + idat.parseIDAT(byteArray) + + this.delay = delay + + x_offsets = xOffsets + y_offsets = yOffsets + + this.maxWidth = maxWidth + this.maxHeight = maxHeight + } else { + throw NotPngException() + } + } +} \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/IDAT.kt b/apng_library/src/main/java/oupson/apng/IDAT.kt new file mode 100644 index 0000000..032d22b --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/IDAT.kt @@ -0,0 +1,30 @@ +package oupson.apng + +class IDAT { + private var bodySize = -1 + var IDATBody: ArrayList = ArrayList() + + fun parseIDAT(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 + var lengthString = "" + byteArray.copyOfRange( i - 4, i).forEach { + lengthString += String.format("%02x", it) + } + bodySize = lengthString.toLong(16).toInt() + + // Get image bytes + val _IDATbody = ArrayList() + for (j in i +4 until i + 4 + bodySize) { + _IDATbody.add(byteArray[j]) + } + IDATBody.add(_IDATbody.toByteArray()) + } + } + + } + +} \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/IHDR.kt b/apng_library/src/main/java/oupson/apng/IHDR.kt new file mode 100644 index 0000000..38493a5 --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/IHDR.kt @@ -0,0 +1,45 @@ +package oupson.apng + +class IHDR { + private var corpsSize = -1 + + var ihdrCorps = byteArrayOf() + + var pngWidth = -1 + var pngHeight = -1 + + fun parseIHDR(byteArray: ByteArray) { + for (i in 0 until byteArray.size) { + // Find IHDR chunk + if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x48.toByte() && byteArray[ i + 2 ] == 0x44.toByte() && byteArray[ i + 3 ] == 0x52.toByte()) { + // Get length of the corps of the chunk + var lengthString = "" + byteArray.copyOfRange(i - 4, i).forEach { + lengthString += String.format("%02x", it) + } + corpsSize = lengthString.toLong(16).toInt() + + // Get the width of the png + var pngwidth = "" + byteArray.copyOfRange(i + 4, i + 8).forEach { + pngwidth += String.format("%02x", it) + } + pngWidth = pngwidth.toLong(16).toInt() + + // Get the height of the png + var pngheight = "" + byteArray.copyOfRange(i + 8, i + 12).forEach { + pngheight += String.format("%02x", it) + } + pngHeight = pngheight.toLong(16).toInt() + + val _ihdrCorps = ArrayList() + byteArray.copyOfRange(i + 4, i + corpsSize + 4).forEach { + _ihdrCorps.add(it) + } + ihdrCorps = _ihdrCorps.toByteArray() + + } + } + } +} \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/customException.kt b/apng_library/src/main/java/oupson/apng/customException.kt new file mode 100644 index 0000000..72c2592 --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/customException.kt @@ -0,0 +1,6 @@ +package oupson.apng + +class NoFrameException() : Exception() +class NotPngException() : Exception() +class NotApngException() : Exception() +class NoFcTL() : Exception() \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/customView/ApngImageView.kt b/apng_library/src/main/java/oupson/apng/customView/ApngImageView.kt new file mode 100644 index 0000000..f0faccb --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/customView/ApngImageView.kt @@ -0,0 +1,46 @@ +package oupson.apng.customView + +import android.content.Context +import android.widget.ImageView +import android.graphics.Bitmap +import android.graphics.Canvas +import android.os.Handler +import android.util.AttributeSet +import oupson.apng.extractedFrame + + +class ApngImageView(context: Context, attrs: AttributeSet) : ImageView(context, attrs) { + var Frames = ArrayList() + var myHandler: Handler + var counter = 0 + + val generatedFrame = ArrayList() + + init { + myHandler = Handler() + } + fun load(frames : ArrayList) { + Frames = frames + + Frames.forEach { + val btm = Bitmap.createBitmap(Frames[0].maxWidth, Frames[0].maxHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(btm) + canvas.drawBitmap(it.bitmap, it.xoffset.toFloat(), it.yoffset.toFloat(), null) + generatedFrame.add(btm) + } + + nextFrame() + } + + fun nextFrame() { + if (counter == Frames.size) { + counter = 0 + } + val delay = Frames[counter].delay + this.setImageBitmap(generatedFrame[counter]) + counter++ + myHandler.postDelayed({ + nextFrame() + }, delay.toLong()) + } +} \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/fcTL.kt b/apng_library/src/main/java/oupson/apng/fcTL.kt new file mode 100644 index 0000000..b2166ad --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/fcTL.kt @@ -0,0 +1,99 @@ +package oupson.apng + +import android.util.Log + +class fcTL(byteArray: ByteArray) { + + private var corpsSize = -1 + var fcTLBody = byteArrayOf() + + // Height and width of frame + var pngWidth = -1 + var pngHeight = -1 + + // Delay to wait after the frame + var delay : Float = -1f + + // x and y offsets + var x_offset : Int = 0 + var y_offset : Int = 0 + + init { + 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 lengthString = "" + byteArray.copyOfRange(i - 4, i).forEach { + lengthString += String.format("%02x", it) + } + corpsSize = lengthString.toLong(16).toInt() + + // Get the width of the png + var pngwidth = "" + byteArray.copyOfRange(i + 8, i + 12).forEach { + pngwidth += String.format("%02x", it) + } + pngWidth = pngwidth.toLong(16).toInt() + + // Get the height of the png + var pngheight = "" + byteArray.copyOfRange(i + 12, i + 16).forEach { + pngheight += String.format("%02x", it) + } + pngHeight = pngheight.toLong(16).toInt() + + /** + * 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 + var delayNum = "" + byteArray.copyOfRange(i + 24, i+ 26).forEach { + delayNum += String.format("%02x", it) + } + val delay_num = delayNum.toLong(16).toFloat() + + // Get delay denominator + var delayDen = "" + byteArray.copyOfRange(i + 26, i+ 28).forEach { + delayDen += String.format("%02x", it) + } + var delay_den = delayDen.toLong(16).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 + var xOffset = "" + byteArray.copyOfRange(i + 16, i+ 20).forEach { + xOffset += String.format("%02x", it) + } + + x_offset = xOffset.toLong(16).toInt() + + var yOffset = "" + byteArray.copyOfRange(i + 20, i+ 24).forEach { + yOffset += String.format("%02x", it) + } + + y_offset = yOffset.toLong(16).toInt() + + val _fcTLBody = ArrayList() + byteArray.copyOfRange(i + 4, i + corpsSize + 4).forEach { + _fcTLBody.add(it) + } + fcTLBody= _fcTLBody.toByteArray() + + } + } + } +} \ No newline at end of file diff --git a/apng_library/src/main/java/oupson/apng/fdAT.kt b/apng_library/src/main/java/oupson/apng/fdAT.kt new file mode 100644 index 0000000..fa7f27b --- /dev/null +++ b/apng_library/src/main/java/oupson/apng/fdAT.kt @@ -0,0 +1,45 @@ +package oupson.apng + +class fdAT { + private var bodySize = -1 + var fdATBody: ArrayList = ArrayList() + + fun parsefdAT(byteArray: ByteArray, position: Int) { + for (i in position until byteArray.size) { + // Find fdAT chunk + 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) + } + bodySize = lengthString.toLong(16).toInt() + + // Get image bytes + val _fdATbody = ArrayList() + for (j in i +4 until i + 4 + bodySize) { + _fdATbody.add(byteArray[j]) + } + fdATBody.add(_fdATbody.toByteArray()) + } + // Find idat chunk + else if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x44.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) + } + bodySize = lengthString.toLong(16).toInt() + + // Get image bytes + val _fdATbody = ArrayList() + for (j in i +4 until i + 4 + bodySize) { + _fdATbody.add(byteArray[j]) + } + fdATBody.add(_fdATbody.toByteArray()) + } + } + + } +} \ No newline at end of file diff --git a/apng_library/src/main/res/values/strings.xml b/apng_library/src/main/res/values/strings.xml new file mode 100644 index 0000000..b4e25b6 --- /dev/null +++ b/apng_library/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + apng + diff --git a/apng_library/src/test/java/oupson/apng/ExampleUnitTest.java b/apng_library/src/test/java/oupson/apng/ExampleUnitTest.java new file mode 100644 index 0000000..147e845 --- /dev/null +++ b/apng_library/src/test/java/oupson/apng/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package oupson.apng; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/app-test/.gitignore b/app-test/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app-test/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app-test/build.gradle b/app-test/build.gradle new file mode 100644 index 0000000..b6cfa01 --- /dev/null +++ b/app-test/build.gradle @@ -0,0 +1,42 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 27 + defaultConfig { + applicationId "oupson.apngcreator" + minSdkVersion 25 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + // Configure only for each module that uses Java 8 + // language features (either in its source code or + // through dependencies). + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + productFlavors { + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + 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') +} diff --git a/app-test/libs/KApng-0.0.3.jar b/app-test/libs/KApng-0.0.3.jar new file mode 100644 index 0000000..3dfd0c0 Binary files /dev/null and b/app-test/libs/KApng-0.0.3.jar differ diff --git a/app-test/proguard-rules.pro b/app-test/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app-test/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app-test/src/androidTest/java/oupson/apngcreator/ExampleInstrumentedTest.kt b/app-test/src/androidTest/java/oupson/apngcreator/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..3991700 --- /dev/null +++ b/app-test/src/androidTest/java/oupson/apngcreator/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package oupson.apngcreator + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("oupson.apngcreator", appContext.packageName) + } +} diff --git a/app-test/src/main/AndroidManifest.xml b/app-test/src/main/AndroidManifest.xml new file mode 100644 index 0000000..55661fa --- /dev/null +++ b/app-test/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app-test/src/main/java/oupson/apngcreator/BitmapDrawable.java b/app-test/src/main/java/oupson/apngcreator/BitmapDrawable.java new file mode 100644 index 0000000..0d2de27 --- /dev/null +++ b/app-test/src/main/java/oupson/apngcreator/BitmapDrawable.java @@ -0,0 +1,57 @@ +package oupson.apngcreator; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; + +public class BitmapDrawable extends Drawable { + private Bitmap mBitmap; + + BitmapDrawable(Bitmap b) { + mBitmap = b; + } + + @Override + public void draw(Canvas canvas) { + canvas.drawBitmap(mBitmap, 0.0f, 0.0f, null); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + + @Override + public int getIntrinsicWidth() { + return mBitmap.getWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mBitmap.getHeight(); + } + + @Override + public int getMinimumWidth() { + return mBitmap.getWidth(); + } + + @Override + public int getMinimumHeight() { + return mBitmap.getHeight(); + } + + public Bitmap getBitmap() { + return mBitmap; + } +} diff --git a/app-test/src/main/java/oupson/apngcreator/MainActivity.kt b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt new file mode 100644 index 0000000..2f6cd0c --- /dev/null +++ b/app-test/src/main/java/oupson/apngcreator/MainActivity.kt @@ -0,0 +1,34 @@ +package oupson.apngcreator + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import android.graphics.drawable.Drawable +import android.graphics.drawable.AnimationDrawable +import android.os.Environment +import android.system.Os +import oupson.apng.APNGDisassembler +import java.io.File +import android.os.Environment.getExternalStorageDirectory +import kotlinx.android.synthetic.main.activity_main.* +import oupson.apng.ApngAnimator + + +class MainActivity : AppCompatActivity() { + lateinit var animator : ApngAnimator + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + animator = ApngAnimator(imageView) + animator.load(File(File(Environment.getExternalStorageDirectory(), "documents"), "image_3.png")) + + play.setOnClickListener { + animator.play() + } + + pause.setOnClickListener { + animator.pause() + } + } +} diff --git a/app-test/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app-test/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/app-test/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app-test/src/main/res/drawable/ic_launcher_background.xml b/app-test/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/app-test/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-test/src/main/res/layout/activity_main.xml b/app-test/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..7d97a71 --- /dev/null +++ b/app-test/src/main/res/layout/activity_main.xml @@ -0,0 +1,42 @@ + + + +