Add create method - Work in progress

This commit is contained in:
oupson 2018-10-23 15:51:28 +02:00
parent ffec8cc269
commit 681fc1ea8d
10 changed files with 461 additions and 8 deletions

View File

@ -105,6 +105,7 @@ class APNGDisassembler(val byteArray: ByteArray) {
// Check if is IDAT
else if (byteArray[i] == 0x49.toByte() && byteArray[i + 1] == 0x44.toByte() && byteArray[ i + 2 ] == 0x41.toByte() && byteArray[ i + 3 ] == 0x54.toByte()) {
if (png == null) {
png = ArrayList()
if (cover == null) {
cover = ArrayList()
png!!.addAll(pngSignature.toList())

View File

@ -0,0 +1,352 @@
package oupson.apng
import android.graphics.Bitmap
import android.graphics.BitmapFactory
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 java.util.zip.CRC32
class Apng {
private var seq = 0
var maxWidth : Int? = null
var maxHeight : Int? = null
var cover : Bitmap? = null
set(value) {
field = convertImage(value!!)
}
var frames : ArrayList<Frame> = ArrayList()
init {
}
// region addFrames
fun addFrames(bitmap: Bitmap) {
frames.add(Frame(toByteArray(bitmap)))
}
fun addFrames(bitmap: Bitmap, delay : Float) {
frames.add(Frame(toByteArray(bitmap), delay))
}
fun addFrames(bitmap: Bitmap, delay: Float, xOffset : Int, yOffset : Int, blend_op: Utils.Companion.blend_op, dispose_op: Utils.Companion.dispose_op) {
frames.add(Frame(toByteArray(bitmap), delay, xOffset, yOffset, blend_op, dispose_op))
}
//endregion
fun generateAPNGByteArray() : ByteArray {
val res = ArrayList<Byte>()
// Add PNG signature
res.addAll(ApngFactory.pngSignature.toList())
// Add Image Header
res.addAll(generateIhdr().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
if (cover == null) {
val framesByte = ArrayList<Byte>()
// region fcTL
// Create the 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())
// Add the frame number
fcTL.addAll(to4Bytes(seq).toList())
seq++
// Add width and height
fcTL.addAll(to4Bytes(frames[0].width).toList())
fcTL.addAll(to4Bytes(frames[0].height).toList())
// Calculate offset
if (frames[0].width < maxWitdh) {
val xOffset = (maxWitdh / 2) - (frames[0].width / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
if (frames[0].height < maxHeight) {
val xOffset = (maxHeight / 2) - (frames[0].height / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
// Set frame delay
fcTL.addAll(to2Bytes(frames[0].delay.toInt()).toList())
fcTL.addAll(to2Bytes(1000).toList())
fcTL.add(getDispose_op(frames[0].dispose_op).toByte())
fcTL.add(getBlend_op(frames[0].blend_op).toByte())
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
framesByte.addAll(to4Bytes(crc.value.toInt()).toList())
// endregion
// region idat
frames[0].idat.IDATBody.forEach {
val fdat = ArrayList<Byte>()
// 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<Byte>()
// Add cover image : Not part of annimation
// region idat
val idat = IDAT()
idat.parseIDAT(toByteArray(cover!!))
idat.IDATBody.forEach {
val idatByteArray = ArrayList<Byte>()
framesByte.addAll(to4Bytes(it.size).toList())
idatByteArray.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).toList())
idatByteArray.addAll(it.toList())
val crc1 = CRC32()
crc1.update(idatByteArray.toByteArray(), 0, idatByteArray.size)
framesByte.addAll(idatByteArray)
framesByte.addAll(to4Bytes(crc1.value.toInt()).toList())
}
// endregion
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())
// Add the frame number
fcTL.addAll(to4Bytes(seq).toList())
seq++
// Add width and height
fcTL.addAll(to4Bytes(frames[0].width).toList())
fcTL.addAll(to4Bytes(frames[0].height).toList())
// Calculate offset
if (frames[0].width < maxWitdh) {
val xOffset = (maxWitdh / 2) - (frames[0].width / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
if (frames[0].height < maxHeight) {
val xOffset = (maxHeight / 2) - (frames[0].height / 2)
fcTL.addAll(to4Bytes(xOffset).toList())
} else {
fcTL.addAll(to4Bytes(0).toList())
}
// Set frame delay
fcTL.addAll(to2Bytes(frames[0].delay.toInt()).toList())
fcTL.addAll(to2Bytes(1000).toList())
fcTL.add(getDispose_op(frames[0].dispose_op).toByte())
fcTL.add(getBlend_op(frames[0].blend_op).toByte())
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
framesByte.addAll(to4Bytes(crc.value.toInt()).toList())
// endregion
// region fdat
frames[0].idat.IDATBody.forEach {
val fdat = ArrayList<Byte>()
// Add the chunk body length
framesByte.addAll(to4Bytes(it.size + 4).toList())
// Add fdat
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).toList())
fdat.addAll(to4Bytes(seq).toList())
seq++
// 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)
}
for (i in 1 until frames.size) {
// If it's the first frame
val framesByte = ArrayList<Byte>()
val fcTL = ArrayList<Byte>()
// 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(getDispose_op(frames[i].dispose_op).toByte())
fcTL.add(getBlend_op(frames[i].blend_op).toByte())
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<Byte>()
// 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)
}
if (frames.isNotEmpty()) {
// 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()
}
}
fun generateCover(bitmap: Bitmap, maxWidth : Int, maxHeight : Int) : Bitmap {
return Bitmap.createScaledBitmap(bitmap, maxWidth, maxHeight, false)
}
private fun generateIhdr(): 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
maxHeight = frames.sortedByDescending { it.height }[0].height
maxWidth = frames.sortedByDescending { it.width }[0].width
if (((maxWidth != frames[0].width) && (maxHeight != frames[0].height)) && cover == null) {
cover = generateCover(BitmapFactory.decodeByteArray(frames[0].byteArray, 0, frames[0].byteArray.size), maxWidth!!, maxHeight!!)
}
// 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(maxWidth!!).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()
}
// Animation Control chunk
private fun generateACTL(): ArrayList<Byte> {
val res = ArrayList<Byte>()
val actl = ArrayList<Byte>()
// 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
}
}

View File

@ -14,7 +14,9 @@ import java.net.URL
class ApngAnimator {
var play = true
var isPlaying = true
get() = field
private set(value) {field = value}
var Frames = ArrayList<Frame>()
@ -282,18 +284,18 @@ class ApngAnimator {
}
fun pause() {
play = false
isPlaying = false
}
fun play() {
if (!play) {
play = true
if (!isPlaying) {
isPlaying = true
ifmustPlay()
}
}
private fun ifmustPlay() {
if (play) {
if (isPlaying) {
nextFrame()
}
}

View File

@ -87,6 +87,34 @@ class Frame {
}
}
constructor(byteArray: ByteArray, delay : Float, xOffsets : Int, yOffsets : Int, blend_op: Utils.Companion.blend_op, dispose_op: Utils.Companion.dispose_op) {
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 = -1
this.maxHeight = -1
this.blend_op = blend_op
this.dispose_op = dispose_op
} else {
throw NotPngException()
}
}
constructor(byteArray: ByteArray, delay : Float, xOffsets : Int, yOffsets : Int, maxWidth : Int, maxHeight : Int, blend_op: Utils.Companion.blend_op, dispose_op: Utils.Companion.dispose_op) {
if (isPng(byteArray)) {
this.byteArray = byteArray

View File

@ -1,5 +1,9 @@
package oupson.apng
import android.graphics.Bitmap
import android.graphics.Canvas
import java.io.ByteArrayOutputStream
class Utils {
companion object {
enum class dispose_op {
@ -44,5 +48,42 @@ class Utils {
else -> blend_op.APNG_BLEND_OP_SOURCE
}
}
fun toByteArray(bitmap: Bitmap) : ByteArray {
val bos = ByteArrayOutputStream();
convertImage(bitmap).compress(Bitmap.CompressFormat.PNG, 0 /*ignored for PNG*/, bos);
return bos.toByteArray();
}
fun convertImage(bitmap: Bitmap) : Bitmap{
val btm = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
val canvas = Canvas(btm)
canvas.drawBitmap(bitmap, 0f, 0f, null)
return btm
}
/**
* 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
}
}
}

View File

@ -43,3 +43,8 @@ dependencies {
implementation "org.jetbrains.anko:anko:$anko_version"
implementation 'com.squareup.picasso:picasso:2.71828'
}
kotlin {
experimental {
coroutines "enable"
}
}

View File

@ -7,6 +7,7 @@ import android.provider.MediaStore
import android.support.v7.app.AppCompatActivity
import android.util.Log
import kotlinx.android.synthetic.main.activity_main2.*
import org.jetbrains.anko.sdk27.coroutines.onClick
import oupson.apng.ApngAnimator
class Main2Activity : AppCompatActivity() {
@ -26,6 +27,14 @@ class Main2Activity : AppCompatActivity() {
}
Log.e("TAG", intent.data.toString())
animator.load(getImageRealPath(contentResolver, uri, null))
imageView3.onClick {
if (animator.isPlaying) {
animator.pause()
} else {
animator.play()
}
}
}
private fun getImageRealPath(contentResolver: ContentResolver, uri: Uri, whereClause: String?): String {

View File

@ -1,10 +1,12 @@
package oupson.apngcreator
import android.graphics.BitmapFactory
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.os.Environment.DIRECTORY_PICTURES
import android.system.Os
import oupson.apng.APNGDisassembler
import java.io.File
@ -17,7 +19,10 @@ import android.system.Os.mkdir
import android.util.Log
import android.widget.Toast
import com.squareup.picasso.Picasso
import oupson.apng.Apng
import oupson.apng.Utils
import java.io.ByteArrayOutputStream
import java.lang.reflect.Field
import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_OK
import java.net.URL
@ -31,11 +36,21 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val apng = Apng()
val file1 = File(Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES), "hopital.jpg")
val file2 = File(Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES), "test.jpg")
apng.addFrames(BitmapFactory.decodeByteArray(file1.readBytes(), 0, file1.readBytes().size))
apng.addFrames(BitmapFactory.decodeByteArray(file2.readBytes(), 0, file2.readBytes().size), 2000f, 0, 0, Utils.Companion.blend_op.APNG_BLEND_OP_OVER, Utils.Companion.dispose_op.APNG_DISPOSE_OP_NONE)
animator = ApngAnimator(imageView)
animator.load(imageUrl)
animator.load(apng.generateAPNGByteArray())
Picasso.get().load(imageUrl).into(imageView2);
val out = File(Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES), "out.png")
out.createNewFile()
out.writeBytes(apng.generateAPNGByteArray())
play.setOnClickListener {
animator.play()

View File

@ -8,7 +8,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0'
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong