From a2444ffa695a44de8275a34a210d5ac5e40d59aa Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Mon, 18 Aug 2025 18:12:51 -0400 Subject: [PATCH] Fix GIF animations. `StreamBitmapDecoder` was handling GIF images and rendering them as static bitmaps. This change fixes that by adding a `StreamBitmapDecoder` wrapper that returns `handles=false` for images of type GIF and APNG, to enable `StreamFactoryGifDecoder` to decode GIF images. - Resolves signalapp/Signal-Android#14300 --- ...ecoder.java => ByteBufferApngDecoder.java} | 2 +- ...cheDecoder.java => StreamApngDecoder.java} | 4 +- .../glide/cache/StreamBitmapDecoder.kt | 62 +++++++++++++++++++ ...Decoder.kt => StreamFactoryApngDecoder.kt} | 6 +- .../glide/cache/StreamFactoryGifDecoder.kt | 32 ++++++++++ .../securesms/mms/SignalGlideComponents.java | 35 ++++++----- 6 files changed, 121 insertions(+), 20 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/glide/cache/{ApngBufferCacheDecoder.java => ByteBufferApngDecoder.java} (95%) rename app/src/main/java/org/thoughtcrime/securesms/glide/cache/{ApngStreamCacheDecoder.java => StreamApngDecoder.java} (88%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamBitmapDecoder.kt rename app/src/main/java/org/thoughtcrime/securesms/glide/cache/{ApngStreamFactoryDecoder.kt => StreamFactoryApngDecoder.kt} (81%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamFactoryGifDecoder.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ByteBufferApngDecoder.java similarity index 95% rename from app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java rename to app/src/main/java/org/thoughtcrime/securesms/glide/cache/ByteBufferApngDecoder.java index 7b72f838e9..a2e2812a37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ByteBufferApngDecoder.java @@ -16,7 +16,7 @@ import org.signal.glide.common.loader.Loader; import java.io.IOException; import java.nio.ByteBuffer; -public class ApngBufferCacheDecoder implements ResourceDecoder { +public class ByteBufferApngDecoder implements ResourceDecoder { @Override public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamApngDecoder.java similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java rename to app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamApngDecoder.java index 37954ba1ee..ad23d350fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamApngDecoder.java @@ -16,14 +16,14 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; -public class ApngStreamCacheDecoder implements ResourceDecoder { +public class StreamApngDecoder implements ResourceDecoder { /** Set to match {@link com.bumptech.glide.load.data.InputStreamRewinder}'s read limit */ private static final int READ_LIMIT = 5 * 1024 * 1024; private final ResourceDecoder byteBufferDecoder; - public ApngStreamCacheDecoder(ResourceDecoder byteBufferDecoder) { + public StreamApngDecoder(ResourceDecoder byteBufferDecoder) { this.byteBufferDecoder = byteBufferDecoder; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamBitmapDecoder.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamBitmapDecoder.kt new file mode 100644 index 0000000000..10a4bef44f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamBitmapDecoder.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.glide.cache + +import android.content.Context +import android.graphics.Bitmap +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.load.ImageHeaderParser +import com.bumptech.glide.load.ImageHeaderParserUtils +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import org.signal.core.util.logging.Log +import java.io.IOException +import java.io.InputStream + +typealias GlideStreamBitmapDecoder = com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder +typealias GlideDownsampler = com.bumptech.glide.load.resource.bitmap.Downsampler + +class StreamBitmapDecoder( + context: Context, + glide: Glide, + registry: Registry +) : ResourceDecoder { + + private val imageHeaderParsers = registry.imageHeaderParsers + private val arrayPool = glide.arrayPool + private val downsampler = GlideDownsampler(imageHeaderParsers, context.resources.displayMetrics, glide.bitmapPool, arrayPool) + private val delegate = GlideStreamBitmapDecoder(downsampler, arrayPool) + + override fun handles(source: InputStream, options: Options): Boolean { + if (!delegate.handles(source, options)) { + return false + } + + val imageType = try { + ImageHeaderParserUtils.getType(imageHeaderParsers, source, arrayPool) + } catch (e: IOException) { + Log.w(TAG, "Error checking image type.", e) + ImageHeaderParser.ImageType.UNKNOWN + } + + return when (imageType) { + ImageHeaderParser.ImageType.GIF, ImageHeaderParser.ImageType.PNG_A -> false + ImageHeaderParser.ImageType.WEBP_A -> true + ImageHeaderParser.ImageType.ANIMATED_WEBP -> true + else -> true + } + } + + override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource? { + return delegate.decode(source, width, height, options) + } + + companion object { + private val TAG = Log.tag(StreamBitmapDecoder::class) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamFactoryDecoder.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamFactoryApngDecoder.kt similarity index 81% rename from app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamFactoryDecoder.kt rename to app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamFactoryApngDecoder.kt index ec766d51e3..d1c3fd88c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamFactoryDecoder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamFactoryApngDecoder.kt @@ -17,15 +17,15 @@ import org.thoughtcrime.securesms.mms.InputStreamFactory import java.nio.ByteBuffer /** - * A variant of [ApngStreamCacheDecoder] that decodes animated PNGs from [org.thoughtcrime.securesms.mms.InputStreamFactory] sources. + * A variant of [StreamApngDecoder] that decodes animated PNGs from [InputStreamFactory] sources. */ -class ApngStreamFactoryDecoder( +class StreamFactoryApngDecoder( private val byteBufferDecoder: ResourceDecoder ) : ResourceDecoder { override fun handles(source: InputStreamFactory, options: Options): Boolean { return if (options.get(ApngOptions.ANIMATE) == true) { - return APNGParser.isAPNG(LimitedReader(StreamReader(source.create()), GlideStreamConfig.markReadLimitBytes)) + APNGParser.isAPNG(LimitedReader(StreamReader(source.create()), GlideStreamConfig.markReadLimitBytes)) } else { false } diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamFactoryGifDecoder.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamFactoryGifDecoder.kt new file mode 100644 index 0000000000..757fccf525 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/cache/StreamFactoryGifDecoder.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.glide.cache + +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.resource.gif.GifDrawable +import com.bumptech.glide.load.resource.gif.StreamGifDecoder +import org.thoughtcrime.securesms.mms.InputStreamFactory + +/** + * A variant of [StreamGifDecoder] that decodes animated PNGs from [InputStreamFactory] sources. + */ +class StreamFactoryGifDecoder( + private val streamGifDecoder: StreamGifDecoder +) : ResourceDecoder { + + override fun handles(source: InputStreamFactory, options: Options): Boolean = true + + override fun decode( + source: InputStreamFactory, + width: Int, + height: Int, + options: Options + ): Resource? { + return streamGifDecoder.decode(source.create(), width, height, options) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java index 67ae6a4efe..337a8d6ac5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java @@ -1,3 +1,8 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.mms; import android.content.Context; @@ -12,8 +17,6 @@ import com.bumptech.glide.Registry; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.UnitModelLoader; import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder; -import com.bumptech.glide.load.resource.bitmap.Downsampler; -import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.load.resource.gif.StreamGifDecoder; @@ -32,15 +35,17 @@ import org.thoughtcrime.securesms.glide.ChunkedImageUrlLoader; import org.thoughtcrime.securesms.glide.ContactPhotoLoader; import org.thoughtcrime.securesms.glide.GiftBadgeModel; import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; -import org.thoughtcrime.securesms.glide.cache.ApngBufferCacheDecoder; import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder; -import org.thoughtcrime.securesms.glide.cache.ApngStreamCacheDecoder; -import org.thoughtcrime.securesms.glide.cache.ApngStreamFactoryDecoder; +import org.thoughtcrime.securesms.glide.cache.ByteBufferApngDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder; import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder; import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder; import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder; +import org.thoughtcrime.securesms.glide.cache.StreamApngDecoder; +import org.thoughtcrime.securesms.glide.cache.StreamBitmapDecoder; +import org.thoughtcrime.securesms.glide.cache.StreamFactoryApngDecoder; +import org.thoughtcrime.securesms.glide.cache.StreamFactoryGifDecoder; import org.thoughtcrime.securesms.glide.cache.WebpSanDecoder; import org.thoughtcrime.securesms.stickers.StickerRemoteUri; import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader; @@ -68,10 +73,12 @@ public class SignalGlideComponents implements RegisterGlideComponents { registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); - registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); + registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(context, glide, registry))); - StreamGifDecoder streamGifDecoder = new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()); + StreamGifDecoder streamGifDecoder = new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()); + StreamFactoryGifDecoder streamFactoryGifDecoder = new StreamFactoryGifDecoder(streamGifDecoder); registry.prepend(InputStream.class, GifDrawable.class, streamGifDecoder); + registry.prepend(InputStreamFactory.class, GifDrawable.class, streamFactoryGifDecoder); registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); registry.prepend(File.class, GifDrawable.class, new EncryptedCacheDecoder<>(secret, streamGifDecoder)); @@ -79,15 +86,15 @@ public class SignalGlideComponents implements RegisterGlideComponents { registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); registry.prepend(BitmapDrawable.class, new BitmapDrawableEncoder(glide.getBitmapPool(), encryptedBitmapResourceEncoder)); - ApngBufferCacheDecoder apngBufferCacheDecoder = new ApngBufferCacheDecoder(); - ApngStreamCacheDecoder apngStreamCacheDecoder = new ApngStreamCacheDecoder(apngBufferCacheDecoder); - ApngStreamFactoryDecoder apngStreamFactoryDecoder = new ApngStreamFactoryDecoder(apngBufferCacheDecoder); + ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder(); + StreamApngDecoder streamApngDecoder = new StreamApngDecoder(byteBufferApngDecoder); + StreamFactoryApngDecoder streamFactoryApngDecoder = new StreamFactoryApngDecoder(byteBufferApngDecoder); - registry.prepend(InputStream.class, APNGDecoder.class, apngStreamCacheDecoder); - registry.prepend(InputStreamFactory.class, APNGDecoder.class, apngStreamFactoryDecoder); - registry.prepend(ByteBuffer.class, APNGDecoder.class, apngBufferCacheDecoder); + registry.prepend(InputStream.class, APNGDecoder.class, streamApngDecoder); + registry.prepend(InputStreamFactory.class, APNGDecoder.class, streamFactoryApngDecoder); + registry.prepend(ByteBuffer.class, APNGDecoder.class, byteBufferApngDecoder); registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret)); - registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, apngStreamCacheDecoder)); + registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, streamApngDecoder)); registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder()); registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());