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