APNGデコーダにjapngを利用するのをやめて、フルスクラッチで書き起こした
This commit is contained in:
parent
226e0602f7
commit
6fada3fb93
|
@ -8,6 +8,7 @@
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/apng" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/colorpicker" />
|
<option value="$PROJECT_DIR$/colorpicker" />
|
||||||
<option value="$PROJECT_DIR$/emoji" />
|
<option value="$PROJECT_DIR$/emoji" />
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/SubwayTooter.iml" filepath="$PROJECT_DIR$/SubwayTooter.iml" />
|
<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$/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$/colorpicker/colorpicker.iml" filepath="$PROJECT_DIR$/colorpicker/colorpicker.iml" />
|
||||||
<module fileurl="file://$PROJECT_DIR$/emoji/emoji.iml" filepath="$PROJECT_DIR$/emoji/emoji.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(':exif')
|
||||||
compile project(':colorpicker')
|
compile project(':colorpicker')
|
||||||
compile project(':emoji')
|
compile project(':emoji')
|
||||||
|
compile project(':apng')
|
||||||
|
|
||||||
compile 'com.android.support:support-v4:27.0.2'
|
compile 'com.android.support:support-v4:27.0.2'
|
||||||
compile 'com.android.support:appcompat-v7: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 "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||||
testCompile 'junit:junit:4.12' // しばらくはjunitと併用
|
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 'uk.co.chrisjenx:calligraphy:2.3.0'
|
||||||
compile 'com.github.woxthebox:draglistview:1.5.1'
|
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.App1
|
||||||
import jp.juggler.subwaytooter.Pref
|
import jp.juggler.subwaytooter.Pref
|
||||||
import jp.juggler.subwaytooter.util.APNGFrames
|
import jp.juggler.subwaytooter.util.ApngFrames
|
||||||
import jp.juggler.subwaytooter.util.LogCategory
|
import jp.juggler.subwaytooter.util.LogCategory
|
||||||
import java.lang.ref.WeakReference
|
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 var refDrawTarget : WeakReference<Any>? = null
|
||||||
|
|
||||||
// フレーム探索結果を格納する構造体を確保しておく
|
// フレーム探索結果を格納する構造体を確保しておく
|
||||||
private val mFrameFindResult = APNGFrames.FindFrameResult()
|
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mPaint.isFilterBitmap = true
|
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()
|
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
|
var time_used : Long = elapsedTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ class CustomEmojiCache(internal val context : Context) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFrames(refDrawTarget: WeakReference<Any>?, url : String, onLoadComplete : ()->Unit) : APNGFrames? {
|
fun getFrames(refDrawTarget: WeakReference<Any>?, url : String, onLoadComplete : ()->Unit) : ApngFrames? {
|
||||||
try {
|
try {
|
||||||
if( refDrawTarget?.get() == null ){
|
if( refDrawTarget?.get() == null ){
|
||||||
NetworkEmojiSpan.log.e("draw: DrawTarget is null ")
|
NetworkEmojiSpan.log.e("draw: DrawTarget is null ")
|
||||||
|
@ -169,7 +169,7 @@ class CustomEmojiCache(internal val context : Context) {
|
||||||
if(DEBUG)
|
if(DEBUG)
|
||||||
log.d("start get image. queue_size=%d, cache_size=%d url=%s", queue_size, cache_size, request.url)
|
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 {
|
try {
|
||||||
val data = App1.getHttpCached(request.url)
|
val data = App1.getHttpCached(request.url)
|
||||||
if(data == null) {
|
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 {
|
try {
|
||||||
// PNGヘッダを確認
|
// PNGヘッダを確認
|
||||||
if( data.size >= 8
|
if( data.size >= 8
|
||||||
|
@ -249,19 +249,13 @@ class CustomEmojiCache(internal val context : Context) {
|
||||||
&& (data[1].toInt() and 0xff) == 0x50
|
&& (data[1].toInt() and 0xff) == 0x50
|
||||||
){
|
){
|
||||||
// APNGをデコード
|
// APNGをデコード
|
||||||
val frames = APNGFrames.parseAPNG(ByteArrayInputStream(data), 64)
|
return 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.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fall thru
|
// fall thru
|
||||||
} catch(ex : Throwable) {
|
} catch(ex : Throwable) {
|
||||||
if(DEBUG) log.trace(ex)
|
if(DEBUG) log.trace(ex)
|
||||||
log.e(ex, "PNG decode failed. %s ", url)
|
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)
|
val b = decodeBitmap(data, 128)
|
||||||
if(b != null) {
|
if(b != null) {
|
||||||
if(DEBUG) log.d("bitmap decoded.")
|
if(DEBUG) log.d("bitmap decoded.")
|
||||||
return APNGFrames(b)
|
return ApngFrames(b)
|
||||||
} else {
|
} else {
|
||||||
log.e("Bitmap decode returns null. %s", url)
|
log.e("Bitmap decode returns null. %s", url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
||||||
import jp.juggler.subwaytooter.App1
|
import jp.juggler.subwaytooter.App1
|
||||||
import jp.juggler.subwaytooter.util.APNGFrames
|
import jp.juggler.subwaytooter.util.ApngFrames
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class NetworkEmojiView : View {
|
class NetworkEmojiView : View {
|
||||||
|
@ -26,7 +26,7 @@ class NetworkEmojiView : View {
|
||||||
private val tagDrawTarget : WeakReference<Any>
|
private val tagDrawTarget : WeakReference<Any>
|
||||||
|
|
||||||
// フレーム探索結果を格納する構造体を確保しておく
|
// フレーム探索結果を格納する構造体を確保しておく
|
||||||
private val mFrameFindResult = APNGFrames.FindFrameResult()
|
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
||||||
|
|
||||||
// 最後に描画した時刻
|
// 最後に描画した時刻
|
||||||
private var t_last_draw : Long = 0
|
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