1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.M; 4 import static android.os.Build.VERSION_CODES.O; 5 import static android.os.Build.VERSION_CODES.Q; 6 import static android.os.Build.VERSION_CODES.S; 7 import static com.google.common.base.Preconditions.checkArgument; 8 import static com.google.common.base.Preconditions.checkNotNull; 9 import static java.lang.Integer.max; 10 import static java.lang.Integer.min; 11 import static org.robolectric.util.reflector.Reflector.reflector; 12 13 import android.graphics.Bitmap; 14 import android.graphics.ColorSpace; 15 import android.graphics.Matrix; 16 import android.graphics.Paint; 17 import android.graphics.Rect; 18 import android.graphics.RectF; 19 import android.os.Build; 20 import android.os.Parcel; 21 import android.os.Parcelable; 22 import android.util.DisplayMetrics; 23 import java.awt.Color; 24 import java.awt.Graphics2D; 25 import java.awt.geom.Rectangle2D; 26 import java.awt.image.BufferedImage; 27 import java.awt.image.ColorModel; 28 import java.awt.image.DataBufferInt; 29 import java.awt.image.WritableRaster; 30 import java.io.FileDescriptor; 31 import java.io.InputStream; 32 import java.io.OutputStream; 33 import java.nio.Buffer; 34 import java.nio.ByteBuffer; 35 import java.nio.IntBuffer; 36 import java.util.Arrays; 37 import org.robolectric.RuntimeEnvironment; 38 import org.robolectric.annotation.Implementation; 39 import org.robolectric.annotation.Implements; 40 import org.robolectric.shadow.api.Shadow; 41 import org.robolectric.util.ReflectionHelpers; 42 import org.robolectric.versioning.AndroidVersions.U; 43 44 @SuppressWarnings({"UnusedDeclaration"}) 45 @Implements(value = Bitmap.class, isInAndroidSdk = false) 46 public class ShadowLegacyBitmap extends ShadowBitmap { 47 /** Number of bytes used internally to represent each pixel */ 48 private static final int INTERNAL_BYTES_PER_PIXEL = 4; 49 50 int createdFromResId = -1; 51 String createdFromPath; 52 InputStream createdFromStream; 53 FileDescriptor createdFromFileDescriptor; 54 byte[] createdFromBytes; 55 private Bitmap createdFromBitmap; 56 private Bitmap scaledFromBitmap; 57 private int createdFromX = -1; 58 private int createdFromY = -1; 59 private int createdFromWidth = -1; 60 private int createdFromHeight = -1; 61 private int[] createdFromColors; 62 private Matrix createdFromMatrix; 63 private boolean createdFromFilter; 64 65 private int width; 66 private int height; 67 private BufferedImage bufferedImage; 68 private Bitmap.Config config; 69 private boolean mutable = true; 70 private String description = ""; 71 private boolean recycled = false; 72 private boolean hasMipMap; 73 private boolean requestPremultiplied = true; 74 private boolean hasAlpha; 75 private ColorSpace colorSpace; 76 77 @Implementation createBitmap(int width, int height, Bitmap.Config config)78 protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) { 79 return createBitmap((DisplayMetrics) null, width, height, config); 80 } 81 82 @Implementation createBitmap( DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config)83 protected static Bitmap createBitmap( 84 DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) { 85 return createBitmap(displayMetrics, width, height, config, true); 86 } 87 88 @Implementation createBitmap( DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config, boolean hasAlpha)89 protected static Bitmap createBitmap( 90 DisplayMetrics displayMetrics, 91 int width, 92 int height, 93 Bitmap.Config config, 94 boolean hasAlpha) { 95 if (width <= 0 || height <= 0) { 96 throw new IllegalArgumentException("width and height must be > 0"); 97 } 98 checkNotNull(config); 99 Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 100 ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap); 101 shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")"); 102 103 shadowBitmap.width = width; 104 shadowBitmap.height = height; 105 shadowBitmap.config = config; 106 shadowBitmap.hasAlpha = hasAlpha; 107 shadowBitmap.setMutable(true); 108 if (displayMetrics != null) { 109 scaledBitmap.setDensity(displayMetrics.densityDpi); 110 } 111 shadowBitmap.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 112 if (RuntimeEnvironment.getApiLevel() >= O) { 113 shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB); 114 } 115 return scaledBitmap; 116 } 117 118 @Implementation(minSdk = O) createBitmap( int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace)119 protected static Bitmap createBitmap( 120 int width, int height, Bitmap.Config config, boolean hasAlpha, ColorSpace colorSpace) { 121 checkArgument(colorSpace != null || config == Bitmap.Config.ALPHA_8); 122 Bitmap bitmap = createBitmap(null, width, height, config, hasAlpha); 123 ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap); 124 shadowBitmap.colorSpace = colorSpace; 125 return bitmap; 126 } 127 128 @Implementation createBitmap( Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter)129 protected static Bitmap createBitmap( 130 Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) { 131 if (x == 0 132 && y == 0 133 && width == src.getWidth() 134 && height == src.getHeight() 135 && (matrix == null || matrix.isIdentity())) { 136 return src; // Return the original. 137 } 138 139 if (x + width > src.getWidth()) { 140 throw new IllegalArgumentException("x + width must be <= bitmap.width()"); 141 } 142 if (y + height > src.getHeight()) { 143 throw new IllegalArgumentException("y + height must be <= bitmap.height()"); 144 } 145 146 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 147 ShadowLegacyBitmap shadowNewBitmap = Shadow.extract(newBitmap); 148 149 ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src); 150 shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription()); 151 shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")"); 152 shadowNewBitmap.appendDescription(" with width " + width + " and height " + height); 153 154 shadowNewBitmap.createdFromBitmap = src; 155 shadowNewBitmap.createdFromX = x; 156 shadowNewBitmap.createdFromY = y; 157 shadowNewBitmap.createdFromWidth = width; 158 shadowNewBitmap.createdFromHeight = height; 159 shadowNewBitmap.createdFromMatrix = matrix; 160 shadowNewBitmap.createdFromFilter = filter; 161 shadowNewBitmap.config = src.getConfig(); 162 if (matrix != null) { 163 ShadowMatrix shadowMatrix = Shadow.extract(matrix); 164 shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription()); 165 166 // Adjust width and height by using the matrix. 167 RectF mappedRect = new RectF(); 168 matrix.mapRect(mappedRect, new RectF(0, 0, width, height)); 169 width = Math.round(mappedRect.width()); 170 height = Math.round(mappedRect.height()); 171 } 172 if (filter) { 173 shadowNewBitmap.appendDescription(" with filter"); 174 } 175 176 // updated if matrix is non-null 177 shadowNewBitmap.width = width; 178 shadowNewBitmap.height = height; 179 shadowNewBitmap.setMutable(true); 180 newBitmap.setDensity(src.getDensity()); 181 if ((matrix == null || matrix.isIdentity()) && shadowSrcBitmap.bufferedImage != null) { 182 // Only simple cases are supported for setting image data to the new Bitmap. 183 shadowNewBitmap.bufferedImage = 184 shadowSrcBitmap.bufferedImage.getSubimage(x, y, width, height); 185 } 186 if (RuntimeEnvironment.getApiLevel() >= O) { 187 shadowNewBitmap.colorSpace = shadowSrcBitmap.colorSpace; 188 } 189 return newBitmap; 190 } 191 192 @Implementation createBitmap( int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)193 protected static Bitmap createBitmap( 194 int[] colors, int offset, int stride, int width, int height, Bitmap.Config config) { 195 return createBitmap(null, colors, offset, stride, width, height, config); 196 } 197 198 @Implementation createBitmap( DisplayMetrics displayMetrics, int[] colors, int offset, int stride, int width, int height, Bitmap.Config config)199 protected static Bitmap createBitmap( 200 DisplayMetrics displayMetrics, 201 int[] colors, 202 int offset, 203 int stride, 204 int width, 205 int height, 206 Bitmap.Config config) { 207 if (width <= 0) { 208 throw new IllegalArgumentException("width must be > 0"); 209 } 210 if (height <= 0) { 211 throw new IllegalArgumentException("height must be > 0"); 212 } 213 if (Math.abs(stride) < width) { 214 throw new IllegalArgumentException("abs(stride) must be >= width"); 215 } 216 checkNotNull(config); 217 int lastScanline = offset + (height - 1) * stride; 218 int length = colors.length; 219 if (offset < 0 220 || (offset + width > length) 221 || lastScanline < 0 222 || (lastScanline + width > length)) { 223 throw new ArrayIndexOutOfBoundsException(); 224 } 225 226 BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 227 bufferedImage.setRGB(0, 0, width, height, colors, offset, stride); 228 Bitmap bitmap = createBitmap(bufferedImage, width, height, config); 229 ShadowLegacyBitmap shadowBitmap = Shadow.extract(bitmap); 230 shadowBitmap.setMutable(false); 231 shadowBitmap.createdFromColors = colors; 232 if (displayMetrics != null) { 233 bitmap.setDensity(displayMetrics.densityDpi); 234 } 235 if (RuntimeEnvironment.getApiLevel() >= O) { 236 shadowBitmap.colorSpace = ColorSpace.get(ColorSpace.Named.SRGB); 237 } 238 return bitmap; 239 } 240 createBitmap( BufferedImage bufferedImage, int width, int height, Bitmap.Config config)241 private static Bitmap createBitmap( 242 BufferedImage bufferedImage, int width, int height, Bitmap.Config config) { 243 Bitmap newBitmap = Bitmap.createBitmap(width, height, config); 244 ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap); 245 shadowBitmap.bufferedImage = bufferedImage; 246 return newBitmap; 247 } 248 249 @Implementation createScaledBitmap( Bitmap src, int dstWidth, int dstHeight, boolean filter)250 protected static Bitmap createScaledBitmap( 251 Bitmap src, int dstWidth, int dstHeight, boolean filter) { 252 if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) { 253 return src; // Return the original. 254 } 255 if (dstWidth <= 0 || dstHeight <= 0) { 256 throw new IllegalArgumentException("width and height must be > 0"); 257 } 258 Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 259 ShadowLegacyBitmap shadowBitmap = Shadow.extract(scaledBitmap); 260 261 ShadowLegacyBitmap shadowSrcBitmap = Shadow.extract(src); 262 shadowBitmap.appendDescription(shadowSrcBitmap.getDescription()); 263 shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight); 264 if (filter) { 265 shadowBitmap.appendDescription(" with filter " + filter); 266 } 267 268 shadowBitmap.createdFromBitmap = src; 269 shadowBitmap.scaledFromBitmap = src; 270 shadowBitmap.createdFromFilter = filter; 271 shadowBitmap.width = dstWidth; 272 shadowBitmap.height = dstHeight; 273 shadowBitmap.config = src.getConfig(); 274 shadowBitmap.mutable = true; 275 if (!ImageUtil.scaledBitmap(src, scaledBitmap, filter)) { 276 shadowBitmap.bufferedImage = 277 new BufferedImage(dstWidth, dstHeight, BufferedImage.TYPE_INT_ARGB); 278 shadowBitmap.setPixelsInternal( 279 new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 280 0, 281 0, 282 0, 283 0, 284 shadowBitmap.getWidth(), 285 shadowBitmap.getHeight()); 286 } 287 if (RuntimeEnvironment.getApiLevel() >= O) { 288 shadowBitmap.colorSpace = shadowSrcBitmap.colorSpace; 289 } 290 return scaledBitmap; 291 } 292 293 @Implementation nativeCreateFromParcel(Parcel p)294 protected static Bitmap nativeCreateFromParcel(Parcel p) { 295 int parceledWidth = p.readInt(); 296 int parceledHeight = p.readInt(); 297 Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable(); 298 299 int[] parceledColors = new int[parceledHeight * parceledWidth]; 300 p.readIntArray(parceledColors); 301 302 return createBitmap( 303 parceledColors, 0, parceledWidth, parceledWidth, parceledHeight, parceledConfig); 304 } 305 getBytesPerPixel(Bitmap.Config config)306 static int getBytesPerPixel(Bitmap.Config config) { 307 if (config == null) { 308 throw new NullPointerException("Bitmap config was null."); 309 } 310 switch (config) { 311 case RGBA_F16: 312 return 8; 313 case ARGB_8888: 314 case HARDWARE: 315 return 4; 316 case RGB_565: 317 case ARGB_4444: 318 return 2; 319 case ALPHA_8: 320 return 1; 321 default: 322 throw new IllegalArgumentException("Unknown bitmap config: " + config); 323 } 324 } 325 326 /** 327 * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap 328 * was not copied from another instance. 329 * 330 * @return Original Bitmap from which this Bitmap was created. 331 */ 332 @Override getCreatedFromBitmap()333 public Bitmap getCreatedFromBitmap() { 334 return createdFromBitmap; 335 } 336 337 /** 338 * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created from a 339 * resource. 340 * 341 * @return Resource ID from which this Bitmap was created. 342 */ 343 @Override getCreatedFromResId()344 public int getCreatedFromResId() { 345 return createdFromResId; 346 } 347 348 /** 349 * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a 350 * path. 351 * 352 * @return Path from which this Bitmap was created. 353 */ 354 @Override getCreatedFromPath()355 public String getCreatedFromPath() { 356 return createdFromPath; 357 } 358 359 /** 360 * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not 361 * created from a stream. 362 * 363 * @return InputStream from which this Bitmap was created. 364 */ 365 @Override getCreatedFromStream()366 public InputStream getCreatedFromStream() { 367 return createdFromStream; 368 } 369 370 /** 371 * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from 372 * bytes. 373 * 374 * @return Bytes from which this Bitmap was created. 375 */ 376 @Override getCreatedFromBytes()377 public byte[] getCreatedFromBytes() { 378 return createdFromBytes; 379 } 380 381 /** 382 * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 383 * 384 * @return Horizontal offset within {@link #getCreatedFromBitmap()}. 385 */ 386 @Override getCreatedFromX()387 public int getCreatedFromX() { 388 return createdFromX; 389 } 390 391 /** 392 * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 393 * 394 * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 395 */ 396 @Override getCreatedFromY()397 public int getCreatedFromY() { 398 return createdFromY; 399 } 400 401 /** 402 * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 403 * content, or -1. 404 * 405 * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this 406 * Bitmap's content, or -1. 407 */ 408 @Override getCreatedFromWidth()409 public int getCreatedFromWidth() { 410 return createdFromWidth; 411 } 412 413 /** 414 * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 415 * content, or -1. 416 * 417 * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this 418 * Bitmap's content, or -1. 419 */ 420 @Override getCreatedFromHeight()421 public int getCreatedFromHeight() { 422 return createdFromHeight; 423 } 424 425 /** 426 * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created 427 * from a color array. 428 * 429 * @return Color array from which this Bitmap was created. 430 */ 431 @Override getCreatedFromColors()432 public int[] getCreatedFromColors() { 433 return createdFromColors; 434 } 435 436 /** 437 * Matrix from which this Bitmap's content was transformed, or {@code null}. 438 * 439 * @return Matrix from which this Bitmap's content was transformed, or {@code null}. 440 */ 441 @Override getCreatedFromMatrix()442 public Matrix getCreatedFromMatrix() { 443 return createdFromMatrix; 444 } 445 446 /** 447 * {@code true} if this Bitmap was created with filtering. 448 * 449 * @return {@code true} if this Bitmap was created with filtering. 450 */ 451 @Override getCreatedFromFilter()452 public boolean getCreatedFromFilter() { 453 return createdFromFilter; 454 } 455 456 @Implementation(minSdk = S) asShared()457 protected Bitmap asShared() { 458 setMutable(false); 459 return realBitmap; 460 } 461 462 @Implementation compress(Bitmap.CompressFormat format, int quality, OutputStream stream)463 protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) { 464 appendDescription(" compressed as " + format + " with quality " + quality); 465 return ImageUtil.writeToStream(realBitmap, format, quality, stream); 466 } 467 468 @Implementation setPixels( int[] pixels, int offset, int stride, int x, int y, int width, int height)469 protected void setPixels( 470 int[] pixels, int offset, int stride, int x, int y, int width, int height) { 471 checkBitmapMutable(); 472 setPixelsInternal(pixels, offset, stride, x, y, width, height); 473 } 474 setPixelsInternal( int[] pixels, int offset, int stride, int x, int y, int width, int height)475 void setPixelsInternal( 476 int[] pixels, int offset, int stride, int x, int y, int width, int height) { 477 if (bufferedImage == null) { 478 bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); 479 } 480 bufferedImage.setRGB(x, y, width, height, pixels, offset, stride); 481 } 482 483 @Implementation getPixel(int x, int y)484 protected int getPixel(int x, int y) { 485 internalCheckPixelAccess(x, y); 486 if (bufferedImage != null) { 487 // Note that getPixel() returns a non-premultiplied ARGB value; if 488 // config is RGB_565, our return value will likely be more precise than 489 // on a physical device, since it needs to map each color component from 490 // 5 or 6 bits to 8 bits. 491 return bufferedImage.getRGB(x, y); 492 } else { 493 return 0; 494 } 495 } 496 497 @Implementation setPixel(int x, int y, int color)498 protected void setPixel(int x, int y, int color) { 499 checkBitmapMutable(); 500 internalCheckPixelAccess(x, y); 501 if (bufferedImage == null) { 502 bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 503 } 504 bufferedImage.setRGB(x, y, color); 505 } 506 507 /** 508 * Note that this method will return a RuntimeException unless: - {@code pixels} has the same 509 * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width} 510 * and {@code height} height match the current bitmap's dimensions. 511 */ 512 @Implementation getPixels( int[] pixels, int offset, int stride, int x, int y, int width, int height)513 protected void getPixels( 514 int[] pixels, int offset, int stride, int x, int y, int width, int height) { 515 bufferedImage.getRGB(x, y, width, height, pixels, offset, stride); 516 } 517 518 @Implementation getRowBytes()519 protected int getRowBytes() { 520 return getBytesPerPixel(config) * getWidth(); 521 } 522 523 @Implementation getByteCount()524 protected int getByteCount() { 525 return getRowBytes() * getHeight(); 526 } 527 528 @Implementation recycle()529 protected void recycle() { 530 recycled = true; 531 } 532 533 @Implementation isRecycled()534 protected boolean isRecycled() { 535 return recycled; 536 } 537 538 @Implementation copy(Bitmap.Config config, boolean isMutable)539 protected Bitmap copy(Bitmap.Config config, boolean isMutable) { 540 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 541 ShadowLegacyBitmap shadowBitmap = Shadow.extract(newBitmap); 542 shadowBitmap.createdFromBitmap = realBitmap; 543 shadowBitmap.config = config; 544 shadowBitmap.mutable = isMutable; 545 shadowBitmap.height = getHeight(); 546 shadowBitmap.width = getWidth(); 547 if (bufferedImage != null) { 548 ColorModel cm = bufferedImage.getColorModel(); 549 WritableRaster raster = 550 bufferedImage.copyData(bufferedImage.getRaster().createCompatibleWritableRaster()); 551 shadowBitmap.bufferedImage = new BufferedImage(cm, raster, false, null); 552 } 553 return newBitmap; 554 } 555 556 @Implementation getAllocationByteCount()557 protected int getAllocationByteCount() { 558 return getRowBytes() * getHeight(); 559 } 560 561 @Implementation getConfig()562 protected Bitmap.Config getConfig() { 563 return config; 564 } 565 566 @Implementation setConfig(Bitmap.Config config)567 protected void setConfig(Bitmap.Config config) { 568 this.config = config; 569 } 570 571 @Implementation isMutable()572 protected boolean isMutable() { 573 return mutable; 574 } 575 576 @Override setMutable(boolean mutable)577 public void setMutable(boolean mutable) { 578 this.mutable = mutable; 579 } 580 581 @Override appendDescription(String s)582 public void appendDescription(String s) { 583 description += s; 584 } 585 586 @Override getDescription()587 public String getDescription() { 588 return description; 589 } 590 591 @Override setDescription(String s)592 public void setDescription(String s) { 593 description = s; 594 } 595 596 @Implementation hasAlpha()597 protected boolean hasAlpha() { 598 return hasAlpha && config != Bitmap.Config.RGB_565; 599 } 600 601 @Implementation setHasAlpha(boolean hasAlpha)602 protected void setHasAlpha(boolean hasAlpha) { 603 this.hasAlpha = hasAlpha; 604 } 605 606 @Implementation extractAlpha()607 protected Bitmap extractAlpha() { 608 WritableRaster raster = bufferedImage.getAlphaRaster(); 609 BufferedImage alphaImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 610 alphaImage.getAlphaRaster().setRect(raster); 611 return createBitmap(alphaImage, getWidth(), getHeight(), Bitmap.Config.ALPHA_8); 612 } 613 614 /** 615 * This shadow implementation ignores the given paint and offsetXY and simply calls {@link 616 * #extractAlpha()}. 617 */ 618 @Implementation extractAlpha(Paint paint, int[] offsetXY)619 protected Bitmap extractAlpha(Paint paint, int[] offsetXY) { 620 return extractAlpha(); 621 } 622 623 @Implementation hasMipMap()624 protected boolean hasMipMap() { 625 return hasMipMap; 626 } 627 628 @Implementation setHasMipMap(boolean hasMipMap)629 protected void setHasMipMap(boolean hasMipMap) { 630 this.hasMipMap = hasMipMap; 631 } 632 633 @Implementation getWidth()634 protected int getWidth() { 635 return width; 636 } 637 638 @Implementation setWidth(int width)639 protected void setWidth(int width) { 640 this.width = width; 641 } 642 643 @Implementation getHeight()644 protected int getHeight() { 645 return height; 646 } 647 648 @Implementation setHeight(int height)649 protected void setHeight(int height) { 650 this.height = height; 651 } 652 653 @Implementation getGenerationId()654 protected int getGenerationId() { 655 return 0; 656 } 657 658 @Implementation(minSdk = M) createAshmemBitmap()659 protected Bitmap createAshmemBitmap() { 660 return realBitmap; 661 } 662 663 @Implementation eraseColor(int color)664 protected void eraseColor(int color) { 665 if (bufferedImage != null) { 666 int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); 667 Arrays.fill(pixels, color); 668 } 669 setDescription(String.format("Bitmap (%d, %d)", width, height)); 670 if (color != 0) { 671 appendDescription(String.format(" erased with 0x%08x", color)); 672 } 673 } 674 675 @Implementation writeToParcel(Parcel p, int flags)676 protected void writeToParcel(Parcel p, int flags) { 677 p.writeInt(width); 678 p.writeInt(height); 679 p.writeSerializable(config); 680 int[] pixels = new int[width * height]; 681 getPixels(pixels, 0, width, 0, 0, width, height); 682 p.writeIntArray(pixels); 683 684 if (RuntimeEnvironment.getApiLevel() >= U.SDK_INT) { 685 Object gainmap = reflector(BitmapReflector.class, realBitmap).getGainmap(); 686 if (gainmap != null) { 687 p.writeBoolean(true); 688 p.writeTypedObject((Parcelable) gainmap, flags); 689 } else { 690 p.writeBoolean(false); 691 } 692 } 693 } 694 695 @Implementation copyPixelsFromBuffer(Buffer dst)696 protected void copyPixelsFromBuffer(Buffer dst) { 697 if (isRecycled()) { 698 throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap"); 699 } 700 701 // See the related comment in #copyPixelsToBuffer(Buffer). 702 if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) { 703 throw new RuntimeException( 704 "Not implemented: only Bitmaps with " 705 + INTERNAL_BYTES_PER_PIXEL 706 + " bytes per pixel are supported"); 707 } 708 if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) { 709 throw new RuntimeException("Not implemented: unsupported Buffer subclass"); 710 } 711 712 ByteBuffer byteBuffer = null; 713 IntBuffer intBuffer; 714 if (dst instanceof IntBuffer) { 715 intBuffer = (IntBuffer) dst; 716 } else { 717 byteBuffer = (ByteBuffer) dst; 718 intBuffer = byteBuffer.asIntBuffer(); 719 } 720 721 if (intBuffer.remaining() < (width * height)) { 722 throw new RuntimeException("Buffer not large enough for pixels"); 723 } 724 725 int[] colors = new int[width * height]; 726 intBuffer.get(colors); 727 if (byteBuffer != null) { 728 byteBuffer.position(byteBuffer.position() + intBuffer.position() * INTERNAL_BYTES_PER_PIXEL); 729 } 730 int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); 731 System.arraycopy(colors, 0, pixels, 0, pixels.length); 732 } 733 734 @Implementation copyPixelsToBuffer(Buffer dst)735 protected void copyPixelsToBuffer(Buffer dst) { 736 // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels 737 // internally. Clients of this API probably expect that the buffer size must be >= 738 // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other 739 // configs that value would be smaller then the buffer size we actually need. 740 if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) { 741 throw new RuntimeException( 742 "Not implemented: only Bitmaps with " 743 + INTERNAL_BYTES_PER_PIXEL 744 + " bytes per pixel are supported"); 745 } 746 747 if (!(dst instanceof ByteBuffer) && !(dst instanceof IntBuffer)) { 748 throw new RuntimeException("Not implemented: unsupported Buffer subclass"); 749 } 750 int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); 751 if (dst instanceof ByteBuffer) { 752 IntBuffer intBuffer = ((ByteBuffer) dst).asIntBuffer(); 753 intBuffer.put(pixels); 754 dst.position(intBuffer.position() * 4); 755 } else if (dst instanceof IntBuffer) { 756 ((IntBuffer) dst).put(pixels); 757 } 758 } 759 760 @Implementation reconfigure(int width, int height, Bitmap.Config config)761 protected void reconfigure(int width, int height, Bitmap.Config config) { 762 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) { 763 throw new IllegalStateException("native-backed bitmaps may not be reconfigured"); 764 } 765 766 // This should throw if the resulting allocation size is greater than the initial allocation 767 // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to 768 // assume that our original dimensions and config are large enough to fit the new dimensions and 769 // config 770 this.width = width; 771 this.height = height; 772 this.config = config; 773 bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 774 } 775 776 @Implementation isPremultiplied()777 protected boolean isPremultiplied() { 778 return requestPremultiplied && hasAlpha(); 779 } 780 781 @Implementation setPremultiplied(boolean isPremultiplied)782 protected void setPremultiplied(boolean isPremultiplied) { 783 this.requestPremultiplied = isPremultiplied; 784 } 785 786 @Implementation(minSdk = O) getColorSpace()787 protected ColorSpace getColorSpace() { 788 return colorSpace; 789 } 790 791 @Implementation(minSdk = Q) setColorSpace(ColorSpace colorSpace)792 protected void setColorSpace(ColorSpace colorSpace) { 793 this.colorSpace = checkNotNull(colorSpace); 794 } 795 796 @Implementation sameAs(Bitmap other)797 protected boolean sameAs(Bitmap other) { 798 if (other == null) { 799 return false; 800 } 801 ShadowLegacyBitmap shadowOtherBitmap = Shadow.extract(other); 802 if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) { 803 return false; 804 } 805 if (this.config != shadowOtherBitmap.config) { 806 return false; 807 } 808 809 if (bufferedImage == null && shadowOtherBitmap.bufferedImage != null) { 810 return false; 811 } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage == null) { 812 return false; 813 } else if (bufferedImage != null && shadowOtherBitmap.bufferedImage != null) { 814 int[] pixels = ((DataBufferInt) bufferedImage.getData().getDataBuffer()).getData(); 815 int[] otherPixels = 816 ((DataBufferInt) shadowOtherBitmap.bufferedImage.getData().getDataBuffer()).getData(); 817 if (!Arrays.equals(pixels, otherPixels)) { 818 return false; 819 } 820 } 821 // When Bitmap.createScaledBitmap is called, the colors array is cleared, so we need a basic 822 // way to detect if two scaled bitmaps are the same. 823 if (scaledFromBitmap != null && shadowOtherBitmap.scaledFromBitmap != null) { 824 return scaledFromBitmap.sameAs(shadowOtherBitmap.scaledFromBitmap); 825 } 826 return true; 827 } 828 setCreatedFromResId(int resId, String description)829 void setCreatedFromResId(int resId, String description) { 830 this.createdFromResId = resId; 831 appendDescription(" for resource:" + description); 832 } 833 checkBitmapMutable()834 private void checkBitmapMutable() { 835 if (isRecycled()) { 836 throw new IllegalStateException("Can't call setPixel() on a recycled bitmap"); 837 } else if (!isMutable()) { 838 throw new IllegalStateException("Bitmap is immutable"); 839 } 840 } 841 internalCheckPixelAccess(int x, int y)842 private void internalCheckPixelAccess(int x, int y) { 843 if (x < 0) { 844 throw new IllegalArgumentException("x must be >= 0"); 845 } 846 if (y < 0) { 847 throw new IllegalArgumentException("y must be >= 0"); 848 } 849 if (x >= getWidth()) { 850 throw new IllegalArgumentException("x must be < bitmap.width()"); 851 } 852 if (y >= getHeight()) { 853 throw new IllegalArgumentException("y must be < bitmap.height()"); 854 } 855 } 856 drawRect(Rect r, Paint paint)857 void drawRect(Rect r, Paint paint) { 858 if (bufferedImage == null) { 859 return; 860 } 861 int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); 862 863 Rect toDraw = 864 new Rect( 865 max(0, r.left), max(0, r.top), min(getWidth(), r.right), min(getHeight(), r.bottom)); 866 if (toDraw.left == 0 && toDraw.top == 0 && toDraw.right == getWidth()) { 867 Arrays.fill(pixels, 0, getWidth() * toDraw.bottom, paint.getColor()); 868 return; 869 } 870 for (int y = toDraw.top; y < toDraw.bottom; y++) { 871 Arrays.fill( 872 pixels, y * getWidth() + toDraw.left, y * getWidth() + toDraw.right, paint.getColor()); 873 } 874 } 875 drawRect(RectF r, Paint paint)876 void drawRect(RectF r, Paint paint) { 877 if (bufferedImage == null) { 878 return; 879 } 880 881 Graphics2D graphics2D = bufferedImage.createGraphics(); 882 Rectangle2D r2d = new Rectangle2D.Float(r.left, r.top, r.right - r.left, r.bottom - r.top); 883 graphics2D.setColor(new Color(paint.getColor())); 884 graphics2D.draw(r2d); 885 graphics2D.dispose(); 886 } 887 drawBitmap(Bitmap source, int left, int top)888 void drawBitmap(Bitmap source, int left, int top) { 889 ShadowLegacyBitmap shadowSource = Shadow.extract(source); 890 if (bufferedImage == null || shadowSource.bufferedImage == null) { 891 // pixel data not available, so there's nothing we can do 892 return; 893 } 894 895 int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); 896 int[] sourcePixels = 897 ((DataBufferInt) shadowSource.bufferedImage.getRaster().getDataBuffer()).getData(); 898 899 // fast path 900 if (left == 0 && top == 0 && getWidth() == source.getWidth()) { 901 int size = min(getWidth() * getHeight(), source.getWidth() * source.getHeight()); 902 System.arraycopy(sourcePixels, 0, pixels, 0, size); 903 return; 904 } 905 // slower (row-by-row) path 906 int startSourceY = max(0, -top); 907 int startSourceX = max(0, -left); 908 int startY = max(0, top); 909 int startX = max(0, left); 910 int endY = min(getHeight(), top + source.getHeight()); 911 int endX = min(getWidth(), left + source.getWidth()); 912 int lenY = endY - startY; 913 int lenX = endX - startX; 914 for (int y = 0; y < lenY; y++) { 915 System.arraycopy( 916 sourcePixels, 917 (startSourceY + y) * source.getWidth() + startSourceX, 918 pixels, 919 (startY + y) * getWidth() + startX, 920 lenX); 921 } 922 } 923 getBufferedImage()924 BufferedImage getBufferedImage() { 925 return bufferedImage; 926 } 927 setBufferedImage(BufferedImage bufferedImage)928 void setBufferedImage(BufferedImage bufferedImage) { 929 this.bufferedImage = bufferedImage; 930 } 931 } 932