bumptech / glide

An image loading and caching library for Android focused on smooth scrolling
https://bumptech.github.io/glide/
Other
34.62k stars 6.12k forks source link

Request: allow to extract Bitmaps from animated GIF/WEBP, including how long each should last #4396

Open AndroidDeveloperLB opened 3 years ago

AndroidDeveloperLB commented 3 years ago

Currently there are still only workarounds for this, but even then, I couldn't find the missing information, of how long each frame I get should last.

Here's one way: https://medium.com/@bogomazartem/extract-bitmap-frames-from-gif-file-android-9e06d8b709f8

And another, which I'm not sure if it's ok, as it actually plays the animation :

private fun testWebp() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_webp).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .submit().get() as WebpDrawable
    val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    drawable.setBounds(0, 0, bitmap.width, bitmap.height)
    drawable.loopCount = 1
    val callback = object : CallbackEx() {
        override fun invalidateDrawable(who: Drawable) {
            val webp = who as WebpDrawable
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
            who.draw(canvas)
            //image is available here on the bitmap object
            Log.d("AppLog", "frameIndex:${webp.frameIndex} frameCount:${webp.frameCount} firstFrame:${webp.firstFrame}")
        }
    }
    drawable.callback = callback
    drawable.start()
}

And for WEBP (using this library with Glide), something similar:

private fun testGif() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_gif).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE).submit().get() as GifDrawable
    val bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    drawable.setBounds(0, 0, bitmap.width, bitmap.height)
    drawable.setLoopCount(1)
    val callback = object : CallbackEx() {
        override fun invalidateDrawable(who: Drawable) {
            super.invalidateDrawable(who)
            val gif = who as GifDrawable
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
            who.draw(canvas)
            //image is available here on the bitmap object
            Log.d("AppLog", "frameIndex:${gif.frameIndex} frameCount:${gif.frameCount} firstFrame:${gif.firstFrame}")
        }
    }
    drawable.callback = callback
    drawable.start()
}

But there is no official way to do it. Please offer an official way using Glide, to extract the frames and check how long each frame should last.

I know it's open sourced and technically I could dig the code to find it (I can see it's in ByteBufferGifDecoder, using GifDecoder), but I'd like to request it still.

Please do consider adding this functionality, and if there is one already, please show me a link about this.

AndroidDeveloperLB commented 3 years ago

Another workaround, again with reflection, but this time with the duration of the frames:

private fun testGif2() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_gif).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE).submit().get() as GifDrawable
    val state = drawable.constantState as Drawable.ConstantState
    val frameLoader: Field = state::class.java.getDeclaredField("frameLoader")
    frameLoader.isAccessible = true
    val gifFrameLoader: Any = frameLoader.get(state)
    val gifDecoder: Field = gifFrameLoader.javaClass.getDeclaredField("gifDecoder")
    gifDecoder.isAccessible = true
    val standardGifDecoder = gifDecoder.get(gifFrameLoader) as StandardGifDecoder
    Log.d("AppLog", "got ${standardGifDecoder.frameCount} frames:")
    val parent = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "gifFrames")
    parent.mkdirs()
    for (i in 0 until standardGifDecoder.frameCount) {
        val file = File(parent, "${String.format("%07d", i)}.png")
        val delay = standardGifDecoder.nextDelay
        val bitmap = standardGifDecoder.nextFrame
        if (bitmap == null) {
            Log.d("AppLog", "error getting frame")
            break
        }
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(file))
        Log.d("AppLog", "${standardGifDecoder.currentFrameIndex} - $delay ${bitmap?.width}x${bitmap?.height}")
        standardGifDecoder.advance()
    }
    Log.d("AppLog", "done")
}

private fun testWebp2() {
    val drawable = GlideApp.with(applicationContext).load(R.raw.test_webp).skipMemoryCache(true)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .submit().get() as WebpDrawable
    drawable.constantState
    val state = drawable.constantState as Drawable.ConstantState
    val frameLoader: Field = state::class.java.getDeclaredField("frameLoader")
    frameLoader.isAccessible = true
    @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
    val webpFrameLoader = frameLoader.get(state) as WebpFrameLoader
    val webpDecoder: Field = webpFrameLoader.javaClass.getDeclaredField("webpDecoder")
    webpDecoder.isAccessible = true
    val standardGifDecoder = webpDecoder.get(webpFrameLoader) as GifDecoder
    Log.d("AppLog", "got ${standardGifDecoder.frameCount} frames:")
    for (i in 0 until standardGifDecoder.frameCount) {
        val delay = standardGifDecoder.nextDelay
        val bitmap = standardGifDecoder.nextFrame
        Log.d("AppLog", "${standardGifDecoder.currentFrameIndex} - $delay ${bitmap?.width}x${bitmap?.height}")
        standardGifDecoder.advance()
    }
    Log.d("AppLog", "done")
}
AndroidDeveloperLB commented 3 years ago

Anyway, I think I got it, but used too much things of Glide even though I don't need caching of anything:

For GIF:

private fun testGif3() {
    val data = resources.openRawResource(R.raw.test_gif).readBytes()
    val byteBuffer = ByteBuffer.wrap(data)
    val memorySizeCalculator = MemorySizeCalculator.Builder(this).build()
    val bitmapPoolSize = memorySizeCalculator.bitmapPoolSize.toLong()
    val bitmapPool = if (bitmapPoolSize > 0) LruBitmapPool(bitmapPoolSize) else BitmapPoolAdapter()
    val arrayPool = LruArrayPool(memorySizeCalculator.arrayPoolSizeInBytes)
    val gifBitmapProvider = GifBitmapProvider(bitmapPool, arrayPool)
    val header = GifHeaderParser().setData(byteBuffer).parseHeader()
    val standardGifDecoder = StandardGifDecoder(gifBitmapProvider, header, byteBuffer, 1)
    //alternative, without getting header and needing sample size:
//        val standardGifDecoder = StandardGifDecoder(gifBitmapProvider)
//        standardGifDecoder.read(data)
    val frameCount = standardGifDecoder.frameCount
    standardGifDecoder.advance()
    for (i in 0 until frameCount) {
        val delay = standardGifDecoder.nextDelay
        val bitmap = standardGifDecoder.nextFrame
        //bitmap ready here
        standardGifDecoder.advance()
    }
}

For WEBP:

private fun testWebP3() {
    val data = resources.openRawResource(R.raw.test_webp).readBytes()
    val byteBuffer = ByteBuffer.wrap(data)
    val webp = WebpImage.create(data)
    val sampleSize = 1
    val cacheStrategy: WebpFrameCacheStrategy? = Options().get(WebpFrameLoader.FRAME_CACHE_STRATEGY)
    val memorySizeCalculator = MemorySizeCalculator.Builder(this).build()
    val bitmapPoolSize = memorySizeCalculator.bitmapPoolSize.toLong()
    val bitmapPool = if (bitmapPoolSize > 0) LruBitmapPool(bitmapPoolSize) else BitmapPoolAdapter()
    val arrayPool = LruArrayPool(memorySizeCalculator.arrayPoolSizeInBytes)
    val gifBitmapProvider = GifBitmapProvider(bitmapPool, arrayPool)
    val webpDecoder = WebpDecoder(gifBitmapProvider, webp, byteBuffer, sampleSize, cacheStrategy)
    val frameCount = webpDecoder.frameCount
    webpDecoder.advance()
    for (i in 0 until frameCount) {
        val delay = webpDecoder.nextDelay
        val bitmap = webpDecoder.nextFrame
        //bitmap ready here
        webpDecoder.advance()
    }
}

If anyone knows of a shorter way, please let me know.

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had activity in the last seven days. It will be closed if no further activity occurs within the next seven days. Thank you for your contributions.

AndroidDeveloperLB commented 3 years ago

Can't you at least consider?

Luosoha commented 8 months ago

Anyway, I think I got it, but used too much things of Glide even though I don't need caching of anything:

For GIF:

private fun testGif3() {
    val data = resources.openRawResource(R.raw.test_gif).readBytes()
    val byteBuffer = ByteBuffer.wrap(data)
    val memorySizeCalculator = MemorySizeCalculator.Builder(this).build()
    val bitmapPoolSize = memorySizeCalculator.bitmapPoolSize.toLong()
    val bitmapPool = if (bitmapPoolSize > 0) LruBitmapPool(bitmapPoolSize) else BitmapPoolAdapter()
    val arrayPool = LruArrayPool(memorySizeCalculator.arrayPoolSizeInBytes)
    val gifBitmapProvider = GifBitmapProvider(bitmapPool, arrayPool)
    val header = GifHeaderParser().setData(byteBuffer).parseHeader()
    val standardGifDecoder = StandardGifDecoder(gifBitmapProvider, header, byteBuffer, 1)
    //alternative, without getting header and needing sample size:
//        val standardGifDecoder = StandardGifDecoder(gifBitmapProvider)
//        standardGifDecoder.read(data)
    val frameCount = standardGifDecoder.frameCount
    standardGifDecoder.advance()
    for (i in 0 until frameCount) {
        val delay = standardGifDecoder.nextDelay
        val bitmap = standardGifDecoder.nextFrame
        //bitmap ready here
        standardGifDecoder.advance()
    }
}

For WEBP:

private fun testWebP3() {
    val data = resources.openRawResource(R.raw.test_webp).readBytes()
    val byteBuffer = ByteBuffer.wrap(data)
    val webp = WebpImage.create(data)
    val sampleSize = 1
    val cacheStrategy: WebpFrameCacheStrategy? = Options().get(WebpFrameLoader.FRAME_CACHE_STRATEGY)
    val memorySizeCalculator = MemorySizeCalculator.Builder(this).build()
    val bitmapPoolSize = memorySizeCalculator.bitmapPoolSize.toLong()
    val bitmapPool = if (bitmapPoolSize > 0) LruBitmapPool(bitmapPoolSize) else BitmapPoolAdapter()
    val arrayPool = LruArrayPool(memorySizeCalculator.arrayPoolSizeInBytes)
    val gifBitmapProvider = GifBitmapProvider(bitmapPool, arrayPool)
    val webpDecoder = WebpDecoder(gifBitmapProvider, webp, byteBuffer, sampleSize, cacheStrategy)
    val frameCount = webpDecoder.frameCount
    webpDecoder.advance()
    for (i in 0 until frameCount) {
        val delay = webpDecoder.nextDelay
        val bitmap = webpDecoder.nextFrame
        //bitmap ready here
        webpDecoder.advance()
    }
}

If anyone knows of a shorter way, please let me know.

Hello, WebpImage.create(data) throws me "Failed to create demuxer" exception. Do you by any chance know why it throws that?

AndroidDeveloperLB commented 8 months ago

@Luosoha Please create a sample project and then share it. Also, here's a sample webp file that should work fine: https://mathiasbynens.be/demo/animated-webp https://mathiasbynens.be/demo/animated-webp-supported.webp

Luosoha commented 8 months ago

@Luosoha Please create a sample project and then share it. Also, here's a sample webp file that should work fine: https://mathiasbynens.be/demo/animated-webp https://mathiasbynens.be/demo/animated-webp-supported.webp

I double checked again and my webp file was actually a gif file. After making sure that it is a webp file all worked fine. Thanks for the quick reply :D