APNGデコーダにjapngを利用するのをやめて、フルスクラッチで書き起こした
This commit is contained in:
parent
226e0602f7
commit
6fada3fb93
|
@ -8,6 +8,7 @@
|
|||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/apng" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/colorpicker" />
|
||||
<option value="$PROJECT_DIR$/emoji" />
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/SubwayTooter.iml" filepath="$PROJECT_DIR$/SubwayTooter.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/apng/apng.iml" filepath="$PROJECT_DIR$/apng/apng.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/colorpicker/colorpicker.iml" filepath="$PROJECT_DIR$/colorpicker/colorpicker.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/emoji/emoji.iml" filepath="$PROJECT_DIR$/emoji/emoji.iml" />
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'java-library'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
|
||||
sourceCompatibility = "1.7"
|
||||
targetCompatibility = "1.7"
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
class Apng {
|
||||
var header: ApngImageHeader? = null
|
||||
var background: ApngBackground? = null
|
||||
var animationControl: ApngAnimationControl? = null
|
||||
internal var palette: ApngPalette? = null
|
||||
internal var transparentColor: ApngTransparentColor? = null
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.ByteArrayTokenizer
|
||||
|
||||
|
||||
class ApngAnimationControl internal constructor(bat: ByteArrayTokenizer) {
|
||||
|
||||
companion object {
|
||||
const val PLAY_INDEFINITELY =0
|
||||
}
|
||||
|
||||
// This must equal the number of `fcTL` chunks.
|
||||
// 0 is not a valid value.
|
||||
// 1 is a valid value for a single-frame APNG.
|
||||
val numFrames: Int
|
||||
|
||||
// if it is 0, the animation should play indefinitely.
|
||||
// If nonzero, the animation should come to rest on the final frame at the end of the last play.
|
||||
val numPlays: Int
|
||||
|
||||
init {
|
||||
numFrames = bat.readInt32()
|
||||
numPlays = bat.readInt32()
|
||||
}
|
||||
|
||||
override fun toString() ="ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"
|
||||
|
||||
val isPlayIndefinitely :Boolean
|
||||
get() = numPlays == PLAY_INDEFINITELY
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.ByteArrayTokenizer
|
||||
|
||||
class ApngBackground internal constructor(colorType: ColorType, bat: ByteArrayTokenizer) {
|
||||
|
||||
val red: Int
|
||||
val green: Int
|
||||
val blue: Int
|
||||
val index: Int
|
||||
|
||||
init {
|
||||
when (colorType) {
|
||||
ColorType.GREY, ColorType.GREY_ALPHA -> {
|
||||
val v = bat.readUInt16()
|
||||
red = v
|
||||
green = v
|
||||
blue = v
|
||||
index = -1
|
||||
}
|
||||
ColorType.RGB, ColorType.RGBA -> {
|
||||
red = bat.readUInt16()
|
||||
green = bat.readUInt16()
|
||||
blue = bat.readUInt16()
|
||||
index = -1
|
||||
}
|
||||
ColorType.INDEX -> {
|
||||
red = -1
|
||||
green = -1
|
||||
blue = -1
|
||||
index = bat.readUInt8()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
class ApngBitmap(var width: Int, var height: Int) {
|
||||
|
||||
val colors = IntArray( width * height)
|
||||
|
||||
fun reset(width: Int, height: Int) {
|
||||
val newSize = width * height
|
||||
if( newSize > colors.size )
|
||||
throw ParseError("can't resize to ${width}x${height} , it's greater than initial size")
|
||||
this.width = width
|
||||
this.height = height
|
||||
colors.fill( 0,fromIndex = 0,toIndex = newSize)
|
||||
}
|
||||
|
||||
inner class Pointer(private var pos: Int) {
|
||||
|
||||
fun plusX(x: Int): Pointer {
|
||||
pos += x
|
||||
return this
|
||||
}
|
||||
|
||||
fun setPixel(a: Int, r: Int, g: Int, b: Int): Pointer {
|
||||
colors[pos] = ((a and 255) shl 24) or ((r and 255) shl 16) or ((g and 255) shl 8) or (b and 255)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setPixel(a: Byte, r: Byte, g: Byte, b: Byte): Pointer {
|
||||
colors[pos] = ((a.toInt() and 255) shl 24) or ((r.toInt() and 255) shl 16) or ((g.toInt() and 255) shl 8) or (b.toInt() and 255)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
fun pointer(x: Int, y: Int) = Pointer( x + y * width )
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.StreamTokenizer
|
||||
import java.util.zip.CRC32
|
||||
|
||||
internal class ApngChunk(crc32:CRC32,tokenizer: StreamTokenizer) {
|
||||
val size: Int
|
||||
val type: String
|
||||
|
||||
init {
|
||||
size = tokenizer.readInt32()
|
||||
val typeBytes = tokenizer.readBytes(4)
|
||||
type = typeBytes.toString(Charsets.UTF_8)
|
||||
|
||||
crc32.update(typeBytes)
|
||||
}
|
||||
|
||||
fun readBody(crc32:CRC32,tokenizer: StreamTokenizer): ByteArray {
|
||||
val bytes = tokenizer.readBytes(size)
|
||||
val crcExpect = tokenizer.readUInt32()
|
||||
|
||||
crc32.update(bytes, 0, size)
|
||||
val crcActual = crc32.value
|
||||
if (crcActual != crcExpect) throw ParseError("CRC not match.")
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
fun skipBody(tokenizer: StreamTokenizer) {
|
||||
tokenizer.skipBytes((size + 4).toLong())
|
||||
}
|
||||
|
||||
fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) {
|
||||
val crcExpect = tokenizer.readUInt32()
|
||||
if (crcActual != crcExpect) throw ParseError("CRC not match.")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.BufferPool
|
||||
import jp.juggler.apng.util.ByteArrayTokenizer
|
||||
import jp.juggler.apng.util.StreamTokenizer
|
||||
import java.io.InputStream
|
||||
import java.util.zip.CRC32
|
||||
|
||||
object ApngDecoder {
|
||||
|
||||
private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a)
|
||||
|
||||
fun parseStream(
|
||||
_inStream: InputStream,
|
||||
callback: ApngDecoderCallback
|
||||
) {
|
||||
val apng = Apng()
|
||||
val tokenizer = StreamTokenizer(_inStream)
|
||||
|
||||
val pngHeader = tokenizer.readBytes(8)
|
||||
if (!pngHeader.contentEquals(PNG_SIGNATURE)) {
|
||||
throw ParseError("header not match")
|
||||
}
|
||||
|
||||
var lastSequenceNumber: Int? = null
|
||||
fun checkSequenceNumber(n: Int) {
|
||||
val last = lastSequenceNumber
|
||||
if (last != null && n <= last) {
|
||||
throw ParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n")
|
||||
}
|
||||
lastSequenceNumber = n
|
||||
}
|
||||
|
||||
val inBuffer = ByteArray(4096)
|
||||
val inflateBufferPool = BufferPool(8192)
|
||||
var idatDecoder: IdatDecoder? = null
|
||||
var fdatDecoder: IdatDecoder? = null
|
||||
val crc32 = CRC32()
|
||||
var lastFctl: ApngFrameControl? = null
|
||||
var bitmap: ApngBitmap? = null
|
||||
|
||||
loop@ while (true) {
|
||||
crc32.reset()
|
||||
val chunk = ApngChunk(crc32, tokenizer)
|
||||
when (chunk.type) {
|
||||
|
||||
"IEND" -> break@loop
|
||||
|
||||
"IHDR" -> {
|
||||
val header = ApngImageHeader(ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
|
||||
bitmap = ApngBitmap(header.width, header.height)
|
||||
apng.header = header
|
||||
callback.onHeader(apng, header)
|
||||
}
|
||||
|
||||
"PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer))
|
||||
|
||||
"bKGD" -> {
|
||||
val header = apng.header ?: throw ParseError("missing IHDR")
|
||||
apng.background = ApngBackground(header.colorType, ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
|
||||
}
|
||||
|
||||
"tRNS" -> {
|
||||
val header = apng.header ?: throw ParseError("missing IHDR")
|
||||
val body = chunk.readBody(crc32, tokenizer)
|
||||
when (header.colorType) {
|
||||
ColorType.GREY -> apng.transparentColor = ApngTransparentColor(true, ByteArrayTokenizer(body))
|
||||
ColorType.RGB -> apng.transparentColor = ApngTransparentColor(false, ByteArrayTokenizer(body))
|
||||
ColorType.INDEX -> apng.palette?.parseTRNS(body) ?: throw ParseError("missing palette")
|
||||
else -> callback.log("tRNS ignored. colorType =${header.colorType}")
|
||||
}
|
||||
}
|
||||
|
||||
"IDAT" -> {
|
||||
val header = apng.header ?: throw ParseError("missing IHDR")
|
||||
if (idatDecoder == null) {
|
||||
bitmap ?: throw ParseError("missing bitmap")
|
||||
bitmap.reset(header.width, header.height)
|
||||
idatDecoder = IdatDecoder(apng, bitmap, inflateBufferPool) {
|
||||
callback.onDefaultImage(apng, bitmap)
|
||||
val fctl = lastFctl
|
||||
if (fctl != null) {
|
||||
// IDATより前にfcTLが登場しているなら、そのfcTLの画像はIDATと同じ
|
||||
callback.onAnimationFrame(apng, fctl, bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
idatDecoder.addData(
|
||||
tokenizer.inStream,
|
||||
chunk.size,
|
||||
inBuffer,
|
||||
crc32
|
||||
)
|
||||
chunk.checkCRC(tokenizer, crc32.value)
|
||||
}
|
||||
|
||||
"acTL" -> {
|
||||
val animationControl = ApngAnimationControl(ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
|
||||
apng.animationControl = animationControl
|
||||
callback.onAnimationInfo(apng, animationControl)
|
||||
}
|
||||
|
||||
"fcTL" -> {
|
||||
val bat = ByteArrayTokenizer(chunk.readBody(crc32, tokenizer))
|
||||
checkSequenceNumber(bat.readInt32())
|
||||
lastFctl = ApngFrameControl(bat)
|
||||
fdatDecoder = null
|
||||
}
|
||||
|
||||
"fdAT" -> {
|
||||
val fctl = lastFctl ?: throw ParseError("missing fCTL before fdAT")
|
||||
if (fdatDecoder == null) {
|
||||
bitmap ?: throw ParseError("missing bitmap")
|
||||
bitmap.reset(fctl.width, fctl.height)
|
||||
fdatDecoder = IdatDecoder(apng, bitmap, inflateBufferPool) {
|
||||
callback.onAnimationFrame(apng, fctl, bitmap)
|
||||
}
|
||||
}
|
||||
checkSequenceNumber(tokenizer.readInt32(crc32))
|
||||
fdatDecoder.addData(
|
||||
tokenizer.inStream,
|
||||
chunk.size - 4,
|
||||
inBuffer,
|
||||
crc32
|
||||
)
|
||||
chunk.checkCRC(tokenizer, crc32.value)
|
||||
}
|
||||
|
||||
// 無視するチャンク
|
||||
"cHRM", "gAMA", "iCCP", "sBIT", "sRGB", // color space information
|
||||
"tEXt", "zTXt", "iTXt", // text information
|
||||
"tIME", // timestamp
|
||||
"hIST", // histogram
|
||||
"pHYs", // Physical pixel dimensions
|
||||
"sPLT" // Suggested palette (おそらく減色用?)
|
||||
-> chunk.skipBody(tokenizer)
|
||||
|
||||
else -> {
|
||||
callback.log("unknown chunk: type=%s,size=0x%x".format(chunk.type, chunk.size))
|
||||
chunk.skipBody(tokenizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
interface ApngDecoderCallback{
|
||||
fun onHeader(apng: Apng, header: ApngImageHeader)
|
||||
fun onAnimationInfo(apng: Apng, animationControl: ApngAnimationControl)
|
||||
fun onDefaultImage(apng: Apng, bitmap: ApngBitmap)
|
||||
fun onAnimationFrame(apng: Apng, frameControl: ApngFrameControl, bitmap: ApngBitmap)
|
||||
fun log(message:String)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
enum class ColorType(val num:Int ){
|
||||
GREY(0),
|
||||
RGB ( 2),
|
||||
INDEX( 3),
|
||||
GREY_ALPHA ( 4),
|
||||
RGBA ( 6),
|
||||
}
|
||||
|
||||
enum class CompressionMethod(val num:Int ){
|
||||
Standard(0)
|
||||
}
|
||||
|
||||
enum class FilterMethod(val num:Int ){
|
||||
Standard(0)
|
||||
}
|
||||
|
||||
enum class InterlaceMethod(val num:Int ){
|
||||
None(0),
|
||||
Standard(1)
|
||||
}
|
||||
|
||||
enum class FilterType(val num:Int ){
|
||||
None(0),
|
||||
Sub(1),
|
||||
Up(2),
|
||||
Average(3),
|
||||
Paeth(4)
|
||||
}
|
||||
|
||||
enum class DisposeOp(val num :Int){
|
||||
//no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
|
||||
None(0),
|
||||
// the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
|
||||
Background(1),
|
||||
// the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
|
||||
Previous(2)
|
||||
}
|
||||
|
||||
enum class BlendOp(val num :Int){
|
||||
// all color components of the frame, including alpha, overwrite the current contents of the frame's output buffer region.
|
||||
Source(0),
|
||||
// the frame should be composited onto the output buffer based on its alpha, using a simple OVER operation as described in the "Alpha Channel Processing" section of the PNG specification [PNG-1.2].
|
||||
Over(1)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.ByteArrayTokenizer
|
||||
|
||||
|
||||
class ApngFrameControl internal constructor(bat: ByteArrayTokenizer) {
|
||||
|
||||
val width: Int
|
||||
val height: Int
|
||||
val xOffset: Int
|
||||
val yOffset: Int
|
||||
val delayNum: Int
|
||||
val delayDen: Int
|
||||
val disposeOp: DisposeOp
|
||||
val blendOp: BlendOp
|
||||
|
||||
init {
|
||||
width = bat.readInt32()
|
||||
height = bat.readInt32()
|
||||
xOffset = bat.readInt32()
|
||||
yOffset = bat.readInt32()
|
||||
delayNum = bat.readUInt16()
|
||||
delayDen = bat.readUInt16().let{ if(it==0) 100 else it}
|
||||
|
||||
var num:Int
|
||||
|
||||
num = bat.readUInt8()
|
||||
disposeOp = DisposeOp.values().first{it.num==num}
|
||||
|
||||
num = bat.readUInt8()
|
||||
blendOp = BlendOp.values().first{it.num==num}
|
||||
}
|
||||
|
||||
override fun toString() ="ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayNum=$delayNum,delayDen=$delayDen,disposeOp=$disposeOp,blendOp=$blendOp)"
|
||||
|
||||
val delayMilliseconds : Long
|
||||
get() = when(delayDen) {
|
||||
1000 -> delayNum.toLong()
|
||||
else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.ByteArrayTokenizer
|
||||
|
||||
|
||||
class ApngImageHeader internal constructor(bat: ByteArrayTokenizer) {
|
||||
val width: Int
|
||||
val height: Int
|
||||
val bitDepth: Int
|
||||
val colorType: ColorType
|
||||
val compressionMethod: CompressionMethod
|
||||
val filterMethod: FilterMethod
|
||||
val interlaceMethod: InterlaceMethod
|
||||
|
||||
init {
|
||||
|
||||
width = bat.readInt32()
|
||||
height = bat.readInt32()
|
||||
bitDepth = bat.readUInt8()
|
||||
|
||||
var num:Int
|
||||
//
|
||||
num =bat.readUInt8()
|
||||
colorType = ColorType.values().first { it.num==num }
|
||||
//
|
||||
num =bat.readUInt8()
|
||||
compressionMethod = CompressionMethod.values().first { it.num==num }
|
||||
//
|
||||
num =bat.readUInt8()
|
||||
filterMethod = FilterMethod.values().first { it.num==num }
|
||||
//
|
||||
num =bat.readUInt8()
|
||||
interlaceMethod = InterlaceMethod.values().first { it.num==num }
|
||||
}
|
||||
|
||||
override fun toString() = "ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)"
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
|
||||
class ApngPalette(rgb: ByteArray) {
|
||||
val list: ByteArray
|
||||
var hasAlpha: Boolean = false
|
||||
|
||||
init {
|
||||
val entryCount = rgb.size / 3
|
||||
list = ByteArray(4 * entryCount)
|
||||
for (i in 0 until entryCount) {
|
||||
list[i * 4] = 255.toByte()
|
||||
list[i * 4 + 1] = rgb[i * 3 + 0]
|
||||
list[i * 4 + 2] = rgb[i * 3 + 1]
|
||||
list[i * 4 + 3] = rgb[i * 3 + 2]
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
|
||||
|
||||
fun parseTRNS(ba: ByteArray) {
|
||||
hasAlpha = true
|
||||
for (i in 0 until Math.min(list.size, ba.size)) {
|
||||
list[i * 4] = ba[i]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.ByteArrayTokenizer
|
||||
|
||||
class ApngTransparentColor internal constructor(isGreyScale:Boolean, bat: ByteArrayTokenizer) {
|
||||
val red:Int
|
||||
val green:Int
|
||||
val blue:Int
|
||||
init{
|
||||
if( isGreyScale){
|
||||
val v = bat.readUInt16()
|
||||
red =v
|
||||
green =v
|
||||
blue =v
|
||||
}else{
|
||||
red =bat.readUInt16()
|
||||
green =bat.readUInt16()
|
||||
blue =bat.readUInt16()
|
||||
}
|
||||
}
|
||||
|
||||
fun match(grey:Int) = red == grey
|
||||
fun match(r:Int,g:Int,b:Int) = (r==red && g == green && b == blue)
|
||||
}
|
|
@ -0,0 +1,612 @@
|
|||
@file:Suppress("JoinDeclarationAndAssignment")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
import jp.juggler.apng.util.BufferPool
|
||||
import jp.juggler.apng.util.ByteArrayQueue
|
||||
import jp.juggler.apng.util.ByteArrayRange
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
import java.util.zip.CRC32
|
||||
import java.util.zip.Inflater
|
||||
|
||||
internal class IdatDecoder(
|
||||
apng: Apng,
|
||||
private val bitmap: ApngBitmap,
|
||||
private val inflateBufferPool: BufferPool,
|
||||
private val onCompleted: () -> Unit
|
||||
) {
|
||||
|
||||
class PassInfo(val xStep: Int, val xStart: Int, val yStep: Int, val yStart: Int)
|
||||
|
||||
companion object {
|
||||
|
||||
private val passInfoList = listOf(
|
||||
PassInfo(1, 0, 1, 0), // [0]:no interlacing
|
||||
PassInfo(8, 0, 8, 0), // Adam7 pass 1
|
||||
PassInfo(8, 4, 8, 0), // Adam7 pass 2
|
||||
PassInfo(4, 0, 8, 4), // Adam7 pass 3
|
||||
PassInfo(4, 2, 4, 0), // Adam7 pass 4
|
||||
PassInfo(2, 0, 4, 2), // Adam7 pass 5
|
||||
PassInfo(2, 1, 2, 0), // Adam7 pass 6
|
||||
PassInfo(1, 0, 2, 1) // Adam7 pass 7
|
||||
|
||||
)
|
||||
|
||||
private fun abs(v: Int) = if (v >= 0) v else -v
|
||||
|
||||
private val dummyPaletteData = ByteArray(0)
|
||||
|
||||
// a = left, b = above, c = upper left
|
||||
private fun paeth(a: Int, b: Int, c: Int): Int {
|
||||
val p = a + b - c
|
||||
val pa = abs(p - a)
|
||||
val pb = abs(p - b)
|
||||
val pc = abs(p - c)
|
||||
return when {
|
||||
(pa <= pb && pa <= pc) -> a
|
||||
(pb <= pc) -> b
|
||||
else -> c
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine1(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (pass_w - x >= 8) {
|
||||
val v = baLine[pos++].toInt()
|
||||
block((v shr 7) and 1)
|
||||
block((v shr 6) and 1)
|
||||
block((v shr 5) and 1)
|
||||
block((v shr 4) and 1)
|
||||
block((v shr 3) and 1)
|
||||
block((v shr 2) and 1)
|
||||
block((v shr 1) and 1)
|
||||
block(v and 1)
|
||||
x += 8
|
||||
}
|
||||
val remain = pass_w - x
|
||||
if (remain > 0) {
|
||||
val v = baLine[pos].toInt()
|
||||
block((v shr 7) and 1)
|
||||
if (remain > 1) block((v shr 6) and 1)
|
||||
if (remain > 2) block((v shr 5) and 1)
|
||||
if (remain > 3) block((v shr 4) and 1)
|
||||
if (remain > 4) block((v shr 3) and 1)
|
||||
if (remain > 5) block((v shr 2) and 1)
|
||||
if (remain > 6) block((v shr 1) and 1)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine2(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (pass_w - x >= 4) {
|
||||
val v = baLine[pos++].toInt()
|
||||
block((v shr 6) and 3)
|
||||
block((v shr 4) and 3)
|
||||
block((v shr 2) and 3)
|
||||
block(v and 3)
|
||||
x += 4
|
||||
}
|
||||
val remain = pass_w - x
|
||||
if (remain > 0) {
|
||||
val v = baLine[pos].toInt()
|
||||
block((v shr 6) and 3)
|
||||
if (remain > 1) block((v shr 4) and 3)
|
||||
if (remain > 2) block((v shr 2) and 3)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine4(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (pass_w - x >= 2) {
|
||||
val v = baLine[pos++].toInt()
|
||||
block((v shr 4) and 15)
|
||||
block(v and 15)
|
||||
x += 2
|
||||
}
|
||||
val remain = pass_w - x
|
||||
if (remain > 0) {
|
||||
val v = baLine[pos].toInt()
|
||||
block((v shr 4) and 15)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLine8(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val v = baLine[pos++].toInt()
|
||||
block(v and 255)
|
||||
++x
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseUInt16(ba: ByteArray, pos: Int): Int {
|
||||
val b0 = ba[pos].toInt() and 255
|
||||
val b1 = ba[pos].toInt() and 255
|
||||
return (b0 shl 8) or b1
|
||||
}
|
||||
|
||||
private inline fun scanLine16(baLine: ByteArray, pass_w: Int, block: (v: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val v = parseUInt16(baLine, pos)
|
||||
pos += 2
|
||||
block(v)
|
||||
++x
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLineRGB8(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val r = baLine[pos].toInt() and 255
|
||||
val g = baLine[pos + 1].toInt() and 255
|
||||
val b = baLine[pos + 2].toInt() and 255
|
||||
pos += 3
|
||||
block(r, g, b)
|
||||
++x
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLineRGB16(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val r = parseUInt16(baLine, pos)
|
||||
val g = parseUInt16(baLine, pos + 2)
|
||||
val b = parseUInt16(baLine, pos + 4)
|
||||
pos += 6
|
||||
block(r, g, b)
|
||||
++x
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLineRGBA8(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int, a: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val r = baLine[pos].toInt() and 255
|
||||
val g = baLine[pos + 1].toInt() and 255
|
||||
val b = baLine[pos + 2].toInt() and 255
|
||||
val a = baLine[pos + 3].toInt() and 255
|
||||
pos += 4
|
||||
block(r, g, b, a)
|
||||
++x
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLineRGBA16(baLine: ByteArray, pass_w: Int, block: (r: Int, g: Int, b: Int, a: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val r = parseUInt16(baLine, pos)
|
||||
val g = parseUInt16(baLine, pos + 2)
|
||||
val b = parseUInt16(baLine, pos + 4)
|
||||
val a = parseUInt16(baLine, pos + 6)
|
||||
pos += 8
|
||||
block(r, g, b, a)
|
||||
++x
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLineGA8(baLine: ByteArray, pass_w: Int, block: (g: Int, a: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val g = baLine[pos].toInt() and 255
|
||||
val a = baLine[pos + 1].toInt() and 255
|
||||
pos += 2
|
||||
block(g, a)
|
||||
++x
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun scanLineGA16(baLine: ByteArray, pass_w: Int, block: (g: Int, a: Int) -> Unit) {
|
||||
var pos = 1
|
||||
var x = 0
|
||||
while (x < pass_w) {
|
||||
val g = parseUInt16(baLine, pos)
|
||||
val a = parseUInt16(baLine, pos + 2)
|
||||
pos += 4
|
||||
block(g, a)
|
||||
++x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val inflater = Inflater()
|
||||
private val bytesQueue = ByteArrayQueue { inflateBufferPool.recycle(it.array) }
|
||||
private val colorType: ColorType
|
||||
private val bitDepth: Int
|
||||
private val plteData: ByteArray
|
||||
private val sampleBits: Int
|
||||
private val sampleBytes: Int
|
||||
private val scanLineBytesMax: Int
|
||||
private val linePool = LinkedList<ByteArray>()
|
||||
private val transparentCheckerGrey: (v: Int) -> Int
|
||||
private val transparentCheckerRGB: (r: Int, g: Int, b: Int) -> Int
|
||||
private val renderScanLineFunc: (baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) -> Unit
|
||||
|
||||
private var pass: Int
|
||||
private lateinit var passInfo: PassInfo
|
||||
private var passWidth: Int = 0
|
||||
private var passHeight: Int = 0
|
||||
private var passY: Int = 0
|
||||
private var scanLineBytes: Int = 0
|
||||
private var baPreviousLine: ByteArray? = null
|
||||
private var isCompleted = false
|
||||
|
||||
init {
|
||||
val header = requireNotNull(apng.header)
|
||||
this.colorType = header.colorType
|
||||
this.bitDepth = header.bitDepth
|
||||
|
||||
this.plteData = if (colorType == ColorType.INDEX) {
|
||||
apng.palette?.list
|
||||
?: throw ParseError("missing ApngPalette for index color")
|
||||
} else {
|
||||
dummyPaletteData
|
||||
}
|
||||
|
||||
sampleBits = when (colorType) {
|
||||
ColorType.GREY,ColorType.INDEX -> bitDepth
|
||||
ColorType.GREY_ALPHA -> bitDepth * 2
|
||||
ColorType.RGB -> bitDepth * 3
|
||||
ColorType.RGBA -> bitDepth * 4
|
||||
}
|
||||
|
||||
sampleBytes = (sampleBits + 7) / 8
|
||||
scanLineBytesMax = 1 + (bitmap.width * sampleBits + 7) / 8
|
||||
|
||||
linePool.add(ByteArray(scanLineBytesMax))
|
||||
linePool.add(ByteArray(scanLineBytesMax))
|
||||
|
||||
this.pass = when (header.interlaceMethod) {
|
||||
InterlaceMethod.None -> 0
|
||||
InterlaceMethod.Standard -> 1
|
||||
}
|
||||
|
||||
val transparentColor = apng.transparentColor
|
||||
transparentCheckerGrey = if (transparentColor != null) {
|
||||
{ v: Int -> if (transparentColor.match(v)) 0 else 255 }
|
||||
} else {
|
||||
{ _: Int -> 255 }
|
||||
}
|
||||
|
||||
transparentCheckerRGB = if (transparentColor != null) {
|
||||
{ r: Int, g: Int, b: Int -> if (transparentColor.match(r,g,b)) 0 else 255 }
|
||||
} else {
|
||||
{ _: Int, _: Int, _: Int -> 255 }
|
||||
}
|
||||
|
||||
renderScanLineFunc = selectRenderFunc()
|
||||
|
||||
initializePass()
|
||||
}
|
||||
|
||||
private fun renderGrey1(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine1(baLine, passWidth) { v ->
|
||||
val g8 = if (v == 0) 0 else 255
|
||||
val a8 = transparentCheckerGrey(v)
|
||||
bitmapPointer.setPixel(a8, g8, g8, g8)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderGrey2(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine2(baLine, passWidth) { v ->
|
||||
val g8 = v or (v shl 2) or (v shl 4) or (v shl 6)
|
||||
val a8 = transparentCheckerGrey(v)
|
||||
bitmapPointer.setPixel(a8, g8, g8, g8)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderGrey4(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine4(baLine, passWidth) { v ->
|
||||
val g8 = v or (v shl 4)
|
||||
val a8 = transparentCheckerGrey(v)
|
||||
bitmapPointer.setPixel(a8, g8, g8, g8)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderGrey8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine8(baLine, passWidth) { v ->
|
||||
val a8 = transparentCheckerGrey(v)
|
||||
bitmapPointer.setPixel(a8, v, v, v)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderGrey16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine16(baLine, passWidth) { v ->
|
||||
val g8 = v shr 8
|
||||
val a8 = transparentCheckerGrey(v)
|
||||
bitmapPointer.setPixel(a8, g8, g8, g8)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderRGB8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLineRGB8(baLine, passWidth) { r, g, b ->
|
||||
val a8 = transparentCheckerRGB(r, g, b)
|
||||
bitmapPointer.setPixel(a8, r, g, b)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderRGB16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLineRGB16(baLine, passWidth) { r, g, b ->
|
||||
val a8 = transparentCheckerRGB(r, g, b)
|
||||
bitmapPointer.setPixel(a8, r shr 8, g shr 8, b shr 8)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderIndex1(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine1(baLine, passWidth) { v ->
|
||||
val plteOffset = v * 4
|
||||
bitmapPointer.setPixel(
|
||||
plteData[plteOffset],
|
||||
plteData[plteOffset + 1],
|
||||
plteData[plteOffset + 2],
|
||||
plteData[plteOffset + 3]
|
||||
)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderIndex2(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine2(baLine, passWidth) { v ->
|
||||
val plteOffset = v * 4
|
||||
bitmapPointer.setPixel(
|
||||
plteData[plteOffset],
|
||||
plteData[plteOffset + 1],
|
||||
plteData[plteOffset + 2],
|
||||
plteData[plteOffset + 3]
|
||||
)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderIndex4(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine4(baLine, passWidth) { v ->
|
||||
val plteOffset = v * 4
|
||||
bitmapPointer.setPixel(
|
||||
plteData[plteOffset],
|
||||
plteData[plteOffset + 1],
|
||||
plteData[plteOffset + 2],
|
||||
plteData[plteOffset + 3]
|
||||
)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderIndex8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLine8(baLine, passWidth) { v ->
|
||||
val plteOffset = v * 4
|
||||
bitmapPointer.setPixel(
|
||||
plteData[plteOffset],
|
||||
plteData[plteOffset + 1],
|
||||
plteData[plteOffset + 2],
|
||||
plteData[plteOffset + 3]
|
||||
)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderGA8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLineGA8(baLine, passWidth) { g, a ->
|
||||
bitmapPointer.setPixel(a, g, g, g)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderGA16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLineGA16(baLine, passWidth) { g, a ->
|
||||
val g8 = g shr 8
|
||||
bitmapPointer.setPixel(a shr 8, g8, g8, g8)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderRGBA8(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLineRGBA8(baLine, passWidth) { r, g, b, a ->
|
||||
bitmapPointer.setPixel(a, r, g, b)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderRGBA16(baLine: ByteArray, bitmapPointer: ApngBitmap.Pointer, xStep: Int) {
|
||||
scanLineRGBA16(baLine, passWidth) { r, g, b, a ->
|
||||
bitmapPointer.setPixel(a shr 8, r shr 8, g shr 8, b shr 8)
|
||||
.plusX(xStep)
|
||||
}
|
||||
}
|
||||
|
||||
private fun colorBitsNotSupported(): Nothing {
|
||||
throw ParseError("bitDepth $bitDepth is not supported for $colorType")
|
||||
}
|
||||
|
||||
private fun selectRenderFunc() = when (colorType) {
|
||||
ColorType.GREY -> when (bitDepth) {
|
||||
1 -> ::renderGrey1
|
||||
2 -> ::renderGrey2
|
||||
4 -> ::renderGrey4
|
||||
8 -> ::renderGrey8
|
||||
16 -> ::renderGrey16
|
||||
else -> colorBitsNotSupported()
|
||||
}
|
||||
ColorType.RGB -> when (bitDepth) {
|
||||
8 -> ::renderRGB8
|
||||
16 -> ::renderRGB16
|
||||
else -> colorBitsNotSupported()
|
||||
}
|
||||
ColorType.INDEX -> when (bitDepth) {
|
||||
1 -> ::renderIndex1
|
||||
2 -> ::renderIndex2
|
||||
4 -> ::renderIndex4
|
||||
8 -> ::renderIndex8
|
||||
else -> colorBitsNotSupported()
|
||||
}
|
||||
ColorType.GREY_ALPHA -> when (bitDepth) {
|
||||
8 -> ::renderGA8
|
||||
16 -> ::renderGA16
|
||||
else -> colorBitsNotSupported()
|
||||
}
|
||||
ColorType.RGBA -> when (bitDepth) {
|
||||
8 -> ::renderRGBA8
|
||||
16 -> ::renderRGBA16
|
||||
else -> colorBitsNotSupported()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializePass() {
|
||||
passInfo = passInfoList[pass]
|
||||
passWidth = (bitmap.width + passInfo.xStep - passInfo.xStart - 1) / passInfo.xStep
|
||||
passHeight = (bitmap.height + passInfo.yStep - passInfo.yStart - 1) / passInfo.yStep
|
||||
passY = 0
|
||||
scanLineBytes = 1 + (passWidth * sampleBits + 7) / 8
|
||||
|
||||
baPreviousLine?.let { linePool.add(it) }
|
||||
baPreviousLine = null
|
||||
|
||||
if (passWidth <= 0 || passHeight <= 0) {
|
||||
incrementPassOrComplete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun incrementPassOrComplete(){
|
||||
if (pass in 1..6) {
|
||||
++pass
|
||||
initializePass()
|
||||
} else if (!isCompleted) {
|
||||
isCompleted = true
|
||||
onCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun readScanLine(): Boolean {
|
||||
if (bytesQueue.remain < scanLineBytes) return false
|
||||
|
||||
val baLine = linePool.removeFirst()
|
||||
bytesQueue.readBytes(baLine, 0, scanLineBytes)
|
||||
|
||||
val filterNum = baLine[0].toInt() and 255
|
||||
val filterType = FilterType.values().first { it.num == filterNum }
|
||||
|
||||
when (filterType) {
|
||||
FilterType.None -> {
|
||||
}
|
||||
FilterType.Sub -> {
|
||||
for (pos in 1 until scanLineBytes) {
|
||||
val vLeft = if (pos <= sampleBytes) 0 else baLine[pos - sampleBytes].toInt() and 255
|
||||
val vCur = baLine[pos].toInt() and 255
|
||||
baLine[pos] = (vCur + vLeft).toByte()
|
||||
}
|
||||
}
|
||||
FilterType.Up -> {
|
||||
for (pos in 1 until scanLineBytes) {
|
||||
val vUp = (baPreviousLine?.get(pos)?.toInt() ?: 0) and 255
|
||||
val vCur = baLine[pos].toInt() and 255
|
||||
baLine[pos] = (vCur + vUp).toByte()
|
||||
}
|
||||
}
|
||||
FilterType.Average -> {
|
||||
for (pos in 1 until scanLineBytes) {
|
||||
val vLeft = if (pos <= sampleBytes) 0 else baLine[pos - sampleBytes].toInt() and 255
|
||||
val vUp = (baPreviousLine?.get(pos)?.toInt() ?: 0) and 255
|
||||
val vCur = baLine[pos].toInt() and 255
|
||||
baLine[pos] = (vCur + ((vLeft + vUp) shr 1)).toByte()
|
||||
}
|
||||
}
|
||||
FilterType.Paeth -> {
|
||||
for (pos in 1 until scanLineBytes) {
|
||||
val vLeft = if (pos <= sampleBytes) 0 else baLine[pos - sampleBytes].toInt() and 255
|
||||
val vUp = (baPreviousLine?.get(pos)?.toInt() ?: 0) and 255
|
||||
val vUpperLeft = if (pos <= sampleBytes) 0 else (baPreviousLine?.get(pos - sampleBytes)?.toInt()
|
||||
?: 0) and 255
|
||||
val vCur = baLine[pos].toInt() and 255
|
||||
baLine[pos] = (vCur + paeth(vLeft, vUp, vUpperLeft)).toByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// render scanline
|
||||
renderScanLineFunc(
|
||||
baLine,
|
||||
bitmap.pointer(
|
||||
passInfo.xStart,
|
||||
passInfo.yStart + passInfo.yStep * passY
|
||||
),
|
||||
passInfo.xStep
|
||||
)
|
||||
// save previous line
|
||||
baPreviousLine?.let { linePool.add(it) }
|
||||
baPreviousLine = baLine
|
||||
|
||||
// complete pass?
|
||||
if (++passY >= passHeight) {
|
||||
incrementPassOrComplete()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// returns CRC32 value
|
||||
fun addData(
|
||||
inStream: InputStream,
|
||||
size: Int,
|
||||
inBuffer: ByteArray,
|
||||
crc32: CRC32
|
||||
){
|
||||
var foundEnd = false
|
||||
var inRemain = size
|
||||
while (inRemain > 0 && !foundEnd) {
|
||||
// read from inStream( max 4096 byte)
|
||||
var nRead = 0
|
||||
val nReadMax = Math.min(inBuffer.size, inRemain)
|
||||
while (nRead < nReadMax) {
|
||||
val delta = inStream.read(inBuffer, nRead, nReadMax - nRead)
|
||||
if (delta < 0) {
|
||||
foundEnd = true
|
||||
break
|
||||
}
|
||||
nRead += delta
|
||||
}
|
||||
if (nRead > 0) {
|
||||
inRemain -= nRead
|
||||
crc32.update(inBuffer, 0, nRead)
|
||||
|
||||
// inflate
|
||||
inflater.setInput(inBuffer, 0, nRead)
|
||||
while (!inflater.needsInput()) {
|
||||
val inflateBuffer = inflateBufferPool.obtain()
|
||||
val delta = inflater.inflate(inflateBuffer)
|
||||
if (delta > 0) {
|
||||
bytesQueue.add(ByteArrayRange(inflateBuffer, 0, delta))
|
||||
} else {
|
||||
inflateBufferPool.recycle(inflateBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
// read scanLine
|
||||
while (!isCompleted && readScanLine()) {
|
||||
}
|
||||
|
||||
if (isCompleted) {
|
||||
bytesQueue.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package jp.juggler.apng
|
||||
|
||||
class ParseError(message: String) : IllegalArgumentException(message)
|
|
@ -0,0 +1,15 @@
|
|||
package jp.juggler.apng.util
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal class BufferPool(private val arraySize:Int){
|
||||
private val list =LinkedList<ByteArray>()
|
||||
|
||||
fun recycle(array: ByteArray) {
|
||||
list.add( array)
|
||||
}
|
||||
|
||||
fun obtain(): ByteArray {
|
||||
return if( list.isEmpty() ) ByteArray(arraySize) else list.removeFirst()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package jp.juggler.apng.util
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal class ByteArrayQueue(private val bufferRecycler :(ByteArrayRange)->Unit) {
|
||||
|
||||
private val list = LinkedList<ByteArrayRange>()
|
||||
|
||||
val remain: Int
|
||||
get() = list.sumBy { it.remain }
|
||||
|
||||
fun add(range: ByteArrayRange) {
|
||||
list.add(range)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
for( item in list ){
|
||||
bufferRecycler(item)
|
||||
}
|
||||
list.clear()
|
||||
}
|
||||
|
||||
fun readBytes(dst: ByteArray, offset: Int, length: Int): Int {
|
||||
var nRead = 0
|
||||
while (nRead < length && list.isNotEmpty()) {
|
||||
val item = list.first()
|
||||
if (item.remain <= 0) {
|
||||
bufferRecycler(item)
|
||||
list.removeFirst()
|
||||
} else {
|
||||
val delta = Math.min(item.remain, length - nRead)
|
||||
System.arraycopy(item.array, item.start, dst, offset + nRead, delta)
|
||||
item.start += delta
|
||||
item.remain -= delta
|
||||
nRead += delta
|
||||
}
|
||||
}
|
||||
return nRead
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package jp.juggler.apng.util
|
||||
|
||||
internal class ByteArrayRange(
|
||||
val array: ByteArray,
|
||||
var start: Int,
|
||||
var remain: Int
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
package jp.juggler.apng.util
|
||||
|
||||
import jp.juggler.apng.ParseError
|
||||
|
||||
|
||||
internal class ByteArrayTokenizer(ba: ByteArray) {
|
||||
private val array: ByteArray = ba
|
||||
private val arraySize: Int = ba.size
|
||||
private var pos = 0
|
||||
|
||||
val size: Int
|
||||
get()= arraySize
|
||||
|
||||
val remain: Int
|
||||
get()= arraySize -pos
|
||||
|
||||
fun skipBytes(size: Int) {
|
||||
pos += size
|
||||
}
|
||||
|
||||
fun readBytes(size: Int): ByteArrayRange {
|
||||
if (pos + size > arraySize) {
|
||||
throw ParseError("readBytes: unexpected EoS")
|
||||
}
|
||||
val result = ByteArrayRange(array, pos, size)
|
||||
pos+=size
|
||||
return result
|
||||
}
|
||||
|
||||
private fun readByte(): Int {
|
||||
if (pos >= arraySize) {
|
||||
throw ParseError("readBytes: unexpected EoS")
|
||||
}
|
||||
return array[pos++].toInt() and 0xff
|
||||
}
|
||||
|
||||
fun readInt32(): Int {
|
||||
val b0 = readByte()
|
||||
val b1 = readByte()
|
||||
val b2 = readByte()
|
||||
val b3 = readByte()
|
||||
|
||||
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
|
||||
}
|
||||
|
||||
fun readUInt16(): Int {
|
||||
val b0 = readByte()
|
||||
val b1 = readByte()
|
||||
return (b0 shl 8) or b1
|
||||
}
|
||||
|
||||
fun readUInt8() = readByte()
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package jp.juggler.apng.util
|
||||
|
||||
import jp.juggler.apng.ParseError
|
||||
import java.io.InputStream
|
||||
import java.util.zip.CRC32
|
||||
|
||||
internal class StreamTokenizer(val inStream: InputStream) {
|
||||
|
||||
fun skipBytes(size: Long) {
|
||||
var nRead = 0L
|
||||
while (true) {
|
||||
val remain = size - nRead
|
||||
if (remain <= 0) break
|
||||
val delta = inStream.skip(size - nRead)
|
||||
if (delta <= 0) throw ParseError("skipBytes: unexpected EoS")
|
||||
nRead += delta
|
||||
}
|
||||
}
|
||||
|
||||
fun readBytes(size: Int): ByteArray {
|
||||
val dst = ByteArray(size)
|
||||
var nRead = 0
|
||||
while (true) {
|
||||
val remain = size - nRead
|
||||
if (remain <= 0) break
|
||||
val delta = inStream.read(dst, nRead, size - nRead)
|
||||
if (delta < 0) throw ParseError("readBytes: unexpected EoS")
|
||||
nRead += delta
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
private fun readByte(): Int {
|
||||
val b = inStream.read()
|
||||
if( b == -1 ) throw ParseError("readBytes: unexpected EoS")
|
||||
return b and 0xff
|
||||
}
|
||||
|
||||
fun readInt32(): Int {
|
||||
val b0 = readByte()
|
||||
val b1 = readByte()
|
||||
val b2 = readByte()
|
||||
val b3 = readByte()
|
||||
|
||||
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
|
||||
}
|
||||
|
||||
fun readInt32(crc32: CRC32): Int {
|
||||
val ba = readBytes(4)
|
||||
crc32.update(ba)
|
||||
val b0 = ba[0].toInt() and 255
|
||||
val b1 = ba[1].toInt() and 255
|
||||
val b2 = ba[2].toInt() and 255
|
||||
val b3 = ba[3].toInt() and 255
|
||||
|
||||
return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3
|
||||
}
|
||||
|
||||
fun readUInt32(): Long {
|
||||
val b0 = readByte()
|
||||
val b1 = readByte()
|
||||
val b2 = readByte()
|
||||
val b3 = readByte()
|
||||
return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong()
|
||||
}
|
||||
}
|
|
@ -67,6 +67,7 @@ dependencies {
|
|||
compile project(':exif')
|
||||
compile project(':colorpicker')
|
||||
compile project(':emoji')
|
||||
compile project(':apng')
|
||||
|
||||
compile 'com.android.support:support-v4:27.0.2'
|
||||
compile 'com.android.support:appcompat-v7:27.0.2'
|
||||
|
@ -82,6 +83,7 @@ dependencies {
|
|||
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
testCompile 'junit:junit:4.12' // しばらくはjunitと併用
|
||||
|
||||
// compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.21.2'
|
||||
|
||||
compile 'uk.co.chrisjenx:calligraphy:2.3.0'
|
||||
compile 'com.github.woxthebox:draglistview:1.5.1'
|
||||
|
|
Binary file not shown.
|
@ -9,7 +9,7 @@ import android.text.style.ReplacementSpan
|
|||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Pref
|
||||
import jp.juggler.subwaytooter.util.APNGFrames
|
||||
import jp.juggler.subwaytooter.util.ApngFrames
|
||||
import jp.juggler.subwaytooter.util.LogCategory
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
|
@ -31,7 +31,7 @@ class NetworkEmojiSpan internal constructor(private val url : String) : Replacem
|
|||
private var refDrawTarget : WeakReference<Any>? = null
|
||||
|
||||
// フレーム探索結果を格納する構造体を確保しておく
|
||||
private val mFrameFindResult = APNGFrames.FindFrameResult()
|
||||
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
||||
|
||||
init {
|
||||
mPaint.isFilterBitmap = true
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -30,7 +30,7 @@ class CustomEmojiCache(internal val context : Context) {
|
|||
get() = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
||||
private class CacheItem(val url : String, var frames : APNGFrames?) {
|
||||
private class CacheItem(val url : String, var frames : ApngFrames?) {
|
||||
var time_used : Long = elapsedTime
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ class CustomEmojiCache(internal val context : Context) {
|
|||
return null
|
||||
}
|
||||
|
||||
fun getFrames(refDrawTarget: WeakReference<Any>?, url : String, onLoadComplete : ()->Unit) : APNGFrames? {
|
||||
fun getFrames(refDrawTarget: WeakReference<Any>?, url : String, onLoadComplete : ()->Unit) : ApngFrames? {
|
||||
try {
|
||||
if( refDrawTarget?.get() == null ){
|
||||
NetworkEmojiSpan.log.e("draw: DrawTarget is null ")
|
||||
|
@ -169,7 +169,7 @@ class CustomEmojiCache(internal val context : Context) {
|
|||
if(DEBUG)
|
||||
log.d("start get image. queue_size=%d, cache_size=%d url=%s", queue_size, cache_size, request.url)
|
||||
|
||||
var frames : APNGFrames? = null
|
||||
var frames : ApngFrames? = null
|
||||
try {
|
||||
val data = App1.getHttpCached(request.url)
|
||||
if(data == null) {
|
||||
|
@ -241,7 +241,7 @@ class CustomEmojiCache(internal val context : Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun decodeAPNG(data : ByteArray, url : String) : APNGFrames? {
|
||||
private fun decodeAPNG(data : ByteArray, url : String) : ApngFrames? {
|
||||
try {
|
||||
// PNGヘッダを確認
|
||||
if( data.size >= 8
|
||||
|
@ -249,19 +249,13 @@ class CustomEmojiCache(internal val context : Context) {
|
|||
&& (data[1].toInt() and 0xff) == 0x50
|
||||
){
|
||||
// APNGをデコード
|
||||
val frames = APNGFrames.parseAPNG(ByteArrayInputStream(data), 64)
|
||||
if(frames?.hasMultipleFrame == true) return frames
|
||||
frames?.dispose()
|
||||
|
||||
// mastodonのstatic_urlが返すPNG画像はAPNGだと透明になってる場合がある。BitmapFactoryでデコードしなおすべき
|
||||
if(DEBUG) log.d("parseAPNG returns null or single frame.")
|
||||
return ApngFrames.parseApng(ByteArrayInputStream(data), 64)
|
||||
}
|
||||
|
||||
// fall thru
|
||||
} catch(ex : Throwable) {
|
||||
if(DEBUG) log.trace(ex)
|
||||
log.e(ex, "PNG decode failed. %s ", url)
|
||||
// PngFeatureException Interlaced images are not yet supported
|
||||
}
|
||||
|
||||
// 通常のビットマップでのロードを試みる
|
||||
|
@ -269,7 +263,7 @@ class CustomEmojiCache(internal val context : Context) {
|
|||
val b = decodeBitmap(data, 128)
|
||||
if(b != null) {
|
||||
if(DEBUG) log.d("bitmap decoded.")
|
||||
return APNGFrames(b)
|
||||
return ApngFrames(b)
|
||||
} else {
|
||||
log.e("Bitmap decode returns null. %s", url)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.util.AttributeSet
|
|||
import android.view.View
|
||||
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.util.APNGFrames
|
||||
import jp.juggler.subwaytooter.util.ApngFrames
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class NetworkEmojiView : View {
|
||||
|
@ -26,7 +26,7 @@ class NetworkEmojiView : View {
|
|||
private val tagDrawTarget : WeakReference<Any>
|
||||
|
||||
// フレーム探索結果を格納する構造体を確保しておく
|
||||
private val mFrameFindResult = APNGFrames.FindFrameResult()
|
||||
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
||||
|
||||
// 最後に描画した時刻
|
||||
private var t_last_draw : Long = 0
|
||||
|
|
|
@ -1 +1 @@
|
|||
include ':app', ':exif', ':colorpicker', ':emoji'
|
||||
include ':app', ':exif', ':colorpicker', ':emoji', ':apng'
|
||||
|
|
Loading…
Reference in New Issue