From b979db9acb19ecad53ea30454a870d135ee6c2c6 Mon Sep 17 00:00:00 2001 From: Constantin Piber <59023762+cpiber@users.noreply.github.com> Date: Sun, 11 Jan 2026 22:47:52 +0100 Subject: [PATCH] BitmapFactory: Support basic options (#1853) * BitmapFactory: Support basic options * Bitmap: Support querying image type * Bitmap: Support all BufferedImage image types Required to be able to construct a bitmap with exactly the same parameters --- .../main/java/android/graphics/Bitmap.java | 77 ++- .../java/android/graphics/BitmapFactory.java | 480 +++++++++++++++++- 2 files changed, 543 insertions(+), 14 deletions(-) diff --git a/AndroidCompat/src/main/java/android/graphics/Bitmap.java b/AndroidCompat/src/main/java/android/graphics/Bitmap.java index 7101bb75..07ace14e 100644 --- a/AndroidCompat/src/main/java/android/graphics/Bitmap.java +++ b/AndroidCompat/src/main/java/android/graphics/Bitmap.java @@ -2,6 +2,8 @@ package android.graphics; import android.annotation.ColorInt; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Log; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.IOException; @@ -58,7 +60,23 @@ public final class Bitmap { ARGB_8888(5), RGBA_F16(6), HARDWARE(7), - RGBA_1010102(8); + RGBA_1010102(8), + + _TYPE_3BYTE_BGR(BufferedImage.TYPE_3BYTE_BGR), + _TYPE_4BYTE_ABGR(BufferedImage.TYPE_4BYTE_ABGR), + _TYPE_4BYTE_ABGR_PRE(BufferedImage.TYPE_4BYTE_ABGR_PRE), + _TYPE_BYTE_BINARY(BufferedImage.TYPE_BYTE_BINARY), + _TYPE_BYTE_GRAY(BufferedImage.TYPE_BYTE_GRAY), + _TYPE_BYTE_INDEXED(BufferedImage.TYPE_BYTE_INDEXED), + _TYPE_CUSTOM(BufferedImage.TYPE_CUSTOM), + _TYPE_INT_ARGB(BufferedImage.TYPE_INT_ARGB), + _TYPE_INT_ARGB_PRE(BufferedImage.TYPE_INT_ARGB_PRE), + _TYPE_INT_BGR(BufferedImage.TYPE_INT_BGR), + _TYPE_INT_RGB(BufferedImage.TYPE_INT_RGB), + _TYPE_USHORT_555_RGB(BufferedImage.TYPE_USHORT_555_RGB), + _TYPE_USHORT_565_RGB(BufferedImage.TYPE_USHORT_565_RGB), + _TYPE_USHORT_GRAY(BufferedImage.TYPE_USHORT_GRAY), + ; final int nativeInt; @@ -83,11 +101,62 @@ public final class Bitmap { return BufferedImage.TYPE_USHORT_565_RGB; case ARGB_8888: return BufferedImage.TYPE_INT_ARGB; + case _TYPE_3BYTE_BGR: + case _TYPE_4BYTE_ABGR: + case _TYPE_4BYTE_ABGR_PRE: + case _TYPE_BYTE_BINARY: + case _TYPE_BYTE_GRAY: + case _TYPE_BYTE_INDEXED: + case _TYPE_CUSTOM: + case _TYPE_INT_ARGB: + case _TYPE_INT_ARGB_PRE: + case _TYPE_INT_BGR: + case _TYPE_INT_RGB: + case _TYPE_USHORT_555_RGB: + case _TYPE_USHORT_565_RGB: + case _TYPE_USHORT_GRAY: + return config.ordinal(); default: throw new UnsupportedOperationException("Bitmap.Config(" + config + ") not supported"); } } + private static Config bufferedImageTypeToConfig(int type) { + switch (type) { + case BufferedImage.TYPE_BYTE_GRAY: + return Config.ALPHA_8; + case BufferedImage.TYPE_USHORT_565_RGB: + return Config.RGB_565; + case BufferedImage.TYPE_INT_ARGB: + return Config.ARGB_8888; + case BufferedImage.TYPE_3BYTE_BGR: + return Config._TYPE_3BYTE_BGR; + case BufferedImage.TYPE_4BYTE_ABGR: + return Config._TYPE_4BYTE_ABGR; + case BufferedImage.TYPE_4BYTE_ABGR_PRE: + return Config._TYPE_4BYTE_ABGR_PRE; + case BufferedImage.TYPE_BYTE_BINARY: + return Config._TYPE_BYTE_BINARY; + case BufferedImage.TYPE_BYTE_INDEXED: + return Config._TYPE_BYTE_INDEXED; + case BufferedImage.TYPE_CUSTOM: + return Config._TYPE_CUSTOM; + case BufferedImage.TYPE_INT_ARGB_PRE: + return Config._TYPE_INT_ARGB_PRE; + case BufferedImage.TYPE_INT_BGR: + return Config._TYPE_INT_BGR; + case BufferedImage.TYPE_INT_RGB: + return Config._TYPE_INT_RGB; + case BufferedImage.TYPE_USHORT_555_RGB: + return Config._TYPE_USHORT_555_RGB; + case BufferedImage.TYPE_USHORT_GRAY: + return Config._TYPE_USHORT_GRAY; + default: + Log.w("Bitmap", "Encountered unsupported image type " + type); + return null; + } + } + /** * Common code for checking that x and y are >= 0 * @@ -264,4 +333,10 @@ public final class Bitmap { public void recycle() { // do nothing } + + @Nullable + public final Config getConfig() { + int type = image.getType(); + return bufferedImageTypeToConfig(type); + } } diff --git a/AndroidCompat/src/main/java/android/graphics/BitmapFactory.java b/AndroidCompat/src/main/java/android/graphics/BitmapFactory.java index dbbe1267..943711aa 100644 --- a/AndroidCompat/src/main/java/android/graphics/BitmapFactory.java +++ b/AndroidCompat/src/main/java/android/graphics/BitmapFactory.java @@ -8,13 +8,462 @@ import java.util.Iterator; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; +import androidx.annotation.Nullable; public class BitmapFactory { + public static class Options { + /** + * Create a default Options object, which if left unchanged will give + * the same result from the decoder as if null were passed. + */ + public Options() { + inScaled = true; + inPremultiplied = true; + } + + /** + * If set, decode methods that take the Options object will attempt to + * reuse this bitmap when loading content. If the decode operation + * cannot use this bitmap, the decode method will throw an + * {@link java.lang.IllegalArgumentException}. The + * current implementation necessitates that the reused bitmap be + * mutable, and the resulting reused bitmap will continue to remain + * mutable even when decoding a resource which would normally result in + * an immutable bitmap.

+ * + *

You should still always use the returned Bitmap of the decode + * method and not assume that reusing the bitmap worked, due to the + * constraints outlined above and failure situations that can occur. + * Checking whether the return value matches the value of the inBitmap + * set in the Options structure will indicate if the bitmap was reused, + * but in all cases you should use the Bitmap returned by the decoding + * function to ensure that you are using the bitmap that was used as the + * decode destination.

+ * + *

Usage with BitmapFactory

+ * + *

As of {@link android.os.Build.VERSION_CODES#KITKAT}, any + * mutable bitmap can be reused by {@link BitmapFactory} to decode any + * other bitmaps as long as the resulting {@link Bitmap#getByteCount() + * byte count} of the decoded bitmap is less than or equal to the {@link + * Bitmap#getAllocationByteCount() allocated byte count} of the reused + * bitmap. This can be because the intrinsic size is smaller, or its + * size post scaling (for density / sample size) is smaller.

+ * + *

Prior to {@link android.os.Build.VERSION_CODES#KITKAT} + * additional constraints apply: The image being decoded (whether as a + * resource or as a stream) must be in jpeg or png format. Only equal + * sized bitmaps are supported, with {@link #inSampleSize} set to 1. + * Additionally, the {@link android.graphics.Bitmap.Config + * configuration} of the reused bitmap will override the setting of + * {@link #inPreferredConfig}, if set.

+ * + *

Usage with BitmapRegionDecoder

+ * + *

BitmapRegionDecoder will draw its requested content into the Bitmap + * provided, clipping if the output content size (post scaling) is larger + * than the provided Bitmap. The provided Bitmap's width, height, and + * {@link Bitmap.Config} will not be changed. + * + *

BitmapRegionDecoder support for {@link #inBitmap} was + * introduced in {@link android.os.Build.VERSION_CODES#JELLY_BEAN}. All + * formats supported by BitmapRegionDecoder support Bitmap reuse via + * {@link #inBitmap}.

+ * + * @see Bitmap#reconfigure(int,int, android.graphics.Bitmap.Config) + */ + public Bitmap inBitmap; + + /** + * If set, decode methods will always return a mutable Bitmap instead of + * an immutable one. This can be used for instance to programmatically apply + * effects to a Bitmap loaded through BitmapFactory. + *

Can not be set simultaneously with inPreferredConfig = + * {@link android.graphics.Bitmap.Config#HARDWARE}, + * because hardware bitmaps are always immutable. + */ + public boolean inMutable; + + /** + * If set to true, the decoder will return null (no bitmap), but + * the out... fields will still be set, allowing the caller to + * query the bitmap without having to allocate the memory for its pixels. + */ + public boolean inJustDecodeBounds; + + /** + * If set to a value > 1, requests the decoder to subsample the original + * image, returning a smaller image to save memory. The sample size is + * the number of pixels in either dimension that correspond to a single + * pixel in the decoded bitmap. For example, inSampleSize == 4 returns + * an image that is 1/4 the width/height of the original, and 1/16 the + * number of pixels. Any value <= 1 is treated the same as 1. Note: the + * decoder uses a final value based on powers of 2, any other value will + * be rounded down to the nearest power of 2. + */ + public int inSampleSize; + + /** + * If this is non-null, the decoder will try to decode into this + * internal configuration. If it is null, or the request cannot be met, + * the decoder will try to pick the best matching config based on the + * system's screen depth, and characteristics of the original image such + * as if it has per-pixel alpha (requiring a config that also does). + * + * Image are loaded with the {@link Bitmap.Config#ARGB_8888} config by + * default. + */ + public Bitmap.Config inPreferredConfig = null; + + /** + *

If this is non-null, the decoder will try to decode into this + * color space. If it is null, or the request cannot be met, + * the decoder will pick either the color space embedded in the image + * or the color space best suited for the requested image configuration + * (for instance {@link ColorSpace.Named#SRGB sRGB} for + * {@link Bitmap.Config#ARGB_8888} configuration and + * {@link ColorSpace.Named#EXTENDED_SRGB EXTENDED_SRGB} for + * {@link Bitmap.Config#RGBA_F16}).

+ * + *

Only {@link ColorSpace.Model#RGB} color spaces are + * currently supported. An IllegalArgumentException will + * be thrown by the decode methods when setting a non-RGB color space + * such as {@link ColorSpace.Named#CIE_LAB Lab}.

+ * + *

+ * Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * the specified color space's transfer function must be + * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An + * IllegalArgumentException will be thrown by the decode methods + * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the + * specified color space returns null. + * + * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * non ICC parametric curve transfer function is allowed. + * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.

+ * + *

After decode, the bitmap's color space is stored in + * {@link #outColorSpace}.

+ */ + public ColorSpace inPreferredColorSpace = null; + + /** + * If true (which is the default), the resulting bitmap will have its + * color channels pre-multiplied by the alpha channel. + * + *

This should NOT be set to false for images to be directly drawn by + * the view system or through a {@link Canvas}. The view system and + * {@link Canvas} assume all drawn images are pre-multiplied to simplify + * draw-time blending, and will throw a RuntimeException when + * un-premultiplied are drawn.

+ * + *

This is likely only useful if you want to manipulate raw encoded + * image data, e.g. with RenderScript or custom OpenGL.

+ * + *

This does not affect bitmaps without an alpha channel.

+ * + *

Setting this flag to false while setting {@link #inScaled} to true + * may result in incorrect colors.

+ * + * @see Bitmap#hasAlpha() + * @see Bitmap#isPremultiplied() + * @see #inScaled + */ + public boolean inPremultiplied; + + /** + * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is + * ignored. + * + * In {@link android.os.Build.VERSION_CODES#M} and below, if dither is + * true, the decoder will attempt to dither the decoded image. + */ + @Deprecated + public boolean inDither; + + /** + * The pixel density to use for the bitmap. This will always result + * in the returned bitmap having a density set for it (see + * {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition, + * if {@link #inScaled} is set (which it is by default} and this + * density does not match {@link #inTargetDensity}, then the bitmap + * will be scaled to the target density before being returned. + * + *

If this is 0, + * {@link BitmapFactory#decodeResource(Resources, int)}, + * {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)}, + * and {@link BitmapFactory#decodeResourceStream} + * will fill in the density associated with the resource. The other + * functions will leave it as-is and no density will be applied. + * + * @see #inTargetDensity + * @see #inScreenDensity + * @see #inScaled + * @see Bitmap#setDensity(int) + * @see android.util.DisplayMetrics#densityDpi + */ + public int inDensity; + + /** + * The pixel density of the destination this bitmap will be drawn to. + * This is used in conjunction with {@link #inDensity} and + * {@link #inScaled} to determine if and how to scale the bitmap before + * returning it. + * + *

If this is 0, + * {@link BitmapFactory#decodeResource(Resources, int)}, + * {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)}, + * and {@link BitmapFactory#decodeResourceStream} + * will fill in the density associated the Resources object's + * DisplayMetrics. The other + * functions will leave it as-is and no scaling for density will be + * performed. + * + * @see #inDensity + * @see #inScreenDensity + * @see #inScaled + * @see android.util.DisplayMetrics#densityDpi + */ + public int inTargetDensity; + + /** + * The pixel density of the actual screen that is being used. This is + * purely for applications running in density compatibility code, where + * {@link #inTargetDensity} is actually the density the application + * sees rather than the real screen density. + * + *

By setting this, you + * allow the loading code to avoid scaling a bitmap that is currently + * in the screen density up/down to the compatibility density. Instead, + * if {@link #inDensity} is the same as {@link #inScreenDensity}, the + * bitmap will be left as-is. Anything using the resulting bitmap + * must also used {@link Bitmap#getScaledWidth(int) + * Bitmap.getScaledWidth} and {@link Bitmap#getScaledHeight + * Bitmap.getScaledHeight} to account for any different between the + * bitmap's density and the target's density. + * + *

This is never set automatically for the caller by + * {@link BitmapFactory} itself. It must be explicitly set, since the + * caller must deal with the resulting bitmap in a density-aware way. + * + * @see #inDensity + * @see #inTargetDensity + * @see #inScaled + * @see android.util.DisplayMetrics#densityDpi + */ + public int inScreenDensity; + + /** + * When this flag is set, if {@link #inDensity} and + * {@link #inTargetDensity} are not 0, the + * bitmap will be scaled to match {@link #inTargetDensity} when loaded, + * rather than relying on the graphics system scaling it each time it + * is drawn to a Canvas. + * + *

BitmapRegionDecoder ignores this flag, and will not scale output + * based on density. (though {@link #inSampleSize} is supported)

+ * + *

This flag is turned on by default and should be turned off if you need + * a non-scaled version of the bitmap. Nine-patch bitmaps ignore this + * flag and are always scaled. + * + *

If {@link #inPremultiplied} is set to false, and the image has alpha, + * setting this flag to true may result in incorrect colors. + */ + public boolean inScaled; + + /** + * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is + * ignored. + * + * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, if this + * is set to true, then the resulting bitmap will allocate its + * pixels such that they can be purged if the system needs to reclaim + * memory. In that instance, when the pixels need to be accessed again + * (e.g. the bitmap is drawn, getPixels() is called), they will be + * automatically re-decoded. + * + *

For the re-decode to happen, the bitmap must have access to the + * encoded data, either by sharing a reference to the input + * or by making a copy of it. This distinction is controlled by + * inInputShareable. If this is true, then the bitmap may keep a shallow + * reference to the input. If this is false, then the bitmap will + * explicitly make a copy of the input data, and keep that. Even if + * sharing is allowed, the implementation may still decide to make a + * deep copy of the input data.

+ * + *

While inPurgeable can help avoid big Dalvik heap allocations (from + * API level 11 onward), it sacrifices performance predictability since any + * image that the view system tries to draw may incur a decode delay which + * can lead to dropped frames. Therefore, most apps should avoid using + * inPurgeable to allow for a fast and fluid UI. To minimize Dalvik heap + * allocations use the {@link #inBitmap} flag instead.

+ * + *

Note: This flag is ignored when used + * with {@link #decodeResource(Resources, int, + * android.graphics.BitmapFactory.Options)} or {@link #decodeFile(String, + * android.graphics.BitmapFactory.Options)}.

+ */ + @Deprecated + public boolean inPurgeable; + + /** + * @deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this is + * ignored. + * + * In {@link android.os.Build.VERSION_CODES#KITKAT} and below, this + * field works in conjunction with inPurgeable. If inPurgeable is false, + * then this field is ignored. If inPurgeable is true, then this field + * determines whether the bitmap can share a reference to the input + * data (inputstream, array, etc.) or if it must make a deep copy. + */ + @Deprecated + public boolean inInputShareable; + + /** + * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this is + * ignored. The output will always be high quality. + * + * In {@link android.os.Build.VERSION_CODES#M} and below, if + * inPreferQualityOverSpeed is set to true, the decoder will try to + * decode the reconstructed image to a higher quality even at the + * expense of the decoding speed. Currently the field only affects JPEG + * decode, in the case of which a more accurate, but slightly slower, + * IDCT method will be used instead. + */ + @Deprecated + public boolean inPreferQualityOverSpeed; + + /** + * The resulting width of the bitmap. If {@link #inJustDecodeBounds} is + * set to false, this will be width of the output bitmap after any + * scaling is applied. If true, it will be the width of the input image + * without any accounting for scaling. + * + *

outWidth will be set to -1 if there is an error trying to decode.

+ */ + public int outWidth; + + /** + * The resulting height of the bitmap. If {@link #inJustDecodeBounds} is + * set to false, this will be height of the output bitmap after any + * scaling is applied. If true, it will be the height of the input image + * without any accounting for scaling. + * + *

outHeight will be set to -1 if there is an error trying to decode.

+ */ + public int outHeight; + + /** + * If known, this string is set to the mimetype of the decoded image. + * If not known, or there is an error, it is set to null. + */ + public String outMimeType; + + /** + * If known, the config the decoded bitmap will have. + * If not known, or there is an error, it is set to null. + */ + public Bitmap.Config outConfig; + + /** + * If known, the color space the decoded bitmap will have. Note that the + * output color space is not guaranteed to be the color space the bitmap + * is encoded with. If not known (when the config is + * {@link Bitmap.Config#ALPHA_8} for instance), or there is an error, + * it is set to null. + */ + public ColorSpace outColorSpace; + + /** + * Temp storage to use for decoding. Suggest 16K or so. + */ + public byte[] inTempStorage; + + /** + * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, see + * comments on {@link #requestCancelDecode()}. + * + * Flag to indicate that cancel has been called on this object. This + * is useful if there's an intermediary that wants to first decode the + * bounds and then decode the image. In that case the intermediary + * can check, inbetween the bounds decode and the image decode, to see + * if the operation is canceled. + */ + @Deprecated + public boolean mCancel; + + /** + * @deprecated As of {@link android.os.Build.VERSION_CODES#N}, this + * will not affect the decode, though it will still set mCancel. + * + * In {@link android.os.Build.VERSION_CODES#M} and below, if this can + * be called from another thread while this options object is inside + * a decode... call. Calling this will notify the decoder that it + * should cancel its operation. This is not guaranteed to cancel the + * decode, but if it does, the decoder... operation will return null, + * or if inJustDecodeBounds is true, will set outWidth/outHeight + * to -1 + */ + @Deprecated + public void requestCancelDecode() { + mCancel = true; + } + + static void validate(Options opts) { + if (opts == null) return; + + if (opts.inBitmap != null) { + /* + if (opts.inBitmap.getConfig() == Bitmap.Config.HARDWARE) { + throw new IllegalArgumentException( + "Bitmaps with Config.HARDWARE are always immutable"); + } + if (opts.inBitmap.isRecycled()) { + throw new IllegalArgumentException( + "Cannot reuse a recycled Bitmap"); + } + */ + } + + if (opts.inMutable && opts.inPreferredConfig == Bitmap.Config.HARDWARE) { + throw new IllegalArgumentException("Bitmaps with Config.HARDWARE cannot be " + + "decoded into - they are immutable"); + } + + if (opts.inPreferredColorSpace != null) { + if (!(opts.inPreferredColorSpace instanceof ColorSpace.Rgb)) { + throw new IllegalArgumentException("The destination color space must use the " + + "RGB color model"); + } + if (!opts.inPreferredColorSpace.equals(ColorSpace.get(ColorSpace.Named.BT2020_HLG)) + && !opts.inPreferredColorSpace.equals( + ColorSpace.get(ColorSpace.Named.BT2020_PQ)) + && ((ColorSpace.Rgb) opts.inPreferredColorSpace) + .getTransferParameters() == null) { + throw new IllegalArgumentException("The destination color space must use an " + + "ICC parametric transfer function"); + } + } + } + } + + public static Bitmap decodeStream(InputStream inputStream) { + return decodeStream(inputStream, null, null); + } + + @Nullable + public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding, + @Nullable Options opts) { + if (is == null) return null; + if (outPadding != null) throw new RuntimeException("OutPadding is not implemented"); + Options.validate(opts); Bitmap bitmap = null; + // TODO: Support options with in parameters try { - ImageInputStream imageInputStream = ImageIO.createImageInputStream(inputStream); + ImageInputStream imageInputStream = ImageIO.createImageInputStream(is); Iterator imageReaders = ImageIO.getImageReaders(imageInputStream); if (!imageReaders.hasNext()) { @@ -24,8 +473,18 @@ public class BitmapFactory { ImageReader imageReader = imageReaders.next(); imageReader.setInput(imageInputStream); - BufferedImage image = imageReader.read(0, imageReader.getDefaultReadParam()); - bitmap = new Bitmap(image); + if (opts != null) { + opts.outHeight = imageReader.getHeight(0); + opts.outWidth = imageReader.getWidth(0); + opts.outMimeType = imageReader.getOriginatingProvider().getMIMETypes()[0]; + opts.outColorSpace = null; // TODO: support? see imageReader.getImageTypeSpecifier().getColorSpace() + opts.outConfig = null; // TODO: support? + } + + if (opts == null || !opts.inJustDecodeBounds) { + BufferedImage image = imageReader.read(0, imageReader.getDefaultReadParam()); + bitmap = new Bitmap(image); + } imageReader.dispose(); } catch (IOException ex) { @@ -36,16 +495,11 @@ public class BitmapFactory { } public static Bitmap decodeByteArray(byte[] data, int offset, int length) { - Bitmap bitmap = null; + return decodeByteArray(data, offset, length, null); + } - ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(data); - try { - BufferedImage image = ImageIO.read(byteArrayStream); - bitmap = new Bitmap(image); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - - return bitmap; + public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) { + ByteArrayInputStream byteArrayStream = new ByteArrayInputStream(data, offset, length); + return decodeStream(byteArrayStream, null, opts); } }