New png encoding method

This commit is contained in:
oupson 2018-12-10 20:17:00 +01:00
parent f7f1bb7025
commit 1a462780c7
6 changed files with 433 additions and 61 deletions

View File

@ -7,7 +7,7 @@ android {
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0.8"
versionName "1.0.9"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

View File

@ -5,13 +5,13 @@ import android.graphics.BitmapFactory
import oupson.apng.ImageUtils.PnnQuantizer
import oupson.apng.chunks.IDAT
import oupson.apng.exceptions.NoFrameException
import oupson.apng.utils.PngEncoder
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.getBlend_op
import oupson.apng.utils.Utils.Companion.getDispose_op
import oupson.apng.utils.Utils.Companion.pngSignature
import oupson.apng.utils.Utils.Companion.to2Bytes
import oupson.apng.utils.Utils.Companion.to4Bytes
import oupson.apng.utils.Utils.Companion.toByteArray
import java.util.zip.CRC32
/**
@ -37,7 +37,7 @@ class Apng {
* @param bitmap The bitamp to add
*/
fun addFrames(bitmap: Bitmap) {
frames.add(Frame(toByteArray(bitmap)))
frames.add(Frame(PngEncoder.encode(bitmap, true, 1)))
}
/**
@ -46,7 +46,7 @@ class Apng {
* @param delay Delay of the frame
*/
fun addFrames(bitmap: Bitmap, delay : Float) {
frames.add(Frame(toByteArray(bitmap), delay))
frames.add(Frame(PngEncoder.encode(bitmap, true, 1), delay))
}
/**
@ -57,7 +57,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(bitmap: Bitmap, delay: Float, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(Frame(toByteArray(bitmap), delay, blend_op, dispose_op))
frames.add(Frame(PngEncoder.encode(bitmap, true, 1), delay, blend_op, dispose_op))
}
/**
@ -70,7 +70,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(bitmap: Bitmap, delay: Float, xOffset : Int, yOffset : Int, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(Frame(toByteArray(bitmap), delay, xOffset, yOffset, blend_op, dispose_op))
frames.add(Frame(PngEncoder.encode(bitmap, true, 1), delay, xOffset, yOffset, blend_op, dispose_op))
}
/**
@ -79,7 +79,7 @@ class Apng {
* @param bitmap The bitamp to add
*/
fun addFrames(index : Int, bitmap: Bitmap) {
frames.add(index, Frame(toByteArray(bitmap)))
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1)))
}
/**
@ -89,7 +89,7 @@ class Apng {
* @param delay Delay of the frame
*/
fun addFrames(index : Int, bitmap: Bitmap, delay : Float) {
frames.add(index, Frame(toByteArray(bitmap), delay))
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1), delay))
}
/**
@ -101,7 +101,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(index: Int, bitmap: Bitmap, delay: Float, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(index, Frame(toByteArray(bitmap), delay, blend_op, dispose_op))
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1), delay, blend_op, dispose_op))
}
/**
@ -115,7 +115,7 @@ class Apng {
* @param blend_op `blend_op` 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.
*/
fun addFrames(index: Int, bitmap: Bitmap, delay: Float, xOffset : Int, yOffset : Int, dispose_op: Utils.Companion.dispose_op, blend_op: Utils.Companion.blend_op) {
frames.add(index, Frame(toByteArray(bitmap), delay, xOffset, yOffset, blend_op, dispose_op))
frames.add(index, Frame(PngEncoder.encode(bitmap, true, 1), delay, xOffset, yOffset, blend_op, dispose_op))
}
fun addFrames(frame : Frame) {
@ -224,7 +224,7 @@ class Apng {
// Add cover image : Not part of animation
// region IDAT
val idat = IDAT()
idat.parse(toByteArray(cover!!))
idat.parse(PngEncoder.encode(cover!!, true, 1))
idat.IDATBody.forEach {
val idatByteArray = ArrayList<Byte>()
framesByte.addAll(to4Bytes(it.size).toList())

View File

@ -1,12 +1,10 @@
package oupson.apng
import android.graphics.BitmapFactory
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.isPng
import oupson.apng.utils.Utils.Companion.toByteArray
/**
* A frame for an animated png
@ -40,19 +38,17 @@ class Frame {
constructor(byteArray: ByteArray) {
if (isPng(byteArray)) {
val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
val bytes = toByteArray(btm)
this.byteArray = bytes
this.byteArray = byteArray
// Get width and height for image
ihdr = IHDR()
ihdr.parse(bytes)
ihdr.parse(byteArray)
width = ihdr.pngWidth
height = ihdr.pngHeight
// Get IDAT Bytes
idat = IDAT()
idat.parse(bytes)
idat.parse(byteArray)
delay = 1000f
@ -64,19 +60,17 @@ class Frame {
}
constructor(byteArray: ByteArray, delay : Float) {
if (isPng(byteArray)) {
val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
val bytes = toByteArray(btm)
this.byteArray = bytes
this.byteArray = byteArray
// Get width and height for image
ihdr = IHDR()
ihdr.parse(bytes)
ihdr.parse(byteArray)
width = ihdr.pngWidth
height = ihdr.pngHeight
// Get IDAT Bytes
idat = IDAT()
idat.parse(bytes)
idat.parse(byteArray)
this.delay = delay
blend_op = Utils.Companion.blend_op.APNG_BLEND_OP_SOURCE
@ -88,19 +82,17 @@ class Frame {
constructor(byteArray: ByteArray, delay : Float, blend_op: Utils.Companion.blend_op, dispose_op: Utils.Companion.dispose_op) {
if (isPng(byteArray)) {
val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
val bytes = toByteArray(btm)
this.byteArray = bytes
this.byteArray = byteArray
// Get width and height for image
ihdr = IHDR()
ihdr.parse(bytes)
ihdr.parse(byteArray)
width = ihdr.pngWidth
height = ihdr.pngHeight
// Get IDAT Bytes
idat = IDAT()
idat.parse(bytes)
idat.parse(byteArray)
this.delay = delay
@ -116,19 +108,17 @@ class Frame {
constructor(byteArray: ByteArray, delay : Float, xOffsets : Int, yOffsets : Int, blend_op: Utils.Companion.blend_op, dispose_op: Utils.Companion.dispose_op) {
if (isPng(byteArray)) {
val btm = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
val bytes = toByteArray(btm)
this.byteArray = bytes
this.byteArray = byteArray
// Get width and height for image
ihdr = IHDR()
ihdr.parse(bytes)
ihdr.parse(byteArray)
width = ihdr.pngWidth
height = ihdr.pngHeight
// Get IDAT Bytes
idat = IDAT()
idat.parse(bytes)
idat.parse(byteArray)
this.delay = delay

View File

@ -0,0 +1,410 @@
package oupson.apng.utils
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
/**
* Taken from http://catcode.com/pngencoder/com/keypoint/PngEncoder.java
*/
class PngEncoder {
companion object {
/** Encode alpha ? */
private var encodeAlpha = true
/** 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
/** 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, Math.min(oldLength, newLength))
return newArray
}
/**
* 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 = Math.max(maxPos, offset + data.size)
if (data.size + offset > pngBytes!!.size) {
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + Math.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 = Math.max(maxPos, offset + nBytes)
if (nBytes + offset > pngBytes!!.size) {
pngBytes = resizeByteArray(pngBytes!!, pngBytes!!.size + Math.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.
*/
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() {
val startPos: Int = bytePos
bytePos = writeInt4(13, 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 = Math.min(32767 / (width * (bytesPerPixel + 1)), rowsLeft)
nRows = Math.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()
return true
} catch (e: IOException) {
System.err.println(e.toString())
return false
}
}
/**
* Write a PNG "IEND" chunk into the pngBytes array.
*/
private fun writeEnd() {
bytePos = writeInt4(0, bytePos)
bytePos = writeBytes(IEND, bytePos)
crc.reset()
crc.update(IEND)
crcValue = crc.value
bytePos = writeInt4(crcValue.toInt(), bytePos)
}
}
}

View File

@ -1,8 +1,5 @@
package oupson.apng.utils
import android.graphics.Bitmap
import android.graphics.Canvas
import java.io.ByteArrayOutputStream
import java.util.*
class Utils {
@ -43,7 +40,6 @@ class Utils {
*/
val pngSignature: ByteArray = byteArrayOf(0x89.toByte(), 0x50.toByte(), 0x4E.toByte(), 0x47.toByte(), 0x0D.toByte(), 0x0A.toByte(), 0x1A.toByte(), 0x0A.toByte())
enum class dispose_op {
APNG_DISPOSE_OP_NONE,
APNG_DISPOSE_OP_BACKGROUND,
@ -107,30 +103,6 @@ class Utils {
}
}
/**
* Get PNG ByteArray of Bitmap
* @param bitmap The bitmap to convert
* @return PNG ByteArray of the Bitmap
*/
fun toByteArray(bitmap: Bitmap) : ByteArray {
val bos = ByteArrayOutputStream();
convertImage(bitmap).compress(Bitmap.CompressFormat.PNG, 100 /*ignored for PNG*/, bos);
return bos.toByteArray();
}
/**
* Convert the image
* @param bitmap Bitmap to convert
* @return Bitmap converted
*/
fun convertImage(bitmap: Bitmap) : Bitmap{
val btm = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
btm.setHasAlpha(true)
val canvas = Canvas(btm)
canvas.drawBitmap(bitmap, 0f, 0f, null)
return btm
}
/**
* Generate a 4 bytes array from an Int
* @param i The int