Add ApngLoader

This commit is contained in:
Oupson 2021-06-22 15:30:21 +02:00
parent f7d76f26e7
commit af21d501f6
6 changed files with 422 additions and 286 deletions

View File

@ -3,13 +3,11 @@ package oupson.apng.decoder
import android.content.Context
import android.graphics.*
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.ImageView
import androidx.annotation.RawRes
import kotlinx.coroutines.*
import oupson.apng.BuildConfig
@ -30,20 +28,6 @@ import java.util.zip.CRC32
* Call [decodeApng]
*/
class ApngDecoder {
interface Callback {
/**
* Function called when the file was successfully decoded.
* @param drawable Can be an [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].
*/
fun onSuccess(drawable: Drawable)
/**
* Function called when something gone wrong.
* @param error The problem.
*/
fun onError(error: Exception)
}
class Config(
internal var speed: Float = 1f,
internal var bitmapConfig: Bitmap.Config = Bitmap.Config.ARGB_8888,
@ -645,252 +629,7 @@ 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", "BlockingMethodInNonBlockingContext")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
file: File,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope: CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.Default) {
try {
val drawable =
decodeApng(
context,
withContext(Dispatchers.IO) {
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.Default) {
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.Default) {
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", "BlockingMethodInNonBlockingContext")
@JvmStatic
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
url: URL,
imageView: ImageView,
callback: Callback? = null,
config: Config = Config(),
scope: CoroutineScope = GlobalScope
) {
scope.launch(Dispatchers.Default) {
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.Default) {
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(Exception("Cannot open string"))
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
callback?.onError(e)
}
}
}
}
/**
* Generate a correct IHDR from the IHDR chunk of the APNG.

View File

@ -0,0 +1,353 @@
package oupson.apng.decoder
import android.content.Context
import android.graphics.drawable.AnimatedImageDrawable
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
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
import java.net.URL
class ApngLoader(parent: Job? = null) {
interface Callback {
/**
* Function called when the file was successfully decoded.
* @param drawable Can be an [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].
*/
fun onSuccess(drawable: Drawable)
/**
* Function called when something gone wrong.
* @param error The problem.
*/
fun onError(error: Exception)
}
private val job = SupervisorJob(parent)
private val coroutineScope: CoroutineScope = CoroutineScope(job)
fun cancelAll() {
coroutineScope.cancel(CancellationException("Loading was canceled"))
}
/**
* Load Apng into an imageView.
* @param context Context needed for animation drawable.
* @param file File to decode.
* @param imageView Image View.
* @param config Decoder configuration
*/
suspend fun decodeApngInto(
context: Context,
file: File,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
val drawable =
ApngDecoder.decodeApng(
context,
withContext(Dispatchers.IO) {
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()
}
}
return drawable
}
/**
* Load Apng into an imageView.
* @param context Context needed for animation drawable and content resolver.
* @param uri Uri to load.
* @param imageView Image View.
* @param config Decoder configuration
*/
suspend fun decodeApngInto(
context: Context,
uri: Uri,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
val inputStream =
withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }
?: throw FileNotFoundException("Failed to load $uri") // TODO Better err ?
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()
}
}
return drawable
}
/**
* Load Apng into an imageView.
* @param context Context needed to decode the resource and for the animation drawable.
* @param res Raw resource to load.
* @param imageView Image View.
* @param config Decoder configuration
*/
suspend fun decodeApngInto(
context: Context, @RawRes res: Int,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
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()
}
}
return drawable
}
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for the animation drawable.
* @param url URL to load.
* @param imageView Image View.
* @param config Decoder configuration
*/
suspend fun decodeApngInto(
context: Context,
url: URL,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
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()
}
}
return drawable
}
/**
* 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 config Decoder configuration
*/
@Suppress("unused")
suspend fun decodeApngInto(
context: Context,
string: String,
imageView: ImageView,
config: ApngDecoder.Config = ApngDecoder.Config()
): Drawable {
return if (string.startsWith("http://") || string.startsWith("https://")) {
decodeApngInto(
context,
URL(string),
imageView,
config
)
} else if (File(string).exists()) {
var pathToLoad =
if (string.startsWith("content://")) string else "file://$string"
pathToLoad = pathToLoad.replace("%", "%25").replace("#", "%23")
decodeApngInto(
context,
Uri.parse(pathToLoad),
imageView,
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()
}
}
drawable
} else {
throw Exception("Cannot open string")
}
}
// region with callback
/**
* Load Apng into an imageView, asynchronously.
* @param context Context needed for animation drawable.
* @param file File to decode.
* @param imageView Image View.
* @param callback [ApngLoader.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused", "BlockingMethodInNonBlockingContext")
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
file: File,
imageView: ImageView,
callback: Callback? = 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)
}
}
/**
* 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 [ApngLoader.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
uri: Uri,
imageView: ImageView,
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)
}
}
/**
* 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 [ApngLoader.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmOverloads
fun decodeApngAsyncInto(
context: Context, @RawRes res: Int,
imageView: ImageView,
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)
}
}
/**
* 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 [ApngLoader.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused", "BlockingMethodInNonBlockingContext")
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
url: URL,
imageView: ImageView,
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)
}
}
/**
* 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 [ApngLoader.Callback] to handle success and error.
* @param config Decoder configuration
*/
@Suppress("unused")
@JvmOverloads
fun decodeApngAsyncInto(
context: Context,
string: String,
imageView: ImageView,
callback: Callback? = 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)
}
}
}
// endregion with callback
}

View File

@ -11,13 +11,17 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_viewer.*
import oupson.apng.decoder.ApngDecoder
import oupson.apng.decoder.ApngLoader
import oupson.apngcreator.R
class ViewerActivity : AppCompatActivity() {
private var apngLoader: ApngLoader? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewer)
this.apngLoader = ApngLoader()
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@ -50,16 +54,24 @@ class ViewerActivity : AppCompatActivity() {
private fun load() {
val uri = intent.data ?: return
ApngDecoder.decodeApngAsyncInto(this, uri, viewerImageView, callback = object : ApngDecoder.Callback {
apngLoader?.decodeApngAsyncInto(
this,
uri,
viewerImageView,
callback = object : ApngLoader.Callback {
override fun onSuccess(drawable: Drawable) {}
override fun onError(error: Exception) {
Log.e("ViewerActivity", "Error when loading file", error)
}
}, ApngDecoder.Config(decodeCoverFrame = false))
},
ApngDecoder.Config(decodeCoverFrame = false)
)
}
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
2 -> {

View File

@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import androidx.fragment.app.Fragment
import oupson.apng.decoder.ApngDecoder
import oupson.apng.decoder.ApngLoader
import oupson.apng.drawable.ApngDrawable
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
@ -19,11 +20,14 @@ import java.net.URL
class ApngDecoderFragment : Fragment() {
companion object {
private const val TAG = "ApngDecoderFragment"
@JvmStatic
fun newInstance() =
ApngDecoderFragment()
}
private var apngLoader: ApngLoader? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -32,16 +36,24 @@ class ApngDecoderFragment : Fragment() {
val imageView: ImageView = view.findViewById(R.id.apngDecoderImageView) ?: return view
apngLoader = ApngLoader()
if (context != null) {
ApngDecoder.decodeApngAsyncInto(
apngLoader?.decodeApngAsyncInto(
this.requireContext(),
URL("https://metagif.files.wordpress.com/2015/01/bugbuckbunny.png"),
imageView,
config = ApngDecoder.Config(bitmapConfig = Bitmap.Config.RGB_565, decodeCoverFrame = true),
callback = object : ApngDecoder.Callback {
config = ApngDecoder.Config(
bitmapConfig = Bitmap.Config.RGB_565,
decodeCoverFrame = true
),
callback = object : ApngLoader.Callback {
override fun onSuccess(drawable: Drawable) {
if (BuildConfig.DEBUG)
Log.i(TAG, "onSuccess(), has cover frame : ${(drawable as? ApngDrawable)?.coverFrame != null}")
Log.i(
TAG,
"onSuccess(), has cover frame : ${(drawable as? ApngDrawable)?.coverFrame != null}"
)
}
override fun onError(error: Exception) {

View File

@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment;
import org.jetbrains.annotations.NotNull;
import oupson.apng.decoder.ApngDecoder;
import oupson.apng.decoder.ApngLoader;
import oupson.apngcreator.BuildConfig;
import oupson.apngcreator.R;
@ -23,6 +24,8 @@ import oupson.apngcreator.R;
public class JavaFragment extends Fragment {
private static final String TAG = "JavaActivity";
private ApngLoader apngLoader = null;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -36,10 +39,12 @@ public class JavaFragment extends Fragment {
Context context = this.getContext();
this.apngLoader = new ApngLoader();
if (imageView != null && context != null) {
if (BuildConfig.DEBUG)
Log.v(TAG, "Loading " + imageUrl);
ApngDecoder.decodeApngAsyncInto(context, imageUrl, imageView, new ApngDecoder.Callback() {
this.apngLoader.decodeApngAsyncInto(context, imageUrl, imageView, new ApngLoader.Callback() {
@Override
public void onSuccess(@NotNull Drawable drawable) {
if (BuildConfig.DEBUG)
@ -56,4 +61,11 @@ public class JavaFragment extends Fragment {
return v;
}
@Override
public void onStop() {
super.onStop();
apngLoader.cancelAll();
}
}

View File

@ -13,7 +13,7 @@ import android.widget.SeekBar
import androidx.fragment.app.Fragment
import com.squareup.picasso.Picasso
import kotlinx.android.synthetic.main.activity_creator.*
import oupson.apng.decoder.ApngDecoder
import oupson.apng.decoder.ApngLoader
import oupson.apng.drawable.ApngDrawable
import oupson.apngcreator.BuildConfig
import oupson.apngcreator.R
@ -22,6 +22,7 @@ import oupson.apngcreator.R
class KotlinFragment : Fragment() {
companion object {
private const val TAG = "KotlinFragment"
@JvmStatic
fun newInstance() =
KotlinFragment()
@ -51,6 +52,8 @@ class KotlinFragment : Fragment() {
)
private val selected = 4
private var apngLoader: ApngLoader? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -60,6 +63,8 @@ class KotlinFragment : Fragment() {
val view = inflater.inflate(R.layout.fragment_kotlin, container, false)
apngLoader = ApngLoader()
apngImageView = view.findViewById(R.id.ApngImageView)
normalImageView = view.findViewById(R.id.NormalImageView)
@ -126,7 +131,10 @@ class KotlinFragment : Fragment() {
res.coverFrame = animation.coverFrame
for (i in 0 until animation.numberOfFrames) {
res.addFrame(animation.getFrame(i), (durations!![i].toFloat() / speed).toInt())
res.addFrame(
animation.getFrame(i),
(durations!![i].toFloat() / speed).toInt()
)
}
apngImageView?.setImageDrawable(res)
@ -138,11 +146,11 @@ class KotlinFragment : Fragment() {
})
if (animation == null) {
ApngDecoder.decodeApngAsyncInto(
apngLoader?.decodeApngAsyncInto(
requireContext(),
imageUrls[selected],
apngImageView!!,
callback = object : ApngDecoder.Callback {
callback = object : ApngLoader.Callback {
override fun onSuccess(drawable: Drawable) {
animation = (drawable as? ApngDrawable)
durations = IntArray(animation?.numberOfFrames ?: 0) { i ->