Remove deprecated classes

This commit is contained in:
Oupson 2021-02-22 12:03:10 +01:00
parent 3f5f95c795
commit c4e38bf54f
17 changed files with 68 additions and 2721 deletions

View File

@ -1,338 +0,0 @@
package oupson.apng
import android.graphics.BitmapFactory
import oupson.apng.chunks.IHDR
import oupson.apng.chunks.fcTL
import oupson.apng.exceptions.BadApngException
import oupson.apng.exceptions.BadCRCException
import oupson.apng.exceptions.NotApngException
import oupson.apng.exceptions.NotPngException
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.isApng
import oupson.apng.utils.Utils.Companion.isPng
import oupson.apng.utils.Utils.Companion.pngSignature
import oupson.apng.utils.Utils.Companion.uIntFromBytesBigEndian
import oupson.apng.utils.Utils.Companion.uIntToByteArray
import java.io.InputStream
import java.util.*
import java.util.zip.CRC32
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
class APNGDisassembler {
private var png: ArrayList<Byte>? = null
private var cover: ArrayList<Byte>? = null
private var delay = -1f
private var yOffset = -1
private var xOffset = -1
private var plte: ByteArray? = null
private var tnrs: ByteArray? = null
private var maxWidth = 0
private var maxHeight = 0
private var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
private var disposeOp: Utils.Companion.DisposeOp =
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
private var ihdr = IHDR()
private var isApng = false
var apng: Apng = Apng()
/**
* Disassemble an Apng file
* @param byteArray The Byte Array of the file
* @return [Apng] The apng decoded
*/
fun disassemble(byteArray: ByteArray): Apng {
reset()
if (isApng(byteArray)) {
var cursor = 8
while (cursor < byteArray.size) {
val length = uIntFromBytesBigEndian(byteArray.copyOfRange(cursor, cursor + 4).map { it.toInt() })
val chunk = byteArray.copyOfRange(cursor, cursor + length + 12)
parseChunk(chunk)
cursor += length + 12
}
return apng
} else {
throw NotApngException()
}
}
/**
* Disassemble an Apng file
* @param input Input Stream
* @return [Apng] The apng decoded
*/
fun disassemble(input: InputStream): Apng {
reset()
val buffer = ByteArray(8)
input.read(buffer)
if (!isPng(buffer))
throw NotPngException()
var byteRead: Int
val lengthChunk = ByteArray(4)
do {
byteRead = input.read(lengthChunk)
if (byteRead == -1)
break
val length = uIntFromBytesBigEndian(lengthChunk.map(Byte::toInt))
val chunk = ByteArray(length + 8)
byteRead = input.read(chunk)
parseChunk(lengthChunk.plus(chunk))
} while (byteRead != -1)
return apng
}
/**
* Generate a correct IHDR from the IHDR chunk of the APNG
* @param ihdrOfApng The IHDR of the APNG
* @param width The width of the frame
* @param height The height of the frame
* @return [ByteArray] The generated IHDR
*/
private fun generateIhdr(ihdrOfApng: IHDR, width: Int, height: Int): ByteArray {
val ihdr = ArrayList<Byte>()
// We need a body var to know body length and generate crc
val ihdrBody = ArrayList<Byte>()
// Add chunk body length
ihdr.addAll(uIntToByteArray(ihdrOfApng.body.size).asList())
// Add IHDR
ihdrBody.addAll(
byteArrayOf(
0x49.toByte(),
0x48.toByte(),
0x44.toByte(),
0x52.toByte()
).asList()
)
// Add the max width and height
ihdrBody.addAll(uIntToByteArray(width).asList())
ihdrBody.addAll(uIntToByteArray(height).asList())
// Add complicated stuff like depth color ...
// If you want correct png you need same parameters. Good solution is to create new png.
ihdrBody.addAll(ihdrOfApng.body.copyOfRange(8, 13).asList())
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdrBody.toByteArray(), 0, ihdrBody.size)
ihdr.addAll(ihdrBody)
ihdr.addAll(uIntToByteArray(crC32.value.toInt()).asList())
return ihdr.toByteArray()
}
/**
* Parse the chunk
* @param byteArray The chunk with length and crc
*/
private fun parseChunk(byteArray: ByteArray) {
val i = 4
val chunkCRC = uIntFromBytesBigEndian(byteArray.copyOfRange(byteArray.size - 4, byteArray.size).map(Byte::toInt))
val crc = CRC32()
crc.update(byteArray.copyOfRange(i, byteArray.size - 4))
if (chunkCRC == crc.value.toInt()) {
val name = byteArray.copyOfRange(i, i + 4)
when {
name.contentEquals(Utils.fcTL) -> {
if (png == null) {
cover?.let {
it.addAll(uIntToByteArray(0).asList())
// 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.asList())
it.addAll(uIntToByteArray(crC32.value.toInt()).asList())
apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size)
}
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
delay = fcTL.delay
yOffset = fcTL.yOffset
xOffset = fcTL.xOffset
blendOp = fcTL.blendOp
disposeOp = fcTL.disposeOp
val width = fcTL.pngWidth
val height = fcTL.pngHeight
if (xOffset + width > maxWidth) {
throw BadApngException("`yOffset` + `height` must be <= `IHDR` height")
} else if (yOffset + height > maxHeight) {
throw BadApngException("`yOffset` + `height` must be <= `IHDR` height")
}
png?.addAll(pngSignature.asList())
png?.addAll(generateIhdr(ihdr, width, height).asList())
plte?.let {
png?.addAll(it.asList())
}
tnrs?.let {
png?.addAll(it.asList())
}
} else {
// Add IEND body length : 0
png?.addAll(uIntToByteArray(0).asList())
// 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.asList())
png?.addAll(uIntToByteArray(crC32.value.toInt()).asList())
apng.frames.add(
Frame(
png!!.toByteArray(),
delay,
xOffset,
yOffset,
blendOp,
disposeOp,
maxWidth,
maxHeight
)
)
png = ArrayList()
val fcTL = fcTL()
fcTL.parse(byteArray)
delay = fcTL.delay
yOffset = fcTL.yOffset
xOffset = fcTL.xOffset
blendOp = fcTL.blendOp
disposeOp = fcTL.disposeOp
val width = fcTL.pngWidth
val height = fcTL.pngHeight
png?.addAll(pngSignature.asList())
png?.addAll(generateIhdr(ihdr, width, height).asList())
plte?.let {
png?.addAll(it.asList())
}
tnrs?.let {
png?.addAll(it.asList())
}
}
}
name.contentEquals(Utils.IEND) -> {
if (isApng) {
png?.addAll(uIntToByteArray(0).asList())
// 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.asList())
png?.addAll(uIntToByteArray(crC32.value.toInt()).asList())
apng.frames.add(
Frame(
png!!.toByteArray(),
delay,
xOffset,
yOffset,
blendOp,
disposeOp,
maxWidth,
maxHeight
)
)
} else {
cover?.let {
it.addAll(uIntToByteArray(0).asList())
// 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.asList())
it.addAll(uIntToByteArray(crC32.value.toInt()).asList())
apng.cover = BitmapFactory.decodeByteArray(it.toByteArray(), 0, it.size)
}
apng.isApng = false
}
}
name.contentEquals(Utils.IDAT) -> {
if (png == null) {
if (cover == null) {
cover = ArrayList()
cover?.addAll(pngSignature.asList())
cover?.addAll(generateIhdr(ihdr, maxWidth, maxHeight).asList())
}
// Find the chunk length
val bodySize = uIntFromBytesBigEndian(byteArray.copyOfRange(i - 4, i).map(Byte::toInt))
cover?.addAll(byteArray.copyOfRange(i - 4, i).asList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).asList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
cover?.addAll(body)
cover?.addAll(uIntToByteArray(crC32.value.toInt()).asList())
} else {
// Find the chunk length
val bodySize = uIntFromBytesBigEndian(byteArray.copyOfRange(i - 4, i).map(Byte::toInt))
png?.addAll(byteArray.copyOfRange(i - 4, i).asList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 4, i + 4 + bodySize).asList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png?.addAll(body)
png?.addAll(uIntToByteArray(crC32.value.toInt()).asList())
}
}
name.contentEquals(Utils.fdAT) -> {
// Find the chunk length
val bodySize = uIntFromBytesBigEndian(byteArray.copyOfRange(i - 4, i).map(Byte::toInt))
png?.addAll(uIntToByteArray(bodySize - 4).asList())
val body = ArrayList<Byte>()
body.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Get image bytes
body.addAll(byteArray.copyOfRange(i + 8, i + 4 + bodySize).asList())
val crC32 = CRC32()
crC32.update(body.toByteArray(), 0, body.size)
png?.addAll(body)
png?.addAll(uIntToByteArray(crC32.value.toInt()).asList())
}
name.contentEquals(Utils.plte) -> {
plte = byteArray
}
name.contentEquals(Utils.tnrs) -> {
tnrs = byteArray
}
name.contentEquals(Utils.IHDR) -> {
ihdr.parse(byteArray)
maxWidth = ihdr.pngWidth
maxHeight = ihdr.pngHeight
}
name.contentEquals(Utils.acTL) -> {
isApng = true
}
}
} else throw BadCRCException()
}
/**
* Reset all var before parsing APNG
*/
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()
isApng = false
}
}

View File

@ -1,435 +0,0 @@
package oupson.apng
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Environment
import oupson.apng.chunks.IDAT
import oupson.apng.exceptions.NoFrameException
import oupson.apng.imageUtils.BitmapDiffCalculator
import oupson.apng.imageUtils.PngEncoder
import oupson.apng.imageUtils.PnnQuantizer
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.encodeBlendOp
import oupson.apng.utils.Utils.Companion.encodeDisposeOp
import oupson.apng.utils.Utils.Companion.pngSignature
import java.io.File
import java.util.zip.CRC32
// TODO REMOVE
/**
* Create an APNG file
* If you want to create an APNG, use ApngEncoder instead
*/
@Suppress("unused")
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
class Apng {
@Suppress("MemberVisibilityCanBePrivate")
var maxWidth : Int? = null
@Suppress("MemberVisibilityCanBePrivate")
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
var frames : ArrayList<Frame> = ArrayList()
var isApng = true
// region addFrames
/**
* Add a frame to the APNG
* @param bitmap The bitmap to add
* @param index Index of the frame in the animation
* @param delay Delay of the frame
* @param xOffset The X offset where the frame should be rendered
* @param yOffset The Y offset where the frame should be rendered
* @param disposeOp `DisposeOp` specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
* @param blendOp `BlendOp` 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.
*/
@Suppress("MemberVisibilityCanBePrivate")
fun addFrames(bitmap : Bitmap, index : Int? = null, delay : Float = 1000f, xOffset : Int = 0, yOffset : Int = 0, disposeOp: Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE, blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
if (index == null)
frames.add(Frame(PngEncoder().encode(bitmap, true), delay, xOffset, yOffset, blendOp, disposeOp))
else
frames.add(index, Frame(PngEncoder().encode(bitmap, true), delay, xOffset, yOffset, blendOp, disposeOp))
}
/**
* Add a frame to the APNG
* @param frame The frame to add
* @param index Index of the frame in the animation
*/
fun addFrames(frame : Frame, index: Int? = null) {
if (index == null)
frames.add(frame)
else
frames.add(index, frame)
}
//endregion
/**
* Generate a Bytes Array of the APNG
* @return [ByteArray] The Bytes Array of the APNG
*/
fun toByteArray() : ByteArray {
var seq = 0
val res = ArrayList<Byte>()
// Add PNG signature
res.addAll(pngSignature.asList())
// Add Image Header
res.addAll(generateIhdr().asList())
// Add Animation Controller
res.addAll(generateACTL())
// Get max height and max width
maxHeight = frames.sortedByDescending { it.height }[0].height
maxWidth = 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).asList())
// Add fcTL
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).asList())
// Add the frame number
fcTL.addAll(Utils.uIntToByteArray(seq).asList())
// foreach fcTL or fdAT we must increment seq
seq++
// Add width and height
fcTL.addAll(Utils.uIntToByteArray(frames[0].width).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[0].height).asList())
// Add offsets
fcTL.addAll(Utils.uIntToByteArray(frames[0].xOffsets).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[0].yOffsets).asList())
// Set frame delay
fcTL.addAll(Utils.uShortToArray(frames[0].delay.toInt()).asList())
fcTL.addAll(Utils.uShortToArray(1000).asList())
// Add DisposeOp and BlendOp
fcTL.add(encodeDisposeOp(frames[0].disposeOp).toByte())
fcTL.add(encodeBlendOp(frames[0].blendOp).toByte())
// Create CRC
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
framesByte.addAll(Utils.uIntToByteArray(crc.value.toInt()).asList())
// endregion
// region idat
frames[0].idat.IDATBody.forEach {
val idat = ArrayList<Byte>()
// Add the chunk body length
framesByte.addAll(Utils.uIntToByteArray(it.size).asList())
// Add IDAT
idat.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
// Add chunk body
idat.addAll(it.asList())
// Generate CRC
val crc1 = CRC32()
crc1.update(idat.toByteArray(), 0, idat.size)
framesByte.addAll(idat)
framesByte.addAll(Utils.uIntToByteArray(crc1.value.toInt()).asList())
}
// endregion
res.addAll(framesByte)
} else {
val framesByte = ArrayList<Byte>()
// Add cover image : Not part of animation
// region IDAT
val idat = IDAT()
idat.parse(PngEncoder().encode(cover!!, true, 1))
idat.IDATBody.forEach {
val idatByteArray = ArrayList<Byte>()
framesByte.addAll(Utils.uIntToByteArray(it.size).asList())
idatByteArray.addAll(byteArrayOf(0x49, 0x44, 0x41, 0x54).asList())
idatByteArray.addAll(it.asList())
val crc1 = CRC32()
crc1.update(idatByteArray.toByteArray(), 0, idatByteArray.size)
framesByte.addAll(idatByteArray)
framesByte.addAll(Utils.uIntToByteArray(crc1.value.toInt()).asList())
}
// endregion
//region fcTL
val fcTL = ArrayList<Byte>()
// Add the length of the chunk body
framesByte.addAll(byteArrayOf(0x00, 0x00, 0x00, 0x1A).asList())
// Add fcTL
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).asList())
// Add the frame number
fcTL.addAll(Utils.uIntToByteArray(seq).asList())
seq++
// Add width and height
fcTL.addAll(Utils.uIntToByteArray(frames[0].width).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[0].height).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[0].xOffsets).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[0].yOffsets).asList())
// Set frame delay
fcTL.addAll(Utils.uShortToArray(frames[0].delay.toInt()).asList())
fcTL.addAll(Utils.uShortToArray(1000).asList())
// Add DisposeOp and BlendOp
fcTL.add(encodeDisposeOp(frames[0].disposeOp).toByte())
fcTL.add(encodeBlendOp(frames[0].blendOp).toByte())
// Generate CRC
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
framesByte.addAll(Utils.uIntToByteArray(crc.value.toInt()).asList())
// endregion
// region fdat
frames[0].idat.IDATBody.forEach {
val fdat = ArrayList<Byte>()
// Add the chunk body length
framesByte.addAll(Utils.uIntToByteArray(it.size + 4).asList())
// Add fdat
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).asList())
fdat.addAll(Utils.uIntToByteArray(seq).asList())
seq++
// Add chunk body
fdat.addAll(it.asList())
// Generate CRC
val crc1 = CRC32()
crc1.update(fdat.toByteArray(), 0, fdat.size)
framesByte.addAll(fdat)
framesByte.addAll(Utils.uIntToByteArray(crc1.value.toInt()).asList())
}
// 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).asList())
fcTL.addAll(byteArrayOf(0x66, 0x63, 0x54, 0x4c).asList())
// Frame number
fcTL.addAll(Utils.uIntToByteArray(seq).asList())
seq++
// width and height
fcTL.addAll(Utils.uIntToByteArray(frames[i].width).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[i].height).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[i].xOffsets).asList())
fcTL.addAll(Utils.uIntToByteArray(frames[i].yOffsets).asList())
// Set frame delay
fcTL.addAll(Utils.uShortToArray(frames[i].delay.toInt()).asList())
fcTL.addAll(Utils.uShortToArray(1000).asList())
fcTL.add(encodeDisposeOp(frames[i].disposeOp).toByte())
fcTL.add(encodeBlendOp(frames[i].blendOp).toByte())
val crc = CRC32()
crc.update(fcTL.toByteArray(), 0, fcTL.size)
framesByte.addAll(fcTL)
framesByte.addAll(Utils.uIntToByteArray(crc.value.toInt()).asList())
// 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(Utils.uIntToByteArray(it.size + 4).asList())
// Add fdAT
fdat.addAll(byteArrayOf(0x66, 0x64, 0x41, 0x54).asList())
// Add Sequence number
// ! THIS IS NOT FRAME NUMBER
fdat.addAll(Utils.uIntToByteArray(seq).asList())
// Increase seq
seq++
fdat.addAll(it.asList())
// Generate CRC
val crc1 = CRC32()
crc1.update(fdat.toByteArray(), 0, fdat.size)
framesByte.addAll(fdat)
framesByte.addAll(Utils.uIntToByteArray(crc1.value.toInt()).asList())
}
// endregion
res.addAll(framesByte)
}
if (frames.isNotEmpty()) {
// Add IEND body length : 0
res.addAll(Utils.uIntToByteArray(0).asList())
// 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.asList())
res.addAll(Utils.uIntToByteArray(crC32.value.toInt()).asList())
return res.toByteArray()
} else {
throw NoFrameException()
}
}
/**
* 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 [Bitmap] An image cover
*/
@Suppress("MemberVisibilityCanBePrivate")
fun generateCover(bitmap: Bitmap, maxWidth : Int, maxHeight : Int) : Bitmap {
return Bitmap.createScaledBitmap(bitmap, maxWidth, maxHeight, false)
}
/**
* Generate the IHDR chunks.
* @return [ByteArray] The byteArray generated
*/
private fun generateIhdr(): ByteArray {
val ihdr = ArrayList<Byte>()
// We need a body var to know body length and generate crc
val ihdrBody = 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(Utils.uIntToByteArray(frames[0].ihdr.body.size).asList())
// Add IHDR
ihdrBody.addAll(byteArrayOf(0x49.toByte(), 0x48.toByte(), 0x44.toByte(), 0x52.toByte()).asList())
// Add the max width and height
ihdrBody.addAll(Utils.uIntToByteArray(maxWidth!!).asList())
ihdrBody.addAll(Utils.uIntToByteArray(maxHeight!!).asList())
// Add complicated stuff like depth color ...
// If you want correct png you need same parameters. Good solution is to create new png.
ihdrBody.addAll(frames[0].ihdr.body.copyOfRange(8, 13).asList())
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdrBody.toByteArray(), 0, ihdrBody.size)
ihdr.addAll(ihdrBody)
ihdr.addAll(Utils.uIntToByteArray(crC32.value.toInt()).asList())
return ihdr.toByteArray()
}
/**
* Generate the animation control chunk
* @return [ArrayList] The byteArray generated
*/
private fun generateACTL(): ArrayList<Byte> {
val res = ArrayList<Byte>()
val actl = ArrayList<Byte>()
// Add length bytes
res.addAll(Utils.uIntToByteArray(8).asList())
// Add acTL
actl.addAll(byteArrayOf(0x61, 0x63, 0x54, 0x4c).asList())
// Add number of frames
actl.addAll(Utils.uIntToByteArray(frames.size).asList())
// Number of repeat, 0 to infinite
actl.addAll(Utils.uIntToByteArray(0).asList())
res.addAll(actl)
// generate crc
val crc = CRC32()
crc.update(actl.toByteArray(), 0, actl.size)
res.addAll(Utils.uIntToByteArray(crc.value.toInt()).asList())
return res
}
/**
* Reduce the apng size
* @param maxColor Max color you want in the image
* @param keepCover Keep the cover
* @param sizePercent Reduce image width/height by percents.
*/
fun reduceSize(maxColor : Int, keepCover : Boolean? = null, sizePercent : Int? = null) {
val apng = Apng()
if (keepCover != false) {
if (cover != null) {
if (sizePercent != null) {
cover = Bitmap.createScaledBitmap(cover!!, (cover!!.width.toFloat() * sizePercent.toFloat() / 100f).toInt(), (cover!!.height.toFloat() * sizePercent.toFloat() / 100f).toInt(), false)
val pnn = PnnQuantizer(cover)
cover = pnn.convert(maxColor, false)
}
}
} else {
cover = null
}
frames.forEach {
var btm = BitmapFactory.decodeByteArray(it.byteArray, 0, it.byteArray.size)
if (sizePercent != null) {
btm = Bitmap.createScaledBitmap(btm, (btm!!.width.toFloat() * sizePercent.toFloat() / 100f).toInt(), (btm.height.toFloat() * sizePercent.toFloat() / 100f).toInt(), false)
}
val pnn = PnnQuantizer(btm)
val btmOptimised = pnn.convert(maxColor, false)
if (sizePercent != null) {
apng.addFrames(btmOptimised, 0, it.delay, (it.xOffsets.toFloat() * sizePercent.toFloat() / 100f).toInt(), (it.yOffsets.toFloat() * sizePercent.toFloat() / 100f).toInt(), it.disposeOp, it.blendOp)
} else {
apng.addFrames(btmOptimised, 0, it.delay, it.xOffsets, it.yOffsets, it.disposeOp, it.blendOp)
}
}
frames = apng.frames
}
/**
* A function to optimise Frame
* WIP !
*/
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)
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "frame0.png").writeBytes(PngEncoder().encode(drawedFrame[0]))
for (i in 1 until frames.size) {
val diffCalculator = BitmapDiffCalculator(drawedFrame[i - 1], drawedFrame[i])
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "frame$i.png").writeBytes(PngEncoder().encode(diffCalculator.res, true))
frames[i].byteArray = PngEncoder().encode(diffCalculator.res, true)
frames[i].xOffsets = diffCalculator.xOffset
frames[i].yOffsets = diffCalculator.yOffset
frames[i].blendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_OVER
}
}
}

View File

@ -1,567 +0,0 @@
package oupson.apng
import android.content.Context
import android.content.SharedPreferences
import android.graphics.*
import android.net.Uri
import android.util.Log
import android.widget.ImageView
import androidx.annotation.RawRes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import oupson.apng.exceptions.NotApngException
import oupson.apng.exceptions.NotPngException
import oupson.apng.utils.ApngAnimatorOptions
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.isApng
import oupson.apng.utils.Utils.Companion.isPng
import java.io.File
import java.net.URL
// TODO REMOVE
/**
* Class to play APNG
* For better performance but lesser features using [oupson.apng.decoder.ApngDecoder] is strongly recommended.
*/
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
class ApngAnimator(private val context: Context?) {
companion object {
/**
* @param file The APNG to load
* @param speed The speed of the APNG
* @param apngAnimatorOptions Options of the animator
* @return [ApngAnimator] The animator
*/
@Suppress("unused")
@JvmOverloads
fun ImageView.loadApng(file: File, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) = ApngAnimator(this.context).loadInto(this).apply {
load(file, speed, apngAnimatorOptions)
}
/**
* @param uri The APNG to load
* @param speed The speed of the APNG
* @param apngAnimatorOptions Options of the animator
* @return [ApngAnimator] The animator
*/
@Suppress("unused")
@JvmOverloads
fun ImageView.loadApng(uri : Uri, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) = ApngAnimator(this.context).loadInto(this).apply {
load(uri, speed, apngAnimatorOptions)
}
/**
* @param url The url of the APNG to load
* @param speed The speed of the APNG
* @param apngAnimatorOptions Options of the animator
* @return [ApngAnimator] The animator
*/
@Suppress("unused")
@JvmOverloads
fun ImageView.loadApng(url: URL, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) = ApngAnimator(this.context).loadInto(this).apply {
loadUrl(url, speed, apngAnimatorOptions)
}
/**
* @param byteArray The APNG to load
* @param speed The speed of the APNG
* @param apngAnimatorOptions Options of the animator
* @return [ApngAnimator] The animator
*/
@Suppress("unused")
@JvmOverloads
fun ImageView.loadApng(byteArray: ByteArray, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) = ApngAnimator(this.context).loadInto(this).apply {
load(byteArray, speed, apngAnimatorOptions)
}
/**
* @param string The path APNG to load
* @param speed The speed of the APNG
* @param apngAnimatorOptions Options of the animator
* @return [ApngAnimator] The animator
*/
@Suppress("unused")
@JvmOverloads
fun ImageView.loadApng(string: String, speed : Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) = ApngAnimator(this.context).loadInto(this).apply {
load(string, speed, apngAnimatorOptions)
}
/**
* @param res The Resource Int of the APNG to load, must be in the raw folder
* @param speed The speed of the APNG
* @param apngAnimatorOptions Options of the animator
* @return [ApngAnimator] The animator
*/
@Suppress("unused")
@JvmOverloads
fun ImageView.loadApng(@RawRes res : Int, speed : Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) = ApngAnimator(this.context).loadInto(this).apply {
load(res, speed, apngAnimatorOptions)
}
}
@Suppress("MemberVisibilityCanBePrivate")
var isPlaying = true
private set
var speed: Float? = null
set(value) {
if (isApng) {
field = value
try {
pause()
play()
} catch (e: Exception) {
}
}
}
private var imageView: ImageView? = null
@Suppress("MemberVisibilityCanBePrivate")
var anim: CustomAnimationDrawable? = null
private var activeAnimation: CustomAnimationDrawable? = null
private var doOnLoaded : (ApngAnimator) -> Unit = {}
private var frameChangeLister: (index : Int) -> Unit? = {}
private var duration : ArrayList<Float>? = null
private var scaleType : ImageView.ScaleType? = null
@Suppress("MemberVisibilityCanBePrivate")
var isApng = false
@Suppress("MemberVisibilityCanBePrivate")
var loadNotApng = true
private val sharedPreferences : SharedPreferences? by lazy {
context?.getSharedPreferences("apngAnimator", Context.MODE_PRIVATE)
}
init {
loadNotApng = sharedPreferences?.getBoolean("loadNotApng", true) ?: true
}
/**
* Specify if the library could load non apng file
* @param boolean If true the file will be loaded even if it is not an APNG
*/
@Suppress("unused")
@SuppressWarnings("WeakerAccess")
fun loadNotApng(boolean: Boolean) {
val editor = sharedPreferences?.edit()
editor?.putBoolean("loadNotApng", boolean)
editor?.apply()
}
/**
* Load into an ImageView
* @param imageView Image view selected.
* @return [ApngAnimator] The Animator
*/
fun loadInto(imageView: ImageView): ApngAnimator {
this.imageView = imageView
return this
}
/**
* Load an APNG file and starts playing the animation.
* @param file The file to load
* @param speed The speed
* @return [ApngAnimator] The Animator
* @throws NotApngException
*/
@JvmOverloads
fun load(file: File, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch(Dispatchers.IO) {
val input = file.inputStream()
val bytes = ByteArray(8)
input.read(bytes)
input.close()
if (isPng(bytes)) {
isApng = true
this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType
// Download PNG
val inputStream = file.inputStream()
APNGDisassembler().disassemble(inputStream).also {
inputStream.close()
if (it.isApng) {
it.frames.also {frames ->
draw(frames).apply {
setupAnimationDrawableAndStart(this)
}
}
} else {
GlobalScope.launch {
imageView?.setImageBitmap(it.cover)
}
}
}
} else {
if (loadNotApng) {
GlobalScope.launch(Dispatchers.Main) {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.size))
}
} else {
throw NotPngException()
}
}
}
return this
}
/**
* Load an APNG file and starts playing the animation.
* @param uri The uri to load
* @param speed The speed
* @return [ApngAnimator] The Animator
* @throws NotApngException
*/
@JvmOverloads
fun load(uri : Uri, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch(Dispatchers.IO) {
val input = context!!.contentResolver.openInputStream(uri)!!
val bytes = ByteArray(8)
input.read(bytes)
input.close()
if (isPng(bytes)) {
this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType
// Download PNG
val inputStream = context.contentResolver.openInputStream(uri)!!
APNGDisassembler().disassemble(inputStream).also {
inputStream.close()
if (it.isApng) {
isApng = true
it.frames.also {frames ->
draw(frames).apply {
setupAnimationDrawableAndStart(this)
}
}
} else {
isApng = false
GlobalScope.launch(Dispatchers.Main) {
imageView?.setImageBitmap(it.cover)
}
}
}
} else {
if (loadNotApng) {
GlobalScope.launch(Dispatchers.Main) {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.size))
}
} else {
throw NotPngException()
}
}
}
return this
}
/**
* Load an APNG file and starts playing the animation.
* @param url URL to load.
* @param speed The speed
* @return [ApngAnimator] The Animator
* @throws NotApngException
*/
@JvmOverloads
fun loadUrl(url: URL, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch(Dispatchers.IO) {
this@ApngAnimator.speed = speed
// Download PNG
try {
Loader.load(url).apply {
try {
this@ApngAnimator.load(this, speed, apngAnimatorOptions)
} catch (e: NotPngException) {
if (loadNotApng) {
GlobalScope.launch(Dispatchers.Main) {
imageView?.scaleType =
this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(
BitmapFactory.decodeByteArray(
this@apply,
0,
this@apply.size
)
)
}
} else {
throw NotApngException()
}
}
}
} catch (e : java.lang.Exception) {
if (BuildConfig.DEBUG)
Log.e("ApngAnimator", "Error : $e")
}
}
return this
}
/**
* Load an APNG file and starts playing the animation.
* @param byteArray ByteArray of the file
* @param speed The speed
* @return [ApngAnimator] The Animator
* @throws NotApngException
*/
@JvmOverloads
fun load(byteArray: ByteArray, speed: Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch {
this@ApngAnimator.speed = speed
if (isApng(byteArray)) {
isApng = true
this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType
// Download PNG
APNGDisassembler().disassemble(byteArray).frames.also { frames ->
draw(frames).apply {
setupAnimationDrawableAndStart(this)
}
}
} else {
if (loadNotApng) {
GlobalScope.launch(Dispatchers.Main) {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size))
}
} else {
throw NotApngException()
}
}
}
return this
}
/**
* Load an APNG file
* @param string Path of the file.
* @param speed The speed
* @return [ApngAnimator] The Animator
* @throws NotApngException
*/
@JvmOverloads
fun load(string: String, speed : Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch(Dispatchers.IO) {
this@ApngAnimator.speed = speed
if (string.contains("http") || string.contains("https")) {
val url = URL(string)
loadUrl(url, speed, apngAnimatorOptions)
} else if (File(string).exists()) {
var pathToLoad = if (string.startsWith("content://")) string else "file://$string"
pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
this@ApngAnimator.load(Uri.parse(pathToLoad), speed, apngAnimatorOptions)
} else if (string.startsWith("file:///android_asset/")) {
val bytes = this@ApngAnimator.context?.assets?.open(string.replace("file:///android_asset/", ""))?.readBytes()
bytes ?: throw Exception("File are empty")
if (isApng(bytes)) {
load(bytes, speed, apngAnimatorOptions)
} else {
if (loadNotApng) {
GlobalScope.launch(Dispatchers.Main) {
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.size))
}
} else {
throw NotApngException()
}
}
}
}
return this
}
/**
* Load an APNG file
* @param res The res of the file
* @param speed The speed
* @return [ApngAnimator] The Animator
* @throws NotApngException
*/
@JvmOverloads
fun load(@RawRes res : Int, speed : Float? = null, apngAnimatorOptions: ApngAnimatorOptions? = null) : ApngAnimator {
GlobalScope.launch {
val byteArray = context?.resources?.openRawResource(res)?.readBytes() ?: byteArrayOf()
this@ApngAnimator.speed = speed
if (isApng(byteArray)) {
isApng = true
this@ApngAnimator.speed = speed
scaleType = apngAnimatorOptions?.scaleType
// Download PNG
APNGDisassembler().disassemble(byteArray).frames.also { frames ->
draw(frames).apply {
setupAnimationDrawableAndStart(this)
}
}
} else {
if (loadNotApng) {
GlobalScope.launch(Dispatchers.Main) {
imageView?.scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
imageView?.setImageBitmap(BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size))
}
} else {
throw NotApngException()
}
}
}
return this
}
/**
* Sets up the animation drawable and any required listeners. The animation will automatically start.
* @param generatedFrame The frames generated by draw function
*/
private fun setupAnimationDrawableAndStart(generatedFrame: ArrayList<Bitmap>) {
GlobalScope.launch {
anim = toAnimationDrawable(generatedFrame)
activeAnimation = anim
GlobalScope.launch(Dispatchers.Main) {
imageView?.apply {
scaleType = this@ApngAnimator.scaleType ?: ImageView.ScaleType.FIT_CENTER
clearAnimation()
setImageDrawable(activeAnimation)
}
activeAnimation?.start()
isPlaying = true
doOnLoaded(this@ApngAnimator)
}
}
}
/**
* Draw frames
* @param extractedFrame The frames extracted by the disassembler
* @return [ArrayList] The drawed frames
*/
fun draw(extractedFrame: ArrayList<Frame>) : ArrayList<Bitmap> {
val generatedFrame = ArrayList<Bitmap>()
// Set last frame
duration = ArrayList()
var bitmapBuffer = Bitmap.createBitmap(extractedFrame[0].maxWidth!!, extractedFrame[0].maxHeight!!, Bitmap.Config.ARGB_8888)
for (i in 0 until extractedFrame.size) {
// Iterator
val it = extractedFrame[i]
// Current bitmap for the frame
val btm = Bitmap.createBitmap(extractedFrame[0].maxWidth!!, extractedFrame[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 `BlendOp` 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.blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
canvas.drawRect(it.xOffsets.toFloat(), it.yOffsets.toFloat(), it.xOffsets + current.width.toFloat(), it.yOffsets + current.height.toFloat(), { val paint = Paint(); paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR); paint }())
}
// Draw the bitmap
canvas.drawBitmap(current, it.xOffsets.toFloat(), it.yOffsets.toFloat(), null)
generatedFrame.add(btm)
// Don't add current frame to bitmap buffer
when {
extractedFrame[i].disposeOp == Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
//Do nothings
}
// Add current frame to bitmap buffer
// APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
it.disposeOp == Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
val res = Bitmap.createBitmap(extractedFrame[0].maxWidth!!, extractedFrame[0].maxHeight!!, Bitmap.Config.ARGB_8888)
val can = Canvas(res)
can.drawBitmap(btm, 0f, 0f, null)
can.drawRect(it.xOffsets.toFloat(), it.yOffsets.toFloat(), it.xOffsets + it.width.toFloat(), it.yOffsets + it.height.toFloat(), { val paint = Paint(); paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR); paint }())
bitmapBuffer = res
}
else -> bitmapBuffer = btm
}
duration?.add(it.delay / (speed ?: 1f))
}
return generatedFrame
}
/**
* Pause the animation
*/
fun pause() {
if (isApng) {
isPlaying = false
val animResume = CustomAnimationDrawable()
activeAnimation?.stop()
val currentFrame = activeAnimation!!.current
val durations = ArrayList<Float>()
frameLoop@ for (i in 0 until anim?.numberOfFrames!!) {
val checkFrame = activeAnimation!!.getFrame(i)
if (checkFrame === currentFrame) {
for (k in i until activeAnimation!!.numberOfFrames) {
val frame = activeAnimation!!.getFrame(k)
animResume.addFrame(frame, (duration!![k] / (speed ?: 1f)).toInt())
durations.add(duration!![k])
}
for (k in 0 until i) {
val frame = activeAnimation!!.getFrame(k)
animResume.addFrame(frame, (duration!![k] / (speed ?: 1f)).toInt())
durations.add(duration!![k])
}
activeAnimation = animResume
imageView?.setImageDrawable(activeAnimation)
activeAnimation?.setOnFrameChangeListener(frameChangeLister)
imageView?.invalidate()
duration = durations
break@frameLoop
}
}
}
}
/**
* Play the animation
*/
fun play() {
if (isApng) {
isPlaying = true
activeAnimation?.start()
}
}
/**
* Set animation loop listener
* @param frameChangeListener The listener.
*/
@Suppress("unused")
fun setOnFrameChangeLister(frameChangeListener : (index : Int) -> Unit?) {
if (isApng) {
this.frameChangeLister = frameChangeListener
anim?.setOnFrameChangeListener(frameChangeListener)
}
}
/**
* Execute on loaded
*/
@Suppress("unused")
fun onLoaded(f : (ApngAnimator) -> Unit) {
doOnLoaded = f
}
/**
* Converts the generated frames into an animation drawable ([CustomAnimationDrawable])
* in the APNG will be used instead.
* @param generatedFrame The frames
* @return [CustomAnimationDrawable] The animation drawable
*/
private fun toAnimationDrawable( generatedFrame : ArrayList<Bitmap> ): CustomAnimationDrawable {
if (isApng) {
return CustomAnimationDrawable().apply {
isOneShot = false
for (i in 0 until generatedFrame.size) {
addFrame(BitmapDrawable(generatedFrame[i]), ((duration!![i]) / (speed ?: 1f)).toInt())
}
}
} else {
throw NotApngException()
}
}
}

View File

@ -1,39 +0,0 @@
package oupson.apng
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
internal class BitmapDrawable(private val bitmap: Bitmap) : Drawable() {
override fun draw(canvas: Canvas) {
canvas.drawBitmap(bitmap, 0.0f, 0.0f, null)
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(cf: ColorFilter?) {}
override fun getIntrinsicWidth(): Int {
return bitmap.width
}
override fun getIntrinsicHeight(): Int {
return bitmap.height
}
override fun getMinimumWidth(): Int {
return bitmap.width
}
override fun getMinimumHeight(): Int {
return bitmap.height
}
}

View File

@ -1,22 +0,0 @@
package oupson.apng
import android.graphics.drawable.AnimationDrawable
/**
* Extension of the [AnimationDrawable] that provides an animationListener This will allow
* for the caller to listen for specific animation related events.
*/
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
class CustomAnimationDrawable : AnimationDrawable() {
private var onFrameChangeListener : (index : Int) -> Unit? = {}
fun setOnFrameChangeListener( f : (index : Int) -> Unit?) {
onFrameChangeListener = f
}
override fun selectDrawable(index: Int): Boolean {
val drawableChanged = super.selectDrawable(index)
onFrameChangeListener(index)
return drawableChanged
}
}

View File

@ -1,97 +0,0 @@
package oupson.apng
import oupson.apng.chunks.IDAT
import oupson.apng.chunks.IHDR
import oupson.apng.exceptions.NotPngException
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
/**
* A frame of the APNG
* @param byteArray The bitmap to add
* @param delay Delay of the frame
* @param xOffsets The X offset where the frame should be rendered
* @param yOffsets The Y offset where the frame should be rendered
* @param disposeOp `DisposeOp` specifies how the output buffer should be changed at the end of the delay (before rendering the next frame).
* @param blendOp `BlendOp` 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.
* @param maxWidth The max width of the APNG
* @param maxHeight The max height of the APNG
*/
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
class Frame // Get width and height for image
(
byteArray: ByteArray,
delay: Float = 1000f,
xOffsets: Int = 0,
yOffsets: Int = 0,
blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE,
disposeOp: Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE,
maxWidth: Int? = null,
maxHeight: Int? = null
) {
var byteArray : ByteArray
var width : Int = -1
var height : Int = -1
lateinit var ihdr : IHDR
lateinit var idat : IDAT
val delay : Float
var xOffsets : Int = 0
var yOffsets : Int = 0
var maxWidth : Int? = null
var maxHeight : Int? = null
var blendOp: Utils.Companion.BlendOp
var disposeOp : Utils.Companion.DisposeOp
init {
if (isPng(byteArray)) {
this.byteArray = byteArray
// Get width and height for image
var cursor = 8
while (cursor < byteArray.size) {
val chunk = byteArray.copyOfRange(cursor, cursor + Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(cursor, cursor + 4).map(Byte::toInt)) + 12)
parseChunk(chunk)
cursor += Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(cursor, cursor + 4).map(Byte::toInt)) + 12
}
this.delay = delay
this.xOffsets = xOffsets
this.yOffsets = yOffsets
this.maxWidth = maxWidth
this.maxHeight = maxHeight
this.blendOp = blendOp
this.disposeOp = disposeOp
} else {
throw NotPngException()
}
}
/**
* Parse the Frame
* @param byteArray The frame
*/
private fun parseChunk(byteArray: ByteArray) {
val name = byteArray.copyOfRange(4, 8)
if (name.contentEquals(IHDR)) {
ihdr = IHDR()
ihdr.parse(byteArray)
width = ihdr.pngWidth
height = ihdr.pngHeight
} else if (name.contentEquals(IDAT)){
// Get IDAT Bytes
idat = IDAT()
idat.parse(byteArray)
}
}
}

View File

@ -1,15 +0,0 @@
package oupson.apng.chunks
/**
* An interface for the png chunks
*/
@Deprecated("Deprecated", level = DeprecationLevel.WARNING)
interface Chunk {
var body : ByteArray
/**
* Parse the chunk
* @param byteArray The chunk with the length and the crc
*/
fun parse(byteArray: ByteArray)
}

View File

@ -1,24 +0,0 @@
package oupson.apng.chunks
import oupson.apng.utils.Utils
@Deprecated("Deprecated", level = DeprecationLevel.WARNING)
class IDAT : Chunk {
@Suppress("PropertyName")
var IDATBody: ArrayList<ByteArray> = ArrayList()
override var body = byteArrayOf()
/**
* Parse the chunk
* @param byteArray The chunk with the length and the crc
*/
override fun parse(byteArray: ByteArray) {
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 = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i - 4, i).map(Byte::toInt))
// Get image bytes
IDATBody.add(byteArray.copyOfRange(i + 4, i + 4 + bodySize))
}
}
}

View File

@ -1,30 +0,0 @@
package oupson.apng.chunks
import oupson.apng.utils.Utils
// TODO REMOVE
@Deprecated("Deprecated", level = DeprecationLevel.WARNING)
class IHDR : Chunk {
override var body = byteArrayOf()
var pngWidth = -1
var pngHeight = -1
/**
* Parse the chunk
* @param byteArray The chunk with the length and the crc
*/
override fun parse(byteArray: ByteArray) {
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
val bodySize = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i - 4, i).map(Byte::toInt))
// Get the width of the png
pngWidth = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i +4, i + 8).map(Byte::toInt))
// Get the height of the png
pngHeight = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i +8, i +12).map(Byte::toInt))
body = byteArray.copyOfRange(i + 4, i + bodySize + 4)
}
}
}
}

View File

@ -1,62 +0,0 @@
package oupson.apng.chunks
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.decodeBlendOp
import oupson.apng.utils.Utils.Companion.decodeDisposeOp
@Suppress("ClassName")
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
class fcTL : Chunk {
override var body : ByteArray = 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 xOffset : Int = 0
var yOffset : Int = 0
var blendOp : Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
var disposeOp : Utils.Companion.DisposeOp = Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
/**
* Parse the chunk
* @param byteArray The chunk with the length and the crc
*/
override fun parse(byteArray: ByteArray) {
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 = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i - 4, i+1).map{it .toInt()})
// Get the width of the png
pngWidth = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i + 8, i + 12).map(Byte::toInt))
// Get the height of the png
pngHeight = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i + 12, i + 16).map(Byte::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
val delayNum = Utils.uShortFromBytesBigEndian(byteArray.copyOfRange(i + 24, i + 26).map(Byte::toInt)).toFloat()
// Get delay denominator
var delayDen = Utils.uShortFromBytesBigEndian(byteArray.copyOfRange(i + 26, i + 28).map(Byte::toInt)).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 (delayDen == 0f) {
delayDen = 100f
}
delay = (delayNum / delayDen * 1000)
// Get x and y offsets
xOffset = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i + 16, i + 20).map(Byte::toInt))
yOffset = Utils.uIntFromBytesBigEndian(byteArray.copyOfRange(i + 20, i + 24).map(Byte::toInt))
body = byteArray.copyOfRange(i + 4, i + bodySize + 4)
blendOp = decodeBlendOp(byteArray[33].toInt())
disposeOp = decodeDisposeOp(byteArray[32].toInt())
}
}
}

View File

@ -16,10 +16,10 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import oupson.apng.BuildConfig
import oupson.apng.Loader
import oupson.apng.decoder.ApngDecoder.Companion.decodeApng
import oupson.apng.exceptions.BadApngException
import oupson.apng.exceptions.BadCRCException
import oupson.apng.utils.Loader
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.isPng
import java.io.*

View File

@ -1,69 +0,0 @@
package oupson.apng.imageUtils
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import oupson.apng.utils.Utils
@Deprecated("")
class BitmapDiffCalculator(firstBitmap: Bitmap, secondBitmap : Bitmap) {
val res : Bitmap
var xOffset : Int = 0
var yOffset : Int = 0
@Suppress("unused")
var blendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_OVER
init {
val difBitmap = Bitmap.createBitmap(firstBitmap.width, firstBitmap.height, Bitmap.Config.ARGB_8888)
val difCanvas = Canvas(difBitmap)
for (y in 0 until firstBitmap.height) {
for (x in 0 until firstBitmap.width) {
if (firstBitmap.getPixel(x, y) != secondBitmap.getPixel(x, y)) {
val colour = secondBitmap.getPixel(x, y)
val paint = Paint().apply {
this.color = colour
}
difCanvas.drawPoint(x.toFloat(), y.toFloat(), paint)
}
}
}
var width = difBitmap.width
var height = difBitmap.height
topLoop@for (y in 0 until difBitmap.height){
for (x in 0 until difBitmap.width) {
if (difBitmap.getPixel(x, y) != Color.TRANSPARENT) {
break@topLoop
}
}
yOffset += 1
}
bottomLoop@ while (true) {
for (x in 0 until difBitmap.width) {
if (height < 0) {
break@bottomLoop
} else if (difBitmap.getPixel(x, height - 1) != Color.TRANSPARENT) {
break@bottomLoop
}
}
height -= 1
}
leftLoop@for (x in 0 until difBitmap.width) {
for (y in 0 until difBitmap.height) {
if (difBitmap.getPixel(x, y) != Color.TRANSPARENT) {
break@leftLoop
}
}
xOffset += 1
}
rightLoop@ while (true) {
for (y in 0 until difBitmap.height) {
if (difBitmap.getPixel(width - 1, y) != Color.TRANSPARENT) {
break@rightLoop
}
}
width -= 1
}
val btm = Bitmap.createBitmap(difBitmap, xOffset, yOffset, width - xOffset, height - yOffset)
res = btm
}
}

View File

@ -1,419 +0,0 @@
package oupson.apng.imageUtils
import android.graphics.Bitmap
import java.io.ByteArrayOutputStream
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
*/
@Deprecated("It now integrated in ApngEncoder and will be removed after the 1.10 release")
class PngEncoder {
companion object {
/** Constants for filter (NONE) */
private const val FILTER_NONE = 0
/** Constants for filter (SUB) */
private const val FILTER_SUB = 1
/** Constants for filter (UP) */
private const val FILTER_UP = 2
/** Constants for filter (LAST) */
private const val FILTER_LAST = 2
}
/** Encode alpha ? */
private var encodeAlpha = true
/** IHDR tag. */
private val ihdr = byteArrayOf(73, 72, 68, 82)
/** IDAT tag. */
private val idat = byteArrayOf(73, 68, 65, 84)
/** IEND tag. */
private val iend = byteArrayOf(73, 69, 78, 68)
/** The image. */
private var image: Bitmap? = null
/** The png bytes. */
private var pngBytes: ByteArray? = null
/** The prior row. */
private var priorRow: ByteArray? = null
/** The left bytes. */
private var leftBytes: ByteArray? = null
/** The width. */
private var width: Int = 0
private var height: Int = 0
/** The byte position. */
private var bytePos: Int = 0
private var maxPos: Int = 0
/** CRC. */
private var crc = CRC32()
/** The CRC value. */
private var crcValue: Long = 0
/** The filter type. */
private var filter: Int = 0
/** The bytes-per-pixel. */
private var bytesPerPixel: Int = 0
/** The compression level. */
private var compressionLevel: Int = 0
/**
* Encode a [Bitmap] into a png
*
* @param image Bitmap to encode
* @param encodeAlpha Specify if the alpha should be encoded or not
* @param filter 0=none, 1=sub, 2=up
* @param compressionLevel ! Don't use it : It's buggy
*/
fun encode(image: Bitmap, encodeAlpha: Boolean = false, filter: Int = 0, compressionLevel: Int = 0): ByteArray {
this.filter = FILTER_NONE
if (filter <= FILTER_LAST) {
this.filter = filter
}
if (compressionLevel in 0..9) {
this.compressionLevel = compressionLevel
}
this.encodeAlpha = encodeAlpha
val pngIdBytes = byteArrayOf(-119, 80, 78, 71, 13, 10, 26, 10)
width = image.width
height = image.height
this.image = image
/*
* start with an array that is big enough to hold all the pixels
* (plus filter bytes), and an extra 200 bytes for header info
*/
pngBytes = ByteArray((width + 1) * height * 3 + 200)
/*
* keep track of largest byte written to the array
*/
maxPos = 0
bytePos = writeBytes(pngIdBytes, 0)
//hdrPos = bytePos;
writeHeader()
//dataPos = bytePos;
if (writeImageData()) {
writeEnd()
pngBytes = resizeByteArray(pngBytes!!, maxPos)
} else {
throw Exception()
}
return pngBytes!!
}
/**
* Increase or decrease the length of a byte array.
*
* @param array ByteArray to resize
* @param newLength The length you wish the new array to have.
* @return Array of newly desired length. If shorter than the
* original, the trailing elements are truncated.
*/
private fun resizeByteArray(array: ByteArray, newLength: Int): ByteArray {
val newArray = ByteArray(newLength)
val oldLength = array.size
System.arraycopy(array, 0, newArray, 0, min(oldLength, newLength))
return newArray
}
@Suppress("unused")
fun release() {
image?.recycle()
image = null
pngBytes = null
}
/**
* Write an array of bytes into the pngBytes array.
* Note: This routine has the side effect of updating
* maxPos, the largest element written in the array.
* The array is resized by 1000 bytes or the length
* of the data to be written, whichever is larger.
*
* @param data The data to be written into pngBytes.
* @param offset The starting point to write to.
* @return The next place to be written to in the pngBytes array.
*/
private fun writeBytes(data: ByteArray, offset: Int): Int {
maxPos = max(maxPos, offset + data.size)
if (data.size + offset > pngBytes!!.size) {
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + max(1000, data.size))
}
System.arraycopy(data, 0, pngBytes!!, offset, data.size)
return offset + data.size
}
/**
* Write an array of bytes into the pngBytes array, specifying number of bytes to write.
* Note: This routine has the side effect of updating
* maxPos, the largest element written in the array.
* The array is resized by 1000 bytes or the length
* of the data to be written, whichever is larger.
*
* @param data The data to be written into pngBytes.
* @param nBytes The number of bytes to be written.
* @param offset The starting point to write to.
* @return The next place to be written to in the pngBytes array.
*/
private fun writeBytes(data: ByteArray, nBytes: Int, offset: Int): Int {
maxPos = max(maxPos, offset + nBytes)
if (nBytes + offset > pngBytes!!.size) {
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + max(1000, nBytes))
}
System.arraycopy(data, 0, pngBytes!!, offset, nBytes)
return offset + nBytes
}
/**
* Write a two-byte integer into the pngBytes array at a given position.
*
* @param n The integer to be written into pngBytes.
* @param offset The starting point to write to.
* @return The next place to be written to in the pngBytes array.
*/
@Suppress("unused")
private fun writeInt2(n: Int, offset: Int): Int {
val temp = byteArrayOf((n shr 8 and 0xff).toByte(), (n and 0xff).toByte())
return writeBytes(temp, offset)
}
/**
* Write a four-byte integer into the pngBytes array at a given position.
*
* @param n The integer to be written into pngBytes.
* @param offset The starting point to write to.
* @return The next place to be written to in the pngBytes array.
*/
private fun writeInt4(n: Int, offset: Int): Int {
val temp = byteArrayOf(
(n shr 24 and 0xff).toByte(),
(n shr 16 and 0xff).toByte(),
(n shr 8 and 0xff).toByte(),
(n and 0xff).toByte()
)
return writeBytes(temp, offset)
}
/**
* Write a single byte into the pngBytes array at a given position.
*
* @param b The integer to be written into pngBytes.
* @param offset The starting point to write to.
* @return The next place to be written to in the pngBytes array.
*/
private fun writeByte(b: Int, offset: Int): Int {
val temp = byteArrayOf(b.toByte())
return writeBytes(temp, offset)
}
/**
* Write a PNG "IHDR" chunk into the pngBytes array.
*/
private fun writeHeader() {
bytePos = writeInt4(13, bytePos)
val startPos: Int = bytePos
bytePos = writeBytes(ihdr, bytePos)
width = image!!.width
height = image!!.height
bytePos = writeInt4(width, bytePos)
bytePos = writeInt4(height, bytePos)
bytePos = writeByte(8, bytePos) // bit depth
bytePos = writeByte(if (encodeAlpha) 6 else 2, bytePos) // direct model
bytePos = writeByte(0, bytePos) // compression method
bytePos = writeByte(0, bytePos) // filter method
bytePos = writeByte(0, bytePos) // no interlace
crc.reset()
crc.update(pngBytes!!, startPos, bytePos - startPos)
crcValue = crc.value
bytePos = writeInt4(crcValue.toInt(), bytePos)
}
/**
* Perform "sub" filtering on the given row.
* Uses temporary array leftBytes to store the original values
* of the previous pixels. The array is 16 bytes long, which
* will easily hold two-byte samples plus two-byte alpha.
*
* @param pixels The array holding the scan lines being built
* @param startPos Starting position within pixels of bytes to be filtered.
* @param width Width of a scanline in pixels.
*/
private fun filterSub(pixels: ByteArray, startPos: Int, width: Int) {
val offset = bytesPerPixel
val actualStart = startPos + offset
val nBytes = width * bytesPerPixel
var leftInsert = offset
var leftExtract = 0
var i: Int = actualStart
while (i < startPos + nBytes) {
leftBytes!![leftInsert] = pixels[i]
pixels[i] = ((pixels[i] - leftBytes!![leftExtract]) % 256).toByte()
leftInsert = (leftInsert + 1) % 0x0f
leftExtract = (leftExtract + 1) % 0x0f
i++
}
}
/**
* Perform "up" filtering on the given row.
* Side effect: refills the prior row with current row
*
* @param pixels The array holding the scan lines being built
* @param startPos Starting position within pixels of bytes to be filtered.
* @param width Width of a scanline in pixels.
*/
private fun filterUp(pixels: ByteArray, startPos: Int, width: Int) {
var i = 0
val nBytes: Int = width * bytesPerPixel
var currentByte: Byte
while (i < nBytes) {
currentByte = pixels[startPos + i]
pixels[startPos + i] = ((pixels[startPos + i] - priorRow!![i]) % 256).toByte()
priorRow!![i] = currentByte
i++
}
}
/**
* Write the image data into the pngBytes array.
* This will write one or more PNG "IDAT" chunks. In order
* to conserve memory, this method grabs as many rows as will
* fit into 32K bytes, or the whole image; whichever is less.
*
*
* @return true if no errors; false if error grabbing pixels
*/
private fun writeImageData(): Boolean {
var rowsLeft = height // number of rows remaining to write
var startRow = 0 // starting row to process this time through
var nRows: Int // how many rows to grab at a time
var scanLines: ByteArray // the scan lines to be compressed
var scanPos: Int // where we are in the scan lines
var startPos: Int // where this line's actual pixels start (used for filtering)
val compressedLines: ByteArray // the resultant compressed lines
val nCompressed: Int // how big is the compressed area?
//int depth; // color depth ( handle only 8 or 32 )
bytesPerPixel = if (encodeAlpha) 4 else 3
val scrunch = Deflater(compressionLevel)
val outBytes = ByteArrayOutputStream(1024)
val compBytes = DeflaterOutputStream(outBytes, scrunch)
try {
while (rowsLeft > 0) {
nRows = min(32767 / (width * (bytesPerPixel + 1)), rowsLeft)
nRows = max(nRows, 1)
val pixels = IntArray(width * nRows)
//pg = new PixelGrabber(image, 0, startRow, width, nRows, pixels, 0, width);
image!!.getPixels(pixels, 0, width, 0, startRow, width, nRows)
/*
* Create a data chunk. scanLines adds "nRows" for
* the filter bytes.
*/
scanLines = ByteArray(width * nRows * bytesPerPixel + nRows)
if (filter == FILTER_SUB) {
leftBytes = ByteArray(16)
}
if (filter == FILTER_UP) {
priorRow = ByteArray(width * bytesPerPixel)
}
scanPos = 0
startPos = 1
for (i in 0 until width * nRows) {
if (i % width == 0) {
scanLines[scanPos++] = filter.toByte()
startPos = scanPos
}
scanLines[scanPos++] = (pixels[i] shr 16 and 0xff).toByte()
scanLines[scanPos++] = (pixels[i] shr 8 and 0xff).toByte()
scanLines[scanPos++] = (pixels[i] and 0xff).toByte()
if (encodeAlpha) {
scanLines[scanPos++] = (pixels[i] shr 24 and 0xff).toByte()
}
if (i % width == width - 1 && filter != FILTER_NONE) {
if (filter == FILTER_SUB) {
filterSub(scanLines, startPos, width)
}
if (filter == FILTER_UP) {
filterUp(scanLines, startPos, width)
}
}
}
/*
* Write these lines to the output area
*/
compBytes.write(scanLines, 0, scanPos)
startRow += nRows
rowsLeft -= nRows
}
compBytes.close()
/*
* Write the compressed bytes
*/
compressedLines = outBytes.toByteArray()
nCompressed = compressedLines.size
crc.reset()
bytePos = writeInt4(nCompressed, bytePos)
bytePos = writeBytes(idat, bytePos)
crc.update(idat)
bytePos = writeBytes(compressedLines, nCompressed, bytePos)
crc.update(compressedLines, 0, nCompressed)
crcValue = crc.value
bytePos = writeInt4(crcValue.toInt(), bytePos)
scrunch.finish()
scrunch.end()
return true
} catch (e: IOException) {
System.err.println(e.toString())
return false
}
}
/**
* Write a PNG "IEND" chunk into the pngBytes array.
*/
private fun writeEnd() {
bytePos = writeInt4(0, bytePos)
bytePos = writeBytes(iend, bytePos)
crc.reset()
crc.update(iend)
crcValue = crc.value
bytePos = writeInt4(crcValue.toInt(), bytePos)
}
}

View File

@ -1,578 +0,0 @@
package oupson.apng.imageUtils;
/* Fast pairwise nearest neighbor based algorithm for multilevel thresholding
Copyright (C) 2004-2016 Mark Tyler and Dmitry Groshev
Copyright (c) 2018 Miller Cy Chan
* error measure; time used is proportional to number of bins squared - WJ */
/*
https://github.com/mcychan/nQuant.android/blob/master/nQuant.master/src/main/java/com/android/nQuant/PnnQuantizer.java
*/
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
@SuppressWarnings("WeakerAccess")
@Deprecated()
public class PnnQuantizer {
private final short SHORT_MAX = Short.MAX_VALUE;
private final char BYTE_MAX = -Byte.MIN_VALUE + Byte.MAX_VALUE;
private boolean hasTransparency = false, hasSemiTransparency = false;
protected int width, height;
protected int[] pixels = null;
private Integer m_transparentColor;
@SuppressWarnings("unchecked")
@SuppressLint("UseSparseArrays")
private final HashMap<Integer, short[]> closestMap = new HashMap();
public PnnQuantizer(String fname) {
fromBitmap(fname);
}
public PnnQuantizer(Bitmap bitmap) {
fromBitmap(bitmap);
}
private void fromBitmap(Bitmap bitmap) {
width = bitmap.getWidth();
height = bitmap.getHeight();
pixels = new int [width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
}
private void fromBitmap(String fname) {
Bitmap bitmap = BitmapFactory.decodeFile(fname);
fromBitmap(bitmap);
}
private static final class Pnnbin {
double ac = 0, rc = 0, gc = 0, bc = 0, err = 0;
int cnt = 0;
int nn, fw, bk, tm, mtm;
}
private int getColorIndex(final int c)
{
if(hasSemiTransparency)
return (Color.alpha(c) & 0xF0) << 8 | (Color.red(c) & 0xF0) << 4 | (Color.green(c) & 0xF0) | (Color.blue(c) >> 4);
if (hasTransparency)
return (Color.alpha(c) & 0x80) << 8 | (Color.red(c) & 0xF8) << 7 | (Color.green(c) & 0xF8) << 2 | (Color.blue(c) >> 3);
return (Color.red(c) & 0xF8) << 8 | (Color.green(c) & 0xFC) << 3 | (Color.blue(c) >> 3);
}
private double sqr(double value)
{
return value * value;
}
private void find_nn(Pnnbin[] bins, int idx)
{
int nn = 0;
double err = 1e100;
Pnnbin bin1 = bins[idx];
int n1 = bin1.cnt;
double wa = bin1.ac;
double wr = bin1.rc;
double wg = bin1.gc;
double wb = bin1.bc;
for (int i = bin1.fw; i != 0; i = bins[i].fw) {
double nerr = sqr(bins[i].ac - wa) + sqr(bins[i].rc - wr) + sqr(bins[i].gc - wg) + sqr(bins[i].bc - wb);
double n2 = bins[i].cnt;
nerr *= (n1 * n2) / (n1 + n2);
if (nerr >= err)
continue;
err = nerr;
nn = i;
}
bin1.err = err;
bin1.nn = nn;
}
private Integer[] pnnquan(final int[] pixels, int nMaxColors, boolean quan_sqrt)
{
Pnnbin[] bins = new Pnnbin[65536];
int[] heap = new int[65537];
double err, n1, n2;
/* Build histogram */
for (final int pixel : pixels) {
// !!! Can throw gamma correction in here, but what to do about perceptual
// !!! nonuniformity then?
int index = getColorIndex(pixel);
if(bins[index] == null)
bins[index] = new Pnnbin();
Pnnbin tb = bins[index];
tb.ac += Color.alpha(pixel);
tb.rc += Color.red(pixel);
tb.gc += Color.green(pixel);
tb.bc += Color.blue(pixel);
tb.cnt++;
}
/* Cluster nonempty bins at one end of array */
int maxbins = 0;
for (int i = 0; i < bins.length; ++i) {
if (bins[i] == null)
continue;
double d = 1.0 / (double)bins[i].cnt;
bins[i].ac *= d;
bins[i].rc *= d;
bins[i].gc *= d;
bins[i].bc *= d;
if (quan_sqrt)
bins[i].cnt = (int) Math.sqrt(bins[i].cnt);
bins[maxbins++] = bins[i];
}
for (int i = 0; i < maxbins - 1; i++) {
bins[i].fw = (i + 1);
bins[i + 1].bk = i;
}
// !!! Already zeroed out by calloc()
// bins[0].bk = bins[i].fw = 0;
int h, l, l2;
/* Initialize nearest neighbors and build heap of them */
for (int i = 0; i < maxbins; i++) {
find_nn(bins, i);
/* Push slot on heap */
err = bins[i].err;
for (l = ++heap[0]; l > 1; l = l2) {
l2 = l >> 1;
if (bins[h = heap[l2]].err <= err)
break;
heap[l] = h;
}
heap[l] = i;
}
/* Merge bins which increase error the least */
int extbins = maxbins - nMaxColors;
for (int i = 0; i < extbins; ) {
Pnnbin tb;
/* Use heap to find which bins to merge */
for (;;) {
int b1 = heap[1];
tb = bins[b1]; /* One with least error */
/* Is stored error up to date? */
if ((tb.tm >= tb.mtm) && (bins[tb.nn].mtm <= tb.tm))
break;
if (tb.mtm == 0xFFFF) /* Deleted node */
b1 = heap[1] = heap[heap[0]--];
else /* Too old error value */
{
find_nn(bins, b1);
tb.tm = i;
}
/* Push slot down */
err = bins[b1].err;
for (l = 1; (l2 = l + l) <= heap[0]; l = l2) {
if ((l2 < heap[0]) && (bins[heap[l2]].err > bins[heap[l2 + 1]].err))
l2++;
if (err <= bins[h = heap[l2]].err)
break;
heap[l] = h;
}
heap[l] = b1;
}
/* Do a merge */
Pnnbin nb = bins[tb.nn];
n1 = tb.cnt;
n2 = nb.cnt;
double d = 1.0 / (n1 + n2);
tb.ac = d * (n1 * tb.ac + n2 * nb.ac);
tb.rc = d * (n1 * tb.rc + n2 * nb.rc);
tb.gc = d * (n1 * tb.gc + n2 * nb.gc);
tb.bc = d * (n1 * tb.bc + n2 * nb.bc);
tb.cnt += nb.cnt;
tb.mtm = ++i;
/* Unchain deleted bin */
bins[nb.bk].fw = nb.fw;
bins[nb.fw].bk = nb.bk;
nb.mtm = 0xFFFF;
}
/* Fill palette */
List<Integer> palette = new ArrayList<>();
short k = 0;
for (int i = 0;; ++k) {
assert bins[i] != null;
int alpha = (int) Math.rint(bins[i].ac);
palette.add(Color.argb(alpha, (int) Math.rint(bins[i].rc), (int) Math.rint(bins[i].gc), (int) Math.rint(bins[i].bc)));
if (hasTransparency && palette.get(k).equals(m_transparentColor)) {
Integer temp = palette.get(0);
palette.set(0, palette.get(k));
palette.set(k, temp);
}
if ((i = bins[i].fw) == 0)
break;
}
return palette.toArray(new Integer[0]);
}
private short nearestColorIndex(final Integer[] palette, final int[] squares3, final int c)
{
short k = 0;
int curdist, mindist = SHORT_MAX;
for (short i=0; i<palette.length; ++i) {
int c2 = palette[i];
int adist = Math.abs(Color.alpha(c2) - Color.alpha(c));
curdist = squares3[adist];
if (curdist > mindist)
continue;
int rdist = Math.abs(Color.red(c2) - Color.red(c));
curdist += squares3[rdist];
if (curdist > mindist)
continue;
int gdist = Math.abs(Color.green(c2) - Color.green(c));
curdist += squares3[gdist];
if (curdist > mindist)
continue;
int bdist = Math.abs(Color.blue(c2) - Color.blue(c));
curdist += squares3[bdist];
if (curdist > mindist)
continue;
mindist = curdist;
k = i;
}
return k;
}
private short closestColorIndex(final Integer[] palette, final int c)
{
short k = 0;
short[] closest = new short[5];
short[] got = closestMap.get(c);
if (got == null) {
closest[2] = closest[3] = SHORT_MAX;
for (; k < palette.length; k++) {
int c2 = palette[k];
closest[4] = (short) (Math.abs(Color.alpha(c) - Color.alpha(c2)) + Math.abs(Color.red(c) - Color.red(c2)) + Math.abs(Color.green(c) - Color.green(c2)) + Math.abs(Color.blue(c) - Color.blue(c2)));
if (closest[4] < closest[2]) {
closest[1] = closest[0];
closest[3] = closest[2];
closest[0] = k;
closest[2] = closest[4];
}
else if (closest[4] < closest[3]) {
closest[1] = k;
closest[3] = closest[4];
}
}
if (closest[3] == SHORT_MAX)
closest[2] = 0;
}
else
closest = got;
Random rand = new Random();
if (closest[2] == 0 || (rand.nextInt(SHORT_MAX) % (closest[3] + closest[2])) <= closest[3])
k = closest[0];
else
k = closest[1];
closestMap.put(c, closest);
return k;
}
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "UnusedAssignment", "SameReturnValue"})
boolean quantize_image(final int[] pixels, final Integer[] palette, int[] qPixels, final boolean dither)
{
int nMaxColors = palette.length;
int[] sqr_tbl = new int[BYTE_MAX + BYTE_MAX + 1];
for (int i = (-BYTE_MAX); i <= BYTE_MAX; i++)
sqr_tbl[i + BYTE_MAX] = i * i;
int[] squares3 = new int[sqr_tbl.length - BYTE_MAX];
//noinspection ManualArrayCopy
for (int i = 0; i < squares3.length; i++)
squares3[i] = sqr_tbl[i + BYTE_MAX];
int pixelIndex = 0;
if (dither) {
boolean odd_scanline = false;
short[] row0, row1;
int a_pix, r_pix, g_pix, b_pix, dir, k;
final int DJ = 4;
final int DITHER_MAX = 20;
final int err_len = (width + 2) * DJ;
int[] clamp = new int[DJ * 256];
int[] limtb = new int[512];
short[] erowerr = new short[err_len];
short[] orowerr = new short[err_len];
int[] lookup = new int[65536];
for (int i = 0; i < 256; i++) {
clamp[i] = 0;
clamp[i + 256] = (short) i;
clamp[i + 512] = BYTE_MAX;
clamp[i + 768] = BYTE_MAX;
limtb[i] = -DITHER_MAX;
limtb[i + 256] = DITHER_MAX;
}
for (int i = -DITHER_MAX; i <= DITHER_MAX; i++)
limtb[i + 256] = i;
for (short i = 0; i < height; i++) {
if (odd_scanline) {
dir = -1;
pixelIndex += (width - 1);
row0 = orowerr;
row1 = erowerr;
}
else {
dir = 1;
row0 = erowerr;
row1 = orowerr;
}
int cursor0 = DJ, cursor1 = width * DJ;
row1[cursor1] = row1[cursor1 + 1] = row1[cursor1 + 2] = row1[cursor1 + 3] = 0;
for (short j = 0; j < width; j++) {
int c = pixels[pixelIndex];
r_pix = clamp[((row0[cursor0] + 0x1008) >> 4) + Color.red(c)];
g_pix = clamp[((row0[cursor0 + 1] + 0x1008) >> 4) + Color.green(c)];
b_pix = clamp[((row0[cursor0 + 2] + 0x1008) >> 4) + Color.blue(c)];
a_pix = clamp[((row0[cursor0 + 3] + 0x1008) >> 4) + Color.alpha(c)];
int c1 = Color.argb(a_pix, r_pix, g_pix, b_pix);
int offset = getColorIndex(c1);
if (lookup[offset] == 0)
lookup[offset] = nearestColorIndex(palette, squares3, c1) + 1;
int c2 = qPixels[pixelIndex] = palette[lookup[offset] - 1];
r_pix = limtb[r_pix - Color.red(c2) + 256];
g_pix = limtb[g_pix - Color.green(c2) + 256];
b_pix = limtb[b_pix - Color.blue(c2) + 256];
a_pix = limtb[a_pix - Color.alpha(c2) + 256];
k = r_pix * 2;
row1[cursor1 - DJ] = (short) r_pix;
row1[cursor1 + DJ] += (r_pix += k);
row1[cursor1] += (r_pix += k);
row0[cursor0 + DJ] += (r_pix += k);
k = g_pix * 2;
row1[cursor1 + 1 - DJ] = (short) g_pix;
row1[cursor1 + 1 + DJ] += (g_pix += k);
row1[cursor1 + 1] += (g_pix += k);
row0[cursor0 + 1 + DJ] += (g_pix += k);
k = b_pix * 2;
row1[cursor1 + 2 - DJ] = (short) b_pix;
row1[cursor1 + 2 + DJ] += (b_pix += k);
row1[cursor1 + 2] += (b_pix += k);
row0[cursor0 + 2 + DJ] += (b_pix += k);
k = a_pix * 2;
row1[cursor1 + 3 - DJ] = (short) a_pix;
row1[cursor1 + 3 + DJ] += (a_pix += k);
row1[cursor1 + 3] += (a_pix += k);
row0[cursor0 + 3 + DJ] += (a_pix += k);
cursor0 += DJ;
cursor1 -= DJ;
pixelIndex += dir;
}
if ((i % 2) == 1)
pixelIndex += (width + 1);
odd_scanline = !odd_scanline;
}
return true;
}
if(hasSemiTransparency || nMaxColors < 256) {
for (int i = 0; i < qPixels.length; i++)
qPixels[i] = palette[nearestColorIndex(palette, squares3, pixels[i])];
}
else {
for (int i = 0; i < qPixels.length; i++)
qPixels[i] = palette[closestColorIndex(palette, pixels[i])];
}
return true;
}
@SuppressWarnings("UnusedAssignment")
private Bitmap quantize_image(final int[] pixels, int[] qPixels)
{
int pixelIndex = 0;
boolean odd_scanline = false;
short[] row0, row1;
int a_pix, r_pix, g_pix, b_pix, dir, k;
final int DJ = 4;
final int DITHER_MAX = 20;
final int err_len = (width + 2) * DJ;
short[] clamp = new short[DJ * 256];
int[] limtb = new int[512];
short[] erowerr = new short[err_len];
short[] orowerr = new short[err_len];
int[] lookup = new int[65536];
for (int i = 0; i < 256; i++) {
clamp[i] = 0;
clamp[i + 256] = (short) i;
clamp[i + 512] = BYTE_MAX;
clamp[i + 768] = BYTE_MAX;
limtb[i] = -DITHER_MAX;
limtb[i + 256] = DITHER_MAX;
}
for (int i = -DITHER_MAX; i <= DITHER_MAX; i++)
limtb[i + 256] = i;
for (int i = 0; i < height; i++) {
if (odd_scanline) {
dir = -1;
pixelIndex += (width - 1);
row0 = orowerr;
row1 = erowerr;
}
else {
dir = 1;
row0 = erowerr;
row1 = orowerr;
}
int cursor0 = DJ, cursor1 = width * DJ;
row1[cursor1] = row1[cursor1 + 1] = row1[cursor1 + 2] = row1[cursor1 + 3] = 0;
for (short j = 0; j < width; j++) {
int c = pixels[pixelIndex];
r_pix = clamp[((row0[cursor0] + 0x1008) >> 4) + Color.red(c)];
g_pix = clamp[((row0[cursor0 + 1] + 0x1008) >> 4) + Color.green(c)];
b_pix = clamp[((row0[cursor0 + 2] + 0x1008) >> 4) + Color.blue(c)];
a_pix = clamp[((row0[cursor0 + 3] + 0x1008) >> 4) + Color.alpha(c)];
int c1 = Color.argb(a_pix, r_pix, g_pix, b_pix);
int offset = getColorIndex(c1);
if (lookup[offset] == 0) {
int argb1 = Color.argb(BYTE_MAX, (Color.red(c1) & 0xF8), (Color.green(c1) & 0xFC), (Color.blue(c1) & 0xF8));
if (hasSemiTransparency)
argb1 = Color.argb((Color.alpha(c1) & 0xF0), (Color.red(c1) & 0xF0), (Color.green(c1) & 0xF0), (Color.blue(c1) & 0xF0));
else if (hasTransparency)
argb1 = Color.argb((Color.alpha(c1) < BYTE_MAX) ? 0 : BYTE_MAX, (Color.red(c1) & 0xF8), (Color.green(c1) & 0xF8), (Color.blue(c1) & 0xF8));
lookup[offset] = argb1;
}
int c2 = qPixels[pixelIndex] = lookup[offset];
r_pix = limtb[r_pix - Color.red(c2) + 256];
g_pix = limtb[g_pix - Color.green(c2) + 256];
b_pix = limtb[b_pix - Color.blue(c2) + 256];
a_pix = limtb[a_pix - Color.alpha(c2) + 256];
k = r_pix * 2;
row1[cursor1 - DJ] = (short) r_pix;
row1[cursor1 + DJ] += (r_pix += k);
row1[cursor1] += (r_pix += k);
row0[cursor0 + DJ] += (r_pix += k);
k = g_pix * 2;
row1[cursor1 + 1 - DJ] = (short) g_pix;
row1[cursor1 + 1 + DJ] += (g_pix += k);
row1[cursor1 + 1] += (g_pix += k);
row0[cursor0 + 1 + DJ] += (g_pix += k);
k = b_pix * 2;
row1[cursor1 + 2 - DJ] = (short) b_pix;
row1[cursor1 + 2 + DJ] += (b_pix += k);
row1[cursor1 + 2] += (b_pix += k);
row0[cursor0 + 2 + DJ] += (b_pix += k);
k = a_pix * 2;
row1[cursor1 + 3 - DJ] = (short) a_pix;
row1[cursor1 + 3 + DJ] += (a_pix += k);
row1[cursor1 + 3] += (a_pix += k);
row0[cursor0 + 3 + DJ] += (a_pix += k);
cursor0 += DJ;
cursor1 -= DJ;
pixelIndex += dir;
}
if ((i % 2) == 1)
pixelIndex += (width + 1);
odd_scanline = !odd_scanline;
}
if (hasTransparency)
return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.ARGB_8888);
return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.RGB_565);
}
@SuppressWarnings("UnusedReturnValue")
public Bitmap convert(int nMaxColors, boolean dither) {
final int[] cPixels = new int[pixels.length];
for (int i =0; i<cPixels.length; ++i) {
int pixel = pixels[i];
int alfa = (pixel >> 24) & 0xff;
int r = (pixel >> 16) & 0xff;
int g = (pixel >> 8) & 0xff;
int b = (pixel ) & 0xff;
cPixels[i] = Color.argb(alfa, r, g, b);
if (alfa < BYTE_MAX) {
hasSemiTransparency = true;
if (alfa == 0) {
hasTransparency = true;
m_transparentColor = cPixels[i];
}
}
}
if (nMaxColors > 256) {
int[] qPixels = new int[cPixels.length];
return quantize_image(cPixels, qPixels);
}
boolean quan_sqrt = nMaxColors > BYTE_MAX;
Integer[] palette = new Integer[nMaxColors];
if (nMaxColors > 2)
palette = pnnquan(cPixels, nMaxColors, quan_sqrt);
else {
if (hasSemiTransparency) {
palette[0] = Color.argb(0, 0, 0, 0);
palette[1] = Color.BLACK;
}
else {
palette[0] = Color.BLACK;
palette[1] = Color.WHITE;
}
}
int[] qPixels = new int[cPixels.length];
quantize_image(cPixels, palette, qPixels, dither);
closestMap.clear();
if (hasTransparency)
return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.ARGB_8888);
return Bitmap.createBitmap(qPixels, width, height, Bitmap.Config.RGB_565);
}
}

View File

@ -1,6 +0,0 @@
package oupson.apng.utils
import android.widget.ImageView
@Deprecated("Deprecated, Use ApngEncoder and ApngDecoder instead", level = DeprecationLevel.WARNING)
class ApngAnimatorOptions(val scaleType: ImageView.ScaleType? = ImageView.ScaleType.FIT_CENTER)

View File

@ -1,4 +1,4 @@
package oupson.apng
package oupson.apng.utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

View File

@ -1,6 +1,8 @@
package oupson.apngcreator.fragments
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -11,8 +13,8 @@ import android.widget.ImageView
import android.widget.SeekBar
import androidx.fragment.app.Fragment
import com.squareup.picasso.Picasso
import oupson.apng.ApngAnimator
import oupson.apng.ApngAnimator.Companion.loadApng
import kotlinx.android.synthetic.main.activity_creator.*
import oupson.apng.decoder.ApngDecoder
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
@ -33,7 +35,11 @@ class KotlinFragment : Fragment() {
private var speedSeekBar : SeekBar? = null
private var animator : ApngAnimator? = null
//private var animator : ApngAnimator? = null
private var animation : AnimationDrawable? = null
private var durations : IntArray? = null
private var frameIndex = 0
private val imageUrls = arrayListOf(
"http://oupson.oupsman.fr/apng/bigApng.png",
@ -71,11 +77,34 @@ class KotlinFragment : Fragment() {
Log.v(TAG, "onResume()")
playButton?.setOnClickListener {
animator?.play()
animation?.start()
}
pauseButton?.setOnClickListener {
animator?.pause()
animation = animation?.let { animation ->
val res = AnimationDrawable()
animation.stop()
val currentFrame = animation.current
frameLoop@ for (i in 0 until animation.numberOfFrames) {
val checkFrame = animation.getFrame(i)
if (checkFrame === currentFrame) {
frameIndex = i
for (k in frameIndex until animation.numberOfFrames) {
val frame: Drawable = animation.getFrame(k)
res.addFrame(frame, animation.getDuration(i))
}
for (k in 0 until frameIndex) {
val frame: Drawable = animation.getFrame(k)
res.addFrame(frame, animation.getDuration(i))
}
apngImageView?.setImageDrawable(res)
animation.invalidateSelf()
break@frameLoop
}
}
res
}
}
speedSeekBar?.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@ -88,23 +117,42 @@ class KotlinFragment : Fragment() {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
if (seekBar != null)
animator?.speed = seekBar.progress.toFloat() / 100f
if (seekBar != null && durations != null) {
val speed = seekBar.progress.toFloat() / 100f
animation = animation?.let { animation ->
val res = AnimationDrawable()
animation.stop()
for (i in 0 until animation.numberOfFrames) {
res.addFrame(animation.getFrame(i), (durations!![i].toFloat() / speed).toInt())
}
apngImageView?.setImageDrawable(res)
animation.invalidateSelf()
res.start()
res
}
}
}
})
if (animator == null) {
try {
animator = apngImageView?.loadApng(imageUrls[selected])?.apply {
onLoaded {
setOnFrameChangeLister {
// Log.v("app-test", "onLoop")
if (animation == null) {
ApngDecoder.decodeApngAsyncInto(
this.context!!,
imageUrls[selected],
apngImageView!!,
callback = object : ApngDecoder.Callback {
override fun onSuccess(drawable: Drawable) {
animation = (drawable as? AnimationDrawable)
durations = IntArray(animation?.numberOfFrames ?: 0) { i ->
animation?.getDuration(i) ?: 0
}
}
}
} catch (e : Exception) {
Log.e(TAG, "Error : $e")
}
override fun onError(error: Exception) {
Log.e(TAG, "Error : $error")
}
})
}
Picasso.get().load(imageUrls[selected]).into(normalImageView)
@ -115,7 +163,7 @@ class KotlinFragment : Fragment() {
if (BuildConfig.DEBUG)
Log.v(TAG, "onPause()")
animator = null
animation = null
normalImageView?.setImageDrawable(null)
apngImageView?.setImageDrawable(null)