mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
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:
committed by
Greyson Parrelli
parent
a549fff6fa
commit
784a64c353
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user