1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.core.graphics;
17 
18 import android.graphics.Bitmap;
19 import android.graphics.BlendMode;
20 import android.graphics.Canvas;
21 import android.graphics.ColorSpace;
22 import android.graphics.Paint;
23 import android.graphics.PorterDuff;
24 import android.graphics.PorterDuffXfermode;
25 import android.graphics.Rect;
26 import android.hardware.HardwareBuffer;
27 import android.os.Build;
28 
29 import androidx.annotation.RequiresApi;
30 import androidx.annotation.VisibleForTesting;
31 
32 import org.jspecify.annotations.NonNull;
33 import org.jspecify.annotations.Nullable;
34 
35 /**
36  * Helper for accessing features in {@link Bitmap}.
37  */
38 public final class BitmapCompat {
39 
40     /**
41      * Indicates whether the renderer responsible for drawing this
42      * bitmap should attempt to use mipmaps when this bitmap is drawn
43      * scaled down.
44      * <p>
45      * If you know that you are going to draw this bitmap at less than
46      * 50% of its original size, you may be able to obtain a higher
47      * quality
48      * <p>
49      * This property is only a suggestion that can be ignored by the
50      * renderer. It is not guaranteed to have any effect.
51      *
52      * @return true if the renderer should attempt to use mipmaps,
53      * false otherwise
54      * @see Bitmap#hasMipMap()
55      * @deprecated Call {@link Bitmap#hasMipMap()} directly.
56      */
57     @Deprecated
58     @androidx.annotation.ReplaceWith(expression = "bitmap.hasMipMap()")
hasMipMap(@onNull Bitmap bitmap)59     public static boolean hasMipMap(@NonNull Bitmap bitmap) {
60         return bitmap.hasMipMap();
61     }
62 
63     /**
64      * Set a hint for the renderer responsible for drawing this bitmap
65      * indicating that it should attempt to use mipmaps when this bitmap
66      * is drawn scaled down.
67      * <p>
68      * If you know that you are going to draw this bitmap at less than
69      * 50% of its original size, you may be able to obtain a higher
70      * quality by turning this property on.
71      * <p>
72      * Note that if the renderer respects this hint it might have to
73      * allocate extra memory to hold the mipmap levels for this bitmap.
74      * <p>
75      * This property is only a suggestion that can be ignored by the
76      * renderer. It is not guaranteed to have any effect.
77      *
78      * @param bitmap bitmap for which to set the state.
79      * @param hasMipMap indicates whether the renderer should attempt
80      *                  to use mipmaps
81      * @see Bitmap#setHasMipMap(boolean)
82      * @deprecated Call {@link Bitmap#setHasMipMap()} directly.
83      */
84     @Deprecated
85     @androidx.annotation.ReplaceWith(expression = "bitmap.setHasMipMap(hasMipMap)")
setHasMipMap(@onNull Bitmap bitmap, boolean hasMipMap)86     public static void setHasMipMap(@NonNull Bitmap bitmap, boolean hasMipMap) {
87         bitmap.setHasMipMap(hasMipMap);
88     }
89 
90     /**
91      * Returns the size of the allocated memory used to store this bitmap's pixels.
92      * <p>
93      * This value will not change over the lifetime of a Bitmap.
94      *
95      * @see Bitmap#getAllocationByteCount()
96      * @deprecated Call {@link Bitmap#getAllocationByteCount()} directly.
97      */
98     @Deprecated
99     @androidx.annotation.ReplaceWith(expression = "bitmap.getAllocationByteCount()")
getAllocationByteCount(@onNull Bitmap bitmap)100     public static int getAllocationByteCount(@NonNull Bitmap bitmap) {
101         return bitmap.getAllocationByteCount();
102     }
103 
104     /**
105      * <p>Return a scaled bitmap.</p>
106      * <p>This algorithm is intended for downscaling by large ratios when high quality is desired.
107      * It is similar to the creation of mipmaps, but stops at the desired size.
108      * Visually, the result is smoother and softer than {@link Bitmap#createScaledBitmap}</p>
109      *
110      * <p>
111      * The returned bitmap will always be a mutable copy with a config matching the input except in
112      * the following scenarios:
113      * <ol>
114      * <li> The source bitmap is returned and the source bitmap is immutable.</li>
115      * <li> The source bitmap is a {@code HARDWARE} bitmap. For this input, a mutable
116      * non-{@code HARDWARE} Bitmap
117      * is returned. On API 31 and up, the internal format of the HardwareBuffer is read to
118      * determine the underlying format, and the returned Bitmap will use a Config to match.
119      * Pre-31, the returned Bitmap will be {@code ARGB_8888}.
120      * </li></ol></p>
121      *
122      * @param srcBm              A source bitmap. It will not be altered.
123      * @param dstW               The output width
124      * @param dstH               The output height
125      * @param srcRect            Uses a region of the input bitmap as the source.
126      * @param scaleInLinearSpace When true, uses {@code LINEAR_EXTENDED_SRGB} as a color space
127      *                           when scaling.
128      *                           Otherwise, uses the color space of the input bitmap. (On API
129      *                           level 26 and earlier, this parameter has no effect).
130      * @return A new bitmap in the requested size.
131      */
createScaledBitmap(@onNull Bitmap srcBm, int dstW, int dstH, @Nullable Rect srcRect, boolean scaleInLinearSpace)132     public static     @NonNull Bitmap createScaledBitmap(@NonNull Bitmap srcBm, int dstW,
133             int dstH, @Nullable Rect srcRect, boolean scaleInLinearSpace) {
134         if (dstW <= 0 || dstH <= 0) {
135             throw new IllegalArgumentException("dstW and dstH must be > 0!");
136         }
137 
138         if (srcRect != null) {
139             if (srcRect.isEmpty() || srcRect.left < 0 || srcRect.right > srcBm.getWidth()
140                     || srcRect.top < 0 || srcRect.bottom > srcBm.getHeight()) {
141                 throw new IllegalArgumentException("srcRect must be contained by srcBm!");
142             }
143         }
144 
145         Bitmap src = srcBm;
146         if (Build.VERSION.SDK_INT >= 27) {
147             // Note that since this uses Bitmap.copy, not canvas.drawBitmap, it cannot be eliminated
148             // by combining it with the first drawBitmap that occurs.
149             src = Api27Impl.copyBitmapIfHardware(srcBm);
150         }
151 
152         int srcW = srcRect != null ? srcRect.width() : srcBm.getWidth();
153         int srcH = srcRect != null ? srcRect.height() : srcBm.getHeight();
154 
155         float sx = dstW / (float) srcW;
156         float sy = dstH / (float) srcH;
157 
158         int srcX = srcRect != null ? srcRect.left : 0;
159         int srcY = srcRect != null ? srcRect.top : 0;
160 
161         // Early return for no-ops
162         if (srcX == 0 && srcY == 0 && dstW == srcBm.getWidth() && dstH == srcBm.getHeight()) {
163             // Don't return inputs if they are mutable.
164             if (srcBm.isMutable() && srcBm == src) {
165                 return srcBm.copy(srcBm.getConfig(), true);
166             } else {
167                 // this may be the original, or it may be a copy of a hardware bitmap
168                 return src;
169             }
170         }
171 
172         Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
173         paint.setFilterBitmap(true);
174         if (Build.VERSION.SDK_INT >= 29) {
175             Api29Impl.setPaintBlendMode(paint);
176         } else {
177             paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
178         }
179 
180         // Special case for copying from sub-rects without scaling
181         if (srcW == dstW && srcH == dstH) {
182             Bitmap out = Bitmap.createBitmap(dstW, dstH, src.getConfig());
183             Canvas canvasForCopy = new Canvas(out);
184             canvasForCopy.drawBitmap(src, -srcX, -srcY, paint);
185             return out;
186         }
187 
188         // How many filtering steps to do in X and Y. + means upscaling, - means downscaling.
189         double log2 = Math.log(2);
190         int stepsX = (sx > 1.0f) ? (int) Math.ceil(Math.log(sx) / log2) :
191                 (int) Math.floor(Math.log(sx) / log2);
192         int stepsY = (sy > 1.0f) ? (int) Math.ceil(Math.log(sy) / log2) :
193                 (int) Math.floor(Math.log(sy) / log2);
194         final int totalStepsX = stepsX;
195         final int totalStepsY = stepsY;
196 
197         // Bitmaps are re-used in order to minimize allocations.
198         // One is a source and one is a destination, and at each step they switch roles.
199         // On the first pass however, srcBm may take the place of src if no linear color space
200         // transformation is being performed.
201         Bitmap dst = null;
202         // A flag indicating the scratch bitmaps will be in a different color space than the
203         // intended output color space and a conversion on the final iteration will be necessary.
204         boolean needFinalConversion = false;
205         if (scaleInLinearSpace) {
206             if (Build.VERSION.SDK_INT >= 27 && !Api27Impl.isAlreadyF16AndLinear(srcBm)) {
207                 int allocW = stepsX > 0 ? sizeAtStep(srcW, dstW, 1, totalStepsX) : srcW;
208                 int allocH = stepsY > 0 ? sizeAtStep(srcH, dstH, 1, totalStepsY) : srcH;
209                 dst = Api27Impl.createBitmapWithSourceColorspace(
210                         allocW, allocH, srcBm, true);
211                 Canvas canvasForCopy = new Canvas(dst);
212                 canvasForCopy.drawBitmap(src, -srcX, -srcY, paint);
213                 srcX = 0;
214                 srcY = 0;
215                 Bitmap swap = dst;
216                 dst = src;
217                 src = swap;
218                 needFinalConversion = true;
219             }
220         }
221 
222         Rect currRect = new Rect(srcX, srcY, srcW, srcH);
223         Rect nextRect = new Rect();
224 
225         while (stepsX != 0 || stepsY != 0) {
226             if (stepsX < 0) {
227                 stepsX++;
228             } else if (stepsX > 0) {
229                 --stepsX;
230             }
231             if (stepsY < 0) {
232                 stepsY++;
233             } else if (stepsY > 0) {
234                 --stepsY;
235             }
236             int nextW = sizeAtStep(srcW, dstW, stepsX, totalStepsX);
237             int nextH = sizeAtStep(srcH, dstH, stepsY, totalStepsY);
238             nextRect.set(0, 0, nextW, nextH);
239 
240             // The purpose of following block is to make dst a suitable size, configuration, and
241             // color space for the next iteration in the loop, while minimizing allocation.
242             // The following constraints/needs are addressed:
243             // * On the first pass, allocate dst for the first time.
244             // * On the second pass, once the scratch bitmaps have been swapped, allocate the
245             //      other bitmap.
246             // * Either of them could have already been allocated for the first time due
247             //      to scaleInLinearSpace or copying out of a hardware buffer.
248             // * On the last pass, convert back to the original config and color space.
249             // * recycle() any bitmap that will no longer be used.
250             // * re-use a region within a bitmap instead of allocating wherever possible.
251             // * If scaling down, it may be a waste of memory to return the user a bitmap with a
252             //      larger footprint than necessary as the costs of using over its lifetime may
253             //      exceed the savings of re-using the allocation here.
254             // * Color spaces are only supported on O or later.
255             // * This function may not alter srcBm.
256             boolean lastStep = (stepsX == 0 && stepsY == 0);
257             boolean dstSizeIsFinal =
258                     dst != null && dst.getWidth() == dstW && dst.getHeight() == dstH;
259             if (
260                 // On first and second passes, scratch bitmaps may not have been allocated yet.
261                 dst == null
262                 // The previous step may have read directly from srcBm then swapped
263                 // it with dst.
264                 || dst == srcBm
265                 // dst may have been allocated by the hardware copy step, but linear is
266                 // requested and dst is not linear yet.
267                 || (scaleInLinearSpace && (Build.VERSION.SDK_INT >= 27
268                 && !Api27Impl.isAlreadyF16AndLinear(dst)))
269                 // If this is the last step and the scratch bitmap cannot be returned,
270                 // because in the wrong color space, allocate a new bitmap that will
271                 // be returned.
272                 || (lastStep && (!dstSizeIsFinal || needFinalConversion))
273             ) {
274                 // Recycle the old one if necessary
275                 if (dst != srcBm && dst != null) {
276                     dst.recycle();
277                 }
278 
279                 // The scratch bitmap may be reused multiple times. Choose a size large enough for
280                 // the largest draw that will be made to them. Each dimension can be considered
281                 // independently. When a dimension is being scaled up, take the size of the
282                 // last step. When a dimension is being scaled down, take the size of the current
283                 // step.
284                 int lastScratchStep = needFinalConversion ? 1 : 0;
285                 int allocW = sizeAtStep(srcW, dstW, stepsX > 0 ? lastScratchStep : stepsX,
286                         totalStepsX);
287                 int allocH = sizeAtStep(srcH, dstH, stepsY > 0 ? lastScratchStep : stepsY,
288                         totalStepsY);
289 
290                 // Create a new bitmap. If possible, use the correct color space.
291                 if (Build.VERSION.SDK_INT >= 27) {
292                     boolean linear = scaleInLinearSpace && !lastStep;
293                     dst = Api27Impl.createBitmapWithSourceColorspace(
294                             allocW, allocH, srcBm, linear);
295                 } else {
296                     dst = Bitmap.createBitmap(allocW, allocH, src.getConfig());
297                 }
298             }
299 
300             // On any iteration where dst did not need to be created anew, it is suitable to draw
301             // into the region of it indicated by nextRect.
302             Canvas canvas = new Canvas(dst);
303             canvas.drawBitmap(src, currRect, nextRect, paint);
304 
305             // swap the two bitmaps
306             Bitmap swap = src;
307             src = dst;
308             dst = swap;
309             currRect.set(nextRect);
310         }
311         if (dst != srcBm && dst != null) {
312             dst.recycle();
313         }
314         return src; // remember they were just swapped
315     }
316 
317     /**
318      * Return the size that a scratch bitmap dimension (x or y) should be at a given step.
319      * When scaling up step counts down to zero from positive numbers.
320      * When scaling down, step counts up to zero from negative numbers.
321      */
322     @VisibleForTesting
sizeAtStep(int srcSize, int dstSize, int step, int totalSteps)323     static int sizeAtStep(int srcSize, int dstSize, int step, int totalSteps) {
324         if (step == 0) {
325             return dstSize;
326         } else if (step > 0) { // upscale
327             return srcSize * (1 << (totalSteps - step));
328         } else { // downscale
329             return dstSize << (-step - 1);
330         }
331     }
332 
BitmapCompat()333     private BitmapCompat() {
334         // This class is not instantiable.
335     }
336 
337     @RequiresApi(27)
338     static class Api27Impl {
Api27Impl()339         private Api27Impl() {
340         }
341 
createBitmapWithSourceColorspace(int w, int h, Bitmap src, boolean linear)342         static Bitmap createBitmapWithSourceColorspace(int w, int h, Bitmap src, boolean linear) {
343             Bitmap.Config config = src.getConfig();
344             ColorSpace colorSpace = src.getColorSpace();
345             ColorSpace linearCs = ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
346             if (linear && !src.getColorSpace().equals(linearCs)) {
347                 // Promote to F16 to preserve precision.
348                 config = Bitmap.Config.RGBA_F16;
349                 colorSpace = linearCs;
350             } else if (src.getConfig() == Bitmap.Config.HARDWARE) {
351                 config = Bitmap.Config.ARGB_8888;
352                 if (Build.VERSION.SDK_INT >= 31) {
353                     config = Api31Impl.getHardwareBitmapConfig(src);
354                 }
355             }
356             return Bitmap.createBitmap(w, h, config, src.hasAlpha(), colorSpace);
357         }
358 
isAlreadyF16AndLinear(Bitmap b)359         static boolean isAlreadyF16AndLinear(Bitmap b) {
360             ColorSpace linearCs = ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
361             return b.getConfig() == Bitmap.Config.RGBA_F16 && b.getColorSpace().equals(linearCs);
362         }
363 
copyBitmapIfHardware(Bitmap bm)364         static Bitmap copyBitmapIfHardware(Bitmap bm) {
365             if (bm.getConfig() == Bitmap.Config.HARDWARE) {
366                 Bitmap.Config newConfig = Bitmap.Config.ARGB_8888;
367                 if (Build.VERSION.SDK_INT >= 31) {
368                     newConfig = Api31Impl.getHardwareBitmapConfig(bm);
369                 }
370                 return bm.copy(newConfig, true);
371             } else {
372                 return bm;
373             }
374         }
375     }
376 
377     @RequiresApi(29)
378     static class Api29Impl {
Api29Impl()379         private Api29Impl() {
380         }
381 
setPaintBlendMode(Paint paint)382         static void setPaintBlendMode(Paint paint) {
383             paint.setBlendMode(BlendMode.SRC);
384         }
385     }
386 
387     @RequiresApi(31)
388     static class Api31Impl {
Api31Impl()389         private Api31Impl() {
390         }
391 
getHardwareBitmapConfig(Bitmap bm)392         static Bitmap.Config getHardwareBitmapConfig(Bitmap bm) {
393             if (bm.getHardwareBuffer().getFormat() == HardwareBuffer.RGBA_FP16) {
394                 return Bitmap.Config.RGBA_F16;
395             } else {
396                 return Bitmap.Config.ARGB_8888;
397             }
398         }
399     }
400 }
401