APNGデコーダにjapngを利用するのをやめて、フルスクラッチで書き起こした

This commit is contained in:
tateisu 2018-01-27 20:00:44 +09:00
parent 226e0602f7
commit 6fada3fb93
30 changed files with 1983 additions and 315 deletions

View File

@ -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" />

View File

@ -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" />

1
apng/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

12
apng/build.gradle Normal file
View File

@ -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"

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}
}
}

View File

@ -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 )
}

View File

@ -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.")
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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)"
}

View File

@ -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]
}
}
}

View File

@ -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)
}

View File

@ -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()
}
}
}
}
}

View File

@ -0,0 +1,3 @@
package jp.juggler.apng
class ParseError(message: String) : IllegalArgumentException(message)

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,7 @@
package jp.juggler.apng.util
internal class ByteArrayRange(
val array: ByteArray,
var start: Int,
var remain: Int
)

View File

@ -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()
}

View File

@ -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()
}
}

View File

@ -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.

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -1 +1 @@
include ':app', ':exif', ':colorpicker', ':emoji'
include ':app', ':exif', ':colorpicker', ':emoji', ':apng'