Work on ApngDecoder
This commit is contained in:
parent
af21d501f6
commit
4ef644632a
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -56,7 +56,7 @@ class ApngDecoderFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
|
||||
override fun onError(error: Exception) {
|
||||
override fun onError(error: Throwable) {
|
||||
Log.e(TAG, "onError : $error")
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue