diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/GlideBitmapFactory.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/GlideBitmapFactory.java index eed68a8bbd..e2ebd90365 100644 --- a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/GlideBitmapFactory.java +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/GlideBitmapFactory.java @@ -4,6 +4,7 @@ import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; +import android.graphics.BitmapRegionDecoder; import android.graphics.Canvas; import android.graphics.ColorMatrixColorFilter; import android.graphics.Gainmap; @@ -18,6 +19,7 @@ import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import com.bumptech.glide.util.Preconditions; import java.io.FileDescriptor; +import java.io.IOException; import java.io.InputStream; /** @@ -37,6 +39,8 @@ */ final class GlideBitmapFactory { + private static final String TAG = "GlideBitmapFactory"; + private GlideBitmapFactory() {} /** Wrapper for {@link BitmapFactory#decodeStream}. */ @@ -72,7 +76,10 @@ public static Bitmap decodeFileDescriptor( /** * Returns a decoded bitmap for the input stream, ensuring that any associated gainmap is decoded - * without errors on Android U if it is a valid gainamp. + * without being silently dropped on Android U. + * + *

If the input stream does not reference an image with a gainmap, then this method simply + * returns a hardware bitmap. * *

This method safely wraps BitmapFactory#decodeStream(InputStream, Rect, Options)} on Android * U. @@ -85,8 +92,33 @@ public static Bitmap decodeFileDescriptor( private static Bitmap safeDecodeHardwareBitmapWithGainmap( InputStream inputStream, Options options) { Preconditions.checkArgument(options.inPreferredConfig == Config.HARDWARE); - Bitmap softwareBitmap = null; + // Check to make sure that the inputStream has an associated bitmap using BitmapRegionDecoder, + // which does not require the workaround. + BitmapRegionDecoder bitmapRegionDecoder = null; + try { + bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream); + if (bitmapRegionDecoder == null) { + return null; + } + boolean bitmapNeedsWorkaround = efficientHasGainmap(bitmapRegionDecoder, options); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "calculateNeedsGainmapDecodeWorkaround(inputStream)=" + bitmapNeedsWorkaround); + } + if (!bitmapNeedsWorkaround) { + return BitmapFactory.decodeStream(inputStream, /* outPadding= */ null, options); + } + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "safeDecodeHardwareBitmapWithGainmap(inputStream)", e); + } + return null; + } finally { + if (bitmapRegionDecoder != null) { + bitmapRegionDecoder.recycle(); + } + } options.inPreferredConfig = Config.ARGB_8888; + Bitmap softwareBitmap = null; try { softwareBitmap = BitmapFactory.decodeStream(inputStream, /* outPadding= */ null, options); if (softwareBitmap == null) { @@ -103,7 +135,10 @@ private static Bitmap safeDecodeHardwareBitmapWithGainmap( /** * Returns a decoded bitmap for the input byte array, ensuring that any associated gainmap is - * decoded without errors on Android U if it is a valid gainmap. + * decoded without being silently dropped on Android U. + * + *

If the input bytes do not reference an image with a gainmap, then this method simply returns + * a hardware bitmap. * *

This method safely wraps BitmapFactory#decodeByteArray(byte[], int, int)} on Android U. * @@ -117,8 +152,30 @@ private static Bitmap safeDecodeHardwareBitmapWithGainmap( @Nullable private static Bitmap safeDecodeHardwareBitmapWithGainmap(byte[] bytes, Options options) { Preconditions.checkArgument(options.inPreferredConfig == Config.HARDWARE); - Bitmap softwareBitmap = null; + // Check to make sure that the inputStream has an associated bitmap using BitmapRegionDecoder, + // which does not require the workaround. + BitmapRegionDecoder bitmapRegionDecoder = null; + try { + bitmapRegionDecoder = BitmapRegionDecoder.newInstance(bytes, /* offset= */ 0, bytes.length); + boolean bitmapNeedsWorkaround = efficientHasGainmap(bitmapRegionDecoder, options); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "calculateNeedsGainmapDecodeWorkaround(bytes)=" + bitmapNeedsWorkaround); + } + if (!bitmapNeedsWorkaround) { + return BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options); + } + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "safeDecodeHardwareBitmapWithGainmap(bytes)", e); + } + return null; + } finally { + if (bitmapRegionDecoder != null) { + bitmapRegionDecoder.recycle(); + } + } options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap softwareBitmap = null; try { softwareBitmap = BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options); if (softwareBitmap == null) { @@ -135,7 +192,10 @@ private static Bitmap safeDecodeHardwareBitmapWithGainmap(byte[] bytes, Options /** * Returns a decoded bitmap for the input file descriptor, ensuring that any associated gainmap is - * decoded without errors on Android U if it is a valid gainmap. + * decoded without being silently dropped on Android U. + * + *

If the input file descriptor does not reference an image with a gainmap, then this method + * simply returns a hardware bitmap. * *

This method safely wraps {@link BitmapFactory#decodeFileDescriptor(FileDescriptor, Rect, * Options)} on Android U. @@ -151,8 +211,30 @@ private static Bitmap safeDecodeHardwareBitmapWithGainmap(byte[] bytes, Options private static Bitmap safeDecodeHardwareBitmapWithGainmap( FileDescriptor fileDescriptor, Options options) { Preconditions.checkArgument(options.inPreferredConfig == Config.HARDWARE); - Bitmap softwareBitmap = null; + // Check to make sure that the inputStream has an associated bitmap using BitmapRegionDecoder, + // which does not require the workaround. + BitmapRegionDecoder bitmapRegionDecoder = null; + try { + bitmapRegionDecoder = BitmapRegionDecoder.newInstance(fileDescriptor, /* isShareable= */ false); + boolean bitmapNeedsWorkaround = efficientHasGainmap(bitmapRegionDecoder, options); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "calculateNeedsGainmapDecodeWorkaround(fd)=" + bitmapNeedsWorkaround); + } + if (!bitmapNeedsWorkaround) { + return BitmapFactory.decodeFileDescriptor(fileDescriptor, /* outPadding= */ null, options); + } + } catch (IOException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "safeDecodeHardwareBitmapWithGainmap(fd)", e); + } + return null; + } finally { + if (bitmapRegionDecoder != null) { + bitmapRegionDecoder.recycle(); + } + } options.inPreferredConfig = Bitmap.Config.ARGB_8888; + Bitmap softwareBitmap = null; try { softwareBitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor, /* outPadding= */ null, options); @@ -168,30 +250,65 @@ private static Bitmap safeDecodeHardwareBitmapWithGainmap( } } + /** + * Returns whether the bitmap associated with the input {@link BitmapRegionDecoder} has a gainmap. + * + *

This method is "efficient" in the sense that it only samples a small region of the image to + * make this determination. + * + * @param bitmapRegionDecoder Must be initialized and not recycled. The caller of this method is + * responsible for recycling the decoder. + */ + @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) + private static boolean efficientHasGainmap(BitmapRegionDecoder bitmapRegionDecoder, Options options) { + // Check to make sure that the inputStream has an associated bitmap using BitmapRegionDecoder, + // which does not require the workaround. + Bitmap onePercentBitmap = null; + try { + // We are using a 1% Rect instead of 1x1 Rect because BitmapRegionDecoder will drop some + // gainmaps if a 1x1 region is requested, whether or not the bitmap config is hardware or + // software. + Rect onePercentRect = + getOnePercentRect(bitmapRegionDecoder.getWidth(), bitmapRegionDecoder.getHeight()); + onePercentBitmap = bitmapRegionDecoder.decodeRegion(onePercentRect, options); + return onePercentBitmap != null && onePercentBitmap.hasGainmap(); + } finally { + if (onePercentBitmap != null) { + onePercentBitmap.recycle(); + } + } + } + /** * Returns a decoded bitmap for the input software bitmap, ensuring that any associated gainmap is * decoded without errors on Android U if it is a valid gainmap. * - * @param softwareBitmap The bitmap to be decoded. Must not be a hardware bitmap. + * @param softwareBitmap The bitmap to be decoded. Must not be a hardware bitmap. The caller of + * this method is responsible for recycling this bitmap. * @throws IllegalArgumentException if {@link Options#inPreferredConfig} is set to any state other * than {@link Config#HARDWARE}. */ @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE) @Nullable private static Bitmap safeDecodeBitmapWithGainmap(Bitmap softwareBitmap) { - try { - Gainmap gainmap = softwareBitmap.getGainmap(); - if (gainmap != null) { - Bitmap gainmapContents = gainmap.getGainmapContents(); - if (gainmapContents.getConfig() == Config.ALPHA_8) { - softwareBitmap.setGainmap( - GainmapCopier.convertSingleChannelGainmapToTripleChannelGainmap(gainmap)); - } + Gainmap gainmap = softwareBitmap.getGainmap(); + if (gainmap != null) { + Bitmap gainmapContents = gainmap.getGainmapContents(); + if (gainmapContents.getConfig() == Config.ALPHA_8) { + softwareBitmap.setGainmap( + GainmapCopier.convertSingleChannelGainmapToTripleChannelGainmap(gainmap)); } - return softwareBitmap.copy(Config.HARDWARE, /* isMutable= */ false); - } finally { - softwareBitmap.recycle(); } + return softwareBitmap.copy(Config.HARDWARE, /* isMutable= */ false); + } + + /** + * Returns a {@link Rect} with the input width and height scaled at 1%. + * + *

The minimum returned size is {@code 1x1}. + */ + private static Rect getOnePercentRect(int width, int height) { + return new Rect(0, 0, Math.max((width - 1) / 100, 1), Math.max((height - 1) / 100, 1)); } /** Utils to copy gainmaps. */ @@ -202,10 +319,10 @@ private static final class GainmapCopier { private static final ColorMatrixColorFilter OPAQUE_FILTER = new ColorMatrixColorFilter( new float[] { - 0f, 0f, 0f, 1f, 0f, - 0f, 0f, 0f, 1f, 0f, - 0f, 0f, 0f, 1f, 0f, - 0f, 0f, 0f, 0f, 255f + 0f, 0f, 0f, 1f, 0f, + 0f, 0f, 0f, 1f, 0f, + 0f, 0f, 0f, 1f, 0f, + 0f, 0f, 0f, 0f, 255f }); private GainmapCopier() {} @@ -315,8 +432,8 @@ private static boolean calculateNeedsGainmapDecodeWorkaround() { Bitmap a8HardwareBitmap = a8Source.copy(Config.HARDWARE, /* isMutable= */ false); a8Source.recycle(); boolean needsGainmapDecodeWorkaround = a8HardwareBitmap == null; - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "calculateNeedsGainmapDecodeWorkaround=" + needsGainmapDecodeWorkaround); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "calculateNeedsGainmapDecodeWorkaround=" + needsGainmapDecodeWorkaround); } if (a8HardwareBitmap != null) { a8HardwareBitmap.recycle();