Work on result
This commit is contained in:
parent
4ef644632a
commit
31c7529d45
|
@ -53,7 +53,7 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val inputStream: InputStream? = input
|
private var inputStream: InputStream? = input
|
||||||
private var result: Result<Drawable>? = null
|
private var result: Result<Drawable>? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -65,470 +65,474 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
||||||
*/
|
*/
|
||||||
suspend fun decodeApng(
|
suspend fun decodeApng(
|
||||||
context: Context
|
context: Context
|
||||||
): Drawable = withContext(Dispatchers.Default) {
|
): Result<Drawable> =
|
||||||
val inputStream = BufferedInputStream(inputStream)
|
kotlin.runCatching {
|
||||||
val bytes = ByteArray(8)
|
withContext(Dispatchers.Default) {
|
||||||
inputStream.mark(8)
|
val inputStream = BufferedInputStream(inputStream)
|
||||||
withContext(Dispatchers.IO) {
|
val bytes = ByteArray(8)
|
||||||
inputStream.read(bytes)
|
inputStream.mark(8)
|
||||||
}
|
withContext(Dispatchers.IO) {
|
||||||
|
inputStream.read(bytes)
|
||||||
if (Utils.isPng(bytes)) {
|
|
||||||
var png: ByteArrayOutputStream? = null
|
|
||||||
var cover: ByteArrayOutputStream? = null
|
|
||||||
var delay = -1f
|
|
||||||
var yOffset = -1
|
|
||||||
var xOffset = -1
|
|
||||||
var plte: ByteArray? = null
|
|
||||||
var tnrs: ByteArray? = null
|
|
||||||
var maxWidth = 0
|
|
||||||
var maxHeight = 0
|
|
||||||
var blendOp: Utils.Companion.BlendOp = Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
|
|
||||||
var disposeOp: Utils.Companion.DisposeOp =
|
|
||||||
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
|
|
||||||
|
|
||||||
var ihdrOfApng = ByteArray(0)
|
|
||||||
|
|
||||||
var isApng = false
|
|
||||||
|
|
||||||
val drawable = ApngDrawable().apply {
|
|
||||||
isOneShot = false
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer: Bitmap? = null
|
|
||||||
|
|
||||||
var byteRead: Int
|
|
||||||
val lengthChunk = ByteArray(4)
|
|
||||||
do {
|
|
||||||
val length: Int
|
|
||||||
val chunk: ByteArray
|
|
||||||
if (withContext(Dispatchers.IO) {
|
|
||||||
byteRead = inputStream.read(lengthChunk)
|
|
||||||
|
|
||||||
|
|
||||||
if (byteRead != -1) {
|
|
||||||
length = Utils.uIntFromBytesBigEndian(lengthChunk)
|
|
||||||
|
|
||||||
chunk = ByteArray(length + 8)
|
|
||||||
byteRead = inputStream.read(chunk)
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
chunk = ByteArray(0)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val byteArray = lengthChunk.plus(chunk)
|
if (Utils.isPng(bytes)) {
|
||||||
val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4)
|
var png: ByteArrayOutputStream? = null
|
||||||
val crc = CRC32()
|
var cover: ByteArrayOutputStream? = null
|
||||||
crc.update(byteArray, 4, byteArray.size - 8)
|
var delay = -1f
|
||||||
if (chunkCRC == crc.value.toInt()) {
|
var yOffset = -1
|
||||||
val name = byteArray.copyOfRange(4, 8)
|
var xOffset = -1
|
||||||
when {
|
var plte: ByteArray? = null
|
||||||
name.contentEquals(Utils.fcTL) -> {
|
var tnrs: ByteArray? = null
|
||||||
if (png == null) {
|
var maxWidth = 0
|
||||||
if (config.decodeCoverFrame) {
|
var maxHeight = 0
|
||||||
drawable.coverFrame = cover?.let {
|
var blendOp: Utils.Companion.BlendOp =
|
||||||
it.write(zeroLength)
|
Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE
|
||||||
|
var disposeOp: Utils.Companion.DisposeOp =
|
||||||
|
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_NONE
|
||||||
|
|
||||||
|
var ihdrOfApng = ByteArray(0)
|
||||||
|
|
||||||
|
var isApng = false
|
||||||
|
|
||||||
|
val drawable = ApngDrawable().apply {
|
||||||
|
isOneShot = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer: Bitmap? = null
|
||||||
|
|
||||||
|
var byteRead: Int
|
||||||
|
val lengthChunk = ByteArray(4)
|
||||||
|
do {
|
||||||
|
val length: Int
|
||||||
|
val chunk: ByteArray
|
||||||
|
if (withContext(Dispatchers.IO) {
|
||||||
|
byteRead = inputStream.read(lengthChunk)
|
||||||
|
|
||||||
|
|
||||||
|
if (byteRead != -1) {
|
||||||
|
length = Utils.uIntFromBytesBigEndian(lengthChunk)
|
||||||
|
|
||||||
|
chunk = ByteArray(length + 8)
|
||||||
|
byteRead = inputStream.read(chunk)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
chunk = ByteArray(0)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
val byteArray = lengthChunk.plus(chunk)
|
||||||
|
val chunkCRC = Utils.uIntFromBytesBigEndian(byteArray, byteArray.size - 4)
|
||||||
|
val crc = CRC32()
|
||||||
|
crc.update(byteArray, 4, byteArray.size - 8)
|
||||||
|
if (chunkCRC == crc.value.toInt()) {
|
||||||
|
val name = byteArray.copyOfRange(4, 8)
|
||||||
|
when {
|
||||||
|
name.contentEquals(Utils.fcTL) -> {
|
||||||
|
if (png == null) {
|
||||||
|
if (config.decodeCoverFrame) {
|
||||||
|
drawable.coverFrame = cover?.let {
|
||||||
|
it.write(zeroLength)
|
||||||
|
// Generate crc for IEND
|
||||||
|
val crC32 = CRC32()
|
||||||
|
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
||||||
|
it.write(Utils.IEND)
|
||||||
|
it.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||||
|
|
||||||
|
val pngBytes = it.toByteArray()
|
||||||
|
BitmapFactory.decodeByteArray(
|
||||||
|
pngBytes,
|
||||||
|
0,
|
||||||
|
pngBytes.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cover = null
|
||||||
|
} else {
|
||||||
|
// Add IEND body length : 0
|
||||||
|
png.write(zeroLength)
|
||||||
|
// Add IEND
|
||||||
// Generate crc for IEND
|
// Generate crc for IEND
|
||||||
val crC32 = CRC32()
|
val crC32 = CRC32()
|
||||||
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
||||||
it.write(Utils.IEND)
|
png.write(Utils.IEND)
|
||||||
it.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
png.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||||
|
|
||||||
val pngBytes = it.toByteArray()
|
val btm = Bitmap.createBitmap(
|
||||||
BitmapFactory.decodeByteArray(
|
|
||||||
pngBytes,
|
|
||||||
0,
|
|
||||||
pngBytes.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cover = null
|
|
||||||
} else {
|
|
||||||
// Add IEND body length : 0
|
|
||||||
png.write(zeroLength)
|
|
||||||
// Add IEND
|
|
||||||
// Generate crc for IEND
|
|
||||||
val crC32 = CRC32()
|
|
||||||
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
|
||||||
png.write(Utils.IEND)
|
|
||||||
png.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
|
||||||
|
|
||||||
val btm = Bitmap.createBitmap(
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
|
|
||||||
val pngBytes = png.toByteArray()
|
|
||||||
val decoded = BitmapFactory.decodeByteArray(
|
|
||||||
pngBytes,
|
|
||||||
0,
|
|
||||||
pngBytes.size
|
|
||||||
)
|
|
||||||
val canvas = Canvas(btm)
|
|
||||||
canvas.drawBitmap(buffer!!, 0f, 0f, null)
|
|
||||||
|
|
||||||
if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
|
|
||||||
canvas.drawRect(
|
|
||||||
xOffset.toFloat(),
|
|
||||||
yOffset.toFloat(),
|
|
||||||
xOffset + decoded.width.toFloat(),
|
|
||||||
yOffset + decoded.height.toFloat(),
|
|
||||||
clearPaint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawBitmap(
|
|
||||||
decoded,
|
|
||||||
xOffset.toFloat(),
|
|
||||||
yOffset.toFloat(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
drawable.addFrame(
|
|
||||||
BitmapDrawable(
|
|
||||||
context.resources,
|
|
||||||
if (btm.config != config.bitmapConfig) {
|
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
Log.v(
|
|
||||||
TAG,
|
|
||||||
"Bitmap Config : ${btm.config}, Config : $config"
|
|
||||||
)
|
|
||||||
btm.copy(config.bitmapConfig, btm.isMutable)
|
|
||||||
} else {
|
|
||||||
btm
|
|
||||||
}
|
|
||||||
),
|
|
||||||
(delay / config.speed).toInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
when (disposeOp) {
|
|
||||||
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
|
|
||||||
//Do nothings
|
|
||||||
}
|
|
||||||
// Add current frame to bitmap buffer
|
|
||||||
// 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.
|
|
||||||
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
|
|
||||||
val res = Bitmap.createBitmap(
|
|
||||||
maxWidth,
|
maxWidth,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
Bitmap.Config.ARGB_8888
|
Bitmap.Config.ARGB_8888
|
||||||
)
|
)
|
||||||
val can = Canvas(res)
|
|
||||||
can.drawBitmap(btm, 0f, 0f, null)
|
val pngBytes = png.toByteArray()
|
||||||
can.drawRect(
|
val decoded = BitmapFactory.decodeByteArray(
|
||||||
|
pngBytes,
|
||||||
|
0,
|
||||||
|
pngBytes.size
|
||||||
|
)
|
||||||
|
val canvas = Canvas(btm)
|
||||||
|
canvas.drawBitmap(buffer!!, 0f, 0f, null)
|
||||||
|
|
||||||
|
if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
|
||||||
|
canvas.drawRect(
|
||||||
|
xOffset.toFloat(),
|
||||||
|
yOffset.toFloat(),
|
||||||
|
xOffset + decoded.width.toFloat(),
|
||||||
|
yOffset + decoded.height.toFloat(),
|
||||||
|
clearPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawBitmap(
|
||||||
|
decoded,
|
||||||
xOffset.toFloat(),
|
xOffset.toFloat(),
|
||||||
yOffset.toFloat(),
|
yOffset.toFloat(),
|
||||||
xOffset + decoded.width.toFloat(),
|
null
|
||||||
yOffset + decoded.height.toFloat(),
|
|
||||||
clearPaint
|
|
||||||
)
|
)
|
||||||
buffer = res
|
|
||||||
|
drawable.addFrame(
|
||||||
|
BitmapDrawable(
|
||||||
|
context.resources,
|
||||||
|
if (btm.config != config.bitmapConfig) {
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
Log.v(
|
||||||
|
TAG,
|
||||||
|
"Bitmap Config : ${btm.config}, Config : $config"
|
||||||
|
)
|
||||||
|
btm.copy(config.bitmapConfig, btm.isMutable)
|
||||||
|
} else {
|
||||||
|
btm
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(delay / config.speed).toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
when (disposeOp) {
|
||||||
|
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
|
||||||
|
//Do nothings
|
||||||
|
}
|
||||||
|
// Add current frame to bitmap buffer
|
||||||
|
// 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.
|
||||||
|
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
|
||||||
|
val res = Bitmap.createBitmap(
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val can = Canvas(res)
|
||||||
|
can.drawBitmap(btm, 0f, 0f, null)
|
||||||
|
can.drawRect(
|
||||||
|
xOffset.toFloat(),
|
||||||
|
yOffset.toFloat(),
|
||||||
|
xOffset + decoded.width.toFloat(),
|
||||||
|
yOffset + decoded.height.toFloat(),
|
||||||
|
clearPaint
|
||||||
|
)
|
||||||
|
buffer = res
|
||||||
|
}
|
||||||
|
else -> buffer = btm
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else -> buffer = btm
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
png = ByteArrayOutputStream(4096)
|
||||||
|
|
||||||
png = ByteArrayOutputStream(4096)
|
// Parse Frame ConTroL chunk
|
||||||
|
// Get the width of the png
|
||||||
|
val width = Utils.uIntFromBytesBigEndian(
|
||||||
|
byteArray, 12
|
||||||
|
)
|
||||||
|
// Get the height of the png
|
||||||
|
val height = Utils.uIntFromBytesBigEndian(
|
||||||
|
byteArray, 16
|
||||||
|
)
|
||||||
|
|
||||||
// Parse Frame ConTroL chunk
|
/*
|
||||||
// Get the width of the png
|
|
||||||
val width = Utils.uIntFromBytesBigEndian(
|
|
||||||
byteArray, 12
|
|
||||||
)
|
|
||||||
// Get the height of the png
|
|
||||||
val height = Utils.uIntFromBytesBigEndian(
|
|
||||||
byteArray, 16
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The `delay_num` and `delay_den` parameters together specify a fraction indicating the time to display the current frame, in seconds.
|
* The `delay_num` and `delay_den` parameters together specify a fraction indicating the time to display the current frame, in seconds.
|
||||||
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound.
|
* If the the value of the numerator is 0 the decoder should render the next frame as quickly as possible, though viewers may impose a reasonable lower bound.
|
||||||
*/
|
*/
|
||||||
// Get delay numerator
|
// Get delay numerator
|
||||||
val delayNum = Utils.uShortFromBytesBigEndian(
|
val delayNum = Utils.uShortFromBytesBigEndian(
|
||||||
byteArray, 28
|
byteArray, 28
|
||||||
).toFloat()
|
).toFloat()
|
||||||
// Get delay denominator
|
// Get delay denominator
|
||||||
var delayDen = Utils.uShortFromBytesBigEndian(
|
var delayDen = Utils.uShortFromBytesBigEndian(
|
||||||
byteArray, 30
|
byteArray, 30
|
||||||
).toFloat()
|
).toFloat()
|
||||||
|
|
||||||
// If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second).
|
// If the denominator is 0, it is to be treated as if it were 100 (that is, `delay_num` then specifies 1/100ths of a second).
|
||||||
if (delayDen == 0f) {
|
if (delayDen == 0f) {
|
||||||
delayDen = 100f
|
delayDen = 100f
|
||||||
}
|
|
||||||
|
|
||||||
delay = (delayNum / delayDen * 1000)
|
|
||||||
|
|
||||||
// Get x and y offsets
|
|
||||||
xOffset = Utils.uIntFromBytesBigEndian(
|
|
||||||
byteArray, 20
|
|
||||||
)
|
|
||||||
yOffset = Utils.uIntFromBytesBigEndian(
|
|
||||||
byteArray, 24
|
|
||||||
)
|
|
||||||
blendOp = Utils.decodeBlendOp(byteArray[33].toInt())
|
|
||||||
disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt())
|
|
||||||
|
|
||||||
if (xOffset + width > maxWidth) {
|
|
||||||
throw BadApngException("`xOffset` + `width` must be <= `IHDR` width")
|
|
||||||
} else if (yOffset + height > maxHeight) {
|
|
||||||
throw BadApngException("`yOffset` + `height` must be <= `IHDR` height")
|
|
||||||
}
|
|
||||||
|
|
||||||
png.write(Utils.pngSignature)
|
|
||||||
png.write(
|
|
||||||
generateIhdr(
|
|
||||||
ihdrOfApng,
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
)
|
|
||||||
)
|
|
||||||
plte?.let {
|
|
||||||
png.write(it)
|
|
||||||
}
|
|
||||||
tnrs?.let {
|
|
||||||
png.write(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
name.contentEquals(Utils.IEND) -> {
|
|
||||||
if (isApng && png != null) {
|
|
||||||
png.write(zeroLength)
|
|
||||||
// Add IEND
|
|
||||||
// Generate crc for IEND
|
|
||||||
val crC32 = CRC32()
|
|
||||||
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
|
||||||
png.write(Utils.IEND)
|
|
||||||
png.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
|
||||||
|
|
||||||
val btm = Bitmap.createBitmap(
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
|
|
||||||
val pngBytes = png.toByteArray()
|
|
||||||
val decoded = BitmapFactory.decodeByteArray(
|
|
||||||
pngBytes,
|
|
||||||
0,
|
|
||||||
pngBytes.size
|
|
||||||
)
|
|
||||||
val canvas = Canvas(btm)
|
|
||||||
canvas.drawBitmap(buffer!!, 0f, 0f, null)
|
|
||||||
|
|
||||||
if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
|
|
||||||
canvas.drawRect(
|
|
||||||
xOffset.toFloat(),
|
|
||||||
yOffset.toFloat(),
|
|
||||||
xOffset + decoded.width.toFloat(),
|
|
||||||
yOffset + decoded.height.toFloat(),
|
|
||||||
clearPaint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawBitmap(
|
|
||||||
decoded,
|
|
||||||
xOffset.toFloat(),
|
|
||||||
yOffset.toFloat(),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
drawable.addFrame(
|
|
||||||
BitmapDrawable(
|
|
||||||
context.resources,
|
|
||||||
if (btm.config != config.bitmapConfig) {
|
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
Log.v(
|
|
||||||
TAG,
|
|
||||||
"Bitmap Config : ${btm.config}, Config : $config"
|
|
||||||
)
|
|
||||||
btm.copy(config.bitmapConfig, btm.isMutable)
|
|
||||||
} else {
|
|
||||||
btm
|
|
||||||
}
|
|
||||||
),
|
|
||||||
(delay / config.speed).toInt()
|
|
||||||
)
|
|
||||||
|
|
||||||
when (disposeOp) {
|
|
||||||
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
|
|
||||||
//Do nothings
|
|
||||||
}
|
}
|
||||||
// Add current frame to bitmap buffer
|
|
||||||
// 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.
|
delay = (delayNum / delayDen * 1000)
|
||||||
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
|
|
||||||
val res = Bitmap.createBitmap(
|
// Get x and y offsets
|
||||||
|
xOffset = Utils.uIntFromBytesBigEndian(
|
||||||
|
byteArray, 20
|
||||||
|
)
|
||||||
|
yOffset = Utils.uIntFromBytesBigEndian(
|
||||||
|
byteArray, 24
|
||||||
|
)
|
||||||
|
blendOp = Utils.decodeBlendOp(byteArray[33].toInt())
|
||||||
|
disposeOp = Utils.decodeDisposeOp(byteArray[32].toInt())
|
||||||
|
|
||||||
|
if (xOffset + width > maxWidth) {
|
||||||
|
throw BadApngException("`xOffset` + `width` must be <= `IHDR` width")
|
||||||
|
} else if (yOffset + height > maxHeight) {
|
||||||
|
throw BadApngException("`yOffset` + `height` must be <= `IHDR` height")
|
||||||
|
}
|
||||||
|
|
||||||
|
png.write(Utils.pngSignature)
|
||||||
|
png.write(
|
||||||
|
generateIhdr(
|
||||||
|
ihdrOfApng,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
)
|
||||||
|
)
|
||||||
|
plte?.let {
|
||||||
|
png.write(it)
|
||||||
|
}
|
||||||
|
tnrs?.let {
|
||||||
|
png.write(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
name.contentEquals(Utils.IEND) -> {
|
||||||
|
if (isApng && png != null) {
|
||||||
|
png.write(zeroLength)
|
||||||
|
// Add IEND
|
||||||
|
// Generate crc for IEND
|
||||||
|
val crC32 = CRC32()
|
||||||
|
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
||||||
|
png.write(Utils.IEND)
|
||||||
|
png.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||||
|
|
||||||
|
val btm = Bitmap.createBitmap(
|
||||||
maxWidth,
|
maxWidth,
|
||||||
maxHeight,
|
maxHeight,
|
||||||
Bitmap.Config.ARGB_8888
|
Bitmap.Config.ARGB_8888
|
||||||
)
|
)
|
||||||
val can = Canvas(res)
|
|
||||||
can.drawBitmap(btm, 0f, 0f, null)
|
|
||||||
can.drawRect(
|
|
||||||
xOffset.toFloat(),
|
|
||||||
yOffset.toFloat(),
|
|
||||||
xOffset + decoded.width.toFloat(),
|
|
||||||
yOffset + decoded.height.toFloat(),
|
|
||||||
clearPaint
|
|
||||||
)
|
|
||||||
buffer = res
|
|
||||||
}
|
|
||||||
else -> buffer = btm
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cover?.let {
|
|
||||||
it.write(zeroLength)
|
|
||||||
// Add IEND
|
|
||||||
// Generate crc for IEND
|
|
||||||
val crC32 = CRC32()
|
|
||||||
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
|
||||||
it.write(Utils.IEND)
|
|
||||||
it.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
inputStream.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
val pngBytes = it.toByteArray()
|
val pngBytes = png.toByteArray()
|
||||||
return@withContext BitmapDrawable(
|
val decoded = BitmapFactory.decodeByteArray(
|
||||||
context.resources,
|
|
||||||
BitmapFactory.decodeByteArray(
|
|
||||||
pngBytes,
|
pngBytes,
|
||||||
0,
|
0,
|
||||||
pngBytes.size
|
pngBytes.size
|
||||||
)
|
)
|
||||||
)
|
val canvas = Canvas(btm)
|
||||||
}
|
canvas.drawBitmap(buffer!!, 0f, 0f, null)
|
||||||
}
|
|
||||||
}
|
if (blendOp == Utils.Companion.BlendOp.APNG_BLEND_OP_SOURCE) {
|
||||||
name.contentEquals(Utils.IDAT) -> {
|
canvas.drawRect(
|
||||||
val w = if (png == null) {
|
xOffset.toFloat(),
|
||||||
if (isApng && !config.decodeCoverFrame) {
|
yOffset.toFloat(),
|
||||||
if (BuildConfig.DEBUG)
|
xOffset + decoded.width.toFloat(),
|
||||||
Log.d(TAG, "Ignoring cover frame")
|
yOffset + decoded.height.toFloat(),
|
||||||
continue
|
clearPaint
|
||||||
}
|
)
|
||||||
if (cover == null) {
|
}
|
||||||
cover = ByteArrayOutputStream()
|
|
||||||
cover.write(Utils.pngSignature)
|
canvas.drawBitmap(
|
||||||
cover.write(
|
decoded,
|
||||||
generateIhdr(
|
xOffset.toFloat(),
|
||||||
ihdrOfApng,
|
yOffset.toFloat(),
|
||||||
maxWidth,
|
null
|
||||||
maxHeight
|
|
||||||
)
|
)
|
||||||
|
drawable.addFrame(
|
||||||
|
BitmapDrawable(
|
||||||
|
context.resources,
|
||||||
|
if (btm.config != config.bitmapConfig) {
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
Log.v(
|
||||||
|
TAG,
|
||||||
|
"Bitmap Config : ${btm.config}, Config : $config"
|
||||||
|
)
|
||||||
|
btm.copy(config.bitmapConfig, btm.isMutable)
|
||||||
|
} else {
|
||||||
|
btm
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(delay / config.speed).toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
when (disposeOp) {
|
||||||
|
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_PREVIOUS -> {
|
||||||
|
//Do nothings
|
||||||
|
}
|
||||||
|
// Add current frame to bitmap buffer
|
||||||
|
// 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.
|
||||||
|
Utils.Companion.DisposeOp.APNG_DISPOSE_OP_BACKGROUND -> {
|
||||||
|
val res = Bitmap.createBitmap(
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val can = Canvas(res)
|
||||||
|
can.drawBitmap(btm, 0f, 0f, null)
|
||||||
|
can.drawRect(
|
||||||
|
xOffset.toFloat(),
|
||||||
|
yOffset.toFloat(),
|
||||||
|
xOffset + decoded.width.toFloat(),
|
||||||
|
yOffset + decoded.height.toFloat(),
|
||||||
|
clearPaint
|
||||||
|
)
|
||||||
|
buffer = res
|
||||||
|
}
|
||||||
|
else -> buffer = btm
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cover?.let {
|
||||||
|
it.write(zeroLength)
|
||||||
|
// Add IEND
|
||||||
|
// Generate crc for IEND
|
||||||
|
val crC32 = CRC32()
|
||||||
|
crC32.update(Utils.IEND, 0, Utils.IEND.size)
|
||||||
|
it.write(Utils.IEND)
|
||||||
|
it.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
inputStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
val pngBytes = it.toByteArray()
|
||||||
|
return@withContext BitmapDrawable(
|
||||||
|
context.resources,
|
||||||
|
BitmapFactory.decodeByteArray(
|
||||||
|
pngBytes,
|
||||||
|
0,
|
||||||
|
pngBytes.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name.contentEquals(Utils.IDAT) -> {
|
||||||
|
val w = if (png == null) {
|
||||||
|
if (isApng && !config.decodeCoverFrame) {
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
Log.d(TAG, "Ignoring cover frame")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (cover == null) {
|
||||||
|
cover = ByteArrayOutputStream()
|
||||||
|
cover.write(Utils.pngSignature)
|
||||||
|
cover.write(
|
||||||
|
generateIhdr(
|
||||||
|
ihdrOfApng,
|
||||||
|
maxWidth,
|
||||||
|
maxHeight
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cover
|
||||||
|
} else {
|
||||||
|
png
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the chunk length
|
||||||
|
val bodySize =
|
||||||
|
Utils.uIntFromBytesBigEndian(
|
||||||
|
byteArray, 0
|
||||||
|
)
|
||||||
|
w.write(byteArray.copyOfRange(0, 4))
|
||||||
|
|
||||||
|
val body = ByteArray(4 + bodySize)
|
||||||
|
|
||||||
|
System.arraycopy(Utils.IDAT, 0, body, 0, 4)
|
||||||
|
|
||||||
|
// Get image bytes
|
||||||
|
System.arraycopy(byteArray, 8, body, 4, bodySize)
|
||||||
|
|
||||||
|
val crC32 = CRC32()
|
||||||
|
crC32.update(body, 0, body.size)
|
||||||
|
w.write(body)
|
||||||
|
w.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||||
|
}
|
||||||
|
name.contentEquals(Utils.fdAT) -> {
|
||||||
|
// Find the chunk length
|
||||||
|
val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
|
||||||
|
png?.write(Utils.uIntToByteArray(bodySize - 4))
|
||||||
|
|
||||||
|
val body = ByteArray(bodySize)
|
||||||
|
System.arraycopy(Utils.IDAT, 0, body, 0, 4)
|
||||||
|
|
||||||
|
// Get image bytes
|
||||||
|
System.arraycopy(byteArray, 12, body, 4, bodySize - 4)
|
||||||
|
|
||||||
|
val crC32 = CRC32()
|
||||||
|
crC32.update(body, 0, body.size)
|
||||||
|
png?.write(body)
|
||||||
|
png?.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
||||||
|
}
|
||||||
|
name.contentEquals(Utils.plte) -> {
|
||||||
|
plte = byteArray
|
||||||
|
}
|
||||||
|
name.contentEquals(Utils.tnrs) -> {
|
||||||
|
tnrs = byteArray
|
||||||
|
}
|
||||||
|
name.contentEquals(Utils.IHDR) -> {
|
||||||
|
// Get length of the body of the chunk
|
||||||
|
val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
|
||||||
|
// Get the width of the png
|
||||||
|
maxWidth = Utils.uIntFromBytesBigEndian(byteArray, 8)
|
||||||
|
// Get the height of the png
|
||||||
|
maxHeight = Utils.uIntFromBytesBigEndian(byteArray, 12)
|
||||||
|
ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4)
|
||||||
|
|
||||||
|
buffer = Bitmap.createBitmap(
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
cover
|
name.contentEquals(Utils.acTL) -> { // TODO GET NBR REPETITIONS
|
||||||
} else {
|
isApng = true
|
||||||
png
|
}
|
||||||
}
|
}
|
||||||
|
} else throw BadCRCException()
|
||||||
// Find the chunk length
|
} while (byteRead != -1 && isActive)
|
||||||
val bodySize =
|
withContext(Dispatchers.IO) {
|
||||||
Utils.uIntFromBytesBigEndian(
|
inputStream.close()
|
||||||
byteArray, 0
|
|
||||||
)
|
|
||||||
w.write(byteArray.copyOfRange(0, 4))
|
|
||||||
|
|
||||||
val body = ByteArray(4 + bodySize)
|
|
||||||
|
|
||||||
System.arraycopy(Utils.IDAT, 0, body, 0, 4)
|
|
||||||
|
|
||||||
// Get image bytes
|
|
||||||
System.arraycopy(byteArray, 8, body, 4, bodySize)
|
|
||||||
|
|
||||||
val crC32 = CRC32()
|
|
||||||
crC32.update(body, 0, body.size)
|
|
||||||
w.write(body)
|
|
||||||
w.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
|
||||||
}
|
|
||||||
name.contentEquals(Utils.fdAT) -> {
|
|
||||||
// Find the chunk length
|
|
||||||
val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
|
|
||||||
png?.write(Utils.uIntToByteArray(bodySize - 4))
|
|
||||||
|
|
||||||
val body = ByteArray(bodySize)
|
|
||||||
System.arraycopy(Utils.IDAT, 0, body, 0, 4)
|
|
||||||
|
|
||||||
// Get image bytes
|
|
||||||
System.arraycopy(byteArray, 12, body, 4, bodySize - 4)
|
|
||||||
|
|
||||||
val crC32 = CRC32()
|
|
||||||
crC32.update(body, 0, body.size)
|
|
||||||
png?.write(body)
|
|
||||||
png?.write(Utils.uIntToByteArray(crC32.value.toInt()))
|
|
||||||
}
|
|
||||||
name.contentEquals(Utils.plte) -> {
|
|
||||||
plte = byteArray
|
|
||||||
}
|
|
||||||
name.contentEquals(Utils.tnrs) -> {
|
|
||||||
tnrs = byteArray
|
|
||||||
}
|
|
||||||
name.contentEquals(Utils.IHDR) -> {
|
|
||||||
// Get length of the body of the chunk
|
|
||||||
val bodySize = Utils.uIntFromBytesBigEndian(byteArray, 0)
|
|
||||||
// Get the width of the png
|
|
||||||
maxWidth = Utils.uIntFromBytesBigEndian(byteArray, 8)
|
|
||||||
// Get the height of the png
|
|
||||||
maxHeight = Utils.uIntFromBytesBigEndian(byteArray, 12)
|
|
||||||
ihdrOfApng = byteArray.copyOfRange(4 + 4, 4 + bodySize + 4)
|
|
||||||
|
|
||||||
buffer = Bitmap.createBitmap(
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
Bitmap.Config.ARGB_8888
|
|
||||||
)
|
|
||||||
}
|
|
||||||
name.contentEquals(Utils.acTL) -> { // TODO GET NBR REPETITIONS
|
|
||||||
isApng = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else throw BadCRCException()
|
drawable
|
||||||
} while (byteRead != -1 && isActive)
|
} else {
|
||||||
withContext(Dispatchers.IO) {
|
if (BuildConfig.DEBUG)
|
||||||
inputStream.close()
|
Log.i(TAG, "Decoding non APNG stream")
|
||||||
}
|
inputStream.reset()
|
||||||
return@withContext drawable
|
|
||||||
} else {
|
|
||||||
if (BuildConfig.DEBUG)
|
|
||||||
Log.i(TAG, "Decoding non APNG stream")
|
|
||||||
inputStream.reset()
|
|
||||||
|
|
||||||
return@withContext if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
val bytesRead: ByteArray
|
val bytesRead: ByteArray
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
bytesRead = inputStream.readBytes()
|
bytesRead = inputStream.readBytes()
|
||||||
inputStream.close()
|
inputStream.close()
|
||||||
|
}
|
||||||
|
val buf = ByteBuffer.wrap(bytesRead)
|
||||||
|
val source = ImageDecoder.createSource(buf)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ImageDecoder.decodeDrawable(source)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val drawable = Drawable.createFromStream(
|
||||||
|
inputStream,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
inputStream.close()
|
||||||
|
}
|
||||||
|
drawable!!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val buf = ByteBuffer.wrap(bytesRead)
|
|
||||||
val source = ImageDecoder.createSource(buf)
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
ImageDecoder.decodeDrawable(source)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val drawable = Drawable.createFromStream(
|
|
||||||
inputStream,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
inputStream.close()
|
|
||||||
}
|
|
||||||
drawable
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getDecoded(context: Context): Result<Drawable> {
|
suspend fun getDecoded(context: Context): Result<Drawable> {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = kotlin.runCatching {
|
result =
|
||||||
decodeApng(context)
|
decodeApng(context)
|
||||||
}
|
|
||||||
|
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
@ -537,6 +541,8 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
return Result.failure(it)
|
return Result.failure(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inputStream = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return result ?: Result.failure(NullPointerException("result is null"))
|
return result ?: Result.failure(NullPointerException("result is null"))
|
||||||
|
@ -639,17 +645,19 @@ class ApngDecoder(input: InputStream, val config: Config) {
|
||||||
* @param config Decoder configuration
|
* @param config Decoder configuration
|
||||||
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused", "BlockingMethodInNonBlockingContext")
|
@Suppress("unused")
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
suspend fun constructFromUrl(
|
suspend fun constructFromUrl(
|
||||||
url: URL,
|
url: URL,
|
||||||
config: Config = Config()
|
config: Config = Config()
|
||||||
) =
|
): Result<ApngDecoder> =
|
||||||
withContext(Dispatchers.IO) {
|
kotlin.runCatching {
|
||||||
ApngDecoder(
|
withContext(Dispatchers.IO) {
|
||||||
ByteArrayInputStream(Loader.load(url)),
|
ApngDecoder(
|
||||||
config
|
ByteArrayInputStream(Loader.load(url)),
|
||||||
)
|
config
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -153,7 +153,9 @@ class ApngLoader(parent: Job? = null) {
|
||||||
imageView: ImageView,
|
imageView: ImageView,
|
||||||
config: ApngDecoder.Config = ApngDecoder.Config()
|
config: ApngDecoder.Config = ApngDecoder.Config()
|
||||||
): Result<Drawable> {
|
): Result<Drawable> {
|
||||||
val result = ApngDecoder.constructFromUrl(url, config).getDecoded(context)
|
val result =
|
||||||
|
ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) }
|
||||||
|
.getDecoded(context)
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val drawable = result.getOrNull()
|
val drawable = result.getOrNull()
|
||||||
|
@ -204,10 +206,10 @@ class ApngLoader(parent: Job? = null) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
context.assets.open(string.replace("file:///android_asset/", ""))
|
context.assets.open(string.replace("file:///android_asset/", ""))
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.getOrElse {
|
||||||
return Result.failure(it)
|
return Result.failure(it)
|
||||||
}
|
}
|
||||||
val result = ApngDecoder(inputStream.getOrThrow(), config).getDecoded(context)
|
val result = ApngDecoder(inputStream, config).getDecoded(context)
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val drawable = result.getOrNull()
|
val drawable = result.getOrNull()
|
||||||
|
@ -246,11 +248,9 @@ class ApngLoader(parent: Job? = null) {
|
||||||
coroutineScope.launch(Dispatchers.Default) {
|
coroutineScope.launch(Dispatchers.Default) {
|
||||||
val drawable = decodeApngInto(context, file, imageView, config)
|
val drawable = decodeApngInto(context, file, imageView, config)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (drawable.isSuccess) {
|
drawable
|
||||||
callback?.onSuccess(drawable.getOrNull()!!)
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
} else {
|
.onFailure { callback?.onError(it) }
|
||||||
callback?.onError(drawable.exceptionOrNull()!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,11 +274,9 @@ class ApngLoader(parent: Job? = null) {
|
||||||
) = coroutineScope.launch(Dispatchers.Default) {
|
) = coroutineScope.launch(Dispatchers.Default) {
|
||||||
val drawable = decodeApngInto(context, uri, imageView, config)
|
val drawable = decodeApngInto(context, uri, imageView, config)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (drawable.isSuccess) {
|
drawable
|
||||||
callback?.onSuccess(drawable.getOrNull()!!)
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
} else {
|
.onFailure { callback?.onError(it) }
|
||||||
callback?.onError(drawable.exceptionOrNull()!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,11 +298,9 @@ class ApngLoader(parent: Job? = null) {
|
||||||
) = coroutineScope.launch(Dispatchers.Default) {
|
) = coroutineScope.launch(Dispatchers.Default) {
|
||||||
val drawable = decodeApngInto(context, res, imageView, config)
|
val drawable = decodeApngInto(context, res, imageView, config)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (drawable.isSuccess) {
|
drawable
|
||||||
callback?.onSuccess(drawable.getOrNull()!!)
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
} else {
|
.onFailure { callback?.onError(it) }
|
||||||
callback?.onError(drawable.exceptionOrNull()!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,14 +323,13 @@ class ApngLoader(parent: Job? = null) {
|
||||||
) = coroutineScope.launch(Dispatchers.Default) {
|
) = coroutineScope.launch(Dispatchers.Default) {
|
||||||
val drawable = decodeApngInto(context, url, imageView, config)
|
val drawable = decodeApngInto(context, url, imageView, config)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (drawable.isSuccess) {
|
drawable
|
||||||
callback?.onSuccess(drawable.getOrNull()!!)
|
.onSuccess { callback?.onSuccess(it) }
|
||||||
} else {
|
.onFailure { callback?.onError(it) }
|
||||||
callback?.onError(drawable.exceptionOrNull()!!)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load Apng into an imageView, asynchronously.
|
* Load Apng into an imageView, asynchronously.
|
||||||
* @param context Context needed for decoding the image and creating the animation drawable.
|
* @param context Context needed for decoding the image and creating the animation drawable.
|
||||||
|
|
Loading…
Reference in New Issue