16bit APNGのバグ修正
This commit is contained in:
parent
ec8527eae0
commit
af5cddf051
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,72 @@
|
|||
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
|
||||
package jp.juggler.apng
|
||||
|
||||
class ApngBitmap(var width: Int, var height: Int) {
|
||||
class ApngBitmap(var width : Int, var height : Int) {
|
||||
|
||||
val colors = IntArray( width * height)
|
||||
// each int value contains 0xAARRGGBB
|
||||
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)
|
||||
}
|
||||
// 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) {
|
||||
// ビットマップ中の位置を保持して、ピクセルへの書き込みと位置の更新を行う
|
||||
inner class Pointer {
|
||||
|
||||
fun plusX(x: Int): Pointer {
|
||||
pos += x
|
||||
return this
|
||||
}
|
||||
private var pos : Int = 0
|
||||
var step : Int = 1
|
||||
|
||||
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(argb : Int) : Pointer {
|
||||
// if( pos == width) println("setPixel 0x%x".format(argb))
|
||||
colors[pos] = argb
|
||||
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 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 pointer(x: Int, y: Int) = Pointer( x + y * width )
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
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)
|
||||
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")
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
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) {
|
||||
loop@ while(true) {
|
||||
crc32.reset()
|
||||
val chunk = ApngChunk(crc32, tokenizer)
|
||||
when(chunk.type) {
|
||||
|
||||
"IEND" -> break@loop
|
||||
"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)
|
||||
}
|
||||
"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))
|
||||
"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)))
|
||||
}
|
||||
"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, 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}")
|
||||
}
|
||||
}
|
||||
"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.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)
|
||||
}
|
||||
"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 animationControl = ApngAnimationControl(ByteArrayTokenizer(chunk.readBody(crc32, tokenizer)))
|
||||
apng.animationControl = animationControl
|
||||
callback.onAnimationInfo(apng, animationControl)
|
||||
}
|
||||
"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 = ByteArrayTokenizer(chunk.readBody(crc32, tokenizer))
|
||||
checkSequenceNumber(bat.readInt32())
|
||||
lastFctl = ApngFrameControl(bat)
|
||||
fdatDecoder = null
|
||||
}
|
||||
"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.onAnimationFrame(apng, fctl, bitmap)
|
||||
}
|
||||
}
|
||||
checkSequenceNumber(tokenizer.readInt32(crc32))
|
||||
fdatDecoder.addData(
|
||||
tokenizer.inStream,
|
||||
chunk.size - 4,
|
||||
inBuffer,
|
||||
crc32
|
||||
)
|
||||
chunk.checkCRC(tokenizer, crc32.value)
|
||||
}
|
||||
"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)
|
||||
// 無視するチャンク
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
callback.onApngWarning(
|
||||
"unknown chunk: type=%s,size=0x%x".format(
|
||||
chunk.type,
|
||||
chunk.size
|
||||
)
|
||||
)
|
||||
chunk.skipBody(tokenizer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
class ApngPalette(
|
||||
src : ByteArray // repeat of R,G,B
|
||||
) {
|
||||
companion object {
|
||||
// full opaque black
|
||||
const val OPAQUE = 255 shl 24
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
val list : IntArray // repeat of 0xAARRGGBB
|
||||
|
||||
override fun toString() = "palette(${list.size} entries,hasAlpha=$hasAlpha)"
|
||||
var hasAlpha : Boolean = false
|
||||
|
||||
fun parseTRNS(ba: ByteArray) {
|
||||
hasAlpha = true
|
||||
for (i in 0 until Math.min(list.size, ba.size)) {
|
||||
list[i * 4] = ba[i]
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package jp.juggler.apng.util
|
||||
|
||||
internal class ByteArrayRange(
|
||||
val array: ByteArray,
|
||||
var start: Int,
|
||||
var remain: Int
|
||||
)
|
|
@ -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()
|
||||
|
||||
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -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"
|
||||
}
|
|
@ -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
|
|
@ -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() );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="jp.juggler.apng"
|
||||
/>
|
|
@ -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);
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">apng_android</string>
|
||||
</resources>
|
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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." );
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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() ){
|
||||
|
|
|
@ -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 );
|
||||
// }
|
||||
// } );
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -88,7 +88,7 @@ object MutedApp {
|
|||
// cursor.close();
|
||||
// }
|
||||
// }catch( Throwable ex ){
|
||||
// log.e( ex, "load failed." );
|
||||
// warning.e( ex, "load failed." );
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
|
|
|
@ -88,7 +88,7 @@ object MutedWord {
|
|||
// cursor.close();
|
||||
// }
|
||||
// }catch( Throwable ex ){
|
||||
// log.e( ex, "load failed." );
|
||||
// warning.e( ex, "load failed." );
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
|
|
|
@ -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." );
|
||||
// }
|
||||
// }
|
||||
|
||||
|
|
|
@ -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;
|
||||
// }
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 ){
|
||||
|
|
|
@ -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 == ' '
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 とすると逆アセンブルできる
|
||||
*/
|
||||
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
include ':app', ':exif', ':colorpicker', ':emoji', ':apng'
|
||||
include ':app', ':exif', ':colorpicker', ':emoji', ':apng', ':apng_android'
|
||||
|
|
Loading…
Reference in New Issue