From 9281d8ed45591279b5fe646b136c956624a5da24 Mon Sep 17 00:00:00 2001 From: Sam Judd Date: Wed, 20 Nov 2019 10:17:56 -0800 Subject: [PATCH] Roll forward "Try using ParcelFileDescriptor for local files" with fixes PiperOrigin-RevId: 281546605 --- .../main/java/com/bumptech/glide/Glide.java | 33 +++- .../glide/load/ImageHeaderParserUtils.java | 157 +++++++++++++++--- .../glide/load/data/InputStreamRewinder.java | 6 +- .../data/ParcelFileDescriptorRewinder.java | 84 ++++++++++ .../load/resource/bitmap/Downsampler.java | 92 ++++++---- .../load/resource/bitmap/ImageReader.java | 108 ++++++++++++ .../ParcelFileDescriptorBitmapDecoder.java | 37 +++++ 7 files changed, 448 insertions(+), 69 deletions(-) create mode 100644 library/src/main/java/com/bumptech/glide/load/data/ParcelFileDescriptorRewinder.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/bitmap/ImageReader.java create mode 100644 library/src/main/java/com/bumptech/glide/load/resource/bitmap/ParcelFileDescriptorBitmapDecoder.java diff --git a/library/src/main/java/com/bumptech/glide/Glide.java b/library/src/main/java/com/bumptech/glide/Glide.java index 33d3e89765..f8ffd69c50 100644 --- a/library/src/main/java/com/bumptech/glide/Glide.java +++ b/library/src/main/java/com/bumptech/glide/Glide.java @@ -27,6 +27,7 @@ import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.data.InputStreamRewinder; +import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; @@ -63,6 +64,7 @@ import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser; import com.bumptech.glide.load.resource.bitmap.InputStreamBitmapImageDecoderResourceDecoder; +import com.bumptech.glide.load.resource.bitmap.ParcelFileDescriptorBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.ResourceBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.UnitBitmapDecoder; @@ -386,18 +388,17 @@ private static void throwIncorrectGlideModule(Exception e) { ResourceDecoder parcelFileDescriptorVideoDecoder = VideoDecoder.parcel(bitmapPool); + // TODO(judds): Make ParcelFileDescriptorBitmapDecoder work with ImageDecoder. + Downsampler downsampler = + new Downsampler( + registry.getImageHeaderParsers(), resources.getDisplayMetrics(), bitmapPool, arrayPool); + ResourceDecoder byteBufferBitmapDecoder; ResourceDecoder streamBitmapDecoder; if (isImageDecoderEnabledForBitmaps && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { streamBitmapDecoder = new InputStreamBitmapImageDecoderResourceDecoder(); byteBufferBitmapDecoder = new ByteBufferBitmapImageDecoderResourceDecoder(); } else { - Downsampler downsampler = - new Downsampler( - registry.getImageHeaderParsers(), - resources.getDisplayMetrics(), - bitmapPool, - arrayPool); byteBufferBitmapDecoder = new ByteBufferBitmapDecoder(downsampler); streamBitmapDecoder = new StreamBitmapDecoder(downsampler, arrayPool); } @@ -422,7 +423,17 @@ private static void throwIncorrectGlideModule(Exception e) { .append(InputStream.class, new StreamEncoder(arrayPool)) /* Bitmaps */ .append(Registry.BUCKET_BITMAP, ByteBuffer.class, Bitmap.class, byteBufferBitmapDecoder) - .append(Registry.BUCKET_BITMAP, InputStream.class, Bitmap.class, streamBitmapDecoder) + .append(Registry.BUCKET_BITMAP, InputStream.class, Bitmap.class, streamBitmapDecoder); + + if (ParcelFileDescriptorRewinder.isSupported()) { + registry.append( + Registry.BUCKET_BITMAP, + ParcelFileDescriptor.class, + Bitmap.class, + new ParcelFileDescriptorBitmapDecoder(downsampler)); + } + + registry .append( Registry.BUCKET_BITMAP, ParcelFileDescriptor.class, @@ -483,7 +494,13 @@ Uri.class, Bitmap.class, new ResourceBitmapDecoder(resourceDrawableDecoder, bitm // Compilation with Gradle requires the type to be specified for UnitModelLoader here. .append(File.class, File.class, UnitModelLoader.Factory.getInstance()) /* Models */ - .register(new InputStreamRewinder.Factory(arrayPool)) + .register(new InputStreamRewinder.Factory(arrayPool)); + + if (ParcelFileDescriptorRewinder.isSupported()) { + registry.register(new ParcelFileDescriptorRewinder.Factory()); + } + + registry .append(int.class, InputStream.class, resourceLoaderStreamFactory) .append(int.class, ParcelFileDescriptor.class, resourceLoaderFileDescriptorFactory) .append(Integer.class, InputStream.class, resourceLoaderStreamFactory) diff --git a/library/src/main/java/com/bumptech/glide/load/ImageHeaderParserUtils.java b/library/src/main/java/com/bumptech/glide/load/ImageHeaderParserUtils.java index 5230662dc8..3492303041 100644 --- a/library/src/main/java/com/bumptech/glide/load/ImageHeaderParserUtils.java +++ b/library/src/main/java/com/bumptech/glide/load/ImageHeaderParserUtils.java @@ -1,10 +1,14 @@ package com.bumptech.glide.load; +import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.bumptech.glide.load.ImageHeaderParser.ImageType; +import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -34,34 +38,83 @@ public static ImageType getType( } is.mark(MARK_READ_LIMIT); - //noinspection ForLoopReplaceableByForEach to improve perf - for (int i = 0, size = parsers.size(); i < size; i++) { - ImageHeaderParser parser = parsers.get(i); - try { - ImageType type = parser.getType(is); - if (type != ImageType.UNKNOWN) { - return type; - } - } finally { - is.reset(); - } - } - - return ImageType.UNKNOWN; + final InputStream finalIs = is; + return getTypeInternal( + parsers, + new TypeReader() { + @Override + public ImageType getType(ImageHeaderParser parser) throws IOException { + try { + return parser.getType(finalIs); + } finally { + finalIs.reset(); + } + } + }); } /** Returns the ImageType for the given ByteBuffer. */ @NonNull public static ImageType getType( - @NonNull List parsers, @Nullable ByteBuffer buffer) throws IOException { + @NonNull List parsers, @Nullable final ByteBuffer buffer) + throws IOException { if (buffer == null) { return ImageType.UNKNOWN; } + return getTypeInternal( + parsers, + new TypeReader() { + @Override + public ImageType getType(ImageHeaderParser parser) throws IOException { + return parser.getType(buffer); + } + }); + } + + @NonNull + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static ImageType getType( + @NonNull List parsers, + @NonNull final ParcelFileDescriptorRewinder parcelFileDescriptorRewinder, + @NonNull final ArrayPool byteArrayPool) + throws IOException { + return getTypeInternal( + parsers, + new TypeReader() { + @Override + public ImageType getType(ImageHeaderParser parser) throws IOException { + // Wrap the FileInputStream into a RecyclableBufferedInputStream to optimize I/O + // performance + InputStream is = null; + try { + is = + new RecyclableBufferedInputStream( + new FileInputStream( + parcelFileDescriptorRewinder.rewindAndGet().getFileDescriptor()), + byteArrayPool); + return parser.getType(is); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + // Ignored. + } + parcelFileDescriptorRewinder.rewindAndGet(); + } + } + }); + } + + @NonNull + private static ImageType getTypeInternal( + @NonNull List parsers, TypeReader reader) throws IOException { //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = parsers.size(); i < size; i++) { ImageHeaderParser parser = parsers.get(i); - ImageType type = parser.getType(buffer); + ImageType type = reader.getType(parser); if (type != ImageType.UNKNOWN) { return type; } @@ -74,7 +127,7 @@ public static ImageType getType( public static int getOrientation( @NonNull List parsers, @Nullable InputStream is, - @NonNull ArrayPool byteArrayPool) + @NonNull final ArrayPool byteArrayPool) throws IOException { if (is == null) { return ImageHeaderParser.UNKNOWN_ORIENTATION; @@ -85,19 +138,75 @@ public static int getOrientation( } is.mark(MARK_READ_LIMIT); + final InputStream finalIs = is; + return getOrientationInternal( + parsers, + new OrientationReader() { + @Override + public int getOrientation(ImageHeaderParser parser) throws IOException { + try { + return parser.getOrientation(finalIs, byteArrayPool); + } finally { + finalIs.reset(); + } + } + }); + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static int getOrientation( + @NonNull List parsers, + @NonNull final ParcelFileDescriptorRewinder parcelFileDescriptorRewinder, + @NonNull final ArrayPool byteArrayPool) + throws IOException { + return getOrientationInternal( + parsers, + new OrientationReader() { + @Override + public int getOrientation(ImageHeaderParser parser) throws IOException { + // Wrap the FileInputStream into a RecyclableBufferedInputStream to optimize I/O + // performance + InputStream is = null; + try { + is = + new RecyclableBufferedInputStream( + new FileInputStream( + parcelFileDescriptorRewinder.rewindAndGet().getFileDescriptor()), + byteArrayPool); + return parser.getOrientation(is, byteArrayPool); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + // Ignored. + } + parcelFileDescriptorRewinder.rewindAndGet(); + } + } + }); + } + + private static int getOrientationInternal( + @NonNull List parsers, OrientationReader reader) throws IOException { //noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = parsers.size(); i < size; i++) { ImageHeaderParser parser = parsers.get(i); - try { - int orientation = parser.getOrientation(is, byteArrayPool); - if (orientation != ImageHeaderParser.UNKNOWN_ORIENTATION) { - return orientation; - } - } finally { - is.reset(); + int orientation = reader.getOrientation(parser); + if (orientation != ImageHeaderParser.UNKNOWN_ORIENTATION) { + return orientation; } } return ImageHeaderParser.UNKNOWN_ORIENTATION; } + + private interface TypeReader { + ImageType getType(ImageHeaderParser parser) throws IOException; + } + + private interface OrientationReader { + int getOrientation(ImageHeaderParser parser) throws IOException; + } } diff --git a/library/src/main/java/com/bumptech/glide/load/data/InputStreamRewinder.java b/library/src/main/java/com/bumptech/glide/load/data/InputStreamRewinder.java index a326cbf9ed..c242e46c18 100644 --- a/library/src/main/java/com/bumptech/glide/load/data/InputStreamRewinder.java +++ b/library/src/main/java/com/bumptech/glide/load/data/InputStreamRewinder.java @@ -18,7 +18,7 @@ public final class InputStreamRewinder implements DataRewinder { private final RecyclableBufferedInputStream bufferedStream; @Synthetic - InputStreamRewinder(InputStream is, ArrayPool byteArrayPool) { + public InputStreamRewinder(InputStream is, ArrayPool byteArrayPool) { // We don't check is.markSupported() here because RecyclableBufferedInputStream allows resetting // after exceeding MARK_READ_LIMIT, which other InputStreams don't guarantee. bufferedStream = new RecyclableBufferedInputStream(is, byteArrayPool); @@ -37,6 +37,10 @@ public void cleanup() { bufferedStream.release(); } + public void fixMarkLimits() { + bufferedStream.fixMarkLimit(); + } + /** * Factory for producing {@link com.bumptech.glide.load.data.InputStreamRewinder}s from {@link * java.io.InputStream}s. diff --git a/library/src/main/java/com/bumptech/glide/load/data/ParcelFileDescriptorRewinder.java b/library/src/main/java/com/bumptech/glide/load/data/ParcelFileDescriptorRewinder.java new file mode 100644 index 0000000000..bd2be60a40 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/data/ParcelFileDescriptorRewinder.java @@ -0,0 +1,84 @@ +package com.bumptech.glide.load.data; + +import static android.system.OsConstants.SEEK_SET; + +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import java.io.IOException; + +/** + * Implementation for {@link ParcelFileDescriptor}s that rewinds file descriptors by seeking to 0. + */ +public final class ParcelFileDescriptorRewinder implements DataRewinder { + + private final InternalRewinder rewinder; + + public static boolean isSupported() { + // Os.lseek() is only supported on API 21+. + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public ParcelFileDescriptorRewinder(ParcelFileDescriptor parcelFileDescriptor) { + rewinder = new InternalRewinder(parcelFileDescriptor); + } + + @NonNull + @Override + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public ParcelFileDescriptor rewindAndGet() throws IOException { + return rewinder.rewind(); + } + + @Override + public void cleanup() { + // Do nothing. + } + + /** + * Factory for producing {@link ParcelFileDescriptorRewinder}s from {@link ParcelFileDescriptor}s. + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public static final class Factory implements DataRewinder.Factory { + + @NonNull + @Override + public DataRewinder build( + @NonNull ParcelFileDescriptor parcelFileDescriptor) { + return new ParcelFileDescriptorRewinder(parcelFileDescriptor); + } + + @NonNull + @Override + public Class getDataClass() { + return ParcelFileDescriptor.class; + } + } + + /** + * Catching ErrnoException cannot be done in classes that are loaded on APIs < Lollipop. To make + * sure that we do not do so, we catch inside this inner class instead of the outer class. The + * only reason this class exists is to avoid VerifyError on older APIs. + */ + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + private static final class InternalRewinder { + private final ParcelFileDescriptor parcelFileDescriptor; + + InternalRewinder(ParcelFileDescriptor parcelFileDescriptor) { + this.parcelFileDescriptor = parcelFileDescriptor; + } + + ParcelFileDescriptor rewind() throws IOException { + try { + Os.lseek(parcelFileDescriptor.getFileDescriptor(), 0, SEEK_SET); + } catch (ErrnoException e) { + throw new IOException(e); + } + return parcelFileDescriptor; + } + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java index 98a1e46470..751ffea352 100644 --- a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/Downsampler.java @@ -6,16 +6,18 @@ import android.graphics.BitmapFactory; import android.graphics.ColorSpace; import android.os.Build; +import android.os.ParcelFileDescriptor; import android.util.DisplayMetrics; import android.util.Log; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; -import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.PreferredColorSpace; +import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; @@ -42,6 +44,7 @@ */ public final class Downsampler { static final String TAG = "Downsampler"; + /** * Indicates the {@link com.bumptech.glide.load.DecodeFormat} that will be used in conjunction * with the image format to determine the {@link android.graphics.Bitmap.Config} to provide to @@ -130,9 +133,6 @@ public void onDecodeComplete(BitmapPool bitmapPool, Bitmap downsampled) { ImageHeaderParser.ImageType.PNG_A, ImageHeaderParser.ImageType.PNG)); private static final Queue OPTIONS_QUEUE = Util.createQueue(0); - // 10MB. This is the max image header size we can handle, we preallocate a much smaller buffer - // but will resize up to this amount if necessary. - private static final int MARK_POSITION = 10 * 1024 * 1024; private final BitmapPool bitmapPool; private final DisplayMetrics displayMetrics; @@ -161,6 +161,10 @@ public boolean handles(@SuppressWarnings("unused") ByteBuffer byteBuffer) { return true; } + public boolean handles(@SuppressWarnings("unused") ParcelFileDescriptor source) { + return ParcelFileDescriptorRewinder.isSupported(); + } + /** * Returns a Bitmap decoded from the given {@link InputStream} that is rotated to match any EXIF * data present in the stream and that is downsampled according to the given dimensions and any @@ -183,10 +187,6 @@ public Resource decode(InputStream is, int outWidth, int outHeight, Opti * of the image for the given InputStream is available, the operation is much less expensive in * terms of memory. * - *

The provided {@link java.io.InputStream} must return true from {@link - * java.io.InputStream#markSupported()} and is expected to support a reasonably large mark limit - * to accommodate reading large image headers (~5MB). - * * @param is An {@link InputStream} 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. @@ -197,7 +197,6 @@ public Resource decode(InputStream is, int outWidth, int outHeight, Opti * @return A new bitmap containing the image from the given InputStream, or recycle if recycle is * not null. */ - @SuppressWarnings({"resource", "deprecation"}) public Resource decode( InputStream is, int requestedWidth, @@ -205,9 +204,34 @@ public Resource decode( Options options, DecodeCallbacks callbacks) throws IOException { - Preconditions.checkArgument( - is.markSupported(), "You must provide an InputStream that supports" + " mark()"); + return decode( + new ImageReader.InputStreamImageReader(is, parsers, byteArrayPool), + requestedWidth, + requestedHeight, + options, + callbacks); + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + public Resource decode( + ParcelFileDescriptor parcelFileDescriptor, int outWidth, int outHeight, Options options) + throws IOException { + return decode( + new ImageReader.ParcelFileDescriptorImageReader( + parcelFileDescriptor, parsers, byteArrayPool), + outWidth, + outHeight, + options, + EMPTY_CALLBACKS); + } + private Resource decode( + ImageReader imageReader, + int requestedWidth, + int requestedHeight, + Options options, + DecodeCallbacks callbacks) + throws IOException { byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class); BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions(); bitmapFactoryOptions.inTempStorage = bytesForOptions; @@ -222,7 +246,7 @@ public Resource decode( try { Bitmap result = decodeFromWrappedStreams( - is, + imageReader, bitmapFactoryOptions, downsampleStrategy, decodeFormat, @@ -240,7 +264,7 @@ public Resource decode( } private Bitmap decodeFromWrappedStreams( - InputStream is, + ImageReader imageReader, BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, DecodeFormat decodeFormat, @@ -253,7 +277,7 @@ private Bitmap decodeFromWrappedStreams( throws IOException { long startTime = LogTime.getLogTime(); - int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool); + int[] sourceDimensions = getDimensions(imageReader, options, callbacks, bitmapPool); int sourceWidth = sourceDimensions[0]; int sourceHeight = sourceDimensions[1]; String sourceMimeType = options.outMimeType; @@ -266,7 +290,7 @@ private Bitmap decodeFromWrappedStreams( isHardwareConfigAllowed = false; } - int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool); + int orientation = imageReader.getImageOrientation(); int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation); boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation); @@ -279,11 +303,11 @@ private Bitmap decodeFromWrappedStreams( ? (isRotationRequired(degreesToRotate) ? sourceWidth : sourceHeight) : requestedHeight; - ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool); + ImageType imageType = imageReader.getImageType(); calculateScaling( imageType, - is, + imageReader, callbacks, bitmapPool, downsampleStrategy, @@ -294,7 +318,7 @@ private Bitmap decodeFromWrappedStreams( targetHeight, options); calculateConfig( - is, + imageReader, decodeFormat, isHardwareConfigAllowed, isExifOrientationRequired, @@ -363,7 +387,7 @@ private Bitmap decodeFromWrappedStreams( options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); } - Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool); + Bitmap downsampled = decodeStream(imageReader, options, callbacks, bitmapPool); callbacks.onDecodeComplete(bitmapPool, downsampled); if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -395,7 +419,7 @@ private Bitmap decodeFromWrappedStreams( private static void calculateScaling( ImageType imageType, - InputStream is, + ImageReader imageReader, DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool, DownsampleStrategy downsampleStrategy, @@ -524,7 +548,7 @@ private static void calculateScaling( || orientedSourceHeight % powerOfTwoSampleSize != 0) { // If we're not confident the image is in one of our types, fall back to checking the // dimensions again. inJustDecodeBounds decodes do obey inSampleSize. - int[] dimensions = getDimensions(is, options, decodeCallbacks, bitmapPool); + int[] dimensions = getDimensions(imageReader, options, decodeCallbacks, bitmapPool); // Power of two downsampling in BitmapFactory uses a variety of random factors to determine // rounding that we can't reliably replicate for all image formats. Use ceiling here to make // sure that we at least provide a Bitmap that's large enough to fit the content we're going @@ -626,7 +650,7 @@ private boolean shouldUsePool(ImageType imageType) { @SuppressWarnings("deprecation") private void calculateConfig( - InputStream is, + ImageReader imageReader, DecodeFormat format, boolean isHardwareConfigAllowed, boolean isExifOrientationRequired, @@ -652,7 +676,7 @@ private void calculateConfig( boolean hasAlpha = false; try { - hasAlpha = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool).hasAlpha(); + hasAlpha = imageReader.getImageType().hasAlpha(); } catch (IOException e) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d( @@ -674,39 +698,39 @@ private void calculateConfig( /** * A method for getting the dimensions of an image from the given InputStream. * - * @param is The InputStream representing the image. + * @param imageReader The {@link ImageReader} representing the image. * @param options The options to pass to {@link BitmapFactory#decodeStream(java.io.InputStream, * android.graphics.Rect, android.graphics.BitmapFactory.Options)}. * @return an array containing the dimensions of the image in the form {width, height}. */ private static int[] getDimensions( - InputStream is, + ImageReader imageReader, BitmapFactory.Options options, DecodeCallbacks decodeCallbacks, BitmapPool bitmapPool) throws IOException { options.inJustDecodeBounds = true; - decodeStream(is, options, decodeCallbacks, bitmapPool); + decodeStream(imageReader, options, decodeCallbacks, bitmapPool); options.inJustDecodeBounds = false; return new int[] {options.outWidth, options.outHeight}; } private static Bitmap decodeStream( - InputStream is, + ImageReader imageReader, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException { - if (options.inJustDecodeBounds) { - is.mark(MARK_POSITION); - } else { + if (!options.inJustDecodeBounds) { // Once we've read the image header, we no longer need to allow the buffer to expand in // size. To avoid unnecessary allocations reading image data, we fix the mark limit so that it // is no larger than our current buffer size here. We need to do so immediately before // decoding the full image to avoid having our mark limit overridden by other calls to // mark and reset. See issue #225. callbacks.onObtainBounds(); + imageReader.stopGrowingBuffers(); } + // BitmapFactory.Options out* variables are reset by most calls to decodeStream, successful or // otherwise, so capture here in case we log below. int sourceWidth = options.outWidth; @@ -715,7 +739,7 @@ private static Bitmap decodeStream( final Bitmap result; TransformationUtils.getBitmapDrawableLock().lock(); try { - result = BitmapFactory.decodeStream(is, null, options); + result = imageReader.decodeBitmap(options); } catch (IllegalArgumentException e) { IOException bitmapAssertionException = newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options); @@ -727,10 +751,9 @@ private static Bitmap decodeStream( } if (options.inBitmap != null) { try { - is.reset(); bitmapPool.put(options.inBitmap); options.inBitmap = null; - return decodeStream(is, options, callbacks, bitmapPool); + return decodeStream(imageReader, options, callbacks, bitmapPool); } catch (IOException resetException) { throw bitmapAssertionException; } @@ -740,9 +763,6 @@ private static Bitmap decodeStream( TransformationUtils.getBitmapDrawableLock().unlock(); } - if (options.inJustDecodeBounds) { - is.reset(); - } return result; } diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/ImageReader.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/ImageReader.java new file mode 100644 index 0000000000..167d4dd2c2 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/ImageReader.java @@ -0,0 +1,108 @@ +package com.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import com.bumptech.glide.load.ImageHeaderParser; +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; +import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; +import com.bumptech.glide.util.Preconditions; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * This is a helper class for {@link Downsampler} that abstracts out image operations from the input + * type wrapped into a {@link DataRewinder}. + */ +interface ImageReader { + @Nullable + Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException; + + ImageHeaderParser.ImageType getImageType() throws IOException; + + int getImageOrientation() throws IOException; + + void stopGrowingBuffers(); + + final class InputStreamImageReader implements ImageReader { + private final InputStreamRewinder dataRewinder; + private final ArrayPool byteArrayPool; + private final List parsers; + + InputStreamImageReader( + InputStream is, List parsers, ArrayPool byteArrayPool) { + this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool); + this.parsers = Preconditions.checkNotNull(parsers); + + dataRewinder = new InputStreamRewinder(is, byteArrayPool); + } + + @Nullable + @Override + public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException { + return BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options); + } + + @Override + public ImageHeaderParser.ImageType getImageType() throws IOException { + return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool); + } + + @Override + public int getImageOrientation() throws IOException { + return ImageHeaderParserUtils.getOrientation( + parsers, dataRewinder.rewindAndGet(), byteArrayPool); + } + + @Override + public void stopGrowingBuffers() { + dataRewinder.fixMarkLimits(); + } + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + final class ParcelFileDescriptorImageReader implements ImageReader { + private final ArrayPool byteArrayPool; + private final List parsers; + private final ParcelFileDescriptorRewinder dataRewinder; + + ParcelFileDescriptorImageReader( + ParcelFileDescriptor parcelFileDescriptor, + List parsers, + ArrayPool byteArrayPool) { + this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool); + this.parsers = Preconditions.checkNotNull(parsers); + + dataRewinder = new ParcelFileDescriptorRewinder(parcelFileDescriptor); + } + + @Nullable + @Override + public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException { + return BitmapFactory.decodeFileDescriptor( + dataRewinder.rewindAndGet().getFileDescriptor(), null, options); + } + + @Override + public ImageHeaderParser.ImageType getImageType() throws IOException { + return ImageHeaderParserUtils.getType(parsers, dataRewinder, byteArrayPool); + } + + @Override + public int getImageOrientation() throws IOException { + return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder, byteArrayPool); + } + + @Override + public void stopGrowingBuffers() { + // Nothing to do here. + } + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/ParcelFileDescriptorBitmapDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/ParcelFileDescriptorBitmapDecoder.java new file mode 100644 index 0000000000..b92fa1983d --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/ParcelFileDescriptorBitmapDecoder.java @@ -0,0 +1,37 @@ +package com.bumptech.glide.load.resource.bitmap; + +import android.graphics.Bitmap; +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.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; +import java.io.IOException; + +/** Decodes {@link Bitmap}s from {@link ParcelFileDescriptor}s. */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +public final class ParcelFileDescriptorBitmapDecoder + implements ResourceDecoder { + + private final Downsampler downsampler; + + public ParcelFileDescriptorBitmapDecoder(Downsampler downsampler) { + this.downsampler = downsampler; + } + + @Override + public boolean handles(@NonNull ParcelFileDescriptor source, @NonNull Options options) { + return downsampler.handles(source); + } + + @Nullable + @Override + public Resource decode( + @NonNull ParcelFileDescriptor source, int width, int height, @NonNull Options options) + throws IOException { + return downsampler.decode(source, width, height, options); + } +}