Remove deprecated classes
This commit is contained in:
parent
3f5f95c795
commit
c4e38bf54f
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.*
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -1,4 +1,4 @@
|
|||
package oupson.apng
|
||||
package oupson.apng.utils
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(error: Exception) {
|
||||
Log.e(TAG, "Error : $error")
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Log.e(TAG, "Error : $e")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue