Fix large image file loading failures.

Replaces `DecryptableStreamUriLoader` with `DecryptableUriStreamLoader`, which addresses `InvalidMarkException` errors that were occurring when loading large image files with Glide. This new model loader provides a more robust approach via multiple fallback mechanisms to try to recover gracefully from errors related to displaying large images.
This commit is contained in:
jeffrey-signal
2025-08-08 10:28:08 -04:00
committed by Greyson Parrelli
parent a549fff6fa
commit 784a64c353
10 changed files with 378 additions and 98 deletions

View File

@@ -14,6 +14,7 @@ import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.DisplayMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
@@ -39,6 +40,7 @@ import com.bumptech.glide.util.Preconditions;
import com.bumptech.glide.util.Util;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.mms.InputStreamFactory;
import java.io.File;
import java.io.IOException;
@@ -184,22 +186,17 @@ public final class Downsampler {
* data present in the stream and that is downsampled according to the given dimensions and any
* provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option.
*
* @see #decode(InputStream, int, int, Options, DecodeCallbacks)
* @see #decode(InputStreamFactory, int, int, Options, DecodeCallbacks)
*/
public Resource<Bitmap> decode(InputStream is, int outWidth, int outHeight, Options options)
throws IOException
{
return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS);
public Resource<Bitmap> decode(@NonNull InputStreamFactory inputStreamFactory, int outWidth, int outHeight, Options options) throws IOException {
return decode(inputStreamFactory, outWidth, outHeight, options, EMPTY_CALLBACKS);
}
/**
* Identical to {@link #decode(InputStream, int, int, Options)}, except that it accepts a {@link
* ByteBuffer} in place of an {@link InputStream}.
* Identical to {@link #decode(InputStreamFactory, int, int, Options)}, except that it accepts a {@link
* ByteBuffer} in place of an {@link InputStreamFactory}.
*/
public Resource<Bitmap> decode(
ByteBuffer buffer, int requestedWidth, int requestedHeight, Options options)
throws IOException
{
public Resource<Bitmap> decode(ByteBuffer buffer, int requestedWidth, int requestedHeight, Options options) throws IOException {
return decode(
new ImageReader.ByteBufferReader(buffer, parsers, byteArrayPool),
requestedWidth,
@@ -218,7 +215,7 @@ public final class Downsampler {
* of the image for the given InputStream is available, the operation is much less expensive in
* terms of memory.
*
* @param is An {@link InputStream} to the data for the image.
* @param inputStreamFactory An {@link InputStreamFactory} to the data for the image.
* @param requestedWidth The width the final image should be close to.
* @param requestedHeight The height the final image should be close to.
* @param options A set of options that may contain one or more supported options that influence
@@ -229,7 +226,7 @@ public final class Downsampler {
* not null.
*/
public Resource<Bitmap> decode(
InputStream is,
@NonNull InputStreamFactory inputStreamFactory,
int requestedWidth,
int requestedHeight,
Options options,
@@ -237,7 +234,7 @@ public final class Downsampler {
throws IOException
{
return decode(
new ImageReader.InputStreamImageReader(is, parsers, byteArrayPool),
new ImageReader.InputStreamImageReader(inputStreamFactory, parsers, byteArrayPool),
requestedWidth,
requestedHeight,
options,

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool
import org.thoughtcrime.securesms.mms.InputStreamFactory
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
typealias GlideImageHeaderParserUtils = com.bumptech.glide.load.ImageHeaderParserUtils
object ImageHeaderParserUtils {
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
inputStream: InputStream,
byteArrayPool: ArrayPool
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, inputStream, byteArrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
buffer: ByteBuffer
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, buffer)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder,
byteArrayPool: ArrayPool
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientationWithFallbacks(
parsers: List<ImageHeaderParser>,
buffer: ByteBuffer,
arrayPool: ArrayPool
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, buffer, arrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientation(
parsers: List<ImageHeaderParser>,
parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder,
byteArrayPool: ArrayPool
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
@JvmStatic
@Throws(IOException::class)
fun getOrientation(
parsers: List<ImageHeaderParser>,
inputStream: InputStream,
byteArrayPool: ArrayPool
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, inputStream, byteArrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientationWithFallbacks(
parsers: List<ImageHeaderParser>,
inputStreamFactory: InputStreamFactory,
byteArrayPool: ArrayPool
): Int {
val orientationFromParsers = getOrientationFromParsers(
parsers = parsers,
inputStream = inputStreamFactory.createRecyclable(byteArrayPool),
byteArrayPool = byteArrayPool
)
if (orientationFromParsers != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromParsers
val orientationFromExif = getOrientationFromExif(inputStream = inputStreamFactory.createRecyclable(byteArrayPool))
if (orientationFromExif != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromExif
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
private fun getOrientationFromParsers(
parsers: List<ImageHeaderParser>,
inputStream: InputStream?,
byteArrayPool: ArrayPool
): Int {
if (inputStream == null) {
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
return getOrientation(
parsers = parsers,
readOrientation = { parser -> parser.getOrientation(inputStream, byteArrayPool) }
)
}
private fun getOrientationFromExif(inputStream: InputStream): Int {
return BitmapUtil.getExifOrientation(ExifInterface(inputStream))
}
private fun getOrientation(
parsers: List<ImageHeaderParser>,
readOrientation: (ImageHeaderParser) -> Int
): Int {
parsers.forEach { parser ->
val orientation = readOrientation(parser)
if (orientation != ImageHeaderParser.UNKNOWN_ORIENTATION) {
return orientation
}
}
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
}

View File

@@ -11,12 +11,12 @@ import android.graphics.BitmapFactory.Options;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.ImageHeaderParserUtils;
import com.bumptech.glide.load.data.DataRewinder;
import com.bumptech.glide.load.data.InputStreamRewinder;
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder;
@@ -25,6 +25,8 @@ import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream;
import com.bumptech.glide.util.ByteBufferUtil;
import com.bumptech.glide.util.Preconditions;
import org.thoughtcrime.securesms.mms.InputStreamFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -72,7 +74,7 @@ interface ImageReader {
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientation(parsers, ByteBuffer.wrap(bytes), byteArrayPool);
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBuffer.wrap(bytes), byteArrayPool);
}
@Override
@@ -128,19 +130,7 @@ interface ImageReader {
@Override
public int getImageOrientation() throws IOException {
InputStream is = null;
try {
is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool);
return ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, InputStreamFactory.build(file), byteArrayPool);
}
@Override
@@ -172,8 +162,7 @@ interface ImageReader {
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientation(
parsers, ByteBufferUtil.rewind(buffer), byteArrayPool);
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBufferUtil.rewind(buffer), byteArrayPool);
}
@Override
@@ -188,31 +177,42 @@ interface ImageReader {
private final InputStreamRewinder dataRewinder;
private final ArrayPool byteArrayPool;
private final List<ImageHeaderParser> parsers;
private final InputStreamFactory inputStreamFactory;
InputStreamImageReader(
InputStream is, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool)
{
InputStreamImageReader(@NonNull InputStreamFactory inputStreamFactory, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
this.parsers = Preconditions.checkNotNull(parsers);
dataRewinder = new InputStreamRewinder(is, byteArrayPool);
this.inputStreamFactory = inputStreamFactory;
this.dataRewinder = new InputStreamRewinder(inputStreamFactory.create(), byteArrayPool);
}
@Nullable
@Override
public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException {
return BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options);
public Bitmap decodeBitmap(@NonNull BitmapFactory.Options options) {
try {
return BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options);
} catch (IOException e) {
return BitmapFactory.decodeStream(inputStreamFactory.createRecyclable(byteArrayPool), null, options);
}
}
@Override
public ImageHeaderParser.ImageType getImageType() throws IOException {
return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool);
try {
return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool);
} catch (IOException e) {
return ImageHeaderParserUtils.getType(parsers, inputStreamFactory.createRecyclable(byteArrayPool), byteArrayPool);
}
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientation(
parsers, dataRewinder.rewindAndGet(), byteArrayPool);
try {
return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder.rewindAndGet(), byteArrayPool);
} catch (IOException e) {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, inputStreamFactory, byteArrayPool);
}
}
@Override