1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; 4 import static android.os.Build.VERSION_CODES.KITKAT; 5 import static android.os.Build.VERSION_CODES.M; 6 7 import android.graphics.Bitmap; 8 import android.graphics.Color; 9 import android.graphics.Matrix; 10 import android.graphics.RectF; 11 import android.os.Build; 12 import android.os.Parcel; 13 import android.util.DisplayMetrics; 14 import java.io.FileDescriptor; 15 import java.io.InputStream; 16 import java.io.OutputStream; 17 import java.nio.Buffer; 18 import java.nio.ByteBuffer; 19 import java.util.Arrays; 20 import org.robolectric.annotation.Implementation; 21 import org.robolectric.annotation.Implements; 22 import org.robolectric.annotation.RealObject; 23 import org.robolectric.shadow.api.Shadow; 24 import org.robolectric.util.ReflectionHelpers; 25 26 @SuppressWarnings({"UnusedDeclaration"}) 27 @Implements(Bitmap.class) 28 public class ShadowBitmap { 29 /** Number of bytes used internally to represent each pixel (in the {@link #colors} array) */ 30 private static final int INTERNAL_BYTES_PER_PIXEL = 4; 31 32 @RealObject 33 private Bitmap realBitmap; 34 35 int createdFromResId = -1; 36 String createdFromPath; 37 InputStream createdFromStream; 38 FileDescriptor createdFromFileDescriptor; 39 byte[] createdFromBytes; 40 private Bitmap createdFromBitmap; 41 private int createdFromX = -1; 42 private int createdFromY = -1; 43 private int createdFromWidth = -1; 44 private int createdFromHeight = -1; 45 private int[] createdFromColors; 46 private Matrix createdFromMatrix; 47 private boolean createdFromFilter; 48 private boolean hasAlpha; 49 50 private int width; 51 private int height; 52 private int density; 53 private int[] colors; 54 private Bitmap.Config config; 55 private boolean mutable; 56 private String description = ""; 57 private boolean recycled = false; 58 private boolean hasMipMap; 59 private boolean isPremultiplied; 60 61 /** 62 * Returns a textual representation of the appearance of the object. 63 * 64 * @param bitmap the bitmap to visualize 65 * @return Textual representation of the appearance of the object. 66 */ 67 public static String visualize(Bitmap bitmap) { 68 ShadowBitmap shadowBitmap = Shadow.extract(bitmap); 69 return shadowBitmap.getDescription(); 70 } 71 72 /** 73 * Reference to original Bitmap from which this Bitmap was created. {@code null} if this Bitmap 74 * was not copied from another instance. 75 * 76 * @return Original Bitmap from which this Bitmap was created. 77 */ 78 public Bitmap getCreatedFromBitmap() { 79 return createdFromBitmap; 80 } 81 82 /** 83 * Resource ID from which this Bitmap was created. {@code 0} if this Bitmap was not created 84 * from a resource. 85 * 86 * @return Resource ID from which this Bitmap was created. 87 */ 88 public int getCreatedFromResId() { 89 return createdFromResId; 90 } 91 92 /** 93 * Path from which this Bitmap was created. {@code null} if this Bitmap was not create from a 94 * path. 95 * 96 * @return Path from which this Bitmap was created. 97 */ 98 public String getCreatedFromPath() { 99 return createdFromPath; 100 } 101 102 /** 103 * {@link InputStream} from which this Bitmap was created. {@code null} if this Bitmap was not 104 * created from a stream. 105 * 106 * @return InputStream from which this Bitmap was created. 107 */ 108 public InputStream getCreatedFromStream() { 109 return createdFromStream; 110 } 111 112 /** 113 * Bytes from which this Bitmap was created. {@code null} if this Bitmap was not created from 114 * bytes. 115 * 116 * @return Bytes from which this Bitmap was created. 117 */ 118 public byte[] getCreatedFromBytes() { 119 return createdFromBytes; 120 } 121 122 /** 123 * Horizontal offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 124 * 125 * @return Horizontal offset within {@link #getCreatedFromBitmap()}. 126 */ 127 public int getCreatedFromX() { 128 return createdFromX; 129 } 130 131 /** 132 * Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 133 * 134 * @return Vertical offset within {@link #getCreatedFromBitmap()} of this Bitmap's content, or -1. 135 */ 136 public int getCreatedFromY() { 137 return createdFromY; 138 } 139 140 /** 141 * Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 142 * content, or -1. 143 * 144 * @return Width from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 145 * content, or -1. 146 */ 147 public int getCreatedFromWidth() { 148 return createdFromWidth; 149 } 150 151 /** 152 * Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 153 * content, or -1. 154 * @return Height from {@link #getCreatedFromX()} within {@link #getCreatedFromBitmap()} of this Bitmap's 155 * content, or -1. 156 */ 157 public int getCreatedFromHeight() { 158 return createdFromHeight; 159 } 160 161 /** 162 * Color array from which this Bitmap was created. {@code null} if this Bitmap was not created 163 * from a color array. 164 * @return Color array from which this Bitmap was created. 165 */ 166 public int[] getCreatedFromColors() { 167 return createdFromColors; 168 } 169 170 /** 171 * Matrix from which this Bitmap's content was transformed, or {@code null}. 172 * @return Matrix from which this Bitmap's content was transformed, or {@code null}. 173 */ 174 public Matrix getCreatedFromMatrix() { 175 return createdFromMatrix; 176 } 177 178 /** 179 * {@code true} if this Bitmap was created with filtering. 180 * @return {@code true} if this Bitmap was created with filtering. 181 */ 182 public boolean getCreatedFromFilter() { 183 return createdFromFilter; 184 } 185 186 @Implementation 187 protected boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream) { 188 appendDescription(" compressed as " + format + " with quality " + quality); 189 return ImageUtil.writeToStream(realBitmap, format, quality, stream); 190 } 191 192 @Implementation 193 protected static Bitmap createBitmap(int width, int height, Bitmap.Config config) { 194 return createBitmap((DisplayMetrics) null, width, height, config); 195 } 196 197 @Implementation(minSdk = JELLY_BEAN_MR1) 198 protected static Bitmap createBitmap( 199 DisplayMetrics displayMetrics, 200 int width, 201 int height, 202 Bitmap.Config config, 203 boolean hasAlpha) { 204 return createBitmap((DisplayMetrics) null, width, height, config); 205 } 206 207 @Implementation(minSdk = JELLY_BEAN_MR1) 208 protected static Bitmap createBitmap( 209 DisplayMetrics displayMetrics, int width, int height, Bitmap.Config config) { 210 if (width <= 0 || height <= 0) { 211 throw new IllegalArgumentException("width and height must be > 0"); 212 } 213 Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 214 ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap); 215 shadowBitmap.setDescription("Bitmap (" + width + " x " + height + ")"); 216 217 shadowBitmap.width = width; 218 shadowBitmap.height = height; 219 shadowBitmap.config = config; 220 shadowBitmap.setMutable(true); 221 if (displayMetrics != null) { 222 shadowBitmap.density = displayMetrics.densityDpi; 223 } 224 shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, shadowBitmap.getWidth(), 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight()); 225 return scaledBitmap; 226 } 227 228 @Implementation 229 protected static Bitmap createBitmap(Bitmap src) { 230 ShadowBitmap shadowBitmap = Shadow.extract(src); 231 shadowBitmap.appendDescription(" created from Bitmap object"); 232 return src; 233 } 234 235 @Implementation 236 protected static Bitmap createScaledBitmap( 237 Bitmap src, int dstWidth, int dstHeight, boolean filter) { 238 if (dstWidth == src.getWidth() && dstHeight == src.getHeight() && !filter) { 239 return src; // Return the original. 240 } 241 242 Bitmap scaledBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 243 ShadowBitmap shadowBitmap = Shadow.extract(scaledBitmap); 244 245 ShadowBitmap shadowSrcBitmap = Shadow.extract(src); 246 shadowBitmap.appendDescription(shadowSrcBitmap.getDescription()); 247 shadowBitmap.appendDescription(" scaled to " + dstWidth + " x " + dstHeight); 248 if (filter) { 249 shadowBitmap.appendDescription(" with filter " + filter); 250 } 251 252 shadowBitmap.createdFromBitmap = src; 253 shadowBitmap.createdFromFilter = filter; 254 shadowBitmap.width = dstWidth; 255 shadowBitmap.height = dstHeight; 256 shadowBitmap.setPixels(new int[shadowBitmap.getHeight() * shadowBitmap.getWidth()], 0, 0, 0, 0, shadowBitmap.getWidth(), shadowBitmap.getHeight()); 257 return scaledBitmap; 258 } 259 260 @Implementation 261 protected static Bitmap createBitmap(Bitmap src, int x, int y, int width, int height) { 262 if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight()) { 263 return src; // Return the original. 264 } 265 266 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 267 ShadowBitmap shadowBitmap = Shadow.extract(newBitmap); 268 269 ShadowBitmap shadowSrcBitmap = Shadow.extract(src); 270 shadowBitmap.appendDescription(shadowSrcBitmap.getDescription()); 271 shadowBitmap.appendDescription(" at (" + x + "," + y); 272 shadowBitmap.appendDescription(" with width " + width + " and height " + height); 273 274 shadowBitmap.createdFromBitmap = src; 275 shadowBitmap.createdFromX = x; 276 shadowBitmap.createdFromY = y; 277 shadowBitmap.createdFromWidth = width; 278 shadowBitmap.createdFromHeight = height; 279 shadowBitmap.width = width; 280 shadowBitmap.height = height; 281 return newBitmap; 282 } 283 284 @Implementation 285 protected void setPixels( 286 int[] pixels, int offset, int stride, int x, int y, int width, int height) { 287 this.colors = pixels; 288 } 289 290 @Implementation 291 protected static Bitmap createBitmap( 292 Bitmap src, int x, int y, int width, int height, Matrix matrix, boolean filter) { 293 if (x == 0 && y == 0 && width == src.getWidth() && height == src.getHeight() && (matrix == null || matrix.isIdentity())) { 294 return src; // Return the original. 295 } 296 297 if (x + width > src.getWidth()) { 298 throw new IllegalArgumentException("x + width must be <= bitmap.width()"); 299 } 300 if (y + height > src.getHeight()) { 301 throw new IllegalArgumentException("y + height must be <= bitmap.height()"); 302 } 303 304 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 305 ShadowBitmap shadowNewBitmap = Shadow.extract(newBitmap); 306 307 ShadowBitmap shadowSrcBitmap = Shadow.extract(src); 308 shadowNewBitmap.appendDescription(shadowSrcBitmap.getDescription()); 309 shadowNewBitmap.appendDescription(" at (" + x + "," + y + ")"); 310 shadowNewBitmap.appendDescription(" with width " + width + " and height " + height); 311 312 shadowNewBitmap.createdFromBitmap = src; 313 shadowNewBitmap.createdFromX = x; 314 shadowNewBitmap.createdFromY = y; 315 shadowNewBitmap.createdFromWidth = width; 316 shadowNewBitmap.createdFromHeight = height; 317 shadowNewBitmap.createdFromMatrix = matrix; 318 shadowNewBitmap.createdFromFilter = filter; 319 320 if (matrix != null) { 321 ShadowMatrix shadowMatrix = Shadow.extract(matrix); 322 shadowNewBitmap.appendDescription(" using matrix " + shadowMatrix.getDescription()); 323 324 // Adjust width and height by using the matrix. 325 RectF mappedRect = new RectF(); 326 matrix.mapRect(mappedRect, new RectF(0, 0, width, height)); 327 width = Math.round(mappedRect.width()); 328 height = Math.round(mappedRect.height()); 329 } 330 if (filter) { 331 shadowNewBitmap.appendDescription(" with filter"); 332 } 333 334 // updated if matrix is non-null 335 shadowNewBitmap.width = width; 336 shadowNewBitmap.height = height; 337 338 return newBitmap; 339 } 340 341 @Implementation 342 protected static Bitmap createBitmap(int[] colors, int width, int height, Bitmap.Config config) { 343 if (colors.length != width * height) { 344 throw new IllegalArgumentException("array length (" + colors.length + ") did not match width * height (" + (width * height) + ")"); 345 } 346 347 Bitmap newBitmap = Bitmap.createBitmap(width, height, config); 348 ShadowBitmap shadowBitmap = Shadow.extract(newBitmap); 349 350 shadowBitmap.setMutable(false); 351 shadowBitmap.createdFromColors = colors; 352 shadowBitmap.colors = new int[colors.length]; 353 System.arraycopy(colors, 0, shadowBitmap.colors, 0, colors.length); 354 return newBitmap; 355 } 356 357 @Implementation 358 protected int getPixel(int x, int y) { 359 internalCheckPixelAccess(x, y); 360 if (colors != null) { 361 // Note that getPixel() returns a non-premultiplied ARGB value; if 362 // config is RGB_565, our return value will likely be more precise than 363 // on a physical device, since it needs to map each color component from 364 // 5 or 6 bits to 8 bits. 365 return colors[y * getWidth() + x]; 366 } else { 367 return 0; 368 } 369 } 370 371 @Implementation 372 protected void setPixel(int x, int y, int color) { 373 if (isRecycled()) { 374 throw new IllegalStateException("Can't call setPixel() on a recycled bitmap"); 375 } else if (!isMutable()) { 376 throw new IllegalStateException("Bitmap is immutable"); 377 } 378 internalCheckPixelAccess(x, y); 379 if (colors == null) { 380 colors = new int[getWidth() * getHeight()]; 381 } 382 colors[y * getWidth() + x] = color; 383 } 384 385 /** 386 * Note that this method will return a RuntimeException unless: - {@code pixels} has the same 387 * length as the number of pixels of the bitmap. - {@code x = 0} - {@code y = 0} - {@code width} 388 * and {@code height} height match the current bitmap's dimensions. 389 */ 390 @Implementation 391 protected void getPixels( 392 int[] pixels, int offset, int stride, int x, int y, int width, int height) { 393 if (x != 0 || 394 y != 0 || 395 width != getWidth() || 396 height != getHeight() || 397 pixels.length != colors.length) { 398 for (int y0 = 0; y0 < height; y0++) { 399 for (int x0 = 0; x0 < width; x0++) { 400 pixels[offset + y0 * stride + x0] = colors[(y0 + y) * getWidth() + x0 + x]; 401 } 402 } 403 } else { 404 System.arraycopy(colors, 0, pixels, 0, colors.length); 405 } 406 } 407 408 @Implementation 409 protected int getRowBytes() { 410 return getBytesPerPixel(config) * getWidth(); 411 } 412 413 @Implementation 414 protected int getByteCount() { 415 return getRowBytes() * getHeight(); 416 } 417 418 @Implementation 419 protected void recycle() { 420 recycled = true; 421 } 422 423 @Implementation 424 protected final boolean isRecycled() { 425 return recycled; 426 } 427 428 @Implementation 429 protected Bitmap copy(Bitmap.Config config, boolean isMutable) { 430 Bitmap newBitmap = ReflectionHelpers.callConstructor(Bitmap.class); 431 ShadowBitmap shadowBitmap = Shadow.extract(newBitmap); 432 shadowBitmap.createdFromBitmap = realBitmap; 433 shadowBitmap.config = config; 434 shadowBitmap.mutable = isMutable; 435 shadowBitmap.height = getHeight(); 436 shadowBitmap.width = getWidth(); 437 if (colors != null) { 438 shadowBitmap.colors = new int[colors.length]; 439 System.arraycopy(shadowBitmap.colors, 0, colors, 0, colors.length); 440 } 441 return newBitmap; 442 } 443 444 @Implementation(minSdk = KITKAT) 445 protected final int getAllocationByteCount() { 446 return getRowBytes() * getHeight(); 447 } 448 449 @Implementation 450 protected final Bitmap.Config getConfig() { 451 return config; 452 } 453 454 @Implementation(minSdk = KITKAT) 455 protected void setConfig(Bitmap.Config config) { 456 this.config = config; 457 } 458 459 @Implementation 460 protected final boolean isMutable() { 461 return mutable; 462 } 463 464 public void setMutable(boolean mutable) { 465 this.mutable = mutable; 466 } 467 468 public void appendDescription(String s) { 469 description += s; 470 } 471 472 public void setDescription(String s) { 473 description = s; 474 } 475 476 public String getDescription() { 477 return description; 478 } 479 480 @Implementation 481 protected final boolean hasAlpha() { 482 return hasAlpha; 483 } 484 485 @Implementation 486 protected void setHasAlpha(boolean hasAlpha) { 487 this.hasAlpha = hasAlpha; 488 } 489 490 @Implementation 491 protected Bitmap extractAlpha() { 492 int[] alphaPixels = new int[colors.length]; 493 for (int i = 0; i < alphaPixels.length; i++) { 494 alphaPixels[i] = Color.alpha(colors[i]); 495 } 496 497 return createBitmap(alphaPixels, getWidth(), getHeight(), Bitmap.Config.ALPHA_8); 498 } 499 500 @Implementation(minSdk = JELLY_BEAN_MR1) 501 protected final boolean hasMipMap() { 502 return hasMipMap; 503 } 504 505 @Implementation(minSdk = JELLY_BEAN_MR1) 506 protected final void setHasMipMap(boolean hasMipMap) { 507 this.hasMipMap = hasMipMap; 508 } 509 510 @Implementation(minSdk = KITKAT) 511 protected void setWidth(int width) { 512 this.width = width; 513 } 514 515 @Implementation 516 protected int getWidth() { 517 return width; 518 } 519 520 @Implementation(minSdk = KITKAT) 521 protected void setHeight(int height) { 522 this.height = height; 523 } 524 525 @Implementation 526 protected int getHeight() { 527 return height; 528 } 529 530 @Implementation 531 protected void setDensity(int density) { 532 this.density = density; 533 } 534 535 @Implementation 536 protected int getDensity() { 537 return density; 538 } 539 540 @Implementation 541 protected int getGenerationId() { 542 return 0; 543 } 544 545 @Implementation(minSdk = M) 546 protected Bitmap createAshmemBitmap() { 547 return realBitmap; 548 } 549 550 @Implementation 551 protected void eraseColor(int color) { 552 if (colors != null) { 553 Arrays.fill(colors, color); 554 } 555 } 556 557 @Implementation 558 protected void writeToParcel(Parcel p, int flags) { 559 p.writeInt(width); 560 p.writeInt(height); 561 p.writeSerializable(config); 562 p.writeIntArray(colors); 563 } 564 565 @Implementation 566 protected static Bitmap nativeCreateFromParcel(Parcel p) { 567 int parceledWidth = p.readInt(); 568 int parceledHeight = p.readInt(); 569 Bitmap.Config parceledConfig = (Bitmap.Config) p.readSerializable(); 570 571 int[] parceledColors = new int[parceledHeight * parceledWidth]; 572 p.readIntArray(parceledColors); 573 574 return createBitmap(parceledColors, parceledWidth, parceledHeight, parceledConfig); 575 } 576 577 @Implementation 578 protected void copyPixelsFromBuffer(Buffer dst) { 579 if (isRecycled()) { 580 throw new IllegalStateException("Can't call copyPixelsFromBuffer() on a recycled bitmap"); 581 } 582 583 // See the related comment in #copyPixelsToBuffer(Buffer). 584 if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) { 585 throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL 586 + " bytes per pixel are supported"); 587 } 588 if (!(dst instanceof ByteBuffer)) { 589 throw new RuntimeException("Not implemented: unsupported Buffer subclass"); 590 } 591 592 ByteBuffer byteBuffer = (ByteBuffer) dst; 593 if (byteBuffer.remaining() < colors.length * INTERNAL_BYTES_PER_PIXEL) { 594 throw new RuntimeException("Buffer not large enough for pixels"); 595 } 596 597 for (int i = 0; i < colors.length; i++) { 598 colors[i] = byteBuffer.getInt(); 599 } 600 } 601 602 @Implementation 603 protected void copyPixelsToBuffer(Buffer dst) { 604 // Ensure that the Bitmap uses 4 bytes per pixel, since we always use 4 bytes per pixels 605 // internally. Clients of this API probably expect that the buffer size must be >= 606 // getByteCount(), but if we don't enforce this restriction then for RGB_4444 and other 607 // configs that value would be smaller then the buffer size we actually need. 608 if (getBytesPerPixel(config) != INTERNAL_BYTES_PER_PIXEL) { 609 throw new RuntimeException("Not implemented: only Bitmaps with " + INTERNAL_BYTES_PER_PIXEL 610 + " bytes per pixel are supported"); 611 } 612 613 if (!(dst instanceof ByteBuffer)) { 614 throw new RuntimeException("Not implemented: unsupported Buffer subclass"); 615 } 616 617 ByteBuffer byteBuffer = (ByteBuffer) dst; 618 for (int color : colors) { 619 byteBuffer.putInt(color); 620 } 621 } 622 623 @Implementation(minSdk = KITKAT) 624 protected void reconfigure(int width, int height, Bitmap.Config config) { 625 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this.config == Bitmap.Config.HARDWARE) { 626 throw new IllegalStateException("native-backed bitmaps may not be reconfigured"); 627 } 628 629 // This should throw if the resulting allocation size is greater than the initial allocation 630 // size of our Bitmap, but we don't keep track of that information reliably, so we're forced to 631 // assume that our original dimensions and config are large enough to fit the new dimensions and 632 // config 633 this.width = width; 634 this.height = height; 635 this.config = config; 636 } 637 638 @Implementation(minSdk = KITKAT) 639 protected void setPremultiplied(boolean isPremultiplied) { 640 this.isPremultiplied = isPremultiplied; 641 } 642 643 @Implementation(minSdk = KITKAT) 644 protected boolean isPremultiplied() { 645 return isPremultiplied; 646 } 647 648 @Implementation 649 protected boolean sameAs(Bitmap other) { 650 if (other == null) { 651 return false; 652 } 653 ShadowBitmap shadowOtherBitmap = Shadow.extract(other); 654 if (this.width != shadowOtherBitmap.width || this.height != shadowOtherBitmap.height) { 655 return false; 656 } 657 if (this.config != null 658 && shadowOtherBitmap.config != null 659 && this.config != shadowOtherBitmap.config) { 660 return false; 661 } 662 if (!Arrays.equals(colors, shadowOtherBitmap.colors)) { 663 return false; 664 } 665 return true; 666 } 667 668 public Bitmap getRealBitmap() { 669 return realBitmap; 670 } 671 672 public static int getBytesPerPixel(Bitmap.Config config) { 673 if (config == null) { 674 throw new NullPointerException("Bitmap config was null."); 675 } 676 switch (config) { 677 case RGBA_F16: 678 return 8; 679 case ARGB_8888: 680 return 4; 681 case RGB_565: 682 case ARGB_4444: 683 return 2; 684 case ALPHA_8: 685 return 1; 686 default: 687 throw new IllegalArgumentException("Unknown bitmap config: " + config); 688 } 689 } 690 691 public void setCreatedFromResId(int resId, String description) { 692 this.createdFromResId = resId; 693 appendDescription(" for resource:" + description); 694 } 695 696 private void internalCheckPixelAccess(int x, int y) { 697 if (x < 0) { 698 throw new IllegalArgumentException("x must be >= 0"); 699 } 700 if (y < 0) { 701 throw new IllegalArgumentException("y must be >= 0"); 702 } 703 if (x >= getWidth()) { 704 throw new IllegalArgumentException("x must be < bitmap.width()"); 705 } 706 if (y >= getHeight()) { 707 throw new IllegalArgumentException("y must be < bitmap.height()"); 708 } 709 } 710 } 711