Work on ApngDecoder

This commit is contained in:
Oupson 2021-06-25 13:31:35 +02:00
parent af21d501f6
commit 4ef644632a
5 changed files with 683 additions and 685 deletions

View File

@ -11,13 +11,11 @@ import android.util.Log
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
import oupson.apng.utils.Loader
import oupson.apng.utils.Utils
import oupson.apng.utils.Utils.Companion.isPng
import java.io.*
import java.net.URL
import java.nio.ByteBuffer
@ -27,7 +25,7 @@ import java.util.zip.CRC32
* An APNG Decoder.
* Call [decodeApng]
*/
class ApngDecoder {
class ApngDecoder(input: InputStream, val config: Config) {
class Config(
internal var speed: Float = 1f,
internal var bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888,
@ -55,18 +53,8 @@ class ApngDecoder {
}
}
companion object {
private const val TAG = "ApngDecoder"
private val zeroLength = byteArrayOf(0x00, 0x00, 0x00, 0x00)
// Paint used to clear the buffer
private val clearPaint: Paint by lazy {
Paint().apply {
xfermode = PorterDuffXfermode(
PorterDuff.Mode.CLEAR
)
}
}
private val inputStream: InputStream? = input
private var result: Result<Drawable>? = null
/**
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
@ -75,26 +63,17 @@ class ApngDecoder {
* @param config Decoder configuration
* @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].
*/
// TODO DOCUMENT CONFIG
@Suppress(
"MemberVisibilityCanBePrivate",
"BlockingMethodInNonBlockingContext"
) // BlockingMethodInNonBlockingContext is a warning generated by java @Throws
@JvmStatic
@JvmOverloads
suspend fun decodeApng(
context: Context,
inStream: InputStream,
config: Config = Config()
context: Context
): Drawable = withContext(Dispatchers.Default) {
val inputStream = BufferedInputStream(inStream)
val inputStream = BufferedInputStream(inputStream)
val bytes = ByteArray(8)
inputStream.mark(8)
withContext(Dispatchers.IO) {
inputStream.read(bytes)
}
if (isPng(bytes)) {
if (Utils.isPng(bytes)) {
var png: ByteArrayOutputStream? = null
var cover: ByteArrayOutputStream? = null
var delay = -1f
@ -545,6 +524,24 @@ class ApngDecoder {
}
}
suspend fun getDecoded(context: Context): Result<Drawable> {
if (result == null) {
result = kotlin.runCatching {
decodeApng(context)
}
kotlin.runCatching {
withContext(Dispatchers.IO) {
inputStream?.close()
}
}.onFailure {
return Result.failure(it)
}
}
return result ?: Result.failure(NullPointerException("result is null"))
}
/**
* 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 animation drawable.
@ -552,18 +549,8 @@ class ApngDecoder {
* @param config Decoder configuration
* @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", "BlockingMethodInNonBlockingContext")
@JvmStatic
// TODO DOCUMENT
suspend fun decodeApng(
context: Context,
file: File,
config: Config = Config()
): Drawable =
decodeApng(
context,
withContext(Dispatchers.IO) { FileInputStream(file) }, config
)
// TODO DOC
constructor(file: File, config: Config = Config()) : this(FileInputStream(file), config)
/**
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
@ -572,20 +559,12 @@ class ApngDecoder {
* @param config Decoder configuration
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/
@Suppress("unused")
@JvmStatic
suspend fun decodeApng(
// TODO DOC + better
constructor(
context: Context,
uri: Uri,
config: Config = Config()
): Drawable {
val inputStream = context.contentResolver.openInputStream(uri)!!
return decodeApng(
context,
inputStream,
config
)
}
) : this(context.contentResolver.openInputStream(uri)!!, config)
/**
* Decode Apng and return a Drawable who can be an [ApngDrawable] if it end successfully. Can also be an [android.graphics.drawable.AnimatedImageDrawable].
@ -594,41 +573,12 @@ class ApngDecoder {
* @param config Decoder configuration
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/
@Suppress("unused")
@JvmStatic
suspend fun decodeApng(
// TODO DOC
constructor(
context: Context,
@RawRes res: Int,
config: Config = Config()
): Drawable =
decodeApng(
context,
context.resources.openRawResource(res),
config
)
/**
* 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 is needed for contentResolver and animation drawable.
* @param url URL to decode.
* @param config Decoder configuration
* @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,
config: Config = Config()
) =
withContext(Dispatchers.IO) {
decodeApng(
context,
ByteArrayInputStream(Loader.load(url)),
config
)
}
) : this(context.resources.openRawResource(res), config)
/**
@ -667,5 +617,39 @@ class ApngDecoder {
System.arraycopy(Utils.uIntToByteArray(crC32.value.toInt()), 0, ihdr, 0xD + 4 + 4, 4)
return ihdr
}
companion object {
private const val TAG = "ApngDecoder"
private val zeroLength = byteArrayOf(0x00, 0x00, 0x00, 0x00)
// Paint used to clear the buffer
private val clearPaint: Paint by lazy {
Paint().apply {
xfermode = PorterDuffXfermode(
PorterDuff.Mode.CLEAR
)
}
}
/**
* 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 is needed for contentResolver and animation drawable.
* @param url URL to decode.
* @param config Decoder configuration
* @return [ApngDrawable] if successful and an [AnimatedImageDrawable] if the image decoded is not an APNG but a gif.
*/
@Suppress("unused", "BlockingMethodInNonBlockingContext")
@JvmStatic
suspend fun constructFromUrl(
url: URL,
config: Config = Config()
) =
withContext(Dispatchers.IO) {
ApngDecoder(
ByteArrayInputStream(Loader.load(url)),
config
)
}
}
}

View File

@ -10,8 +10,6 @@ import android.widget.ImageView
import androidx.annotation.RawRes
import kotlinx.coroutines.*
import oupson.apng.drawable.ApngDrawable
import oupson.apng.utils.Loader
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
@ -29,7 +27,7 @@ class ApngLoader(parent: Job? = null) {
* Function called when something gone wrong.
* @param error The problem.
*/
fun onError(error: Exception)
fun onError(error: Throwable)
}
private val job = SupervisorJob(parent)
@ -52,23 +50,26 @@ class ApngLoader(parent: Job? = null) {
file: File,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
val drawable =
ApngDecoder.decodeApng(
context,
): Result<Drawable> {
val result =
ApngDecoder(
withContext(Dispatchers.IO) {
FileInputStream(file)
},
config
)
).getDecoded(context)
if (result.isSuccess) {
withContext(Dispatchers.Main) {
val drawable = result.getOrNull()
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
}
return drawable
}
return result
}
/**
@ -83,25 +84,27 @@ class ApngLoader(parent: Job? = null) {
uri: Uri,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
): Result<Drawable> {
val inputStream =
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
?: throw FileNotFoundException("Failed to load $uri") // TODO Better err ?
val drawable =
ApngDecoder.decodeApng(
context,
?: throw FileNotFoundException("Failed to load $uri") // TODO Result
val result =
ApngDecoder(
inputStream,
config
)
).getDecoded(context)
if (result.isSuccess) {
withContext(Dispatchers.Main) {
val drawable = result.getOrNull()
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
}
return drawable
}
return result
}
/**
@ -115,21 +118,26 @@ class ApngLoader(parent: Job? = null) {
context: Context, @RawRes res: Int,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
val drawable =
ApngDecoder.decodeApng(
context,
context.resources.openRawResource(res),
): Result<Drawable> {
val result =
ApngDecoder(
withContext(Dispatchers.IO) {
context.resources.openRawResource(res)
},
config
)
).getDecoded(context)
if (result.isSuccess) {
withContext(Dispatchers.Main) {
val drawable = result.getOrNull()
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
}
return drawable
}
return result
}
/**
@ -144,26 +152,20 @@ class ApngLoader(parent: Job? = null) {
url: URL,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
val drawable = ApngDecoder.decodeApng(
context,
ByteArrayInputStream(
Loader.load(
url
)
),
config
)
): Result<Drawable> {
val result = ApngDecoder.constructFromUrl(url, config).getDecoded(context)
if (result.isSuccess) {
withContext(Dispatchers.Main) {
val drawable = result.getOrNull()
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
}
}
return drawable
return result
}
/**
@ -179,7 +181,7 @@ class ApngLoader(parent: Job? = null) {
string: String,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
): Result<Drawable> {
return if (string.startsWith("http://") || string.startsWith("https://")) {
decodeApngInto(
context,
@ -198,21 +200,25 @@ class ApngLoader(parent: Job? = null) {
config
)
} else if (string.startsWith("file://android_asset/")) {
val drawable =
ApngDecoder.decodeApng(
context,
context.assets.open(string.replace("file:///android_asset/", "")),
config
)
val inputStream = kotlin.runCatching {
withContext(Dispatchers.IO) {
context.assets.open(string.replace("file:///android_asset/", ""))
}
}.onFailure {
return Result.failure(it)
}
val result = ApngDecoder(inputStream.getOrThrow(), config).getDecoded(context)
if (result.isSuccess) {
withContext(Dispatchers.Main) {
val drawable = result.getOrNull()
imageView.setImageDrawable(drawable)
(drawable as? AnimationDrawable)?.start()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(drawable as? AnimatedImageDrawable)?.start()
}
}
drawable
}
result
} else {
throw Exception("Cannot open string")
}
@ -228,7 +234,7 @@ class ApngLoader(parent: Job? = null) {
* @param callback [ApngLoader.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused", "BlockingMethodInNonBlockingContext")
@Suppress("unused")
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
@ -238,11 +244,13 @@ class ApngLoader(parent: Job? = null) {
config: ApngDecoder.Config = ApngDecoder.Config()
) =
coroutineScope.launch(Dispatchers.Default) {
try {
val drawable = decodeApngInto(context, file, imageView, config)
callback?.onSuccess(drawable)
} catch (e: Exception) {
callback?.onError(e)
withContext(Dispatchers.Main) {
if (drawable.isSuccess) {
callback?.onSuccess(drawable.getOrNull()!!)
} else {
callback?.onError(drawable.exceptionOrNull()!!)
}
}
}
@ -264,11 +272,13 @@ class ApngLoader(parent: Job? = null) {
callback: Callback? = null,
config: ApngDecoder.Config = ApngDecoder.Config()
) = coroutineScope.launch(Dispatchers.Default) {
try {
val drawable = decodeApngInto(context, uri, imageView, config)
callback?.onSuccess(drawable)
} catch (e: Exception) {
callback?.onError(e)
withContext(Dispatchers.Main) {
if (drawable.isSuccess) {
callback?.onSuccess(drawable.getOrNull()!!)
} else {
callback?.onError(drawable.exceptionOrNull()!!)
}
}
}
@ -288,11 +298,13 @@ class ApngLoader(parent: Job? = null) {
callback: Callback? = null,
config: ApngDecoder.Config = ApngDecoder.Config()
) = coroutineScope.launch(Dispatchers.Default) {
try {
val drawable = decodeApngInto(context, res, imageView, config)
callback?.onSuccess(drawable)
} catch (e: Exception) {
callback?.onError(e)
withContext(Dispatchers.Main) {
if (drawable.isSuccess) {
callback?.onSuccess(drawable.getOrNull()!!)
} else {
callback?.onError(drawable.exceptionOrNull()!!)
}
}
}
@ -304,7 +316,7 @@ class ApngLoader(parent: Job? = null) {
* @param callback [ApngLoader.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused", "BlockingMethodInNonBlockingContext")
@Suppress("unused")
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
@ -313,11 +325,13 @@ class ApngLoader(parent: Job? = null) {
callback: Callback? = null,
config: ApngDecoder.Config = ApngDecoder.Config()
) = coroutineScope.launch(Dispatchers.Default) {
try {
val drawable = decodeApngInto(context, url, imageView, config)
callback?.onSuccess(drawable)
} catch (e: Exception) {
callback?.onError(e)
withContext(Dispatchers.Main) {
if (drawable.isSuccess) {
callback?.onSuccess(drawable.getOrNull()!!)
} else {
callback?.onError(drawable.exceptionOrNull()!!)
}
}
}
@ -339,12 +353,12 @@ class ApngLoader(parent: Job? = null) {
config: ApngDecoder.Config = ApngDecoder.Config()
) =
coroutineScope.launch(Dispatchers.Default) {
try {
val drawable = decodeApngInto(context, string, imageView, config)
callback?.onSuccess(drawable)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
if (drawable.isSuccess) {
callback?.onSuccess(drawable.getOrNull()!!)
} else {
callback?.onError(drawable.exceptionOrNull()!!)
}
}
}

View File

@ -60,7 +60,7 @@ class ViewerActivity : AppCompatActivity() {
viewerImageView,
callback = object : ApngLoader.Callback {
override fun onSuccess(drawable: Drawable) {}
override fun onError(error: Exception) {
override fun onError(error: Throwable) {
Log.e("ViewerActivity", "Error when loading file", error)
}
},

View File

@ -56,7 +56,7 @@ class ApngDecoderFragment : Fragment() {
)
}
override fun onError(error: Exception) {
override fun onError(error: Throwable) {
Log.e(TAG, "onError : $error")
}
})

View File

@ -52,7 +52,7 @@ public class JavaFragment extends Fragment {
}
@Override
public void onError(@NotNull Exception error) {
public void onError(@NotNull Throwable error) {
Log.e(TAG, "Error : " + error.toString());
}
}, new ApngDecoder.Config().setIsDecodingCoverFrame(false));