From 8c46295631863dfb727fbf7f78316cf9635c69f4 Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 1 Feb 2018 18:30:46 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apng/src/main/java/jp/juggler/apng/Apng.kt | 10 +- .../jp/juggler/apng/ApngAnimationControl.kt | 49 +++--- .../java/jp/juggler/apng/ApngBackground.kt | 62 ++++---- .../main/java/jp/juggler/apng/ApngBitmap.kt | 16 +- .../main/java/jp/juggler/apng/ApngChunk.kt | 62 ++++---- .../main/java/jp/juggler/apng/ApngDecoder.kt | 26 ++-- .../jp/juggler/apng/ApngDecoderCallback.kt | 2 +- .../main/java/jp/juggler/apng/ApngEnums.kt | 52 +++---- .../java/jp/juggler/apng/ApngFrameControl.kt | 72 ++++----- .../java/jp/juggler/apng/ApngImageHeader.kt | 68 ++++---- .../main/java/jp/juggler/apng/ApngPalette.kt | 9 +- .../java/jp/juggler/apng/ApngParseError.kt | 3 + .../jp/juggler/apng/ApngTransparentColor.kt | 39 ++--- .../main/java/jp/juggler/apng/IdatDecoder.kt | 146 +++++++++--------- .../main/java/jp/juggler/apng/ParseError.kt | 3 - .../java/jp/juggler/apng/util/BufferPool.kt | 16 +- .../java/jp/juggler/apng/util/ByteSequence.kt | 5 +- .../jp/juggler/apng/util/ByteSequenceQueue.kt | 67 ++++---- .../jp/juggler/apng/util/StreamTokenizer.kt | 120 +++++++------- .../main/java/jp/juggler/apng/ApngFrames.kt | 22 +-- 20 files changed, 414 insertions(+), 435 deletions(-) create mode 100644 apng/src/main/java/jp/juggler/apng/ApngParseError.kt delete mode 100644 apng/src/main/java/jp/juggler/apng/ParseError.kt diff --git a/apng/src/main/java/jp/juggler/apng/Apng.kt b/apng/src/main/java/jp/juggler/apng/Apng.kt index c65c3b24..2b00fd92 100644 --- a/apng/src/main/java/jp/juggler/apng/Apng.kt +++ b/apng/src/main/java/jp/juggler/apng/Apng.kt @@ -1,9 +1,9 @@ package jp.juggler.apng class Apng { - var header: ApngImageHeader? = null - var background: ApngBackground? = null - var animationControl: ApngAnimationControl? = null - internal var palette: ApngPalette? = null - internal var transparentColor: ApngTransparentColor? = null + var header : ApngImageHeader? = null + var background : ApngBackground? = null + var animationControl : ApngAnimationControl? = null + internal var palette : ApngPalette? = null + internal var transparentColor : ApngTransparentColor? = null } diff --git a/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt b/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt index 0e70d049..f5f24fbb 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngAnimationControl.kt @@ -4,29 +4,28 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence - -class ApngAnimationControl internal constructor(src: ByteSequence) { - - companion object { - const val PLAY_INDEFINITELY =0 - } - - // This must equal the number of `fcTL` chunks. - // 0 is not a valid value. - // 1 is a valid value for a single-frame APNG. - val numFrames: Int - - // if it is 0, the animation should play indefinitely. - // If nonzero, the animation should come to rest on the final frame at the end of the last play. - val numPlays: Int - - init { - numFrames = src.readInt32() - numPlays = src.readInt32() - } - - override fun toString() ="ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)" - - val isFinite :Boolean - get() = numPlays > PLAY_INDEFINITELY +class ApngAnimationControl internal constructor(src : ByteSequence) { + + companion object { + const val PLAY_INDEFINITELY = 0 + } + + // This must equal the number of `fcTL` chunks. + // 0 is not a valid value. + // 1 is a valid value for a single-frame APNG. + val numFrames : Int + + // if it is 0, the animation should play indefinitely. + // If nonzero, the animation should come to rest on the final frame at the end of the last play. + val numPlays : Int + + init { + numFrames = src.readInt32() + numPlays = src.readInt32() + } + + override fun toString() = "ApngAnimationControl(numFrames=$numFrames,numPlays=$numPlays)" + + val isFinite : Boolean + get() = numPlays > PLAY_INDEFINITELY } diff --git a/apng/src/main/java/jp/juggler/apng/ApngBackground.kt b/apng/src/main/java/jp/juggler/apng/ApngBackground.kt index 05db9d34..96185fd6 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngBackground.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngBackground.kt @@ -4,34 +4,36 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence -class ApngBackground internal constructor(colorType: ColorType, src: ByteSequence) { - - val red: Int - val green: Int - val blue: Int - val index: Int - - init { - when (colorType) { - ColorType.GREY, ColorType.GREY_ALPHA -> { - val v = src.readUInt16() - red = v - green = v - blue = v - index = -1 - } - ColorType.RGB, ColorType.RGBA -> { - red = src.readUInt16() - green = src.readUInt16() - blue = src.readUInt16() - index = -1 - } - ColorType.INDEX -> { - red = -1 - green = -1 - blue = -1 - index = src.readUInt8() - } - } - } +class ApngBackground internal constructor(colorType : ColorType, src : ByteSequence) { + + val red : Int + val green : Int + val blue : Int + val index : Int + + init { + when(colorType) { + ColorType.GREY, ColorType.GREY_ALPHA -> { + val v = src.readUInt16() + red = v + green = v + blue = v + index = - 1 + } + + ColorType.RGB, ColorType.RGBA -> { + red = src.readUInt16() + green = src.readUInt16() + blue = src.readUInt16() + index = - 1 + } + + ColorType.INDEX -> { + red = - 1 + green = - 1 + blue = - 1 + index = src.readUInt8() + } + } + } } \ No newline at end of file diff --git a/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt b/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt index 9f1b435d..f6479701 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngBitmap.kt @@ -11,7 +11,7 @@ class ApngBitmap(var width : Int, var height : Int) { 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") + throw ApngParseError("can't resize to $width x $height , it's greater than initial size") this.width = width this.height = height // 透明な黒で初期化する @@ -24,11 +24,7 @@ class ApngBitmap(var width : Int, var height : Int) { 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(argb : Int) = apply { colors[pos] = argb } fun setPixel(a : Int, r : Int, g : Int, b : Int) = setPixel( ((a and 255) shl 24) or @@ -37,18 +33,14 @@ class ApngBitmap(var width : Int, var height : Int) { (b and 255) ) - fun setOffset(pos : Int = 0, step : Int = 1) : Pointer { + fun setOffset(pos : Int = 0, step : Int = 1) = apply { 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 plus(x : Int) = apply { pos += x } fun next() = plus(step) diff --git a/apng/src/main/java/jp/juggler/apng/ApngChunk.kt b/apng/src/main/java/jp/juggler/apng/ApngChunk.kt index e1d33f9b..d15702d5 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngChunk.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngChunk.kt @@ -5,35 +5,35 @@ package jp.juggler.apng import jp.juggler.apng.util.StreamTokenizer import java.util.zip.CRC32 -internal class ApngChunk(crc32:CRC32,tokenizer: StreamTokenizer) { - val size: Int - val type: String - - init { - size = tokenizer.readInt32() - val typeBytes = tokenizer.readBytes(4) - type = typeBytes.toString(Charsets.UTF_8) - - crc32.update(typeBytes) - } - - fun readBody(crc32:CRC32,tokenizer: StreamTokenizer): ByteArray { - val bytes = tokenizer.readBytes(size) - val crcExpect = tokenizer.readUInt32() - - crc32.update(bytes, 0, size) - val crcActual = crc32.value - if (crcActual != crcExpect) throw ParseError("CRC not match.") - - return bytes - } - - fun skipBody(tokenizer: StreamTokenizer) { - tokenizer.skipBytes((size + 4).toLong()) - } - - fun checkCRC(tokenizer: StreamTokenizer, crcActual: Long) { - val crcExpect = tokenizer.readUInt32() - if (crcActual != crcExpect) throw ParseError("CRC not match.") - } +internal class ApngChunk(crc32 : CRC32, tokenizer : StreamTokenizer) { + val size : Int + val type : String + + init { + size = tokenizer.readInt32() + val typeBytes = tokenizer.readBytes(4) + type = typeBytes.toString(Charsets.UTF_8) + + crc32.update(typeBytes) + } + + fun readBody(crc32 : CRC32, tokenizer : StreamTokenizer) : ByteArray { + val bytes = tokenizer.readBytes(size) + val crcExpect = tokenizer.readUInt32() + + crc32.update(bytes, 0, size) + val crcActual = crc32.value + if(crcActual != crcExpect) throw ApngParseError("CRC not match.") + + return bytes + } + + fun skipBody(tokenizer : StreamTokenizer) = + tokenizer.skipBytes((size + 4).toLong()) + + + fun checkCRC(tokenizer : StreamTokenizer, crcActual : Long) { + val crcExpect = tokenizer.readUInt32() + if(crcActual != crcExpect) throw ApngParseError("CRC not match.") + } } diff --git a/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt b/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt index 508dcd86..30e52a82 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngDecoder.kt @@ -11,22 +11,22 @@ object ApngDecoder { private val PNG_SIGNATURE = byteArrayOf(0x89.toByte(), 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a) fun parseStream( - _inStream : InputStream, + inStream : InputStream, callback : ApngDecoderCallback ) { val apng = Apng() - val tokenizer = StreamTokenizer(_inStream) + val tokenizer = StreamTokenizer(inStream) val pngHeader = tokenizer.readBytes(8) if(! pngHeader.contentEquals(PNG_SIGNATURE)) { - throw ParseError("header not match") + throw ApngParseError("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") + throw ApngParseError("incorrect sequenceNumber. last=$lastSequenceNumber,current=$n") } lastSequenceNumber = n } @@ -56,7 +56,7 @@ object ApngDecoder { "PLTE" -> apng.palette = ApngPalette(chunk.readBody(crc32, tokenizer)) "bKGD" -> { - val header = apng.header ?: throw ParseError("missing IHDR") + val header = apng.header ?: throw ApngParseError("missing IHDR") apng.background = ApngBackground( header.colorType, ByteSequence(chunk.readBody(crc32, tokenizer)) @@ -64,7 +64,7 @@ object ApngDecoder { } "tRNS" -> { - val header = apng.header ?: throw ParseError("missing IHDR") + val header = apng.header ?: throw ApngParseError("missing IHDR") val body = chunk.readBody(crc32, tokenizer) when(header.colorType) { ColorType.GREY -> apng.transparentColor = @@ -72,15 +72,15 @@ object ApngDecoder { ColorType.RGB -> apng.transparentColor = ApngTransparentColor(false, ByteSequence(body)) ColorType.INDEX -> apng.palette?.parseTRNS(body) - ?: throw ParseError("missing palette") + ?: throw ApngParseError("missing palette") else -> callback.onApngWarning("tRNS ignored. colorType =${header.colorType}") } } "IDAT" -> { - val header = apng.header ?: throw ParseError("missing IHDR") + val header = apng.header ?: throw ApngParseError("missing IHDR") if(idatDecoder == null) { - bitmap ?: throw ParseError("missing bitmap") + bitmap ?: throw ApngParseError("missing bitmap") bitmap.reset(header.width, header.height) idatDecoder = IdatDecoder( apng, @@ -106,11 +106,11 @@ object ApngDecoder { } "acTL" -> { - val header = apng.header ?: throw ParseError("missing IHDR") + val header = apng.header ?: throw ApngParseError("missing IHDR") val animationControl = ApngAnimationControl(ByteSequence(chunk.readBody(crc32, tokenizer))) apng.animationControl = animationControl - callback.onAnimationInfo(apng, header,animationControl) + callback.onAnimationInfo(apng, header, animationControl) } "fcTL" -> { @@ -121,9 +121,9 @@ object ApngDecoder { } "fdAT" -> { - val fctl = lastFctl ?: throw ParseError("missing fCTL before fdAT") + val fctl = lastFctl ?: throw ApngParseError("missing fCTL before fdAT") if(fdatDecoder == null) { - bitmap ?: throw ParseError("missing bitmap") + bitmap ?: throw ApngParseError("missing bitmap") bitmap.reset(fctl.width, fctl.height) fdatDecoder = IdatDecoder( apng, diff --git a/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt b/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt index ab1571d7..188f733c 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngDecoderCallback.kt @@ -8,7 +8,7 @@ interface ApngDecoderCallback { // called for debug message fun onApngDebug(message : String) {} - fun canApngDebug():Boolean = false + fun canApngDebug() : Boolean = false // called when PNG image header is detected. fun onHeader(apng : Apng, header : ApngImageHeader) diff --git a/apng/src/main/java/jp/juggler/apng/ApngEnums.kt b/apng/src/main/java/jp/juggler/apng/ApngEnums.kt index 55a07d75..67bd2900 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngEnums.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngEnums.kt @@ -1,41 +1,41 @@ package jp.juggler.apng -enum class ColorType(val num:Int ){ - GREY(0), - RGB ( 2), - INDEX( 3), - GREY_ALPHA ( 4), - RGBA ( 6), +enum class ColorType(val num : Int) { + GREY(0), + RGB(2), + INDEX(3), + GREY_ALPHA(4), + RGBA(6), } -enum class CompressionMethod(val num:Int ){ - Standard(0) +enum class CompressionMethod(val num : Int) { + Standard(0) } -enum class FilterMethod(val num:Int ){ - Standard(0) +enum class FilterMethod(val num : Int) { + Standard(0) } -enum class InterlaceMethod(val num:Int ){ - None(0), - Standard(1) +enum class InterlaceMethod(val num : Int) { + None(0), + Standard(1) } -enum class FilterType(val num:Int ){ - None(0), - Sub(1), - Up(2), - Average(3), - Paeth(4) +enum class FilterType(val num : Int) { + None(0), + Sub(1), + Up(2), + Average(3), + Paeth(4) } -enum class DisposeOp(val num :Int){ - None(0), - Background(1), - Previous(2) +enum class DisposeOp(val num : Int) { + None(0), + Background(1), + Previous(2) } -enum class BlendOp(val num :Int){ - Source(0), - Over(1) +enum class BlendOp(val num : Int) { + Source(0), + Over(1) } diff --git a/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt b/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt index 262211c8..1d53d22e 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngFrameControl.kt @@ -4,40 +4,40 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence - -class ApngFrameControl internal constructor(src: ByteSequence) { - - val width: Int - val height: Int - val xOffset: Int - val yOffset: Int - val delayNum: Int - val delayDen: Int - val disposeOp: DisposeOp - val blendOp: BlendOp - - init { - width = 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 = src.readUInt8() - disposeOp = DisposeOp.values().first{it.num==num} - - num = src.readUInt8() - blendOp = BlendOp.values().first{it.num==num} - } - - override fun toString() ="ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayNum=$delayNum,delayDen=$delayDen,disposeOp=$disposeOp,blendOp=$blendOp)" - - val delayMilliseconds : Long - get() = when(delayDen) { - 1000 -> delayNum.toLong() - else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong() - } +class ApngFrameControl internal constructor(src : ByteSequence) { + + val width : Int + val height : Int + val xOffset : Int + val yOffset : Int + val delayNum : Int + val delayDen : Int + val disposeOp : DisposeOp + val blendOp : BlendOp + + init { + width = 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 = src.readUInt8() + disposeOp = DisposeOp.values().first { it.num == num } + + num = src.readUInt8() + blendOp = BlendOp.values().first { it.num == num } + } + + override fun toString() = + "ApngFrameControl(width=$width,height=$height,x=$xOffset,y=$yOffset,delayNum=$delayNum,delayDen=$delayDen,disposeOp=$disposeOp,blendOp=$blendOp)" + + val delayMilliseconds : Long + get() = when(delayDen) { + 1000 -> delayNum.toLong() + else -> (1000f * delayNum.toFloat() / delayDen.toFloat() + 0.5f).toLong() + } } \ No newline at end of file diff --git a/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt b/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt index f524476d..57c63ccf 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngImageHeader.kt @@ -5,37 +5,39 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence // information from IHDR chunk. -class ApngImageHeader internal constructor(src: ByteSequence) { - val width: Int - val height: Int - val bitDepth: Int - val colorType: ColorType - val compressionMethod: CompressionMethod - val filterMethod: FilterMethod - val interlaceMethod: InterlaceMethod - - init { - - 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 =src.readUInt8() - colorType = ColorType.values().first { it.num==num } - // - num =src.readUInt8() - compressionMethod = CompressionMethod.values().first { it.num==num } - // - num =src.readUInt8() - filterMethod = FilterMethod.values().first { it.num==num } - // - num =src.readUInt8() - interlaceMethod = InterlaceMethod.values().first { it.num==num } - } - - override fun toString() = "ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)" +class ApngImageHeader internal constructor(src : ByteSequence) { + + val width : Int + val height : Int + val bitDepth : Int + val colorType : ColorType + val compressionMethod : CompressionMethod + val filterMethod : FilterMethod + val interlaceMethod : InterlaceMethod + + init { + + width = src.readInt32() + height = src.readInt32() + if(width <= 0 || height <= 0) throw ApngParseError("w=$width,h=$height is too small") + + bitDepth = src.readUInt8() + + var num : Int + // + num = src.readUInt8() + colorType = ColorType.values().first { it.num == num } + // + num = src.readUInt8() + compressionMethod = CompressionMethod.values().first { it.num == num } + // + num = src.readUInt8() + filterMethod = FilterMethod.values().first { it.num == num } + // + num = src.readUInt8() + interlaceMethod = InterlaceMethod.values().first { it.num == num } + } + + override fun toString() = + "ApngImageHeader(w=$width,h=$height,bits=$bitDepth,color=$colorType,interlace=$interlaceMethod)" } diff --git a/apng/src/main/java/jp/juggler/apng/ApngPalette.kt b/apng/src/main/java/jp/juggler/apng/ApngPalette.kt index 217d7fb6..b5454436 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngPalette.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngPalette.kt @@ -7,6 +7,7 @@ import jp.juggler.apng.util.getUInt8 class ApngPalette( src : ByteArray // repeat of R,G,B ) { + companion object { // full opaque black const val OPAQUE = 255 shl 24 @@ -18,14 +19,14 @@ class ApngPalette( init { val entryCount = src.size / 3 - list = IntArray( entryCount) + 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 + (src.getUInt8(pos + 1) shl 8) or + src.getUInt8(pos + 2) + pos += 3 } } diff --git a/apng/src/main/java/jp/juggler/apng/ApngParseError.kt b/apng/src/main/java/jp/juggler/apng/ApngParseError.kt new file mode 100644 index 00000000..8bb66bae --- /dev/null +++ b/apng/src/main/java/jp/juggler/apng/ApngParseError.kt @@ -0,0 +1,3 @@ +package jp.juggler.apng + +class ApngParseError(message: String) : IllegalArgumentException(message) diff --git a/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt b/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt index d10f58a2..1f910468 100644 --- a/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt +++ b/apng/src/main/java/jp/juggler/apng/ApngTransparentColor.kt @@ -4,23 +4,24 @@ package jp.juggler.apng import jp.juggler.apng.util.ByteSequence -class ApngTransparentColor internal constructor(isGreyScale:Boolean, src: ByteSequence) { - val red:Int - val green:Int - val blue:Int - init{ - if( isGreyScale){ - val v = src.readUInt16() - red =v - green =v - blue =v - }else{ - red =src.readUInt16() - green =src.readUInt16() - blue =src.readUInt16() - } - } - - fun match(grey:Int) = red == grey - fun match(r:Int,g:Int,b:Int) = (r==red && g == green && b == blue) +class ApngTransparentColor internal constructor(isGreyScale : Boolean, src : ByteSequence) { + val red : Int + val green : Int + val blue : Int + + init { + if(isGreyScale) { + val v = src.readUInt16() + red = v + green = v + blue = v + } else { + red = src.readUInt16() + green = src.readUInt16() + blue = src.readUInt16() + } + } + + fun match(grey : Int) = red == grey + fun match(r : Int, g : Int, b : Int) = (r == red && g == green && b == blue) } \ No newline at end of file diff --git a/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt b/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt index 58b1abd5..a2691069 100644 --- a/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt +++ b/apng/src/main/java/jp/juggler/apng/IdatDecoder.kt @@ -4,7 +4,6 @@ package jp.juggler.apng import jp.juggler.apng.util.* import java.io.InputStream -import java.util.* import java.util.zip.CRC32 import java.util.zip.Inflater @@ -33,7 +32,6 @@ internal class IdatDecoder( private val dummyPaletteData = IntArray(0) - private fun abs(v : Int) = if(v >= 0) v else - v // a = left, b = above, c = upper left @@ -51,8 +49,9 @@ internal class IdatDecoder( private inline fun scanLine1(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { var pos = 1 - var x = 0 - while(pass_w - x >= 8) { + var remain = pass_w + while(remain >= 8) { + remain -= 8 val v = baLine[pos ++].toInt() block((v shr 7) and 1) block((v shr 6) and 1) @@ -62,9 +61,7 @@ internal class IdatDecoder( block((v shr 2) and 1) block((v shr 1) and 1) block(v and 1) - x += 8 } - val remain = pass_w - x if(remain > 0) { val v = baLine[pos].toInt() block((v shr 7) and 1) @@ -79,16 +76,15 @@ internal class IdatDecoder( private inline fun scanLine2(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { var pos = 1 - var x = 0 - while(pass_w - x >= 4) { + var remain = pass_w + while(remain >= 4) { + remain -= 4 val v = baLine[pos ++].toInt() block((v shr 6) and 3) block((v shr 4) and 3) block((v shr 2) and 3) block(v and 3) - x += 4 } - val remain = pass_w - x if(remain > 0) { val v = baLine[pos].toInt() block((v shr 6) and 3) @@ -99,14 +95,13 @@ internal class IdatDecoder( private inline fun scanLine4(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { var pos = 1 - var x = 0 - while(pass_w - x >= 2) { + var remain = pass_w + while(remain >= 2) { + remain -= 2 val v = baLine[pos ++].toInt() block((v shr 4) and 15) block(v and 15) - x += 2 } - val remain = pass_w - x if(remain > 0) { val v = baLine[pos].toInt() block((v shr 4) and 15) @@ -115,14 +110,16 @@ internal class IdatDecoder( private inline fun scanLine8(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block(baLine.getUInt8(pos ++)) } } private inline fun scanLine16(baLine : ByteArray, pass_w : Int, block : (v : Int) -> Unit) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block(baLine.getUInt16(pos)) pos += 2 } @@ -134,7 +131,8 @@ internal class IdatDecoder( block : (r : Int, g : Int, b : Int) -> Unit ) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block( baLine.getUInt8(pos), baLine.getUInt8(pos + 1), @@ -150,7 +148,8 @@ internal class IdatDecoder( block : (r : Int, g : Int, b : Int) -> Unit ) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block( baLine.getUInt16(pos), baLine.getUInt16(pos + 2), @@ -166,7 +165,8 @@ internal class IdatDecoder( block : (r : Int, g : Int, b : Int, a : Int) -> Unit ) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block( baLine.getUInt8(pos), baLine.getUInt8(pos + 1), @@ -183,7 +183,8 @@ internal class IdatDecoder( block : (r : Int, g : Int, b : Int, a : Int) -> Unit ) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block( baLine.getUInt16(pos), baLine.getUInt16(pos + 2), @@ -200,7 +201,8 @@ internal class IdatDecoder( block : (g : Int, a : Int) -> Unit ) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block( baLine.getUInt8(pos), baLine.getUInt8(pos + 1) @@ -215,7 +217,8 @@ internal class IdatDecoder( block : (g : Int, a : Int) -> Unit ) { var pos = 1 - for(x in 0 until pass_w) { + var remain = pass_w + while(remain -- > 0) { block( baLine.getUInt16(pos), baLine.getUInt16(pos + 2) @@ -233,7 +236,7 @@ internal class IdatDecoder( private val sampleBits : Int private val sampleBytes : Int private val scanLineBytesMax : Int - private val linePool = LinkedList() + private val scanLinePool : BufferPool private val transparentCheckerGrey : (v : Int) -> Int private val transparentCheckerRGB : (r : Int, g : Int, b : Int) -> Int private val renderScanLineFunc : (baLine : ByteArray) -> Unit @@ -250,15 +253,8 @@ internal class IdatDecoder( init { val header = requireNotNull(apng.header) - this.colorType = header.colorType - this.bitDepth = header.bitDepth - - this.paletteData = if(colorType == ColorType.INDEX) { - apng.palette?.list - ?: throw ParseError("missing ApngPalette for index color") - } else { - dummyPaletteData - } + colorType = header.colorType + bitDepth = header.bitDepth sampleBits = when(colorType) { ColorType.GREY, ColorType.INDEX -> bitDepth @@ -269,9 +265,14 @@ internal class IdatDecoder( sampleBytes = (sampleBits + 7) / 8 scanLineBytesMax = 1 + (bitmap.width * sampleBits + 7) / 8 + scanLinePool = BufferPool(scanLineBytesMax) - linePool.add(ByteArray(scanLineBytesMax)) - linePool.add(ByteArray(scanLineBytesMax)) + paletteData = if(colorType == ColorType.INDEX) { + apng.palette?.list + ?: throw ApngParseError("missing ApngPalette for index color") + } else { + dummyPaletteData + } val transparentColor = apng.transparentColor @@ -358,7 +359,7 @@ internal class IdatDecoder( private fun renderIndex2(baLine : ByteArray) { scanLine2(baLine, passWidth) { v -> - bitmapPointer.setPixel( paletteData[v] ).next() + bitmapPointer.setPixel(paletteData[v]).next() } } @@ -400,7 +401,7 @@ internal class IdatDecoder( } private fun colorBitsNotSupported() : Nothing { - throw ParseError("bitDepth $bitDepth is not supported for $colorType") + throw ApngParseError("bitDepth $bitDepth is not supported for $colorType") } private fun selectRenderFunc() = when(colorType) { @@ -443,11 +444,11 @@ internal class IdatDecoder( passY = 0 scanLineBytes = 1 + (passWidth * sampleBits + 7) / 8 - baPreviousLine?.let { linePool.add(it) } + scanLinePool.recycle(baPreviousLine) baPreviousLine = null if(passWidth <= 0 || passHeight <= 0) { - if( callback.canApngDebug() ) callback.onApngDebug("pass $pass is empty. size=${passWidth}x${passHeight} ") + if(callback.canApngDebug()) callback.onApngDebug("pass $pass is empty. size=${passWidth}x${passHeight} ") incrementPassOrComplete() } } @@ -464,20 +465,20 @@ internal class IdatDecoder( // スキャンラインを読む。行を処理したらtrueを返す private fun readScanLine() : Boolean { - - if(inflateBufferQueue.remain < scanLineBytes){ + + if(inflateBufferQueue.remain < scanLineBytes) { // not yet enough data to process scanline return false } - val baLine = linePool.removeFirst() + val baLine = scanLinePool.obtain() inflateBufferQueue.readBytes(baLine, 0, scanLineBytes) val filterNum = baLine.getUInt8(0) val filterType = FilterType.values().first { it.num == filterNum } -// if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType") - + // if( callback.canApngDebug() ) callback.onApngDebug("y=$passY/${passHeight},filterType=$filterType") + when(filterType) { FilterType.None -> { } @@ -485,23 +486,22 @@ internal class IdatDecoder( FilterType.Sub -> { for(pos in 1 until scanLineBytes) { val vCur = baLine.getUInt8(pos) - val leftPos = pos -sampleBytes - val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos) - + val leftPos = pos - sampleBytes + val vLeft = if(leftPos <= 0) 0 else baLine.getUInt8(leftPos) baLine[pos] = (vCur + vLeft).toByte() -// if( callback.canApngDebug() ){ -// val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes) -// val y = passInfo.yStart + passInfo.yStep * passY -// callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}") -// } - + // if( callback.canApngDebug() ){ + // val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes) + // val y = passInfo.yStart + passInfo.yStep * passY + // callback.onApngDebug("sub pos=$pos,x=$x,y=$y,left=$vLeft,cur=$vCur,after=${baLine[pos].toInt() and 255}") + // } + } } FilterType.Up -> { - val baPreviousLine=this.baPreviousLine + val baPreviousLine = this.baPreviousLine for(pos in 1 until scanLineBytes) { val vCur = baLine.getUInt8(pos) val vUp = baPreviousLine?.getUInt8(pos) ?: 0 @@ -510,47 +510,47 @@ internal class IdatDecoder( } FilterType.Average -> { - val baPreviousLine=this.baPreviousLine + val baPreviousLine = this.baPreviousLine for(pos in 1 until scanLineBytes) { val vCur = baLine.getUInt8(pos) - val leftPos = pos -sampleBytes - val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos) + val leftPos = pos - sampleBytes + val vLeft = if(leftPos <= 0) 0 else baLine.getUInt8(leftPos) val vUp = baPreviousLine?.getUInt8(pos) ?: 0 baLine[pos] = (vCur + ((vLeft + vUp) shr 1)).toByte() } } FilterType.Paeth -> { - val baPreviousLine=this.baPreviousLine + val baPreviousLine = this.baPreviousLine for(pos in 1 until scanLineBytes) { val vCur = baLine.getUInt8(pos) - val leftPos = pos -sampleBytes - val vLeft = if(leftPos <=0 ) 0 else baLine.getUInt8(leftPos) + val leftPos = pos - sampleBytes + val vLeft = if(leftPos <= 0) 0 else baLine.getUInt8(leftPos) val vUp = baPreviousLine?.getUInt8(pos) ?: 0 - val vUpperLeft = if(leftPos <=0 ) 0 else baPreviousLine?.getUInt8(leftPos) ?: 0 + val vUpperLeft = if(leftPos <= 0) 0 else baPreviousLine?.getUInt8(leftPos) ?: 0 baLine[pos] = (vCur + paeth(vLeft, vUp, vUpperLeft)).toByte() -// if( callback.canApngDebug() ){ -// val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes) -// val y = passInfo.yStart + passInfo.yStep * passY -// callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}") -// } - + // if( callback.canApngDebug() ){ + // val x = passInfo.xStart + passInfo.xStep * ((pos-1)/sampleBytes) + // val y = passInfo.yStart + passInfo.yStep * passY + // callback.onApngDebug("paeth pos=$pos,x=$x,y=$y,left=$vLeft,up=$vUp,ul=$vUpperLeft,cur=$vCur,paeth=${paeth(vLeft, vUp, vUpperLeft)}") + // } + } } } // render scanline bitmapPointer.setXY( - x=passInfo.xStart, - y=passInfo.yStart + passInfo.yStep * passY, - step=passInfo.xStep + x = passInfo.xStart, + y = passInfo.yStart + passInfo.yStep * passY, + step = passInfo.xStep ) renderScanLineFunc(baLine) // save previous line - baPreviousLine?.let { linePool.add(it) } + scanLinePool.recycle(baPreviousLine) baPreviousLine = baLine if(++ passY >= passHeight) { @@ -607,14 +607,14 @@ internal class IdatDecoder( while(! inflater.needsInput()) { val buffer = inflateBufferPool.obtain() val nInflated = inflater.inflate(buffer) - if( nInflated <= 0 ){ + if(nInflated <= 0) { inflateBufferPool.recycle(buffer) - }else{ + } else { inflateBufferQueue.add(ByteSequence(buffer, 0, nInflated)) // キューに追加したデータをScanLine単位で消費する while(! isCompleted && readScanLine()) { } - if(isCompleted){ + if(isCompleted) { inflateBufferQueue.clear() break } diff --git a/apng/src/main/java/jp/juggler/apng/ParseError.kt b/apng/src/main/java/jp/juggler/apng/ParseError.kt deleted file mode 100644 index 7b569c73..00000000 --- a/apng/src/main/java/jp/juggler/apng/ParseError.kt +++ /dev/null @@ -1,3 +0,0 @@ -package jp.juggler.apng - -class ParseError(message: String) : IllegalArgumentException(message) diff --git a/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt b/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt index a358ffd8..88b21052 100644 --- a/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt +++ b/apng/src/main/java/jp/juggler/apng/util/BufferPool.kt @@ -2,14 +2,8 @@ package jp.juggler.apng.util import java.util.* -internal class BufferPool(private val arraySize:Int){ - private val list =LinkedList() - - fun recycle(array: ByteArray) { - list.add( array) - } - - fun obtain(): ByteArray { - return if( list.isEmpty() ) ByteArray(arraySize) else list.removeFirst() - } -} \ No newline at end of file +internal class BufferPool(private val arraySize : Int) { + private val list = LinkedList() + fun obtain() : ByteArray = if(list.isEmpty()) ByteArray(arraySize) else list.removeFirst() + fun recycle(array : ByteArray?) = array?.let { list.add(it) } +} diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt b/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt index 8c1cf9d0..02b6ea64 100644 --- a/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt +++ b/apng/src/main/java/jp/juggler/apng/util/ByteSequence.kt @@ -1,6 +1,6 @@ package jp.juggler.apng.util -import jp.juggler.apng.ParseError +import jp.juggler.apng.ApngParseError internal fun ByteArray.getUInt8(pos : Int) = get(pos).toInt() and 255 @@ -16,10 +16,11 @@ internal class ByteSequence( var offset : Int, var length : Int ) { + constructor(ba : ByteArray) : this(ba, 0, ba.size) private inline fun readX(dataSize : Int, block : () -> T) : T { - if(length < dataSize) throw ParseError("readX: unexpected end") + if(length < dataSize) throw ApngParseError("readX: unexpected end") val v = block() offset += dataSize length -= dataSize diff --git a/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt b/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt index b5b63d6f..3c6bf57c 100644 --- a/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt +++ b/apng/src/main/java/jp/juggler/apng/util/ByteSequenceQueue.kt @@ -2,41 +2,34 @@ package jp.juggler.apng.util import java.util.* -internal class ByteSequenceQueue(private val bufferRecycler :(ByteSequence)->Unit) { - - private val list = LinkedList() - - 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 dstOffset = offset - var dstRemain = length - while (dstRemain > 0 && list.isNotEmpty()) { - val item = list.first() - if (item.length <= 0) { - bufferRecycler(item) - list.removeFirst() - }else { - val delta = Math.min(item.length, dstRemain) - System.arraycopy(item.array, item.offset, dst, dstOffset, delta) - dstOffset += delta - dstRemain -= delta - item.offset += delta - item.length -= delta - } - } - return length - dstRemain - } +internal class ByteSequenceQueue(private val bufferRecycler : (ByteSequence) -> Unit) { + + private val list = LinkedList() + + val remain : Int + get() = list.sumBy { it.length } + + fun add(range : ByteSequence) =list.add(range) + + fun clear() = list.also{ it.forEach(bufferRecycler) }.clear() + + fun readBytes(dst : ByteArray, offset : Int, length : Int) : Int { + var dstOffset = offset + var dstRemain = length + while(dstRemain > 0 && list.isNotEmpty()) { + val item = list.first() + if(item.length <= 0) { + bufferRecycler(item) + list.removeFirst() + } else { + val delta = Math.min(item.length, dstRemain) + System.arraycopy(item.array, item.offset, dst, dstOffset, delta) + dstOffset += delta + dstRemain -= delta + item.offset += delta + item.length -= delta + } + } + return length - dstRemain + } } diff --git a/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt b/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt index c5e45500..dfd9eb5d 100644 --- a/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt +++ b/apng/src/main/java/jp/juggler/apng/util/StreamTokenizer.kt @@ -1,66 +1,66 @@ package jp.juggler.apng.util -import jp.juggler.apng.ParseError +import jp.juggler.apng.ApngParseError import java.io.InputStream import java.util.zip.CRC32 -internal class StreamTokenizer(val inStream: InputStream) { - - fun skipBytes(size: Long) { - var nRead = 0L - while (true) { - val remain = size - nRead - if (remain <= 0) break - val delta = inStream.skip(size - nRead) - if (delta <= 0) throw ParseError("skipBytes: unexpected EoS") - nRead += delta - } - } - - fun readBytes(size: Int): ByteArray { - val dst = ByteArray(size) - var nRead = 0 - while (true) { - val remain = size - nRead - if (remain <= 0) break - val delta = inStream.read(dst, nRead, size - nRead) - if (delta < 0) throw ParseError("readBytes: unexpected EoS") - nRead += delta - } - return dst - } - - private fun readByte(): Int { - val b = inStream.read() - if( b == -1 ) throw ParseError("readBytes: unexpected EoS") - return b and 0xff - } - - fun readInt32(): Int { - val b0 = readByte() - val b1 = readByte() - val b2 = readByte() - val b3 = readByte() - - return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 - } - - fun readInt32(crc32: CRC32): Int { - val ba = readBytes(4) - crc32.update(ba) - val b0 = ba[0].toInt() and 255 - val b1 = ba[1].toInt() and 255 - val b2 = ba[2].toInt() and 255 - val b3 = ba[3].toInt() and 255 - - return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 - } - - fun readUInt32(): Long { - val b0 = readByte() - val b1 = readByte() - val b2 = readByte() - val b3 = readByte() - return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong() - } +internal class StreamTokenizer(val inStream : InputStream) { + + fun skipBytes(size : Long) { + var nRead = 0L + while(true) { + val remain = size - nRead + if(remain <= 0) break + val delta = inStream.skip(size - nRead) + if(delta <= 0) throw ApngParseError("skipBytes: unexpected EoS") + nRead += delta + } + } + + fun readBytes(size : Int) : ByteArray { + val dst = ByteArray(size) + var nRead = 0 + while(true) { + val remain = size - nRead + if(remain <= 0) break + val delta = inStream.read(dst, nRead, size - nRead) + if(delta < 0) throw ApngParseError("readBytes: unexpected EoS") + nRead += delta + } + return dst + } + + private fun readByte() : Int { + val b = inStream.read() + if(b == - 1) throw ApngParseError("readByte: unexpected EoS") + return b and 0xff + } + + fun readInt32() : Int { + val b0 = readByte() + val b1 = readByte() + val b2 = readByte() + val b3 = readByte() + + return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 + } + + fun readInt32(crc32 : CRC32) : Int { + val ba = readBytes(4) + crc32.update(ba) + val b0 = ba[0].toInt() and 255 + val b1 = ba[1].toInt() and 255 + val b2 = ba[2].toInt() and 255 + val b3 = ba[3].toInt() and 255 + + return (b0 shl 24) or (b1 shl 16) or (b2 shl 8) or b3 + } + + fun readUInt32() : Long { + val b0 = readByte() + val b1 = readByte() + val b2 = readByte() + val b3 = readByte() + return (b0.toLong() shl 24) or ((b1 shl 16) or (b2 shl 8) or b3).toLong() + } } \ No newline at end of file diff --git a/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt index 65a0cefa..9198d15d 100644 --- a/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt +++ b/apng_android/src/main/java/jp/juggler/apng/ApngFrames.kt @@ -24,11 +24,9 @@ class ApngFrames private constructor( private const val DELAY_AFTER_END = 3000L // アニメーションフレームの描画に使う - private val sPaintDontBlend : Paint by lazy { - val paint = Paint() - paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC) - paint.isFilterBitmap = true - paint + private val sPaintDontBlend = Paint().apply { + xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC) + isFilterBitmap = true } private fun createBlankBitmap(w : Int, h : Int) = @@ -45,7 +43,7 @@ class ApngFrames private constructor( val wSrc = src.width val hSrc = src.height - if(size_max <= 0 || wSrc <= size_max && hSrc <= size_max) { + if(size_max <= 0 || (wSrc <= size_max && hSrc <= size_max)) { return if(recycleSrc) { src } else { @@ -75,8 +73,8 @@ class ApngFrames private constructor( return b2 } - private fun toAndroidBitmap(src : ApngBitmap) : Bitmap { - return Bitmap.createBitmap( + private fun toAndroidBitmap(src : ApngBitmap) = + Bitmap.createBitmap( src.colors, // int[] 配列 0, // offset src.width, //stride @@ -84,7 +82,6 @@ class ApngFrames private constructor( src.height, //height Bitmap.Config.ARGB_8888 ) - } private fun toAndroidBitmap(src : ApngBitmap, size_max : Int) = scaleBitmap(size_max, toAndroidBitmap(src)) @@ -99,11 +96,8 @@ class ApngFrames private constructor( try { ApngDecoder.parseStream(inStream, result) result.onParseComplete() - return if(result.defaultImage != null || result.frames?.isNotEmpty() == true) { - result - } else { - throw RuntimeException("APNG has no image") - } + return result.takeIf { result.defaultImage != null || result.frames?.isNotEmpty() == true } + ?: throw RuntimeException("APNG has no image") } catch(ex : Throwable) { result.dispose() throw ex