This commit is contained in:
Oupson 2021-03-04 12:58:38 +01:00
parent a086e06811
commit 7d7b02f2b9
1 changed files with 555 additions and 547 deletions

View File

@ -13,6 +13,7 @@ import android.widget.ImageView
import androidx.annotation.RawRes
import kotlinx.coroutines.*
import oupson.apng.BuildConfig
import oupson.apng.decoder.ApngDecoder.Companion.decodeApng
import oupson.apng.drawable.ApngDrawable
import oupson.apng.exceptions.BadApngException
import oupson.apng.exceptions.BadCRCException
@ -48,20 +49,22 @@ class ApngDecoder {
internal var bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888,
internal var decodeCoverFrame: Boolean = false
) {
fun getSpeed(): Float = this.speed
fun setSpeed(speed: Float): Config {
fun getSpeed() : Float = this.speed
fun setSpeed(speed : Float) : Config {
this.speed = speed
return this
}
fun getBitmapConfig(): Bitmap.Config = this.bitmapConfig
fun setBitmapConfig(config: Bitmap.Config): Config {
fun getBitmapConfig() : Bitmap.Config = this.bitmapConfig
fun setBitmapConfig(config : Bitmap.Config) : Config {
this.bitmapConfig = config
return this
}
fun isDecodingCoverFrame(): Boolean = this.decodeCoverFrame
fun setIsDecodingCoverFrame(decodeCoverFrame: Boolean): Config {
fun isDecodingCoverFrame() : Boolean {
return this.decodeCoverFrame
}
fun setIsDecodingCoverFrame(decodeCoverFrame : Boolean) : Config {
this.decodeCoverFrame = decodeCoverFrame
return this
}
@ -80,289 +83,6 @@ class ApngDecoder {
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for animation drawable.
* @param file File to decode.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
file: File,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope: CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
val drawable =
ApngDecoder().decodeApng(
context,
FileInputStream(file),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for animation drawable and content resolver.
* @param uri Uri to load.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
uri: Uri,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope: CoroutineScope = GlobalScope
) {
val inputStream = context.contentResolver.openInputStream(uri)!!
scope.launch(Dispatchers.IO) {
try {
val drawable =
ApngDecoder().decodeApng(
context,
inputStream,
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed to decode the resource and for the animation drawable.
* @param res Raw resource to load.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context, @RawRes res: Int,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope: CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
val drawable =
ApngDecoder().decodeApng(
context,
context.resources.openRawResource(res),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for the animation drawable.
* @param url URL to load.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
url: URL,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope: CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
val drawable = ApngDecoder().decodeApng(
context,
ByteArrayInputStream(
Loader.load(
url
)
),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for decoding the image and creating the animation drawable.
* @param string URL to load
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
string: String,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope: CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
if (string.startsWith("http://") || string.startsWith("https://")) {
decodeApngAsyncInto(
context,
URL(string),
imageView,
callback,
config
)
} else if (File(string).exists()) {
var pathToLoad =
if (string.startsWith("content://")) string else "file://$string"
pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
decodeApngAsyncInto(
context,
Uri.parse(pathToLoad),
imageView,
callback,
config
)
} else if (string.startsWith("file://android_asset/")) {
val drawable =
ApngDecoder().decodeApng(
context,
context.assets.open(string.replace("file:///android_asset/", "")),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} else {
withContext(Dispatchers.Main) {
callback?.onError(java.lang.Exception("Cannot open string"))
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Generate a correct IHDR from the IHDR chunk of the APNG.
* @param ihdrOfApng The IHDR of the APNG.
* @param width The width of the frame.
* @param height The height of the frame.
* @return [ByteArray] The generated IHDR.
*/
private fun generateIhdr(ihdrOfApng: ByteArray, width: Int, height: Int): ByteArray {
val ihdr =
ByteArray(0xD + 4 + 4 + 4) // 0xD (IHDR body length) + 4 (0x0, 0x0, 0x0, 0xD : the chunk length) + 4 : IHDR + 4 : CRC
// Add chunk body length
System.arraycopy(Utils.uIntToByteArray(0xD), 0, ihdr, 0, 4)
// We need a body var to know body length and generate crc
val ihdrBody = ByteArray(0xD + 4) // 0xD (IHDR body length) + 4 : IHDR
// Add IHDR
System.arraycopy(Utils.IHDR, 0, ihdrBody, 0, 4)
// Add the max width and height
System.arraycopy(Utils.uIntToByteArray(width), 0, ihdrBody, 4, 4)
System.arraycopy(Utils.uIntToByteArray(height), 0, ihdrBody, 8, 4)
// Add complicated stuff like depth color ...
// If you want correct png you need same parameters.
System.arraycopy(ihdrOfApng, 8, ihdrBody, 12, 5)
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdrBody, 0, 0xD + 4)
System.arraycopy(ihdrBody, 0, ihdr, 4, 0xD + 4)
System.arraycopy(Utils.uIntToByteArray(crC32.value.toInt()), 0, ihdr, 0xD + 4 + 4, 4)
return ihdr
}
}
/**
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
* @param context Context needed for the animation drawable
@ -372,6 +92,7 @@ class ApngDecoder {
*/
// TODO DOCUMENT CONFIG
@Suppress("MemberVisibilityCanBePrivate")
@JvmStatic
@JvmOverloads
fun decodeApng(
context: Context,
@ -821,6 +542,7 @@ class ApngDecoder {
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif. If it is not an animated image, it is a [Drawable].
*/
@Suppress("unused")
@JvmStatic
// TODO DOCUMENT
fun decodeApng(
context: Context,
@ -840,6 +562,7 @@ class ApngDecoder {
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/
@Suppress("unused")
@JvmStatic
fun decodeApng(
context: Context,
uri: Uri,
@ -861,6 +584,7 @@ class ApngDecoder {
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/
@Suppress("unused")
@JvmStatic
fun decodeApng(
context: Context,
@RawRes res: Int,
@ -880,6 +604,7 @@ class ApngDecoder {
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/
@Suppress("unused", "BlockingMethodInNonBlockingContext")
@JvmStatic
suspend fun decodeApng(
context: Context,
url: URL,
@ -892,4 +617,287 @@ class ApngDecoder {
config
)
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for animation drawable.
* @param file File to decode.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
file: File,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope : CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
val drawable =
decodeApng(
context,
FileInputStream(file),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for animation drawable and content resolver.
* @param uri Uri to load.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
uri: Uri,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope : CoroutineScope = GlobalScope
) {
val inputStream = context.contentResolver.openInputStream(uri)!!
scope.launch(Dispatchers.IO) {
try {
val drawable =
decodeApng(
context,
inputStream,
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed to decode the resource and for the animation drawable.
* @param res Raw resource to load.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context, @RawRes res: Int,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope : CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
val drawable =
decodeApng(
context,
context.resources.openRawResource(res),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for the animation drawable.
* @param url URL to load.
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
url: URL,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope : CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
val drawable = decodeApng(
context,
ByteArrayInputStream(
Loader.load(
url
)
),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for decoding the image and creating the animation drawable.
* @param string URL to load
* @param imageView Image View.
* @param callback [ApngDecoder.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
string: String,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope : CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.IO) {
try {
if (string.startsWith("http://") || string.startsWith("https://")) {
decodeApngAsyncInto(
context,
URL(string),
imageView,
callback,
config
)
} else if (File(string).exists()) {
var pathToLoad =
if (string.startsWith("content://")) string else "file://$string"
pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
decodeApngAsyncInto(
context,
Uri.parse(pathToLoad),
imageView,
callback,
config
)
} else if (string.startsWith("file://android_asset/")) {
val drawable =
decodeApng(
context,
context.assets.open(string.replace("file:///android_asset/", "")),
config
)
withContext(Dispatchers.Main) {
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
callback?.onSuccess(drawable)
}
} else {
withContext(Dispatchers.Main) {
callback?.onError(java.lang.Exception("Cannot open string"))
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Generate a correct IHDR from the IHDR chunk of the APNG.
* @param ihdrOfApng The IHDR of the APNG.
* @param width The width of the frame.
* @param height The height of the frame.
* @return [ByteArray] The generated IHDR.
*/
private fun generateIhdr(ihdrOfApng: ByteArray, width: Int, height: Int): ByteArray {
val ihdr =
ByteArray(0xD + 4 + 4 + 4) // 0xD (IHDR body length) + 4 (0x0, 0x0, 0x0, 0xD : the chunk length) + 4 : IHDR + 4 : CRC
// Add chunk body length
System.arraycopy(Utils.uIntToByteArray(0xD), 0, ihdr, 0, 4)
// We need a body var to know body length and generate crc
val ihdrBody = ByteArray(0xD + 4) // 0xD (IHDR body length) + 4 : IHDR
// Add IHDR
System.arraycopy(Utils.IHDR, 0, ihdrBody, 0, 4)
// Add the max width and height
System.arraycopy(Utils.uIntToByteArray(width), 0, ihdrBody, 4, 4)
System.arraycopy(Utils.uIntToByteArray(height), 0, ihdrBody, 8, 4)
// Add complicated stuff like depth color ...
// If you want correct png you need same parameters.
System.arraycopy(ihdrOfApng, 8, ihdrBody, 12, 5)
// Generate CRC
val crC32 = CRC32()
crC32.update(ihdrBody, 0, 0xD + 4)
System.arraycopy(ihdrBody, 0, ihdr, 4, 0xD + 4)
System.arraycopy(Utils.uIntToByteArray(crC32.value.toInt()), 0, ihdr, 0xD + 4 + 4, 4)
return ihdr
}
}
}