Working on ExperimentalApngDecoder

Cleaning Code
Few improvements
This commit is contained in:
oupson 2020-01-23 22:30:47 +01:00
parent c8e8306930
commit d8d9945daf
14 changed files with 119 additions and 54 deletions

View File

@ -128,7 +128,7 @@ class APNGDisassembler {
val crc = CRC32()
crc.update(byteArray.copyOfRange(i, byteArray.size - 4))
if (chunkCRC == crc.value.toInt()) {
when (Arrays.toString(byteArray.copyOfRange(i, i + 4))) {
when (byteArray.copyOfRange(i, i + 4).contentToString()) {
Utils.fcTL -> {
if (png == null) {
cover?.let {

View File

@ -92,8 +92,10 @@ fun ImageView.loadApng(@RawRes res : Int, speed : Float? = null, apngAnimatorOpt
/**
* Class to play APNG
* For better performance but lesser features use [ExperimentalApngDecoder] instead
*/
class ApngAnimator(private val context: Context?) {
@Suppress("MemberVisibilityCanBePrivate")
var isPlaying = true
private set
@ -165,7 +167,7 @@ class ApngAnimator(private val context: Context?) {
*/
@JvmOverloads
fun load(file: File, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch {
GlobalScope.launch(Dispatchers.IO) {
val input = file.inputStream()
val bytes = ByteArray(8)
input.read(bytes)
@ -214,7 +216,7 @@ class ApngAnimator(private val context: Context?) {
*/
@JvmOverloads
fun load(uri : Uri, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch {
GlobalScope.launch(Dispatchers.IO) {
val input = context!!.contentResolver.openInputStream(uri)!!
val bytes = ByteArray(8)
input.read(bytes)
@ -264,7 +266,7 @@ class ApngAnimator(private val context: Context?) {
*/
@JvmOverloads
fun loadUrl(url: URL, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch {
GlobalScope.launch(Dispatchers.IO) {
this@ApngAnimator.speed = speed
// Download PNG
Loader.load(context!!, url).apply {
@ -330,7 +332,7 @@ class ApngAnimator(private val context: Context?) {
*/
@JvmOverloads
fun load(string: String, speed : Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch {
GlobalScope.launch(Dispatchers.IO) {
this@ApngAnimator.speed = speed
if (string.contains("http") || string.contains("https")) {
val url = URL(string)

View File

@ -1,7 +1,13 @@
package oupson.apng
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.Log
import androidx.annotation.RawRes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import oupson.apng.chunks.IHDR
import oupson.apng.chunks.fcTL
import oupson.apng.exceptions.BadApng
@ -9,11 +15,18 @@ import oupson.apng.exceptions.BadCRC
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.isPng
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.net.URL
import java.util.zip.CRC32
// TODO DOC CODE
class ExperimentalApngDecoder {
companion object {
// TODO Change TAG
private const val TAG = "ExperimentalApngDecoder"
private val clearPaint : Paint by lazy {
Paint().apply {
xfermode = PorterDuffXfermode(
@ -22,11 +35,18 @@ class ExperimentalApngDecoder {
}
}
/**
* Decode Apng and return a Drawable who can be an [CustomAnimationDrawable] if it end successfully.
* @param inStream Input Stream to decode. Will be close at the end
* @param speed Optional parameter.
*/
@Suppress("MemberVisibilityCanBePrivate")
fun decodeApng(inStream: InputStream, speed : Float = 1f) : Drawable {
val inputStream = BufferedInputStream(inStream)
val bytes = ByteArray(8)
inputStream.mark(0)
inputStream.read(bytes)
if (isPng(bytes)) {
var png: ArrayList<Byte>? = null
var cover: ArrayList<Byte>? = null
@ -42,12 +62,10 @@ class ExperimentalApngDecoder {
val ihdr = IHDR()
var isApng = false
var drawable = CustomAnimationDrawable()
val drawable = CustomAnimationDrawable()
var buffer : Bitmap? = null
// TODO REMOVE
var byteRead: Int
val lengthChunk = ByteArray(4)
do {
@ -216,6 +234,7 @@ class ExperimentalApngDecoder {
crC32.update(iend, 0, iend.size)
it.addAll(iend.asList())
it.addAll(Utils.to4Bytes(crC32.value.toInt()).asList())
inputStream.close()
return BitmapDrawable(BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size))
}
}
@ -287,8 +306,11 @@ class ExperimentalApngDecoder {
}
} else throw BadCRC()
} while (byteRead != -1)
inputStream.close()
return drawable
} else {
if (BuildConfig.DEBUG)
Log.i(TAG, "Decoding non APNG stream")
inputStream.reset()
return Drawable.createFromStream(
inputStream,
@ -297,6 +319,45 @@ class ExperimentalApngDecoder {
}
}
/**
* Decode Apng and return a Drawable who can be an [CustomAnimationDrawable] if it end successfully.
* @param file File to decode.
* @param speed Optional parameter.
*/
@Suppress("unused")
fun decodeApng(file : File, speed: Float = 1f) : Drawable = decodeApng(FileInputStream(file), speed)
/**
* Decode Apng and return a Drawable who can be an [CustomAnimationDrawable] if it end successfully.
* @param context Context is needed for contentResolver
* @param uri Uri to open.
* @param speed Optional parameter.
*/
fun decodeApng(context : Context, uri : Uri, speed: Float = 1f) : Drawable {
val inputStream = context.contentResolver.openInputStream(uri)
?: throw Exception("Failed to open InputStream, InputStream is null")
return decodeApng(inputStream, speed)
}
/**
* Decode Apng and return a Drawable who can be an [CustomAnimationDrawable] if it end successfully.
* @param context Context is needed for contentResolver
* @param res Resource to decode.
* @param speed Optional parameter.
*/
@Suppress("unused")
fun decodeApng(context : Context, @RawRes res : Int, speed: Float = 1f) : Drawable = decodeApng(context.resources.openRawResource(res), speed)
/**
* Decode Apng and return a Drawable who can be an [CustomAnimationDrawable] if it end successfully.
* @param context Context is needed for contentResolver
* @param url URL to decode.
* @param speed Optional parameter.
*/
suspend fun decodeApng(context : Context, url : URL, speed: Float = 1f) = withContext(Dispatchers.IO) {
decodeApng(FileInputStream(Loader.load(context, url)), speed)
}
/**
* Generate a correct IHDR from the IHDR chunk of the APNG
* @param ihdrOfApng The IHDR of the APNG

View File

@ -7,7 +7,6 @@ import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.IDAT
import oupson.apng.utils.Utils.Companion.IHDR
import oupson.apng.utils.Utils.Companion.isPng
import java.util.*
/**
* A frame of the APNG
@ -82,7 +81,7 @@ class Frame// Get width and height for image
* @param byteArray The frame
*/
private fun parseChunk(byteArray: ByteArray) {
when(Arrays.toString(byteArray.copyOfRange(4, 8))) {
when(byteArray.copyOfRange(4, 8).contentToString()) {
IHDR -> {
ihdr = IHDR()
ihdr.parse(byteArray)

View File

@ -1,6 +1,8 @@
package oupson.apng
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedInputStream
import java.io.File
import java.io.IOException
@ -16,11 +18,11 @@ class Loader {
*/
@Suppress("RedundantSuspendModifier")
@Throws(IOException::class)
suspend fun load(context: Context, url: URL): File {
suspend fun load(context: Context, url: URL): File = withContext(Dispatchers.IO) {
val currentDir = context.filesDir
val fileTXT = File(currentDir, "apngLoader.txt")
val filePNG = File(currentDir, "apngLoader.png")
return if (fileTXT.exists() && url.toString() == fileTXT.readText()) {
if (fileTXT.exists() && url.toString() == fileTXT.readText()) {
filePNG
} else {
val connection = url.openConnection()

View File

@ -12,7 +12,7 @@ class IHDR : Chunk {
* @param byteArray The chunk with the length and the crc
*/
override fun parse(byteArray: ByteArray) {
for (i in 0 until byteArray.size) {
for (i in byteArray.indices) {
// 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 body of the chunk

View File

@ -6,6 +6,8 @@ import java.io.IOException
import java.util.zip.CRC32
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import kotlin.math.max
import kotlin.math.min
/**
* Taken from http://catcode.com/pngencoder/com/keypoint/PngEncoder.java
@ -130,7 +132,7 @@ class PngEncoder {
private fun resizeByteArray(array: ByteArray, newLength: Int): ByteArray {
val newArray = ByteArray(newLength)
val oldLength = array.size
System.arraycopy(array, 0, newArray, 0, Math.min(oldLength, newLength))
System.arraycopy(array, 0, newArray, 0, min(oldLength, newLength))
return newArray
}
@ -146,9 +148,9 @@ class PngEncoder {
* @return The next place to be written to in the pngBytes array.
*/
private fun writeBytes(data: ByteArray, offset: Int): Int {
maxPos = Math.max(maxPos, offset + data.size)
maxPos = max(maxPos, offset + data.size)
if (data.size + offset > pngBytes!!.size) {
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + Math.max(1000, data.size))
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + max(1000, data.size))
}
System.arraycopy(data, 0, pngBytes!!, offset, data.size)
return offset + data.size
@ -167,9 +169,9 @@ class PngEncoder {
* @return The next place to be written to in the pngBytes array.
*/
private fun writeBytes(data: ByteArray, nBytes: Int, offset: Int): Int {
maxPos = Math.max(maxPos, offset + nBytes)
maxPos = max(maxPos, offset + nBytes)
if (nBytes + offset > pngBytes!!.size) {
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + Math.max(1000, nBytes))
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + max(1000, nBytes))
}
System.arraycopy(data, 0, pngBytes!!, offset, nBytes)
return offset + nBytes
@ -317,8 +319,8 @@ class PngEncoder {
val compBytes = DeflaterOutputStream(outBytes, scrunch)
try {
while (rowsLeft > 0) {
nRows = Math.min(32767 / (width * (bytesPerPixel + 1)), rowsLeft)
nRows = Math.max(nRows, 1)
nRows = min(32767 / (width * (bytesPerPixel + 1)), rowsLeft)
nRows = max(nRows, 1)
val pixels = IntArray(width * nRows)

View File

@ -1,7 +1,5 @@
package oupson.apng.utils
import java.util.*
class Utils {
companion object {
/**
@ -148,13 +146,13 @@ class Utils {
return lengthString.toLong(16).toInt()
}
val fcTL : String by lazy { Arrays.toString(byteArrayOf(0x66, 0x63, 0x54, 0x4c)) }
val IEND : String by lazy { Arrays.toString(byteArrayOf(0x49, 0x45, 0x4e, 0x44)) }
val IDAT : String by lazy { Arrays.toString(byteArrayOf(0x49, 0x44, 0x41, 0x54)) }
val fdAT : String by lazy { Arrays.toString(byteArrayOf(0x66, 0x64, 0x41, 0x54)) }
val plte : String by lazy { Arrays.toString(byteArrayOf(0x50, 0x4c, 0x54, 0x45)) }
val tnrs : String by lazy { Arrays.toString(byteArrayOf(0x74, 0x52, 0x4e, 0x53)) }
val IHDR : String by lazy { Arrays.toString(byteArrayOf(0x49, 0x48, 0x44, 0x52)) }
val acTL : String by lazy { Arrays.toString(byteArrayOf(0x61, 0x63, 0x54, 0x4c)) }
val fcTL : String by lazy { byteArrayOf(0x66, 0x63, 0x54, 0x4c).contentToString() }
val IEND : String by lazy { byteArrayOf(0x49, 0x45, 0x4e, 0x44).contentToString() }
val IDAT : String by lazy { byteArrayOf(0x49, 0x44, 0x41, 0x54).contentToString() }
val fdAT : String by lazy { byteArrayOf(0x66, 0x64, 0x41, 0x54).contentToString() }
val plte : String by lazy { byteArrayOf(0x50, 0x4c, 0x54, 0x45).contentToString() }
val tnrs : String by lazy { byteArrayOf(0x74, 0x52, 0x4e, 0x53).contentToString() }
val IHDR : String by lazy { byteArrayOf(0x49, 0x48, 0x44, 0x52).contentToString() }
val acTL : String by lazy { byteArrayOf(0x61, 0x63, 0x54, 0x4c).contentToString() }
}
}

View File

@ -95,6 +95,7 @@ class CreatorActivity : AppCompatActivity() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when(requestCode) {
PICK_IMAGE -> {
if (resultCode == Activity.RESULT_OK) {

View File

@ -12,9 +12,9 @@ import androidx.core.content.ContextCompat
import org.jetbrains.anko.backgroundColor
import org.jetbrains.anko.imageView
import org.jetbrains.anko.matchParent
import org.jetbrains.anko.sdk27.coroutines.onClick
import org.jetbrains.anko.verticalLayout
import oupson.apng.loadApng
import oupson.apng.CustomAnimationDrawable
import oupson.apng.ExperimentalApngDecoder
class Main2Activity : AppCompatActivity() {
private lateinit var imageView : ImageView
@ -50,7 +50,12 @@ class Main2Activity : AppCompatActivity() {
private fun load() {
val uri = intent.data ?: return
val animator = imageView.loadApng(uri, null)
//val animator = imageView.loadApng(uri, null)
val drawable = ExperimentalApngDecoder.decodeApng(this, uri)
imageView.setImageDrawable(drawable)
if (drawable is CustomAnimationDrawable)
drawable.start()
/**
imageView.onClick {
try {
if (animator.isApng) {
@ -64,6 +69,7 @@ class Main2Activity : AppCompatActivity() {
e.printStackTrace()
}
}
*/
}
override fun onRequestPermissionsResult(requestCode: Int,

View File

@ -24,16 +24,16 @@ import org.jetbrains.anko.sdk27.coroutines.onSeekBarChangeListener
import oupson.apng.ApngAnimator
import oupson.apng.CustomAnimationDrawable
import oupson.apng.ExperimentalApngDecoder
import oupson.apng.Loader
import java.io.FileInputStream
import java.net.URL
// TODO REMOVE ANKO
fun ViewManager.xToolbar(init : androidx.appcompat.widget.Toolbar.() -> Unit) = ankoView({androidx.appcompat.widget.Toolbar(it)}, 0, init)
class MainActivity : AppCompatActivity() {
private lateinit var animator: ApngAnimator
private lateinit var tool : androidx.appcompat.widget.Toolbar
// val imageUrl = "http://oupson.oupsman.fr/apng/bigApng.png"
private val imageUrl = "https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"
//private val imageUrl = "https://upload.wikimedia.org/wikipedia/commons/3/3f/JPEG_example_flower.jpg"
// 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 = "file:///android_asset/image.png"
@ -104,23 +104,16 @@ class MainActivity : AppCompatActivity() {
)
val imageView = imageView {
id = View.generateViewId()
GlobalScope.launch {
Loader.load(this@MainActivity, URL(imageUrl)).apply {
val drawable = ExperimentalApngDecoder.decodeApng(FileInputStream(this))
if (drawable is CustomAnimationDrawable) {
println("YEP")
val a = drawable as CustomAnimationDrawable
GlobalScope.launch(Dispatchers.Main) {
(this@imageView).setImageDrawable(a)
a.start()
}
} else
GlobalScope.launch(Dispatchers.Main) {
(this@imageView).setImageDrawable(drawable)
}
GlobalScope.launch(Dispatchers.IO) {
val drawable = ExperimentalApngDecoder.decodeApng(this@MainActivity, URL(imageUrl))
GlobalScope.launch(Dispatchers.Main) {
this@imageView.setImageDrawable(drawable)
if (drawable is CustomAnimationDrawable)
drawable.start()
}
}
/**
animator = this.loadApng(imageUrl).apply {
onLoaded {
@ -128,7 +121,8 @@ class MainActivity : AppCompatActivity() {
// Log.e("app-test", "onLoop")
}
}
}*/
}
*/
}.lparams(
width = matchConstraint,
height = matchConstraint

View File

@ -1,4 +1,4 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
@ -6,7 +6,7 @@
<item name="colorAccent">@color/colorAccent</item>
<item name="android:textColorPrimary">#000</item>
<item name="android:textColorSecondary">#000</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
<item name="toolbarStyle">@style/AppTheme.ActionBar</item>
<item name="colorControlNormal">@color/gray</item>
</style>

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.3.61'
ext.anko_version = '0.10.8'
repositories {
google()