16bit APNGのバグ修正

This commit is contained in:
tateisu 2018-01-29 04:03:04 +09:00
parent ec8527eae0
commit af5cddf051
55 changed files with 1566 additions and 1790 deletions

View File

@ -29,7 +29,9 @@
<w>hashtag</w>
<w>hashtags</w>
<w>hohoemi</w>
<w>idat</w>
<w>idempotency</w>
<w>ihdr</w>
<w>kenglxn</w>
<w>mailto</w>
<w>mimumedon</w>
@ -37,6 +39,7 @@
<w>noto</w>
<w>nsfw</w>
<w>openclose</w>
<w>paeth</w>
<w>pleroma</w>
<w>poller</w>
<w>proc</w>

View File

@ -9,6 +9,7 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/apng" />
<option value="$PROJECT_DIR$/apng_android" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/colorpicker" />
<option value="$PROJECT_DIR$/emoji" />

View File

@ -4,6 +4,7 @@
<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$/apng_android/apng_android.iml" filepath="$PROJECT_DIR$/apng_android/apng_android.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" />

View File

@ -2,10 +2,10 @@
package jp.juggler.apng
import jp.juggler.apng.util.ByteArrayTokenizer
import jp.juggler.apng.util.ByteSequence
class ApngAnimationControl internal constructor(bat: ByteArrayTokenizer) {
class ApngAnimationControl internal constructor(src: ByteSequence) {
companion object {
const val PLAY_INDEFINITELY =0
@ -21,8 +21,8 @@ class ApngAnimationControl internal constructor(bat: ByteArrayTokenizer) {
val numPlays: Int
init {
numFrames = bat.readInt32()
numPlays = bat.readInt32()
numFrames = src.readInt32()
numPlays = src.readInt32()
}
override fun toString() ="ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)"

View File

@ -2,9 +2,9 @@
package jp.juggler.apng
import jp.juggler.apng.util.ByteArrayTokenizer
import jp.juggler.apng.util.ByteSequence
class ApngBackground internal constructor(colorType: ColorType, bat: ByteArrayTokenizer) {
class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) {
val red: Int
val green: Int
@ -14,23 +14,23 @@ class ApngBackground internal constructor(colorType: ColorType, bat: ByteArrayTo
init {
when (colorType) {
ColorType.GREY, ColorType.GREY_ALPHA -> {
val v = bat.readUInt16()
val v = src.readUInt16()
red = v
green = v
blue = v
index = -1
}
ColorType.RGB, ColorType.RGBA -> {
red = bat.readUInt16()
green = bat.readUInt16()
blue = bat.readUInt16()
red = src.readUInt16()
green = src.readUInt16()
blue = src.readUInt16()
index = -1
}
ColorType.INDEX -> {
red = -1
green = -1
blue = -1
index = bat.readUInt8()
index = src.readUInt8()
}
}
}

View File

@ -1,35 +1,72 @@
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
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 )
class ApngBitmap(var width : Int, var height : Int) {
// each int value contains 0xAARRGGBB
val colors = IntArray(width * height)
// 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 = 0
var step : Int = 1
fun setPixel(argb : Int) : Pointer {
// if( pos == width) println("setPixel 0x%x".format(argb))
colors[pos] = argb
return this
}
fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel(
((a and 255) shl 24) or
((r and 255) shl 16) or
((g and 255) shl 8) or
(b and 255)
)
fun setOffset(pos : Int = 0, step : Int = 1) : Pointer {
this.pos = pos
this.step = step
return this
}
fun setXY(x : Int, y : Int, step : Int = 1) = setOffset(x + y * width, step)
fun plus(x : Int) : Pointer {
pos += x
return this
}
fun next() = plus(step)
val color : Int
get() = colors[pos]
val alpha : Int
get() = (colors[pos] shr 24) and 255
val red : Int
get() = (colors[pos] shr 16) and 255
val green : Int
get() = (colors[pos] shr 8) and 255
val blue : Int
get() = (colors[pos]) and 255
}
fun pointer() = Pointer()
}

View File

@ -2,146 +2,167 @@
package jp.juggler.apng
import jp.juggler.apng.util.BufferPool
import jp.juggler.apng.util.ByteArrayTokenizer
import jp.juggler.apng.util.StreamTokenizer
import jp.juggler.apng.util.*
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)
}
}
}
}
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(ByteSequence(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,
ByteSequence(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, ByteSequence(body))
ColorType.RGB -> apng.transparentColor =
ApngTransparentColor(false, ByteSequence(body))
ColorType.INDEX -> apng.palette?.parseTRNS(body)
?: throw ParseError("missing palette")
else -> callback.onApngWarning("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
) {
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 header = apng.header ?: throw ParseError("missing IHDR")
val animationControl =
ApngAnimationControl(ByteSequence(chunk.readBody(crc32, tokenizer)))
apng.animationControl = animationControl
callback.onAnimationInfo(apng, header,animationControl)
}
"fcTL" -> {
val bat = ByteSequence(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
) {
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.onApngWarning(
"unknown chunk: type=%s,size=0x%x".format(
chunk.type,
chunk.size
)
)
chunk.skipBody(tokenizer)
}
}
}
}
}

View File

@ -1,9 +1,31 @@
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)
interface ApngDecoderCallback {
// called for non-fatal warning
fun onApngWarning(message : String)
// called for debug message
fun onApngDebug(message : String) {}
fun canApngDebug():Boolean = false
// called when PNG image header is detected.
fun onHeader(apng : Apng, header : ApngImageHeader)
// called when APNG Animation Control is detected.
fun onAnimationInfo(
apng : Apng,
header : ApngImageHeader,
animationControl : ApngAnimationControl
)
// called when default image bitmap was rendered.
fun onDefaultImage(apng : Apng, bitmap : ApngBitmap)
// called when APNG Frame Control is detected and its bitmap was rendered.
// its bitmap may same to default image for first frame.
// ( in this case, both of onDefaultImage and onAnimationFrame are called for same bitmap)
fun onAnimationFrame(apng : Apng, frameControl : ApngFrameControl, bitmap : ApngBitmap)
}

View File

@ -2,10 +2,10 @@
package jp.juggler.apng
import jp.juggler.apng.util.ByteArrayTokenizer
import jp.juggler.apng.util.ByteSequence
class ApngFrameControl internal constructor(bat: ByteArrayTokenizer) {
class ApngFrameControl internal constructor(src: ByteSequence) {
val width: Int
val height: Int
@ -17,19 +17,19 @@ class ApngFrameControl internal constructor(bat: ByteArrayTokenizer) {
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}
width = src.readInt32()
height = src.readInt32()
xOffset = src.readInt32()
yOffset = src.readInt32()
delayNum = src.readUInt16()
delayDen = src.readUInt16().let{ if(it==0) 100 else it}
var num:Int
num = bat.readUInt8()
num = src.readUInt8()
disposeOp = DisposeOp.values().first{it.num==num}
num = bat.readUInt8()
num = src.readUInt8()
blendOp = BlendOp.values().first{it.num==num}
}

View File

@ -2,10 +2,10 @@
package jp.juggler.apng
import jp.juggler.apng.util.ByteArrayTokenizer
import jp.juggler.apng.util.ByteSequence
class ApngImageHeader internal constructor(bat: ByteArrayTokenizer) {
// information from IHDR chunk.
class ApngImageHeader internal constructor(src: ByteSequence) {
val width: Int
val height: Int
val bitDepth: Int
@ -16,22 +16,24 @@ class ApngImageHeader internal constructor(bat: ByteArrayTokenizer) {
init {
width = bat.readInt32()
height = bat.readInt32()
bitDepth = bat.readUInt8()
width = src.readInt32()
height = src.readInt32()
if(width <=0 || height <=0 ) throw ParseError("w=$width,h=$height is too small")
bitDepth = src.readUInt8()
var num:Int
//
num =bat.readUInt8()
num =src.readUInt8()
colorType = ColorType.values().first { it.num==num }
//
num =bat.readUInt8()
num =src.readUInt8()
compressionMethod = CompressionMethod.values().first { it.num==num }
//
num =bat.readUInt8()
num =src.readUInt8()
filterMethod = FilterMethod.values().first { it.num==num }
//
num =bat.readUInt8()
num =src.readUInt8()
interlaceMethod = InterlaceMethod.values().first { it.num==num }
}

View File

@ -2,28 +2,40 @@
package jp.juggler.apng
import jp.juggler.apng.util.getUInt8
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]
}
}
class ApngPalette(
src : ByteArray // repeat of R,G,B
) {
companion object {
// full opaque black
const val OPAQUE = 255 shl 24
}
val list : IntArray // repeat of 0xAARRGGBB
var hasAlpha : Boolean = false
init {
val entryCount = src.size / 3
list = IntArray( entryCount)
var pos = 0
for(i in 0 until entryCount) {
list[i] = OPAQUE or
(src.getUInt8(pos) shl 16) or
(src.getUInt8(pos+1) shl 8) or
src.getUInt8(pos+2)
pos+=3
}
}
override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
// update alpha value from tRNS chunk data
fun parseTRNS(ba : ByteArray) {
hasAlpha = true
for(i in 0 until Math.min(list.size, ba.size)) {
list[i] = (list[i] and 0xffffff) or (ba.getUInt8(i) shl 24)
}
}
}

View File

@ -1,21 +1,23 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package jp.juggler.apng
import jp.juggler.apng.util.ByteArrayTokenizer
import jp.juggler.apng.util.ByteSequence
class ApngTransparentColor internal constructor(isGreyScale:Boolean, bat: ByteArrayTokenizer) {
class ApngTransparentColor internal constructor(isGreyScale:Boolean, src: ByteSequence) {
val red:Int
val green:Int
val blue:Int
init{
if( isGreyScale){
val v = bat.readUInt16()
val v = src.readUInt16()
red =v
green =v
blue =v
}else{
red =bat.readUInt16()
green =bat.readUInt16()
blue =bat.readUInt16()
red =src.readUInt16()
green =src.readUInt16()
blue =src.readUInt16()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +0,0 @@
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

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

View File

@ -1,55 +0,0 @@
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,33 @@
package jp.juggler.apng.util
import jp.juggler.apng.ParseError
internal fun ByteArray.getUInt8(pos : Int) = this[pos].toInt() and 255
internal fun ByteArray.getUInt16(pos : Int) = (this.getUInt8(pos) shl 8) or this.getUInt8(pos + 1)
internal fun ByteArray.getInt32(pos : Int) = (this.getUInt8(pos) shl 24) or
(this.getUInt8(pos + 1) shl 16) or
(this.getUInt8(pos + 2) shl 8) or
this.getUInt8(pos + 3)
internal class ByteSequence(
val array : ByteArray,
var offset : Int,
var length : Int
) {
constructor(ba : ByteArray) : this(ba, 0, ba.size)
private inline fun <T> readX(dataSize : Int, block : () -> T) : T {
if(length < dataSize) throw ParseError("readIntX: unexpected end")
val v = block()
offset += dataSize
length -= dataSize
return v
}
fun readUInt8() = readX(1) { array.getUInt8(offset) }
fun readUInt16() = readX(2) { array.getUInt16(offset) }
fun readInt32() = readX(4) { array.getInt32(offset) }
}

View File

@ -0,0 +1,40 @@
package jp.juggler.apng.util
import java.util.*
internal class ByteSequenceQueue(private val bufferRecycler :(ByteSequence)->Unit) {
private val list = LinkedList<ByteSequence>()
val remain: Int
get() = list.sumBy { it.length }
fun add(range: ByteSequence) {
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.length <= 0) {
bufferRecycler(item)
list.removeFirst()
continue
}
val delta = Math.min(item.length, length - nRead)
System.arraycopy(item.array, item.offset, dst, offset + nRead, delta)
item.offset += delta
item.length -= delta
nRead += delta
}
return nRead
}
}

1
apng_android/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

42
apng_android/build.gradle Normal file
View File

@ -0,0 +1,42 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 27
buildToolsVersion '27.0.3'
defaultConfig {
targetSdkVersion 27
minSdkVersion 21
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
api project(':apng')
// 'api'
// 'implementation'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

21
apng_android/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,26 @@
package jp.juggler.apng;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception{
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals( "jp.juggler.apng.test", appContext.getPackageName() );
}
}

View File

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.juggler.apng"
/>

View File

@ -0,0 +1,388 @@
package jp.juggler.apng
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.util.Log
import java.io.InputStream
import java.util.ArrayList
class ApngFrames private constructor(
private val pixelSizeMax : Int = 0,
private val debug:Boolean =false
) : ApngDecoderCallback {
companion object {
private const val TAG = "ApngFrames"
// ループしない画像の場合は3秒でまたループさせる
private const val DELAY_AFTER_END = 3000L
// アニメーションフレームの描画に使う
private val sSrcModePaint : Paint by lazy {
val paint = Paint()
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
paint.isFilterBitmap = true
paint
}
private fun createBlankBitmap(w : Int, h : Int) : Bitmap {
return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
}
// WARNING: ownership of "src" will be moved or recycled.
private fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? {
if(src == null) return null
val wSrc = src.width
val hSrc = src.height
if(size_max <= 0 || wSrc <= size_max && hSrc <= size_max) return src
val wDst : Int
val hDst : Int
if(wSrc >= hSrc) {
wDst = size_max
hDst = Math.max(
1,
(size_max.toFloat() * hSrc.toFloat() / wSrc.toFloat() + 0.5f).toInt()
)
} else {
hDst = size_max
wDst = Math.max(
1,
(size_max.toFloat() * wSrc.toFloat() / hSrc.toFloat() + 0.5f).toInt()
)
}
Log.v(TAG,"scaleBitmap: $wSrc,$hSrc => $wDst,$hDst")
val b2 = createBlankBitmap(wDst, hDst)
val canvas = Canvas(b2)
val rectSrc = Rect(0, 0, wSrc, hSrc)
val rectDst = Rect(0, 0, wDst, hDst)
canvas.drawBitmap(src, rectSrc, rectDst,
sSrcModePaint
)
src.recycle()
return b2
}
private fun toBitmap(src : ApngBitmap) : Bitmap {
return Bitmap.createBitmap(
src.colors, // int[] 配列
0, // offset
src.width, //stride
src.width, // width
src.height, //height
Bitmap.Config.ARGB_8888
)
}
private fun toBitmap(src : ApngBitmap, size_max : Int) : Bitmap? {
return scaleBitmap(
toBitmap(
src
), size_max
)
}
@Suppress("unused")
fun parseApng(inStream : InputStream, pixelSizeMax : Int,debug:Boolean=false) : ApngFrames {
val result = ApngFrames(pixelSizeMax,debug)
try {
ApngDecoder.parseStream(inStream, result)
result.onParseComplete()
return if( result.defaultImage != null || result.frames?.isNotEmpty() == true ){
result
}else{
throw RuntimeException("APNG has no image")
}
} catch(ex : Throwable) {
result.dispose()
throw ex
}
}
}
private var header : ApngImageHeader? = null
private var animationControl : ApngAnimationControl? = null
val width : Int
get() = Math.min( pixelSizeMax, header?.width ?: 1)
val height : Int
get() = Math.min( pixelSizeMax, header?.height ?: 1)
@Suppress("MemberVisibilityCanBePrivate")
val numFrames : Int
get() = animationControl?.numFrames ?: 1
@Suppress("unused")
val hasMultipleFrame : Boolean
get() = numFrames > 1
private var timeTotal = 0L
private lateinit var canvas : Canvas
private var canvasBitmap : Bitmap? = null
// 再生速度の調整
private var durationScale = 1f
// APNGじゃなかった場合に使われる
private var defaultImage : Bitmap? = null
private class Frame(
internal val bitmap : Bitmap,
internal val time_start : Long,
internal val time_width : Long
)
private var frames : ArrayList<Frame>? = null
@Suppress("unused")
constructor(bitmap : Bitmap) : this() {
this.defaultImage = bitmap
}
private fun onParseComplete() {
canvasBitmap?.recycle()
canvasBitmap = null
val frames = this.frames
if( frames != null ){
if( frames.size > 1){
defaultImage?.recycle()
defaultImage = null
}else if( frames.size == 1){
defaultImage?.recycle()
defaultImage = frames.first().bitmap
frames.clear()
}
}
}
fun dispose() {
defaultImage?.recycle()
canvasBitmap?.recycle()
val frames = this.frames
if(frames != null) {
for(f in frames) {
f.bitmap.recycle()
}
}
}
class FindFrameResult {
var bitmap : Bitmap? = null // may null
var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
}
// シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する
@Suppress("unused")
fun findFrame(result : FindFrameResult, t : Long) {
if(defaultImage != null) {
result.bitmap = defaultImage
result.delay = Long.MAX_VALUE
return
}
val animationControl = this.animationControl
val frames = this.frames
if(animationControl == null || frames == null) {
// この場合は既に mBitmapNonAnimation が用意されてるはずだ
result.bitmap = null
result.delay = Long.MAX_VALUE
return
}
val frameCount = frames.size
val isFinite = ! animationControl.isPlayIndefinitely
val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
val endWait = if(isFinite) DELAY_AFTER_END else 0L
val timeTotalLoop = Math.max(1,timeTotal * repeatSequenceCount + endWait)
val tf = (if(0.5f + t < 0f) 0f else t / durationScale).toLong()
// 全体の繰り返し時刻で余りを計算
val tl = tf % timeTotalLoop
if(tl >= timeTotalLoop - endWait) {
// 終端で待機状態
result.bitmap = frames[frameCount - 1].bitmap
result.delay = (0.5f + (timeTotalLoop - tl) * durationScale).toLong()
return
}
// 1ループの繰り返し時刻で余りを計算
val tt = tl % timeTotal
// フレームリストを時刻で二分探索
var s = 0
var e = frameCount
while(e - s > 1) {
val mid = s + e shr 1
val frame = frames[mid]
// log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.time_start,frame.time_start+frame.time_width );
if(tt < frame.time_start) {
e = mid
} else if(tt >= frame.time_start + frame.time_width) {
s = mid + 1
} else {
s = mid
break
}
}
s = if(s < 0) 0 else if(s >= frameCount - 1) frameCount - 1 else s
val frame = frames[s]
val delay = frame.time_start + frame.time_width - tt
result.bitmap = frames[s].bitmap
result.delay = (0.5f + durationScale * Math.max(0f, delay.toFloat())).toLong()
// log.d("findFrame tf=%d,tl=%d/%d,tt=%d/%d,s=%d,w=%d,delay=%d",tf,tl,loop_total,tt,timeTotal,s,frame.time_width,result.delay);
}
/////////////////////////////////////////////////////
// implements ApngDecoderCallback
override fun onApngWarning(message : String) {
Log.w(TAG, message)
}
override fun onApngDebug(message : String) {
Log.d(TAG, message)
}
override fun canApngDebug():Boolean = debug
override fun onHeader(apng : Apng, header : ApngImageHeader) {
this.header = header
}
override fun onAnimationInfo(
apng : Apng,
header: ApngImageHeader,
animationControl : ApngAnimationControl
) {
this.animationControl = animationControl
val canvasBitmap =
createBlankBitmap(header.width, header.height)
this.canvasBitmap = canvasBitmap
this.canvas = Canvas(canvasBitmap)
this.frames = ArrayList(animationControl.numFrames)
}
override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
defaultImage?.recycle()
defaultImage = toBitmap(bitmap, pixelSizeMax)
}
override fun onAnimationFrame(
apng : Apng,
frameControl : ApngFrameControl,
bitmap : ApngBitmap
) {
val frames = this.frames ?: return
val canvasBitmap = this.canvasBitmap ?: return
// APNGのフレーム画像をAndroidの形式に変換する。この段階ではリサイズしない
val bitmapNative = toBitmap(bitmap)
val previous : Bitmap? = if(frameControl.disposeOp == DisposeOp.Previous) {
// Capture the current bitmap region IF it needs to be reverted after rendering
Bitmap.createBitmap(
canvasBitmap,
frameControl.xOffset,
frameControl.yOffset,
frameControl.width,
frameControl.height
)
} else {
null
}
val paint = if(frameControl.blendOp == BlendOp.Source) {
sSrcModePaint // SRC_OVER, not blend
} else {
null // (for blend, leave paint null)
}
// Draw the new frame into place
canvas.drawBitmap(
bitmapNative,
frameControl.xOffset.toFloat(),
frameControl.yOffset.toFloat(),
paint
)
// Extract a drawable from the canvas. Have to copy the current bitmap.
// Store the drawable in the sequence of frames
val timeStart = timeTotal
val timeWidth = Math.max(1L, frameControl.delayMilliseconds)
timeTotal += timeWidth
val scaledBitmap =
scaleBitmap(
canvasBitmap.copy(
Bitmap.Config.ARGB_8888,
false
), pixelSizeMax
)
if(scaledBitmap != null) {
frames.add(Frame(scaledBitmap, timeStart, timeWidth))
}
// Now "dispose" of the frame in preparation for the next.
// https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
when(frameControl.disposeOp) {
DisposeOp.None -> {
}
DisposeOp.Background ->
// APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
//System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
//if (true || isFull) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR) // Clear to fully transparent black
DisposeOp.Previous ->
// APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
//System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
// Put the original section back
if(previous != null) {
canvas.drawBitmap(
previous, frameControl.xOffset.toFloat(), frameControl.yOffset.toFloat(),
sSrcModePaint
)
previous.recycle()
}
else -> {
// 0: Default should never happen
// APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
//System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
// do nothing
// } else {
// Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
// paint = new Paint();
// paint.setColor(0);
// paint.setStyle(Paint.Style.FILL);
// canvas.drawRect(rt, paint);
// }
}
}
}
}

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">apng_android</string>
</resources>

View File

@ -0,0 +1,17 @@
package jp.juggler.apng;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception{
assertEquals( 4, 2 + 2 );
}
}

View File

@ -64,10 +64,10 @@ dependencies {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile project(':exif')
compile project(':colorpicker')
compile project(':emoji')
compile project(':apng')
implementation project(':exif')
implementation project(':colorpicker')
implementation project(':emoji')
implementation project(':apng_android')
compile 'com.android.support:support-v4:27.0.2'
compile 'com.android.support:appcompat-v7:27.0.2'

View File

@ -6,7 +6,7 @@ import android.util.AttributeSet
class ListRecyclerView : RecyclerView {
companion object {
// private val log = LogCategory("ListRecyclerView")
// private val warning = LogCategory("ListRecyclerView")
}
constructor(context : Context) : super(context)

View File

@ -1936,7 +1936,7 @@ class ActMain : AppCompatActivity()
App1.openCustomTab(this, opener.url)
} catch(ex : Throwable) {
// log.trace( ex );
// warning.trace( ex );
log.e(ex, "openChromeTab failed. url=%s", opener.url)
}

View File

@ -107,12 +107,12 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
override fun onLoadingChanged(isLoading : Boolean) {
// かなり頻繁に呼ばれる
// log.d( "exoPlayer onLoadingChanged %s" ,isLoading );
// warning.d( "exoPlayer onLoadingChanged %s" ,isLoading );
}
override fun onPlayerStateChanged(playWhenReady : Boolean, playbackState : Int) {
// かなり頻繁に呼ばれる
// log.d( "exoPlayer onPlayerStateChanged %s %s", playWhenReady, playbackState );
// warning.d( "exoPlayer onPlayerStateChanged %s %s", playWhenReady, playbackState );
if(playWhenReady && playbackState == Player.STATE_BUFFERING) {
val now = SystemClock.elapsedRealtime()
if(now - buffering_last_shown >= short_limit && exoPlayer.duration >= short_limit) {

View File

@ -124,7 +124,7 @@ class ActPost : AppCompatActivity(), View.OnClickListener, PostAttachment.Callba
// Intent takeVideoIntent = new Intent( MediaStore.ACTION_VIDEO_CAPTURE );
// startActivityForResult( takeVideoIntent, REQUEST_CODE_VIDEO );
// }catch( Throwable ex ){
// log.trace( ex );
// warning.trace( ex );
// Utils.showToast( this, ex, "opening video app failed." );
// }
// }

View File

@ -182,7 +182,7 @@ class App1 : Application() {
// int memory = am.getMemoryClass();
// int largeMemory = am.getLargeMemoryClass();
// // どちらも単位はMB
// log.d("MemoryClass=%d, LargeMemoryClass = %d",memory,largeMemory);
// warning.d("MemoryClass=%d, LargeMemoryClass = %d",memory,largeMemory);
//
// int maxSize;
// if( am.isLowRamDevice() ){

View File

@ -391,16 +391,16 @@ class AppState(internal val context : Context, internal val pref : SharedPrefere
// tts.setOnUtteranceProgressListener( new UtteranceProgressListener() {
// @Override public void onStart( String utteranceId ){
// log.d( "UtteranceProgressListener.onStart id=%s", utteranceId );
// warning.d( "UtteranceProgressListener.onStart id=%s", utteranceId );
// }
//
// @Override public void onDone( String utteranceId ){
// log.d( "UtteranceProgressListener.onDone id=%s", utteranceId );
// warning.d( "UtteranceProgressListener.onDone id=%s", utteranceId );
// handler.post( proc_flushSpeechQueue );
// }
//
// @Override public void onError( String utteranceId ){
// log.d( "UtteranceProgressListener.onError id=%s", utteranceId );
// warning.d( "UtteranceProgressListener.onError id=%s", utteranceId );
// handler.post( proc_flushSpeechQueue );
// }
// } );

View File

@ -3461,7 +3461,7 @@ class Column(
} else if(holder_sp.adapterIndex == 0 && holder_sp.offset == 0) {
// スクロール位置が先頭なら先頭にする
log.d(
"mergeStreamingMessage: has VH. keep head. pos=%s,offset=%s"
"mergeStreamingMessage: has VH. keep head. offset=%s,offset=%s"
, holder_sp.adapterIndex
, holder_sp.offset
)

View File

@ -341,7 +341,7 @@ class ColumnViewHolder(
}
val sp = column.scroll_save ?: //復元後にもここを通るがこれは正常である
// log.d( "restoreScrollPosition [%d] %s , column has no saved scroll position.", page_idx, column.getColumnName( true ) );
// warning.d( "restoreScrollPosition [%d] %s , column has no saved scroll position.", page_idx, column.getColumnName( true ) );
return
column.scroll_save = null

View File

@ -172,7 +172,7 @@ internal class ItemListAdapter(
// 変更リストを順番に通知する
for(c in changeList) {
val adapterIndex = column.toAdapterIndex(c.listIndex)
log.d("notifyChange: ChangeType=${c.type} pos=$adapterIndex,count=${c.count}")
log.d("notifyChange: ChangeType=${c.type} offset=$adapterIndex,count=${c.count}")
when(c.type) {
AdapterChangeType.RangeInsert -> notifyItemRangeInserted(adapterIndex, c.count)
AdapterChangeType.RangeRemove -> notifyItemRangeRemoved(adapterIndex, c.count)
@ -204,17 +204,17 @@ internal class ItemListAdapter(
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position : Int, count : Int) {
log.d("notifyChange: notifyItemRangeInserted pos=$position,count=$count")
log.d("notifyChange: notifyItemRangeInserted offset=$position,count=$count")
notifyItemRangeInserted(position, count)
}
override fun onRemoved(position : Int, count : Int) {
log.d("notifyChange: notifyItemRangeRemoved pos=$position,count=$count")
log.d("notifyChange: notifyItemRangeRemoved offset=$position,count=$count")
notifyItemRangeRemoved(position, count)
}
override fun onChanged(position : Int, count : Int, payload : Any?) {
log.d("notifyChange: notifyItemRangeChanged pos=$position,count=$count")
log.d("notifyChange: notifyItemRangeChanged offset=$position,count=$count")
notifyItemRangeChanged(position, count, payload)
}

View File

@ -1373,7 +1373,7 @@ class PollingWorker private constructor(c : Context) {
val type = src.parseString("type")
if(id <= nr.nid_read) {
// log.d("update_sub: ignore data that id=%s, <= read id %s ",id,nr.nid_read);
// warning.d("update_sub: ignore data that id=%s, <= read id %s ",id,nr.nid_read);
return
} else {
log.d("update_sub: found data that id=%s, > read id %s ", id, nr.nid_read)

View File

@ -100,7 +100,7 @@ internal class StreamReader(
* Invoked when a text (type `0x1`) message has been received.
*/
override fun onMessage(webSocket : WebSocket, text : String) {
// log.d( "WebSocket onMessage. url=%s, message=%s", webSocket.request().url(), text );
// warning.d( "WebSocket onMessage. url=%s, message=%s", webSocket.request().url(), text );
try {
val obj = text.toJsonObject()

View File

@ -93,7 +93,7 @@ open class TootApiResult(
while(m.find()) {
val url = m.group(1)
val rel = m.group(2)
// log.d("Link %s,%s",rel,url);
// warning.d("Link %s,%s",rel,url);
if("next" == rel) link_older = url
if("prev" == rel) link_newer = url
}

View File

@ -15,7 +15,7 @@ class EmojiImageSpan(context : Context, private val res_id : Int) : ReplacementS
companion object {
// private static final LogCategory log = new LogCategory( "EmojiImageSpan" );
// private static final LogCategory warning = new LogCategory( "EmojiImageSpan" );
// static DynamicDrawableSpan x = null;

View File

@ -6,10 +6,10 @@ import android.graphics.Rect
import android.graphics.RectF
import android.support.annotation.IntRange
import android.text.style.ReplacementSpan
import jp.juggler.apng.ApngFrames
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.Pref
import jp.juggler.subwaytooter.util.ApngFrames
import jp.juggler.subwaytooter.util.LogCategory
import java.lang.ref.WeakReference

View File

@ -8,7 +8,7 @@ import android.util.Log
object LogData {
private const val TAG = "SubwayTooter"
internal const val table = "log"
internal const val table = "warning"
private const val COL_TIME = "t"
private const val COL_LEVEL = "l"

View File

@ -88,7 +88,7 @@ object MutedApp {
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// warning.e( ex, "load failed." );
// }
// return false;
// }

View File

@ -88,7 +88,7 @@ object MutedWord {
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// warning.e( ex, "load failed." );
// }
// return false;
// }

View File

@ -55,7 +55,7 @@ object TagSet {
// App1.database.delete(table, COL_TIME_SAVE + "<?", arrayOf(expire.toString()))
//
// } catch(ex : Throwable) {
// log.e(ex, "deleteOld failed.")
// warning.e(ex, "deleteOld failed.")
// }
// }
@ -67,7 +67,7 @@ object TagSet {
// cv.put( COL_ACCT, acct );
// App1.getDB().replace( table, null, cv );
// }catch( Throwable ex ){
// log.e( ex, "save failed." );
// warning.e( ex, "save failed." );
// }
// }

View File

@ -221,7 +221,7 @@ class UserRelation private constructor() {
// try{
// App1.getDB().delete( table, COL_NAME + "=?", new String[]{ name } );
// }catch( Throwable ex ){
// log.e( ex, "delete failed." );
// warning.e( ex, "delete failed." );
// }
// }
//
@ -241,7 +241,7 @@ class UserRelation private constructor() {
// }
// }
// }catch( Throwable ex ){
// log.e(ex,"getNameSet() failed.")
// warning.e(ex,"getNameSet() failed.")
// }
// return dst;
// }
@ -267,7 +267,7 @@ class UserRelation private constructor() {
// cursor.close();
// }
// }catch( Throwable ex ){
// log.e( ex, "load failed." );
// warning.e( ex, "load failed." );
// }
// return false;
// }

View File

@ -1,810 +0,0 @@
package jp.juggler.subwaytooter.util
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
import android.util.Log
import jp.juggler.apng.*
import java.io.InputStream
import java.util.ArrayList
class ApngFrames(private val pixelSizeMax : Int = 0) : ApngDecoderCallback {
companion object {
private const val TAG = "ApngFrames"
// ループしない画像の場合は3秒でまたループさせる
private const val DELAY_AFTER_END = 3000L
// アニメーションフレームの描画に使う
private val sSrcModePaint : Paint by lazy {
val paint = Paint()
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
paint.isFilterBitmap = true
paint
}
private fun createBlankBitmap(w : Int, h : Int) : Bitmap {
return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
}
// WARNING: ownership of "src" will be moved or recycled.
private fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? {
if(src == null) return null
if(size_max <= 0) return src
val wSrc = src.width
val hSrc = src.height
if(wSrc <= size_max && hSrc <= size_max) return src
val wDst : Int
val hDst : Int
if(wSrc >= hSrc) {
wDst = size_max
hDst = Math.max(
1,
(size_max.toFloat() * hSrc.toFloat() / wSrc.toFloat() + 0.5f).toInt()
)
} else {
hDst = size_max
wDst = Math.max(
1,
(size_max.toFloat() * wSrc.toFloat() / hSrc.toFloat() + 0.5f).toInt()
)
}
val b2 = createBlankBitmap(wDst, hDst)
val canvas = Canvas(b2)
val rectSrc = Rect(0, 0, wSrc, hSrc)
val rectDst = Rect(0, 0, wDst, hDst)
canvas.drawBitmap(src, rectSrc, rectDst, sSrcModePaint)
src.recycle()
return b2
}
private fun toBitmap(src : ApngBitmap) : Bitmap {
return Bitmap.createBitmap(
src.colors, // int[] 配列
0, // offset
src.width, //stride
src.width, // width
src.height, //height
Bitmap.Config.ARGB_8888
)
}
private fun toBitmap(src : ApngBitmap, size_max : Int) : Bitmap? {
return scaleBitmap(toBitmap(src), size_max)
}
@Suppress("unused")
fun parseApng(inStream : InputStream, size_max : Int) : ApngFrames {
val result = ApngFrames(size_max)
try {
ApngDecoder.parseStream(inStream, result)
result.onParseComplete()
return if( result.defaultImage != null || result.frames?.isNotEmpty() == true ){
result
}else{
throw RuntimeException("APNG has no image")
}
} catch(ex : Throwable) {
result.dispose()
throw ex
}
}
}
private var header : ApngImageHeader? = null
private var animationControl : ApngAnimationControl? = null
val width : Int
get() = header?.width ?: 0
val height : Int
get() = header?.height ?: 0
@Suppress("MemberVisibilityCanBePrivate")
val numFrames : Int
get() = animationControl?.numFrames ?: 1
@Suppress("unused")
val hasMultipleFrame : Boolean
get() = numFrames > 1
private var timeTotal = 0L
private lateinit var canvas : Canvas
private var canvasBitmap : Bitmap? = null
// 再生速度の調整
private var durationScale = 1f
// APNGじゃなかった場合に使われる
private var defaultImage : Bitmap? = null
private class Frame(
internal val bitmap : Bitmap,
internal val time_start : Long,
internal val time_width : Long
)
private var frames : ArrayList<Frame>? = null
@Suppress("unused")
constructor(bitmap : Bitmap) : this() {
this.defaultImage = bitmap
}
private fun onParseComplete() {
canvasBitmap?.recycle()
canvasBitmap = null
val frames = this.frames
if( frames != null ){
if( frames.size > 1){
defaultImage?.recycle()
defaultImage = null
}else if( frames.size == 1){
defaultImage?.recycle()
defaultImage = frames.first().bitmap
frames.clear()
}
}
}
fun dispose() {
defaultImage?.recycle()
canvasBitmap?.recycle()
val frames = this.frames
if(frames != null) {
for(f in frames) {
f.bitmap.recycle()
}
}
}
class FindFrameResult {
var bitmap : Bitmap? = null // may null
var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
}
// シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する
@Suppress("unused")
fun findFrame(result : FindFrameResult, t : Long) {
if(defaultImage != null) {
result.bitmap = defaultImage
result.delay = Long.MAX_VALUE
return
}
val animationControl = this.animationControl
val frames = this.frames
if(animationControl == null || frames == null) {
// この場合は既に mBitmapNonAnimation が用意されてるはずだ
result.bitmap = null
result.delay = Long.MAX_VALUE
return
}
val frameCount = frames.size
val isFinite = ! animationControl.isPlayIndefinitely
val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
val endWait = if(isFinite) DELAY_AFTER_END else 0L
val timeTotalLoop = Math.max(1,timeTotal * repeatSequenceCount + endWait)
val tf = (if(0.5f + t < 0f) 0f else t / durationScale).toLong()
// 全体の繰り返し時刻で余りを計算
val tl = tf % timeTotalLoop
if(tl >= timeTotalLoop - endWait) {
// 終端で待機状態
result.bitmap = frames[frameCount - 1].bitmap
result.delay = (0.5f + (timeTotalLoop - tl) * durationScale).toLong()
return
}
// 1ループの繰り返し時刻で余りを計算
val tt = tl % timeTotal
// フレームリストを時刻で二分探索
var s = 0
var e = frameCount
while(e - s > 1) {
val mid = s + e shr 1
val frame = frames[mid]
// log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.time_start,frame.time_start+frame.time_width );
if(tt < frame.time_start) {
e = mid
} else if(tt >= frame.time_start + frame.time_width) {
s = mid + 1
} else {
s = mid
break
}
}
s = if(s < 0) 0 else if(s >= frameCount - 1) frameCount - 1 else s
val frame = frames[s]
val delay = frame.time_start + frame.time_width - tt
result.bitmap = frames[s].bitmap
result.delay = (0.5f + durationScale * Math.max(0f, delay.toFloat())).toLong()
// log.d("findFrame tf=%d,tl=%d/%d,tt=%d/%d,s=%d,w=%d,delay=%d",tf,tl,loop_total,tt,timeTotal,s,frame.time_width,result.delay);
}
/////////////////////////////////////////////////////
// implements ApngDecoderCallback
override fun log(message : String) {
Log.d(ApngFrames.TAG, message)
}
override fun onHeader(apng : Apng, header : ApngImageHeader) {
this.header = header
}
override fun onAnimationInfo(
apng : Apng,
animationControl : ApngAnimationControl
) {
this.animationControl = animationControl
val canvasBitmap = ApngFrames.createBlankBitmap(width, height)
this.canvasBitmap = canvasBitmap
this.canvas = Canvas(canvasBitmap)
this.frames = ArrayList(animationControl.numFrames)
}
override fun onDefaultImage(apng : Apng, bitmap : ApngBitmap) {
defaultImage?.recycle()
defaultImage = ApngFrames.toBitmap(bitmap, pixelSizeMax)
}
override fun onAnimationFrame(
apng : Apng,
frameControl : ApngFrameControl,
bitmap : ApngBitmap
) {
val frames = this.frames ?: return
val canvasBitmap = this.canvasBitmap ?: return
// APNGのフレーム画像をAndroidの形式に変換する。この段階ではリサイズしない
val bitmapNative = ApngFrames.toBitmap(bitmap)
val previous : Bitmap? = if(frameControl.disposeOp == DisposeOp.Previous) {
// Capture the current bitmap region IF it needs to be reverted after rendering
Bitmap.createBitmap(
canvasBitmap,
frameControl.xOffset,
frameControl.yOffset,
frameControl.width,
frameControl.height
)
} else {
null
}
val paint = if(frameControl.blendOp == BlendOp.Source) {
ApngFrames.sSrcModePaint // SRC_OVER, not blend
} else {
null // (for blend, leave paint null)
}
// Draw the new frame into place
canvas.drawBitmap(
bitmapNative,
frameControl.xOffset.toFloat(),
frameControl.yOffset.toFloat(),
paint
)
// Extract a drawable from the canvas. Have to copy the current bitmap.
// Store the drawable in the sequence of frames
val timeStart = timeTotal
val timeWidth = Math.max(1L, frameControl.delayMilliseconds)
timeTotal += timeWidth
val scaledBitmap =
scaleBitmap(canvasBitmap.copy(Bitmap.Config.ARGB_8888, false), pixelSizeMax)
if(scaledBitmap != null) {
frames.add(Frame(scaledBitmap, timeStart, timeWidth))
}
// Now "dispose" of the frame in preparation for the next.
// https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
when(frameControl.disposeOp) {
DisposeOp.None -> {
}
DisposeOp.Background ->
// APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
//System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
//if (true || isFull) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR) // Clear to fully transparent black
DisposeOp.Previous ->
// APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
//System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
// Put the original section back
if(previous != null) {
canvas.drawBitmap(
previous, frameControl.xOffset.toFloat(), frameControl.yOffset.toFloat(),
ApngFrames.sSrcModePaint
)
previous.recycle()
}
else -> {
// 0: Default should never happen
// APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
//System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
// do nothing
// } else {
// Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
// paint = new Paint();
// paint.setColor(0);
// paint.setStyle(Paint.Style.FILL);
// canvas.drawRect(rt, paint);
// }
}
}
}
}
//package jp.juggler.subwaytooter.util
//
//import android.graphics.Bitmap
//import android.graphics.Canvas
//import android.graphics.Paint
//import android.graphics.PorterDuff
//import android.graphics.PorterDuffXfermode
//import android.graphics.Rect
//
//import net.ellerton.japng.PngScanlineBuffer
//import net.ellerton.japng.argb8888.Argb8888Bitmap
//import net.ellerton.japng.argb8888.Argb8888Processor
//import net.ellerton.japng.argb8888.Argb8888Processors
//import net.ellerton.japng.argb8888.Argb8888ScanlineProcessor
//import net.ellerton.japng.argb8888.BasicArgb8888Director
//import net.ellerton.japng.chunks.PngAnimationControl
//import net.ellerton.japng.chunks.PngFrameControl
//import net.ellerton.japng.chunks.PngHeader
//import net.ellerton.japng.error.PngException
//import net.ellerton.japng.reader.DefaultPngChunkReader
//import net.ellerton.japng.reader.PngReadHelper
//
//import java.io.InputStream
//import java.util.ArrayList
//
//// APNGを解釈した結果を保持する
//// (フレーム数分のbitmapと時間情報)
//
//class APNGFrames {
//
// companion object {
//
// internal val log = LogCategory("APNGFrames")
//
// // ループしない画像の場合は3秒でまたループさせる
// private const val DELAY_AFTER_END = 3000L
//
// /**
// * Keep a 1x1 transparent image around as reference for creating a scaled starting bitmap.
// * Considering this because of some reported OutOfMemory errors, and this post:
// *
// *
// * http://stackoverflow.com/a/8527745/963195
// *
// *
// * Specifically: "NEVER use Bitmap.createBitmap(width, height, Config.ARGB_8888). I mean NEVER!"
// *
// *
// * Instead the 1x1 image (68 bytes of resources) is scaled up to the needed size.
// * Whether or not this fixes the OOM problems is TBD...
// */
// //static Bitmap sOnePxTransparent;
// internal val sSrcModePaint : Paint by lazy{
// val paint = Paint()
// paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
// paint.isFilterBitmap = true
// paint
// }
//
// internal fun createBlankBitmap(w : Int, h : Int) : Bitmap {
// return Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
// }
//
// // WARNING: ownership of "src" will be moved or recycled.
// internal fun scaleBitmap(src : Bitmap?, size_max : Int) : Bitmap? {
// if(src == null) return null
//
// val src_w = src.width
// val src_h = src.height
// if(src_w <= size_max && src_h <= size_max) return src
//
// var dst_w : Int
// var dst_h : Int
// if(src_w >= src_h) {
// dst_w = size_max
// dst_h = (0.5f + src_h * size_max / src_w.toFloat()).toInt()
// if(dst_h < 1) dst_h = 1
// } else {
// dst_h = size_max
// dst_w = (0.5f + src_w * size_max / src_h.toFloat()).toInt()
// if(dst_w < 1) dst_w = 1
// }
//
// // この方法だとリークがあるらしい???
// // http://stackoverflow.com/a/8527745/963195
// // return Bitmap.createScaledBitmap( src, dst_w , dst_h , true );
//
// val b2 = createBlankBitmap(dst_w, dst_h)
// val canvas = Canvas(b2)
// val rect_src = Rect(0, 0, src_w, src_h)
// val rect_dst = Rect(0, 0, dst_w, dst_h)
// canvas.drawBitmap(src, rect_src, rect_dst, sSrcModePaint)
// src.recycle()
// return b2
// }
//
// internal fun toBitmap(src : Argb8888Bitmap) : Bitmap {
// val offset = 0
// val stride = src.width
// return Bitmap.createBitmap(src.pixelArray, offset, stride, src.width, src.height, Bitmap.Config.ARGB_8888)
// }
//
// internal fun toBitmap(src : Argb8888Bitmap, size_max : Int) : Bitmap? {
// return scaleBitmap(toBitmap(src), size_max)
// }
//
// /////////////////////////////////////////////////////////////////////
//
// // entry point is here
// @Throws(PngException::class)
// internal fun parseAPNG( inStream : InputStream, size_max : Int) : APNGFrames? {
// val handler = APNGParseEventHandler(size_max)
// try {
// val processor = Argb8888Processor(handler)
// val reader = DefaultPngChunkReader(processor)
// val result = PngReadHelper.read(inStream, reader)
// result?.onParseComplete()
// return result
// } catch(ex : Throwable) {
// handler.dispose()
// throw ex
// }
//
// }
// }
//
// // ピクセルサイズ制限
// private var mPixelSizeMax : Int = 0
//
// // APNGじゃなかった場合に使われる
// private var mBitmapNonAnimation : Bitmap? = null
//
// private lateinit var header : PngHeader
// private lateinit var scanlineProcessor : Argb8888ScanlineProcessor
// private lateinit var canvas : Canvas
//
// private var canvasBitmap : Bitmap? = null
// private var currentFrame : PngFrameControl? = null
// private var animationControl : PngAnimationControl? = null
//
// private var time_total = 0L
//
// private var frames : ArrayList<Frame>? = null
//
// ///////////////////////////////////////////////////////////////
//
// // 再生速度の調整
// private var durationScale = 1f
//
// private val numFrames : Int
// get() = animationControl?.numFrames ?: 1
//
// val hasMultipleFrame : Boolean
// get() = numFrames > 1
//
// private class Frame(
// internal val bitmap : Bitmap,
// internal val time_start : Long,
// internal val time_width : Long
// )
//
// ///////////////////////////////////////////////////////////////
//
// internal constructor(bitmap : Bitmap) {
// this.mBitmapNonAnimation = bitmap
// }
//
// internal constructor(
// header : PngHeader, scanlineProcessor : Argb8888ScanlineProcessor, animationControl : PngAnimationControl, size_max : Int
// ) {
// this.header = header
// this.scanlineProcessor = scanlineProcessor
// this.animationControl = animationControl
// this.mPixelSizeMax = size_max
//
// this.canvasBitmap = createBlankBitmap(header.width, header.height)
// this.canvas = Canvas(this.canvasBitmap)
// this.frames = ArrayList(animationControl.numFrames)
//
// }
//
// internal fun onParseComplete() {
// val frames = this.frames
//
// if(frames != null && frames.size <= 1) {
// mBitmapNonAnimation = toBitmap(scanlineProcessor.bitmap, mPixelSizeMax)
// }
//
// canvasBitmap?.recycle()
// canvasBitmap = null
// }
//
// internal fun dispose() {
// mBitmapNonAnimation?.recycle()
// canvasBitmap?.recycle()
//
// val frames = this.frames
// if(frames != null) {
// for(f in frames) {
// f.bitmap.recycle()
// }
// }
// }
//
// // フレームが追加される
// internal fun beginFrame(frameControl : PngFrameControl) : Argb8888ScanlineProcessor {
// currentFrame = frameControl
// return scanlineProcessor.cloneWithSharedBitmap(header.adjustFor(currentFrame))
// }
//
// // フレームが追加される
// internal fun completeFrame(frameImage : Argb8888Bitmap) {
//
// val frames = this.frames ?: return
//
// val canvasBitmap = this.canvasBitmap ?: return
//
// val currentFrame = this.currentFrame ?: return
// this.currentFrame = null
//
// // APNGのフレーム画像をAndroidの形式に変換する
// val frame = toBitmap(frameImage)
//
// var previous : Bitmap? = null
// // Capture the current bitmap region IF it needs to be reverted after rendering
// if(2 == currentFrame.disposeOp.toInt()) {
// previous = Bitmap.createBitmap(canvasBitmap, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height) // or could use from frames?
// //System.out.println(String.format("Captured previous %d x %d", previous.getWidth(), previous.getHeight()));
// }
//
// var paint : Paint? = null // (for blend, leave paint null)
// if(0 == currentFrame.blendOp.toInt()) { // SRC_OVER, not blend
// paint = sSrcModePaint
// }
//
// // boolean isFull = currentFrame.height == header.height && currentFrame.width == header.width;
//
// // Draw the new frame into place
// canvas.drawBitmap(frame, currentFrame.xOffset.toFloat(), currentFrame.yOffset.toFloat(), paint)
//
// // Extract a drawable from the canvas. Have to copy the current bitmap.
// // Store the drawable in the sequence of frames
// val time_start = time_total
// var time_width = currentFrame.delayMilliseconds.toLong()
// if(time_width <= 0L) time_width = 1L
// time_total = time_start + time_width
//
// val scaledBitmap = scaleBitmap(canvasBitmap.copy(Bitmap.Config.ARGB_8888, false), mPixelSizeMax)
// if(scaledBitmap != null) {
// frames.add(Frame(scaledBitmap, time_start, time_width))
// }
//
// // Now "dispose" of the frame in preparation for the next.
// // https://wiki.mozilla.org/APNG_Specification#.60fcTL.60:_The_Frame_Control_Chunk
//
// when(currentFrame.disposeOp.toInt()) {
//
// 1 ->
// // APNG_DISPOSE_OP_BACKGROUND: the frame's region of the output buffer is to be cleared to fully transparent black before rendering the next frame.
// //System.out.println(String.format("Frame %d clear background (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
// //if (true || isFull) {
// canvas.drawColor(0, PorterDuff.Mode.CLEAR) // Clear to fully transparent black
//
// 2 ->
// // APNG_DISPOSE_OP_PREVIOUS: the frame's region of the output buffer is to be reverted to the previous contents before rendering the next frame.
// //System.out.println(String.format("Frame %d restore previous (full=%s, x=%d y=%d w=%d h=%d) previous=%s", currentFrame.sequenceNumber,
// // isFull, currentFrame.xOffset, currentFrame.yOffset, currentFrame.width, currentFrame.height, previous));
// // Put the original section back
// if(previous != null) {
// canvas.drawBitmap(previous, currentFrame.xOffset.toFloat(), currentFrame.yOffset.toFloat(), sSrcModePaint)
// previous.recycle()
// }
//
// else -> {
// // 0: Default should never happen
//
// // APNG_DISPOSE_OP_NONE: no disposal is done on this frame before rendering the next; the contents of the output buffer are left as is.
// //System.out.println("Frame "+currentFrame.sequenceNumber+" do nothing dispose");
// // do nothing
// // } else {
// // Rect rt = new Rect(currentFrame.xOffset, currentFrame.yOffset, currentFrame.width+currentFrame.xOffset, currentFrame.height+currentFrame.yOffset);
// // paint = new Paint();
// // paint.setColor(0);
// // paint.setStyle(Paint.Style.FILL);
// // canvas.drawRect(rt, paint);
// // }
//
// }
// }
// }
//
// class FindFrameResult {
// var bitmap : Bitmap? = null // may null
// var delay : Long = 0 // 再描画が必要ない場合は Long.MAX_VALUE
// }
//
// // シーク位置に応じたコマ画像と次のコマまでの残り時間をresultに格納する
// fun findFrame(result : FindFrameResult, t : Long) {
//
// if(mBitmapNonAnimation != null) {
// result.bitmap = mBitmapNonAnimation
// result.delay = Long.MAX_VALUE
// return
// }
//
// val animationControl = this.animationControl
// val frames = this.frames
// if(animationControl == null || frames == null) {
// // この場合は既に mBitmapNonAnimation が用意されてるはずだ
// result.bitmap = null
// result.delay = Long.MAX_VALUE
// return
// }
//
// val frame_count = frames.size
//
// val isFinite = ! animationControl.loopForever()
// val repeatSequenceCount = if(isFinite) animationControl.numPlays else 1
// val end_wait = if(isFinite) DELAY_AFTER_END else 0L
// var loop_total = time_total * repeatSequenceCount + end_wait
// if(loop_total <= 0) loop_total = 1
//
// val tf = (if(0.5f + t < 0f) 0f else t / durationScale).toLong()
//
// // 全体の繰り返し時刻で余りを計算
// val tl = tf % loop_total
// if(tl >= loop_total - end_wait) {
// // 終端で待機状態
// result.bitmap = frames[frame_count - 1].bitmap
// result.delay = (0.5f + (loop_total - tl) * durationScale).toLong()
// return
// }
// // 1ループの繰り返し時刻で余りを計算
// val tt = tl % time_total
//
// // フレームリストを時刻で二分探索
// var s = 0
// var e = frame_count
// while(e - s > 1) {
// val mid = s + e shr 1
// val frame = frames[mid]
// // log.d("s=%d,m=%d,e=%d tt=%d,fs=%s,fe=%d",s,mid,e,tt,frame.time_start,frame.time_start+frame.time_width );
// if(tt < frame.time_start) {
// e = mid
// } else if(tt >= frame.time_start + frame.time_width) {
// s = mid + 1
// } else {
// s = mid
// break
// }
// }
// s = if(s < 0) 0 else if(s >= frame_count - 1) frame_count - 1 else s
// val frame = frames[s]
// val delay = frame.time_start + frame.time_width - tt
// result.bitmap = frames[s].bitmap
// result.delay = (0.5f + durationScale * Math.max(0f, delay.toFloat())).toLong()
//
// // log.d("findFrame tf=%d,tl=%d/%d,tt=%d/%d,s=%d,w=%d,delay=%d",tf,tl,loop_total,tt,time_total,s,frame.time_width,result.delay);
// }
//
// /////////////////////////////////////////////////////////////////////
//
// // APNGのパース中に随時呼び出される
// internal class APNGParseEventHandler(
// private val size_max : Int
// ) : BasicArgb8888Director<APNGFrames>() {
//
// private lateinit var header : PngHeader
//
// private var frames : APNGFrames? = null
//
// private val isAnimated : Boolean
// get() = frames != null
//
// // ヘッダが分かった
// @Throws(PngException::class)
// override fun receiveHeader(header : PngHeader, buffer : PngScanlineBuffer) {
// this.header = header
//
// // 親クラスのprotectedフィールドを更新する
// val pngBitmap = Argb8888Bitmap(header.width, header.height)
// this.scanlineProcessor = Argb8888Processors.from(header, buffer, pngBitmap)
// }
//
// // デフォルト画像の手前で呼ばれる
// override fun beforeDefaultImage() : Argb8888ScanlineProcessor {
// return scanlineProcessor
// }
//
// // デフォルト画像が分かった
// // おそらく receiveAnimationControl より先に呼ばれる
// override fun receiveDefaultImage(defaultImage : Argb8888Bitmap) {
// // japng ライブラリの返すデフォルトイメージはあまり信用できないので使わない
// }
//
// // アニメーション制御情報が分かった
// override fun receiveAnimationControl(animationControl : PngAnimationControl) {
// this.frames = APNGFrames(header , scanlineProcessor, animationControl, size_max)
// }
//
// override fun wantDefaultImage() : Boolean {
// return ! isAnimated
// }
//
// override fun wantAnimationFrames() : Boolean {
// return true // isAnimated;
// }
//
// // フレーム制御情報が分かった
// override fun receiveFrameControl(frameControl : PngFrameControl) : Argb8888ScanlineProcessor {
// val frames = this.frames ?: throw RuntimeException("not animation image")
// return frames.beginFrame(frameControl)
// }
//
// // フレーム画像が分かった
// override fun receiveFrameImage(frameImage : Argb8888Bitmap) {
// val frames = this.frames ?: throw RuntimeException("not animation image")
// frames.completeFrame(frameImage)
// }
//
// // 結果を取得する
// override fun getResult() : APNGFrames? {
// val frames = this.frames
// return if( frames?.hasMultipleFrame == true ){
// frames
// }else {
// dispose()
// return null
// }
// }
//
// // 処理中に例外が起きた場合、Bitmapリソースを解放する
// fun dispose() {
// frames?.dispose()
// frames = null
// }
// }
//
//}

View File

@ -14,7 +14,7 @@ import java.util.concurrent.ConcurrentHashMap
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.apng.ApngFrames
class CustomEmojiCache(internal val context : Context) {

View File

@ -446,7 +446,7 @@ class PostHelper(
val part = src.substring(last_sharp + 1, end)
if(reCharsNotTag.matcher(part).find()) {
// log.d( "checkTag: character not tag in string %s", part );
// warning.d( "checkTag: character not tag in string %s", part );
checkEmoji()
return
}

View File

@ -54,7 +54,7 @@ object Utils {
// if( iv != 0 ) return iv;
// }catch(Throwable ex){
// }
// log.e("missing resid for %s",name);
// warning.e("missing resid for %s",name);
// return R.string.Dialog_Cancel;
// }
@ -547,7 +547,7 @@ fun String.digestSHA256() : String {
// sb.append( String.format( Locale.JAPAN, "%dk", n ) );
// t -= n * 1000L;
// }
// // remain
// // length
// if( sb.length() > 0 ){
// sb.append( String.format( Locale.JAPAN, "%03d", t ) );
// }else if( n > 0 ){

View File

@ -51,7 +51,7 @@ class VersionString(src : String?) {
companion object {
// private val log = new LogCategory( "VersionString" )
// private val warning = new LogCategory( "VersionString" )
private fun isDelimiter(c : Char) : Boolean {
return c == '.' || c == ' '

View File

@ -116,7 +116,7 @@ class MyNetworkImageView : AppCompatImageView {
val d = drawable
if(d is Animatable) {
if(d.isRunning) {
//log.d("cancelLoading: Animatable.stop()")
//warning.d("cancelLoading: Animatable.stop()")
d.stop()
}
}

View File

@ -9,10 +9,11 @@ import android.os.SystemClock
import android.util.AttributeSet
import android.view.View
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.util.ApngFrames
import java.lang.ref.WeakReference
import jp.juggler.apng.ApngFrames
import jp.juggler.subwaytooter.App1
class NetworkEmojiView : View {
constructor(context : Context) : super(context)

View File

@ -179,7 +179,7 @@ class TestKotlinFeature {
/*
蛇足だがクラスファイルを読むのは
app/build/tmp/kotlin-classes/*UnitTest\**/TestKotlinFeature.class
javap.exe -c TestKotlinFeature.class > javap.log とすると逆アセンブルできる
javap.exe -c TestKotlinFeature.class > javap.warning とすると逆アセンブルできる
*/
}

View File

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