/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.graphics;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Canvas;
import android.graphics.ColorSpace;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.os.Build;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
/**
* Helper for accessing features in {@link Bitmap}.
*/
public final class BitmapCompat {
/**
* Indicates whether the renderer responsible for drawing this
* bitmap should attempt to use mipmaps when this bitmap is drawn
* scaled down.
* <p>
* If you know that you are going to draw this bitmap at less than
* 50% of its original size, you may be able to obtain a higher
* quality
* <p>
* This property is only a suggestion that can be ignored by the
* renderer. It is not guaranteed to have any effect.
*
* @return true if the renderer should attempt to use mipmaps,
* false otherwise
* @see Bitmap#hasMipMap()
*/
public static boolean hasMipMap(@NonNull Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= 17) {
return Api17Impl.hasMipMap(bitmap);
}
return false;
}
/**
* Set a hint for the renderer responsible for drawing this bitmap
* indicating that it should attempt to use mipmaps when this bitmap
* is drawn scaled down.
* <p>
* If you know that you are going to draw this bitmap at less than
* 50% of its original size, you may be able to obtain a higher
* quality by turning this property on.
* <p>
* Note that if the renderer respects this hint it might have to
* allocate extra memory to hold the mipmap levels for this bitmap.
* <p>
* This property is only a suggestion that can be ignored by the
* renderer. It is not guaranteed to have any effect.
*
* @param hasMipMap indicates whether the renderer should attempt
* to use mipmaps
* @see Bitmap#setHasMipMap(boolean)
*/
public static void setHasMipMap(@NonNull Bitmap bitmap, boolean hasMipMap) {
if (Build.VERSION.SDK_INT >= 17) {
Api17Impl.setHasMipMap(bitmap, hasMipMap);
}
}
/**
* Returns the size of the allocated memory used to store this bitmap's pixels.
* <p>
* This value will not change over the lifetime of a Bitmap.
*
* @see Bitmap#getAllocationByteCount()
*/
public static int getAllocationByteCount(@NonNull Bitmap bitmap) {
if (Build.VERSION.SDK_INT >= 19) {
return Api19Impl.getAllocationByteCount(bitmap);
}
return bitmap.getByteCount();
}
/**
* <p>Return a scaled bitmap.</p>
* <p>This algorithm is intended for downscaling by large ratios when high quality is desired.
* It is similar to the creation of mipmaps, but stops at the desired size.
* Visually, the result is smoother and softer than {@link Bitmap#createScaledBitmap}</p>
*
* <p>
* The returned bitmap will always be a mutable copy with a config matching the input except in
* the following scenarios:
* <ol>
* <li> The source bitmap is returned and the source bitmap is immutable.</li>
* <li> The source bitmap is a {@code HARDWARE} bitmap. For this input, a mutable
* non-{@code HARDWARE} Bitmap
* is returned. On API 31 and up, the internal format of the HardwareBuffer is read to
* determine the underlying format, and the returned Bitmap will use a Config to match.
* Pre-31, the returned Bitmap will be {@code ARGB_8888}.
* </li></ol></p>
*
* @param srcBm A source bitmap. It will not be altered.
* @param dstW The output width
* @param dstH The output height
* @param srcRect Uses a region of the input bitmap as the source.
* @param scaleInLinearSpace When true, uses {@code LINEAR_EXTENDED_SRGB} as a color space
* when scaling.
* Otherwise, uses the color space of the input bitmap. (On API
* level 26 and earlier, this parameter has no effect).
* @return A new bitmap in the requested size.
*/
public static @NonNull
Bitmap createScaledBitmap(@NonNull Bitmap srcBm, int dstW,
int dstH, @Nullable Rect srcRect, boolean scaleInLinearSpace) {
if (dstW <= 0 || dstH <= 0) {
throw new IllegalArgumentException("dstW and dstH must be > 0!");
}
if (srcRect != null) {
if (srcRect.isEmpty() || srcRect.left < 0 || srcRect.right > srcBm.getWidth()
|| srcRect.top < 0 || srcRect.bottom > srcBm.getHeight()) {
throw new IllegalArgumentException("srcRect must be contained by srcBm!");
}
}
Bitmap src = srcBm;
if (Build.VERSION.SDK_INT >= 27) {
// Note that since this uses Bitmap.copy, not canvas.drawBitmap, it cannot be eliminated
// by combining it with the first drawBitmap that occurs.
src = Api27Impl.copyBitmapIfHardware(srcBm);
}
int srcW = srcRect != null ? srcRect.width() : srcBm.getWidth();
int srcH = srcRect != null ? srcRect.height() : srcBm.getHeight();
float sx = dstW / (float) srcW;
float sy = dstH / (float) srcH;
int srcX = srcRect != null ? srcRect.left : 0;
int srcY = srcRect != null ? srcRect.top : 0;
// Early return for no-ops
if (srcX == 0 && srcY == 0 && dstW == srcBm.getWidth() && dstH == srcBm.getHeight()) {
// Don't return inputs if they are mutable.
if (srcBm.isMutable() && srcBm == src) {
return srcBm.copy(srcBm.getConfig(), true);
} else {
// this may be the original, or it may be a copy of a hardware bitmap
return src;
}
}
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setFilterBitmap(true);
if (Build.VERSION.SDK_INT >= 29) {
Api29Impl.setPaintBlendMode(paint);
} else {
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
}
// Special case for copying from sub-rects without scaling
if (srcW == dstW && srcH == dstH) {
Bitmap out = Bitmap.createBitmap(dstW, dstH, src.getConfig());
Canvas canvasForCopy = new Canvas(out);
canvasForCopy.drawBitmap(src, -srcX, -srcY, paint);
return out;
}
// How many filtering steps to do in X and Y. + means upscaling, - means downscaling.
double log2 = Math.log(2);
int stepsX = (sx > 1.0f) ? (int) Math.ceil(Math.log(sx) / log2) :
(int) Math.floor(Math.log(sx) / log2);
int stepsY = (sy > 1.0f) ? (int) Math.ceil(Math.log(sy) / log2) :
(int) Math.floor(Math.log(sy) / log2);
final int totalStepsX = stepsX;
final int totalStepsY = stepsY;
// Bitmaps are re-used in order to minimize allocations.
// One is a source and one is a destination, and at each step they switch roles.
// On the first pass however, srcBm may take the place of src if no linear color space
// transformation is being performed.
Bitmap dst = null;
// A flag indicating the scratch bitmaps will be in a different color space than the
// intended output color space and a conversion on the final iteration will be necessary.
boolean needFinalConversion = false;
if (scaleInLinearSpace) {
if (Build.VERSION.SDK_INT >= 27 && !Api27Impl.isAlreadyF16AndLinear(srcBm)) {
int allocW = stepsX > 0 ? sizeAtStep(srcW, dstW, 1, totalStepsX) : srcW;
int allocH = stepsY > 0 ? sizeAtStep(srcH, dstH, 1, totalStepsY) : srcH;
dst = Api27Impl.createBitmapWithSourceColorspace(
allocW, allocH, srcBm, true);
Canvas canvasForCopy = new Canvas(dst);
canvasForCopy.drawBitmap(src, -srcX, -srcY, paint);
srcX = 0;
srcY = 0;
Bitmap swap = dst;
dst = src;
src = swap;
needFinalConversion = true;
}
}
Rect currRect = new Rect(srcX, srcY, srcW, srcH);
Rect nextRect = new Rect();
while (stepsX != 0 || stepsY != 0) {
if (stepsX < 0) {
stepsX++;
} else if (stepsX > 0) {
--stepsX;
}
if (stepsY < 0) {
stepsY++;
} else if (stepsY > 0) {
--stepsY;
}
int nextW = sizeAtStep(srcW, dstW, stepsX, totalStepsX);
int nextH = sizeAtStep(srcH, dstH, stepsY, totalStepsY);
nextRect.set(0, 0, nextW, nextH);
// The purpose of following block is to make dst a suitable size, configuration, and
// color space for the next iteration in the loop, while minimizing allocation.
// The following constraints/needs are addressed:
// * On the first pass, allocate dst for the first time.
// * On the second pass, once the scratch bitmaps have been swapped, allocate the
// other bitmap.
// * Either of them could have already been allocated for the first time due
// to scaleInLinearSpace or copying out of a hardware buffer.
// * On the last pass, convert back to the original config and color space.
// * recycle() any bitmap that will no longer be used.
// * re-use a region within a bitmap instead of allocating wherever possible.
// * If scaling down, it may be a waste of memory to return the user a bitmap with a
// larger footprint than necessary as the costs of using over its lifetime may
// exceed the savings of re-using the allocation here.
// * Color spaces are only supported on O or later.
// * This function may not alter srcBm.
boolean lastStep = (stepsX == 0 && stepsY == 0);
boolean dstSizeIsFinal =
dst != null && dst.getWidth() == dstW && dst.getHeight() == dstH;
if (
// On first and second passes, scratch bitmaps may not have been allocated yet.
dst == null
// The previous step may have read directly from srcBm then swapped
// it with dst.
|| dst == srcBm
// dst may have been allocated by the hardware copy step, but linear is
// requested and dst is not linear yet.
|| (scaleInLinearSpace && (Build.VERSION.SDK_INT >= 27
&& !Api27Impl.isAlreadyF16AndLinear(dst)))
// If this is the last step and the scratch bitmap cannot be returned,
// because in the wrong color space, allocate a new bitmap that will
// be returned.
|| (lastStep && (!dstSizeIsFinal || needFinalConversion))
) {
// Recycle the old one if necessary
if (dst != srcBm && dst != null) {
dst.recycle();
}
// The scratch bitmap may be reused multiple times. Choose a size large enough for
// the largest draw that will be made to them. Each dimension can be considered
// independently. When a dimension is being scaled up, take the size of the
// last step. When a dimension is being scaled down, take the size of the current
// step.
int lastScratchStep = needFinalConversion ? 1 : 0;
int allocW = sizeAtStep(srcW, dstW, stepsX > 0 ? lastScratchStep : stepsX,
totalStepsX);
int allocH = sizeAtStep(srcH, dstH, stepsY > 0 ? lastScratchStep : stepsY,
totalStepsY);
// Create a new bitmap. If possible, use the correct color space.
if (Build.VERSION.SDK_INT >= 27) {
boolean linear = scaleInLinearSpace && !lastStep;
dst = Api27Impl.createBitmapWithSourceColorspace(
allocW, allocH, srcBm, linear);
} else {
dst = Bitmap.createBitmap(allocW, allocH, src.getConfig());
}
}
// On any iteration where dst did not need to be created anew, it is suitable to draw
// into the region of it indicated by nextRect.
Canvas canvas = new Canvas(dst);
canvas.drawBitmap(src, currRect, nextRect, paint);
// swap the two bitmaps
Bitmap swap = src;
src = dst;
dst = swap;
currRect.set(nextRect);
}
if (dst != srcBm && dst != null) {
dst.recycle();
}
return src; // remember they were just swapped
}
/**
* Return the size that a scratch bitmap dimension (x or y) should be at a given step.
* When scaling up step counts down to zero from positive numbers.
* When scaling down, step counts up to zero from negative numbers.
* @hide
*/
@VisibleForTesting
public static int sizeAtStep(int srcSize, int dstSize, int step, int totalSteps) {
if (step == 0) {
return dstSize;
} else if (step > 0) { // upscale
return srcSize * (1 << (totalSteps - step));
} else { // downscale
return dstSize << (-step - 1);
}
}
private BitmapCompat() {
// This class is not instantiable.
}
@RequiresApi(17)
static class Api17Impl {
private Api17Impl() {
// This class is not instantiable.
}
@DoNotInline
static boolean hasMipMap(Bitmap bitmap) {
return bitmap.hasMipMap();
}
@DoNotInline
static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) {
bitmap.setHasMipMap(hasMipMap);
}
}
@RequiresApi(19)
static class Api19Impl {
private Api19Impl() {
// This class is not instantiable.
}
@DoNotInline
static int getAllocationByteCount(Bitmap bitmap) {
return bitmap.getAllocationByteCount();
}
}
@RequiresApi(27)
static class Api27Impl {
private Api27Impl() {
}
@DoNotInline
static Bitmap createBitmapWithSourceColorspace(int w, int h, Bitmap src, boolean linear) {
Bitmap.Config config = src.getConfig();
ColorSpace colorSpace = src.getColorSpace();
ColorSpace linearCs = ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
if (linear && !src.getColorSpace().equals(linearCs)) {
// Promote to F16 to preserve precision.
config = Bitmap.Config.RGBA_F16;
colorSpace = linearCs;
} else if (src.getConfig() == Bitmap.Config.HARDWARE) {
config = Bitmap.Config.ARGB_8888;
if (Build.VERSION.SDK_INT >= 31) {
config = Api31Impl.getHardwareBitmapConfig(src);
}
}
return Bitmap.createBitmap(w, h, config, src.hasAlpha(), colorSpace);
}
@DoNotInline
static boolean isAlreadyF16AndLinear(Bitmap b) {
ColorSpace linearCs = ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
return b.getConfig() == Bitmap.Config.RGBA_F16 && b.getColorSpace().equals(linearCs);
}
@DoNotInline
static Bitmap copyBitmapIfHardware(Bitmap bm) {
if (bm.getConfig() == Bitmap.Config.HARDWARE) {
Bitmap.Config newConfig = Bitmap.Config.ARGB_8888;
if (Build.VERSION.SDK_INT >= 31) {
newConfig = Api31Impl.getHardwareBitmapConfig(bm);
}
return bm.copy(newConfig, true);
} else {
return bm;
}
}
}
@RequiresApi(29)
static class Api29Impl {
private Api29Impl() {
}
@DoNotInline
static void setPaintBlendMode(Paint paint) {
paint.setBlendMode(BlendMode.SRC);
}
}
@RequiresApi(31)
static class Api31Impl {
private Api31Impl() {
}
@DoNotInline
static Bitmap.Config getHardwareBitmapConfig(Bitmap bm) {
if (bm.getHardwareBuffer().getFormat() == HardwareBuffer.RGBA_FP16) {
return Bitmap.Config.RGBA_F16;
} else {
return Bitmap.Config.ARGB_8888;
}
}
}
}