1 /* 2 * Copyright (C) 2017 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 17 package android.graphics.cts; 18 import static org.junit.Assert.assertEquals; 19 import static org.junit.Assert.assertFalse; 20 import static org.junit.Assert.assertNotEquals; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertNull; 23 import static org.junit.Assert.assertSame; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assert.fail; 26 27 import android.app.ActivityManager; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.res.AssetManager; 31 import android.content.res.Resources; 32 import android.graphics.Bitmap; 33 import android.graphics.BitmapFactory; 34 import android.graphics.Canvas; 35 import android.graphics.Color; 36 import android.graphics.ColorSpace; 37 import android.graphics.ImageDecoder; 38 import android.graphics.ImageDecoder.DecodeException; 39 import android.graphics.ImageDecoder.OnPartialImageListener; 40 import android.graphics.PixelFormat; 41 import android.graphics.PostProcessor; 42 import android.graphics.Rect; 43 import android.graphics.drawable.BitmapDrawable; 44 import android.graphics.drawable.Drawable; 45 import android.graphics.drawable.NinePatchDrawable; 46 import android.net.Uri; 47 import android.support.test.InstrumentationRegistry; 48 import android.support.test.filters.LargeTest; 49 import android.support.test.runner.AndroidJUnit4; 50 import android.util.DisplayMetrics; 51 import android.util.Size; 52 import android.util.TypedValue; 53 54 import androidx.core.content.FileProvider; 55 56 import com.android.compatibility.common.util.BitmapUtils; 57 58 import org.junit.Before; 59 import org.junit.Test; 60 import org.junit.runner.RunWith; 61 62 import java.io.ByteArrayOutputStream; 63 import java.io.File; 64 import java.io.FileOutputStream; 65 import java.io.IOException; 66 import java.io.InputStream; 67 import java.io.OutputStream; 68 import java.nio.ByteBuffer; 69 import java.util.function.IntFunction; 70 import java.util.function.Supplier; 71 import java.util.function.ToIntFunction; 72 73 @RunWith(AndroidJUnit4.class) 74 public class ImageDecoderTest { 75 private Resources mRes; 76 private ContentResolver mContentResolver; 77 78 private static final class Record { 79 public final int resId; 80 public final int width; 81 public final int height; 82 public final String mimeType; 83 public final ColorSpace colorSpace; 84 85 Record(int resId, int width, int height, String mimeType, ColorSpace colorSpace) { 86 this.resId = resId; 87 this.width = width; 88 this.height = height; 89 this.mimeType = mimeType; 90 this.colorSpace = colorSpace; 91 } 92 } 93 94 private static final ColorSpace sSRGB = ColorSpace.get(ColorSpace.Named.SRGB); 95 96 private static final Record[] RECORDS = new Record[] { 97 new Record(R.drawable.baseline_jpeg, 1280, 960, "image/jpeg", sSRGB), 98 new Record(R.drawable.png_test, 640, 480, "image/png", sSRGB), 99 new Record(R.drawable.gif_test, 320, 240, "image/gif", sSRGB), 100 new Record(R.drawable.bmp_test, 320, 240, "image/bmp", sSRGB), 101 new Record(R.drawable.webp_test, 640, 480, "image/webp", sSRGB), 102 new Record(R.drawable.google_chrome, 256, 256, "image/x-ico", sSRGB), 103 new Record(R.drawable.color_wheel, 128, 128, "image/x-ico", sSRGB), 104 new Record(R.raw.sample_1mp, 600, 338, "image/x-adobe-dng", sSRGB), 105 }; 106 107 // offset is how many bytes to offset the beginning of the image. 108 // extra is how many bytes to append at the end. 109 private byte[] getAsByteArray(int resId, int offset, int extra) { 110 ByteArrayOutputStream output = new ByteArrayOutputStream(); 111 writeToStream(output, resId, offset, extra); 112 return output.toByteArray(); 113 } 114 115 private void writeToStream(OutputStream output, int resId, int offset, int extra) { 116 InputStream input = mRes.openRawResource(resId); 117 byte[] buffer = new byte[4096]; 118 int bytesRead; 119 try { 120 for (int i = 0; i < offset; ++i) { 121 output.write(0); 122 } 123 124 while ((bytesRead = input.read(buffer)) != -1) { 125 output.write(buffer, 0, bytesRead); 126 } 127 128 for (int i = 0; i < extra; ++i) { 129 output.write(0); 130 } 131 132 input.close(); 133 } catch (IOException e) { 134 fail(); 135 } 136 } 137 138 private byte[] getAsByteArray(int resId) { 139 return getAsByteArray(resId, 0, 0); 140 } 141 142 private ByteBuffer getAsByteBufferWrap(int resId) { 143 byte[] buffer = getAsByteArray(resId); 144 return ByteBuffer.wrap(buffer); 145 } 146 147 private ByteBuffer getAsDirectByteBuffer(int resId) { 148 byte[] buffer = getAsByteArray(resId); 149 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(buffer.length); 150 byteBuffer.put(buffer); 151 byteBuffer.position(0); 152 return byteBuffer; 153 } 154 155 private ByteBuffer getAsReadOnlyByteBuffer(int resId) { 156 return getAsByteBufferWrap(resId).asReadOnlyBuffer(); 157 } 158 159 private File getAsFile(int resId) { 160 File file = null; 161 try { 162 Context context = InstrumentationRegistry.getTargetContext(); 163 File dir = new File(context.getFilesDir(), "images"); 164 dir.mkdirs(); 165 file = new File(dir, "test_file" + resId); 166 if (!file.createNewFile()) { 167 if (file.exists()) { 168 return file; 169 } 170 fail("Failed to create new File!"); 171 } 172 173 FileOutputStream output = new FileOutputStream(file); 174 writeToStream(output, resId, 0, 0); 175 output.close(); 176 177 } catch (IOException e) { 178 fail("Failed with exception " + e); 179 return null; 180 } 181 return file; 182 } 183 184 private Uri getAsFileUri(int resId) { 185 return Uri.fromFile(getAsFile(resId)); 186 } 187 188 private Uri getAsContentUri(int resId) { 189 Context context = InstrumentationRegistry.getTargetContext(); 190 return FileProvider.getUriForFile(context, 191 "android.graphics.cts.fileprovider", getAsFile(resId)); 192 } 193 194 private Uri getAsResourceUri(int resId) { 195 return new Uri.Builder() 196 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 197 .authority(mRes.getResourcePackageName(resId)) 198 .appendPath(mRes.getResourceTypeName(resId)) 199 .appendPath(mRes.getResourceEntryName(resId)) 200 .build(); 201 } 202 203 private interface SourceCreator extends IntFunction<ImageDecoder.Source> {}; 204 205 private SourceCreator[] mCreators = new SourceCreator[] { 206 resId -> ImageDecoder.createSource(getAsByteBufferWrap(resId)), 207 resId -> ImageDecoder.createSource(getAsDirectByteBuffer(resId)), 208 resId -> ImageDecoder.createSource(getAsReadOnlyByteBuffer(resId)), 209 resId -> ImageDecoder.createSource(getAsFile(resId)), 210 }; 211 212 private interface UriCreator extends IntFunction<Uri> {}; 213 214 private UriCreator[] mUriCreators = new UriCreator[] { 215 resId -> getAsResourceUri(resId), 216 resId -> getAsFileUri(resId), 217 resId -> getAsContentUri(resId), 218 }; 219 220 @Test 221 public void testUris() { 222 for (Record record : RECORDS) { 223 int resId = record.resId; 224 String name = mRes.getResourceEntryName(resId); 225 for (UriCreator f : mUriCreators) { 226 ImageDecoder.Source src = null; 227 Uri uri = f.apply(resId); 228 String fullName = name + ": " + uri.toString(); 229 src = ImageDecoder.createSource(mContentResolver, uri); 230 231 assertNotNull("failed to create Source for " + fullName, src); 232 try { 233 Drawable d = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 234 decoder.setOnPartialImageListener((e) -> { 235 fail("error for image " + fullName + ":\n" + e); 236 return false; 237 }); 238 }); 239 assertNotNull("failed to create drawable for " + fullName, d); 240 } catch (IOException e) { 241 fail("exception for image " + fullName + ":\n" + e); 242 } 243 } 244 } 245 } 246 247 @Before 248 public void setup() { 249 mRes = InstrumentationRegistry.getTargetContext().getResources(); 250 mContentResolver = InstrumentationRegistry.getTargetContext().getContentResolver(); 251 } 252 253 @Test 254 public void testInfo() { 255 for (Record record : RECORDS) { 256 for (SourceCreator f : mCreators) { 257 ImageDecoder.Source src = f.apply(record.resId); 258 assertNotNull(src); 259 try { 260 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 261 assertEquals(record.width, info.getSize().getWidth()); 262 assertEquals(record.height, info.getSize().getHeight()); 263 assertEquals(record.mimeType, info.getMimeType()); 264 assertSame(record.colorSpace, info.getColorSpace()); 265 }); 266 } catch (IOException e) { 267 fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e); 268 } 269 } 270 } 271 } 272 273 @Test 274 public void testDecodeDrawable() { 275 for (Record record : RECORDS) { 276 for (SourceCreator f : mCreators) { 277 ImageDecoder.Source src = f.apply(record.resId); 278 assertNotNull(src); 279 280 try { 281 Drawable drawable = ImageDecoder.decodeDrawable(src); 282 assertNotNull(drawable); 283 assertEquals(record.width, drawable.getIntrinsicWidth()); 284 assertEquals(record.height, drawable.getIntrinsicHeight()); 285 } catch (IOException e) { 286 fail("Failed with exception " + e); 287 } 288 } 289 } 290 } 291 292 @Test 293 public void testDecodeBitmap() { 294 for (Record record : RECORDS) { 295 for (SourceCreator f : mCreators) { 296 ImageDecoder.Source src = f.apply(record.resId); 297 assertNotNull(src); 298 299 try { 300 Bitmap bm = ImageDecoder.decodeBitmap(src); 301 assertNotNull(bm); 302 assertEquals(record.width, bm.getWidth()); 303 assertEquals(record.height, bm.getHeight()); 304 assertFalse(bm.isMutable()); 305 // FIXME: This may change for small resources, etc. 306 assertEquals(Bitmap.Config.HARDWARE, bm.getConfig()); 307 } catch (IOException e) { 308 fail("Failed with exception " + e); 309 } 310 } 311 } 312 } 313 314 @Test(expected=IllegalArgumentException.class) 315 public void testSetBogusAllocator() { 316 ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId); 317 try { 318 ImageDecoder.decodeBitmap(src, (decoder, info, s) -> decoder.setAllocator(15)); 319 } catch (IOException e) { 320 fail("Failed with exception " + e); 321 } 322 } 323 324 private static final int[] ALLOCATORS = new int[] { 325 ImageDecoder.ALLOCATOR_SOFTWARE, 326 ImageDecoder.ALLOCATOR_SHARED_MEMORY, 327 ImageDecoder.ALLOCATOR_HARDWARE, 328 ImageDecoder.ALLOCATOR_DEFAULT, 329 }; 330 331 @Test 332 public void testGetAllocator() { 333 final int resId = RECORDS[0].resId; 334 ImageDecoder.Source src = mCreators[0].apply(resId); 335 try { 336 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 337 assertEquals(ImageDecoder.ALLOCATOR_DEFAULT, decoder.getAllocator()); 338 for (int allocator : ALLOCATORS) { 339 decoder.setAllocator(allocator); 340 assertEquals(allocator, decoder.getAllocator()); 341 } 342 }); 343 } catch (IOException e) { 344 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 345 } 346 } 347 348 @Test 349 public void testSetAllocatorDecodeBitmap() { 350 class Listener implements ImageDecoder.OnHeaderDecodedListener { 351 public int allocator; 352 public boolean doCrop; 353 public boolean doScale; 354 @Override 355 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 356 ImageDecoder.Source src) { 357 decoder.setAllocator(allocator); 358 if (doScale) { 359 decoder.setTargetSampleSize(2); 360 } 361 if (doCrop) { 362 decoder.setCrop(new Rect(1, 1, info.getSize().getWidth() / 2 - 1, 363 info.getSize().getHeight() / 2 - 1)); 364 } 365 } 366 }; 367 Listener l = new Listener(); 368 369 boolean trueFalse[] = new boolean[] { true, false }; 370 for (Record record : RECORDS) { 371 for (SourceCreator f : mCreators) { 372 for (int allocator : ALLOCATORS) { 373 for (boolean doCrop : trueFalse) { 374 for (boolean doScale : trueFalse) { 375 l.doCrop = doCrop; 376 l.doScale = doScale; 377 l.allocator = allocator; 378 ImageDecoder.Source src = f.apply(record.resId); 379 assertNotNull(src); 380 381 Bitmap bm = null; 382 try { 383 bm = ImageDecoder.decodeBitmap(src, l); 384 } catch (IOException e) { 385 fail("Failed " + getAsResourceUri(record.resId) + 386 " with exception " + e); 387 } 388 assertNotNull(bm); 389 390 switch (allocator) { 391 case ImageDecoder.ALLOCATOR_SOFTWARE: 392 // TODO: Once Bitmap provides access to its 393 // SharedMemory, confirm that ALLOCATOR_SHARED_MEMORY 394 // worked. 395 case ImageDecoder.ALLOCATOR_SHARED_MEMORY: 396 assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig()); 397 398 if (!doScale && !doCrop) { 399 Bitmap reference = BitmapFactory.decodeResource(mRes, 400 record.resId, null); 401 assertNotNull(reference); 402 BitmapUtils.compareBitmaps(bm, reference); 403 } 404 break; 405 default: 406 String name = getAsResourceUri(record.resId).toString(); 407 assertEquals("image " + name + "; allocator: " + allocator, 408 Bitmap.Config.HARDWARE, bm.getConfig()); 409 break; 410 } 411 } 412 } 413 } 414 } 415 } 416 } 417 418 @Test 419 public void testGetUnpremul() { 420 final int resId = RECORDS[0].resId; 421 ImageDecoder.Source src = mCreators[0].apply(resId); 422 try { 423 ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 424 assertFalse(decoder.isUnpremultipliedRequired()); 425 426 decoder.setUnpremultipliedRequired(true); 427 assertTrue(decoder.isUnpremultipliedRequired()); 428 429 decoder.setUnpremultipliedRequired(false); 430 assertFalse(decoder.isUnpremultipliedRequired()); 431 }); 432 } catch (IOException e) { 433 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 434 } 435 } 436 437 @Test 438 public void testUnpremul() { 439 int[] resIds = new int[] { R.drawable.png_test, R.drawable.alpha }; 440 boolean[] hasAlpha = new boolean[] { false, true }; 441 for (int i = 0; i < resIds.length; ++i) { 442 for (SourceCreator f : mCreators) { 443 // Normal decode 444 ImageDecoder.Source src = f.apply(resIds[i]); 445 assertNotNull(src); 446 447 try { 448 Bitmap normal = ImageDecoder.decodeBitmap(src); 449 assertNotNull(normal); 450 assertEquals(normal.hasAlpha(), hasAlpha[i]); 451 assertEquals(normal.isPremultiplied(), hasAlpha[i]); 452 453 // Require unpremul 454 src = f.apply(resIds[i]); 455 assertNotNull(src); 456 457 Bitmap unpremul = ImageDecoder.decodeBitmap(src, 458 (decoder, info, s) -> decoder.setUnpremultipliedRequired(true)); 459 assertNotNull(unpremul); 460 assertEquals(unpremul.hasAlpha(), hasAlpha[i]); 461 assertFalse(unpremul.isPremultiplied()); 462 } catch (IOException e) { 463 fail("Failed with exception " + e); 464 } 465 } 466 } 467 } 468 469 @Test 470 public void testGetPostProcessor() { 471 PostProcessor[] processors = new PostProcessor[] { 472 (canvas) -> PixelFormat.UNKNOWN, 473 (canvas) -> PixelFormat.UNKNOWN, 474 null, 475 }; 476 final int resId = RECORDS[0].resId; 477 ImageDecoder.Source src = mCreators[0].apply(resId); 478 try { 479 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 480 assertNull(decoder.getPostProcessor()); 481 482 for (PostProcessor pp : processors) { 483 decoder.setPostProcessor(pp); 484 assertSame(pp, decoder.getPostProcessor()); 485 } 486 }); 487 } catch (IOException e) { 488 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 489 } 490 } 491 492 @Test 493 public void testPostProcessor() { 494 class Listener implements ImageDecoder.OnHeaderDecodedListener { 495 public boolean requireSoftware; 496 @Override 497 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 498 ImageDecoder.Source src) { 499 if (requireSoftware) { 500 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 501 } 502 decoder.setPostProcessor((canvas) -> { 503 canvas.drawColor(Color.BLACK); 504 return PixelFormat.OPAQUE; 505 }); 506 } 507 }; 508 Listener l = new Listener(); 509 boolean trueFalse[] = new boolean[] { true, false }; 510 for (Record record : RECORDS) { 511 for (SourceCreator f : mCreators) { 512 for (boolean requireSoftware : trueFalse) { 513 l.requireSoftware = requireSoftware; 514 ImageDecoder.Source src = f.apply(record.resId); 515 assertNotNull(src); 516 517 Bitmap bitmap = null; 518 try { 519 bitmap = ImageDecoder.decodeBitmap(src, l); 520 } catch (IOException e) { 521 fail("Failed with exception " + e); 522 } 523 assertNotNull(bitmap); 524 assertFalse(bitmap.isMutable()); 525 if (requireSoftware) { 526 assertNotEquals(Bitmap.Config.HARDWARE, bitmap.getConfig()); 527 for (int x = 0; x < bitmap.getWidth(); ++x) { 528 for (int y = 0; y < bitmap.getHeight(); ++y) { 529 int color = bitmap.getPixel(x, y); 530 assertEquals("pixel at (" + x + ", " + y + ") does not match!", 531 color, Color.BLACK); 532 } 533 } 534 } else { 535 assertEquals(bitmap.getConfig(), Bitmap.Config.HARDWARE); 536 } 537 } 538 } 539 } 540 } 541 542 @Test 543 public void testNinepatchWithDensityNone() { 544 TypedValue value = new TypedValue(); 545 InputStream is = mRes.openRawResource(R.drawable.ninepatch_nodpi, value); 546 // This does not call ImageDecoder directly because this entry point is not public. 547 Drawable dr = Drawable.createFromResourceStream(mRes, value, is, null, null); 548 assertNotNull(dr); 549 assertEquals(5, dr.getIntrinsicWidth()); 550 assertEquals(5, dr.getIntrinsicHeight()); 551 } 552 553 @Test 554 public void testPostProcessorOverridesNinepatch() { 555 class Listener implements ImageDecoder.OnHeaderDecodedListener { 556 public boolean requireSoftware; 557 @Override 558 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 559 ImageDecoder.Source src) { 560 if (requireSoftware) { 561 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 562 } 563 decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN); 564 } 565 }; 566 Listener l = new Listener(); 567 int resIds[] = new int[] { R.drawable.ninepatch_0, 568 R.drawable.ninepatch_1 }; 569 boolean trueFalse[] = new boolean[] { true, false }; 570 for (int resId : resIds) { 571 for (SourceCreator f : mCreators) { 572 for (boolean requireSoftware : trueFalse) { 573 l.requireSoftware = requireSoftware; 574 ImageDecoder.Source src = f.apply(resId); 575 try { 576 Drawable drawable = ImageDecoder.decodeDrawable(src, l); 577 assertFalse(drawable instanceof NinePatchDrawable); 578 579 src = f.apply(resId); 580 Bitmap bm = ImageDecoder.decodeBitmap(src, l); 581 assertNull(bm.getNinePatchChunk()); 582 } catch (IOException e) { 583 fail("Failed with exception " + e); 584 } 585 } 586 } 587 } 588 } 589 590 @Test 591 public void testPostProcessorAndMadeOpaque() { 592 class Listener implements ImageDecoder.OnHeaderDecodedListener { 593 public boolean requireSoftware; 594 @Override 595 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 596 ImageDecoder.Source src) { 597 if (requireSoftware) { 598 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 599 } 600 decoder.setPostProcessor((c) -> PixelFormat.OPAQUE); 601 } 602 }; 603 Listener l = new Listener(); 604 boolean trueFalse[] = new boolean[] { true, false }; 605 int resIds[] = new int[] { R.drawable.alpha, R.drawable.google_logo_2 }; 606 for (int resId : resIds) { 607 for (SourceCreator f : mCreators) { 608 for (boolean requireSoftware : trueFalse) { 609 l.requireSoftware = requireSoftware; 610 ImageDecoder.Source src = f.apply(resId); 611 try { 612 Bitmap bm = ImageDecoder.decodeBitmap(src, l); 613 assertFalse(bm.hasAlpha()); 614 assertFalse(bm.isPremultiplied()); 615 } catch (IOException e) { 616 fail("Failed with exception " + e); 617 } 618 } 619 } 620 } 621 } 622 623 @Test 624 public void testPostProcessorAndAddedTransparency() { 625 class Listener implements ImageDecoder.OnHeaderDecodedListener { 626 public boolean requireSoftware; 627 @Override 628 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 629 ImageDecoder.Source src) { 630 if (requireSoftware) { 631 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 632 } 633 decoder.setPostProcessor((c) -> PixelFormat.TRANSLUCENT); 634 } 635 }; 636 Listener l = new Listener(); 637 boolean trueFalse[] = new boolean[] { true, false }; 638 for (Record record : RECORDS) { 639 for (SourceCreator f : mCreators) { 640 for (boolean requireSoftware : trueFalse) { 641 l.requireSoftware = requireSoftware; 642 ImageDecoder.Source src = f.apply(record.resId); 643 try { 644 Bitmap bm = ImageDecoder.decodeBitmap(src, l); 645 assertTrue(bm.hasAlpha()); 646 assertTrue(bm.isPremultiplied()); 647 } catch (IOException e) { 648 fail("Failed with exception " + e); 649 } 650 } 651 } 652 } 653 } 654 655 @Test(expected=IllegalArgumentException.class) 656 public void testPostProcessorTRANSPARENT() { 657 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 658 try { 659 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 660 decoder.setPostProcessor((c) -> PixelFormat.TRANSPARENT); 661 }); 662 } catch (IOException e) { 663 fail("Failed with exception " + e); 664 } 665 } 666 667 @Test(expected=IllegalArgumentException.class) 668 public void testPostProcessorInvalidReturn() { 669 ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId); 670 try { 671 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 672 decoder.setPostProcessor((c) -> 42); 673 }); 674 } catch (IOException e) { 675 fail("Failed with exception " + e); 676 } 677 } 678 679 @Test(expected=IllegalStateException.class) 680 public void testPostProcessorAndUnpremul() { 681 ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId); 682 try { 683 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 684 decoder.setUnpremultipliedRequired(true); 685 decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN); 686 }); 687 } catch (IOException e) { 688 fail("Failed with exception " + e); 689 } 690 } 691 692 @Test 693 public void testPostProcessorAndScale() { 694 class PostProcessorWithSize implements PostProcessor { 695 public int width; 696 public int height; 697 @Override 698 public int onPostProcess(Canvas canvas) { 699 assertEquals(this.width, width); 700 assertEquals(this.height, height); 701 return PixelFormat.UNKNOWN; 702 }; 703 }; 704 final PostProcessorWithSize pp = new PostProcessorWithSize(); 705 for (Record record : RECORDS) { 706 pp.width = record.width / 2; 707 pp.height = record.height / 2; 708 for (SourceCreator f : mCreators) { 709 ImageDecoder.Source src = f.apply(record.resId); 710 try { 711 Drawable drawable = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 712 decoder.setTargetSize(pp.width, pp.height); 713 decoder.setPostProcessor(pp); 714 }); 715 assertEquals(pp.width, drawable.getIntrinsicWidth()); 716 assertEquals(pp.height, drawable.getIntrinsicHeight()); 717 } catch (IOException e) { 718 fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e); 719 } 720 } 721 } 722 } 723 724 private void checkSampleSize(String name, int originalDimension, int sampleSize, int result) { 725 if (originalDimension % sampleSize == 0) { 726 assertEquals("Mismatch for " + name + ": " + originalDimension + " / " + sampleSize 727 + " != " + result, originalDimension / sampleSize, result); 728 } else if (originalDimension <= sampleSize) { 729 assertEquals(1, result); 730 } else { 731 // Rounding may result in differences. 732 int size = result * sampleSize; 733 assertTrue("Rounding mismatch for " + name + ": " + originalDimension + " / " 734 + sampleSize + " = " + result, 735 Math.abs(size - originalDimension) < sampleSize); 736 } 737 } 738 739 @Test 740 public void testSampleSize() { 741 for (Record record : RECORDS) { 742 final String name = getAsResourceUri(record.resId).toString(); 743 for (int sampleSize : new int[] { 2, 3, 4, 8, 32 }) { 744 ImageDecoder.Source src = mCreators[0].apply(record.resId); 745 try { 746 Drawable dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 747 decoder.setTargetSampleSize(sampleSize); 748 }); 749 750 checkSampleSize(name, record.width, sampleSize, dr.getIntrinsicWidth()); 751 checkSampleSize(name, record.height, sampleSize, dr.getIntrinsicHeight()); 752 } catch (IOException e) { 753 fail("Failed " + name + " with exception " + e); 754 } 755 } 756 } 757 } 758 759 private interface SampleSizeSupplier extends ToIntFunction<Size> {}; 760 761 @Test 762 public void testLargeSampleSize() { 763 for (Record record : RECORDS) { 764 for (SourceCreator f : mCreators) { 765 for (SampleSizeSupplier supplySampleSize : new SampleSizeSupplier[] { 766 (size) -> size.getWidth(), 767 (size) -> size.getWidth() + 5, 768 (size) -> size.getWidth() * 5, 769 }) { 770 ImageDecoder.Source src = f.apply(record.resId); 771 try { 772 Drawable dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 773 int sampleSize = supplySampleSize.applyAsInt(info.getSize()); 774 decoder.setTargetSampleSize(sampleSize); 775 }); 776 assertEquals(1, dr.getIntrinsicWidth()); 777 } catch (IOException e) { 778 fail("Failed with exception " + e); 779 } 780 } 781 } 782 } 783 } 784 785 @Test 786 public void testResizeTransparency() { 787 ImageDecoder.Source src = mCreators[0].apply(R.drawable.animated); 788 Drawable dr = null; 789 try { 790 dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 791 Size size = info.getSize(); 792 decoder.setTargetSize(size.getWidth() - 5, size.getHeight() - 5); 793 }); 794 } catch (IOException e) { 795 fail("Failed with exception " + e); 796 } 797 798 final int width = dr.getIntrinsicWidth(); 799 final int height = dr.getIntrinsicHeight(); 800 801 // Draw to a fully transparent Bitmap. Pixels that are transparent in the image will be 802 // transparent. 803 Bitmap normal = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 804 { 805 Canvas canvas = new Canvas(normal); 806 dr.draw(canvas); 807 } 808 809 // Draw to a BLUE Bitmap. Any pixels that are transparent in the image remain BLUE. 810 Bitmap blended = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 811 { 812 Canvas canvas = new Canvas(blended); 813 canvas.drawColor(Color.BLUE); 814 dr.draw(canvas); 815 } 816 817 boolean hasTransparency = false; 818 for (int i = 0; i < width; ++i) { 819 for (int j = 0; j < height; ++j) { 820 int normalColor = normal.getPixel(i, j); 821 int blendedColor = blended.getPixel(i, j); 822 if (normalColor == Color.TRANSPARENT) { 823 hasTransparency = true; 824 assertEquals(Color.BLUE, blendedColor); 825 } else if (Color.alpha(normalColor) == 255) { 826 assertEquals(normalColor, blendedColor); 827 } 828 } 829 } 830 831 // Verify that the image has transparency. Otherwise the test is not useful. 832 assertTrue(hasTransparency); 833 } 834 835 @Test 836 public void testGetOnPartialImageListener() { 837 OnPartialImageListener[] listeners = new OnPartialImageListener[] { 838 (e) -> true, 839 (e) -> false, 840 null, 841 }; 842 843 final int resId = RECORDS[0].resId; 844 ImageDecoder.Source src = mCreators[0].apply(resId); 845 try { 846 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 847 assertNull(decoder.getOnPartialImageListener()); 848 849 for (OnPartialImageListener l : listeners) { 850 decoder.setOnPartialImageListener(l); 851 assertSame(l, decoder.getOnPartialImageListener()); 852 } 853 }); 854 } catch (IOException e) { 855 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 856 } 857 } 858 859 @Test 860 public void testEarlyIncomplete() { 861 byte[] bytes = getAsByteArray(R.raw.basi6a16); 862 // This is too early to create a partial image, so we throw the Exception 863 // without calling the listener. 864 int truncatedLength = 49; 865 ImageDecoder.Source src = ImageDecoder.createSource( 866 ByteBuffer.wrap(bytes, 0, truncatedLength)); 867 try { 868 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 869 decoder.setOnPartialImageListener((e) -> { 870 fail("No need to call listener; no partial image to display!" 871 + " Exception: " + e); 872 return false; 873 }); 874 }); 875 } catch (DecodeException e) { 876 assertEquals(DecodeException.SOURCE_INCOMPLETE, e.getError()); 877 assertSame(src, e.getSource()); 878 } catch (IOException ioe) { 879 fail("Threw some other exception: " + ioe); 880 } 881 } 882 883 private class ExceptionStream extends InputStream { 884 private final InputStream mInputStream; 885 private final int mExceptionPosition; 886 int mPosition; 887 888 ExceptionStream(int resId, int exceptionPosition) { 889 mInputStream = mRes.openRawResource(resId); 890 mExceptionPosition = exceptionPosition; 891 mPosition = 0; 892 } 893 894 @Override 895 public int read() throws IOException { 896 if (mPosition >= mExceptionPosition) { 897 throw new IOException(); 898 } 899 900 int value = mInputStream.read(); 901 mPosition++; 902 return value; 903 } 904 905 @Override 906 public int read(byte[] b, int off, int len) throws IOException { 907 if (mPosition + len <= mExceptionPosition) { 908 final int bytesRead = mInputStream.read(b, off, len); 909 mPosition += bytesRead; 910 return bytesRead; 911 } 912 913 len = mExceptionPosition - mPosition; 914 mPosition += mInputStream.read(b, off, len); 915 throw new IOException(); 916 } 917 } 918 919 @Test 920 public void testExceptionInStream() throws Throwable { 921 InputStream is = new ExceptionStream(R.drawable.animated, 27570); 922 ImageDecoder.Source src = ImageDecoder.createSource(mRes, is, Bitmap.DENSITY_NONE); 923 Drawable dr = null; 924 try { 925 dr = ImageDecoder.decodeDrawable(src); 926 fail("Expected to throw an exception!"); 927 } catch (IOException ioe) { 928 assertTrue(ioe instanceof DecodeException); 929 DecodeException decodeException = (DecodeException) ioe; 930 assertEquals(DecodeException.SOURCE_EXCEPTION, decodeException.getError()); 931 Throwable throwable = decodeException.getCause(); 932 assertNotNull(throwable); 933 assertTrue(throwable instanceof IOException); 934 } 935 assertNull(dr); 936 } 937 938 @Test 939 public void testOnPartialImage() { 940 class PartialImageCallback implements OnPartialImageListener { 941 public boolean wasCalled; 942 public boolean returnDrawable; 943 public ImageDecoder.Source source; 944 @Override 945 public boolean onPartialImage(DecodeException e) { 946 wasCalled = true; 947 assertEquals(DecodeException.SOURCE_INCOMPLETE, e.getError()); 948 assertSame(source, e.getSource()); 949 return returnDrawable; 950 } 951 }; 952 final PartialImageCallback callback = new PartialImageCallback(); 953 boolean abortDecode[] = new boolean[] { true, false }; 954 for (Record record : RECORDS) { 955 byte[] bytes = getAsByteArray(record.resId); 956 int truncatedLength = bytes.length / 2; 957 if (record.mimeType.equals("image/x-ico") 958 || record.mimeType.equals("image/x-adobe-dng")) { 959 // FIXME (scroggo): Some codecs currently do not support incomplete images. 960 continue; 961 } 962 for (boolean abort : abortDecode) { 963 ImageDecoder.Source src = ImageDecoder.createSource( 964 ByteBuffer.wrap(bytes, 0, truncatedLength)); 965 callback.wasCalled = false; 966 callback.returnDrawable = !abort; 967 callback.source = src; 968 try { 969 Drawable drawable = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 970 decoder.setOnPartialImageListener(callback); 971 }); 972 assertFalse(abort); 973 assertNotNull(drawable); 974 assertEquals(record.width, drawable.getIntrinsicWidth()); 975 assertEquals(record.height, drawable.getIntrinsicHeight()); 976 } catch (IOException e) { 977 assertTrue(abort); 978 } 979 assertTrue(callback.wasCalled); 980 } 981 982 // null listener behaves as if onPartialImage returned false. 983 ImageDecoder.Source src = ImageDecoder.createSource( 984 ByteBuffer.wrap(bytes, 0, truncatedLength)); 985 try { 986 ImageDecoder.decodeDrawable(src); 987 fail("Should have thrown an exception!"); 988 } catch (DecodeException incomplete) { 989 // This is the correct behavior. 990 } catch (IOException e) { 991 fail("Failed with exception " + e); 992 } 993 } 994 } 995 996 @Test 997 public void testCorruptException() { 998 class PartialImageCallback implements OnPartialImageListener { 999 public boolean wasCalled = false; 1000 public ImageDecoder.Source source; 1001 @Override 1002 public boolean onPartialImage(DecodeException e) { 1003 wasCalled = true; 1004 assertEquals(DecodeException.SOURCE_MALFORMED_DATA, e.getError()); 1005 assertSame(source, e.getSource()); 1006 return true; 1007 } 1008 }; 1009 final PartialImageCallback callback = new PartialImageCallback(); 1010 byte[] bytes = getAsByteArray(R.drawable.png_test); 1011 // The four bytes starting with byte 40,000 represent the CRC. Changing 1012 // them will cause the decode to fail. 1013 for (int i = 0; i < 4; ++i) { 1014 bytes[40000 + i] = 'X'; 1015 } 1016 ImageDecoder.Source src = ImageDecoder.createSource(ByteBuffer.wrap(bytes)); 1017 callback.wasCalled = false; 1018 callback.source = src; 1019 try { 1020 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1021 decoder.setOnPartialImageListener(callback); 1022 }); 1023 } catch (IOException e) { 1024 fail("Failed with exception " + e); 1025 } 1026 assertTrue(callback.wasCalled); 1027 } 1028 1029 private static class DummyException extends RuntimeException {}; 1030 1031 @Test 1032 public void testPartialImageThrowException() { 1033 byte[] bytes = getAsByteArray(R.drawable.png_test); 1034 ImageDecoder.Source src = ImageDecoder.createSource( 1035 ByteBuffer.wrap(bytes, 0, bytes.length / 2)); 1036 try { 1037 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1038 decoder.setOnPartialImageListener((e) -> { 1039 throw new DummyException(); 1040 }); 1041 }); 1042 fail("Should have thrown an exception"); 1043 } catch (DummyException dummy) { 1044 // This is correct. 1045 } catch (Throwable t) { 1046 fail("Should have thrown DummyException - threw " + t + " instead"); 1047 } 1048 } 1049 1050 @Test 1051 public void testGetMutable() { 1052 final int resId = RECORDS[0].resId; 1053 ImageDecoder.Source src = mCreators[0].apply(resId); 1054 try { 1055 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1056 assertFalse(decoder.isMutableRequired()); 1057 1058 decoder.setMutableRequired(true); 1059 assertTrue(decoder.isMutableRequired()); 1060 1061 decoder.setMutableRequired(false); 1062 assertFalse(decoder.isMutableRequired()); 1063 }); 1064 } catch (IOException e) { 1065 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 1066 } 1067 } 1068 1069 @Test 1070 public void testMutable() { 1071 int allocators[] = new int[] { ImageDecoder.ALLOCATOR_DEFAULT, 1072 ImageDecoder.ALLOCATOR_SOFTWARE, 1073 ImageDecoder.ALLOCATOR_SHARED_MEMORY }; 1074 class HeaderListener implements ImageDecoder.OnHeaderDecodedListener { 1075 int allocator; 1076 boolean postProcess; 1077 @Override 1078 public void onHeaderDecoded(ImageDecoder decoder, 1079 ImageDecoder.ImageInfo info, 1080 ImageDecoder.Source src) { 1081 decoder.setMutableRequired(true); 1082 decoder.setAllocator(allocator); 1083 if (postProcess) { 1084 decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN); 1085 } 1086 } 1087 }; 1088 HeaderListener l = new HeaderListener(); 1089 boolean trueFalse[] = new boolean[] { true, false }; 1090 for (Record record : RECORDS) { 1091 for (SourceCreator f : mCreators) { 1092 for (boolean postProcess : trueFalse) { 1093 for (int allocator : allocators) { 1094 l.allocator = allocator; 1095 l.postProcess = postProcess; 1096 1097 ImageDecoder.Source src = f.apply(record.resId); 1098 try { 1099 Bitmap bm = ImageDecoder.decodeBitmap(src, l); 1100 assertTrue(bm.isMutable()); 1101 assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig()); 1102 } catch (IOException e) { 1103 fail("Failed with exception " + e); 1104 } 1105 } 1106 } 1107 } 1108 } 1109 } 1110 1111 @Test(expected=IllegalStateException.class) 1112 public void testMutableHardware() { 1113 ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId); 1114 try { 1115 ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 1116 decoder.setMutableRequired(true); 1117 decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE); 1118 }); 1119 } catch (IOException e) { 1120 fail("Failed with exception " + e); 1121 } 1122 } 1123 1124 @Test(expected=IllegalStateException.class) 1125 public void testMutableDrawable() { 1126 ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId); 1127 try { 1128 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1129 decoder.setMutableRequired(true); 1130 }); 1131 } catch (IOException e) { 1132 fail("Failed with exception " + e); 1133 } 1134 } 1135 1136 private interface EmptyByteBufferCreator { 1137 public ByteBuffer apply(); 1138 }; 1139 1140 @Test 1141 public void testEmptyByteBuffer() { 1142 class Direct implements EmptyByteBufferCreator { 1143 @Override 1144 public ByteBuffer apply() { 1145 return ByteBuffer.allocateDirect(0); 1146 } 1147 }; 1148 class Wrap implements EmptyByteBufferCreator { 1149 @Override 1150 public ByteBuffer apply() { 1151 byte[] bytes = new byte[0]; 1152 return ByteBuffer.wrap(bytes); 1153 } 1154 }; 1155 class ReadOnly implements EmptyByteBufferCreator { 1156 @Override 1157 public ByteBuffer apply() { 1158 byte[] bytes = new byte[0]; 1159 return ByteBuffer.wrap(bytes).asReadOnlyBuffer(); 1160 } 1161 }; 1162 EmptyByteBufferCreator creators[] = new EmptyByteBufferCreator[] { 1163 new Direct(), new Wrap(), new ReadOnly() }; 1164 for (EmptyByteBufferCreator creator : creators) { 1165 try { 1166 ImageDecoder.decodeDrawable( 1167 ImageDecoder.createSource(creator.apply())); 1168 fail("This should have thrown an exception"); 1169 } catch (IOException e) { 1170 // This is correct. 1171 } 1172 } 1173 } 1174 1175 @Test(expected=IllegalArgumentException.class) 1176 public void testZeroSampleSize() { 1177 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1178 try { 1179 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> decoder.setTargetSampleSize(0)); 1180 } catch (IOException e) { 1181 fail("Failed with exception " + e); 1182 } 1183 } 1184 1185 @Test(expected=IllegalArgumentException.class) 1186 public void testNegativeSampleSize() { 1187 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1188 try { 1189 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> decoder.setTargetSampleSize(-2)); 1190 } catch (IOException e) { 1191 fail("Failed with exception " + e); 1192 } 1193 } 1194 1195 @Test 1196 public void testTargetSize() { 1197 class ResizeListener implements ImageDecoder.OnHeaderDecodedListener { 1198 public int width; 1199 public int height; 1200 @Override 1201 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 1202 ImageDecoder.Source src) { 1203 decoder.setTargetSize(width, height); 1204 } 1205 }; 1206 ResizeListener l = new ResizeListener(); 1207 1208 float[] scales = new float[] { .0625f, .125f, .25f, .5f, .75f, 1.1f, 2.0f }; 1209 for (Record record : RECORDS) { 1210 for (SourceCreator f : mCreators) { 1211 for (int j = 0; j < scales.length; ++j) { 1212 l.width = (int) (scales[j] * record.width); 1213 l.height = (int) (scales[j] * record.height); 1214 1215 ImageDecoder.Source src = f.apply(record.resId); 1216 1217 try { 1218 Drawable drawable = ImageDecoder.decodeDrawable(src, l); 1219 assertEquals(l.width, drawable.getIntrinsicWidth()); 1220 assertEquals(l.height, drawable.getIntrinsicHeight()); 1221 1222 src = f.apply(record.resId); 1223 Bitmap bm = ImageDecoder.decodeBitmap(src, l); 1224 assertEquals(l.width, bm.getWidth()); 1225 assertEquals(l.height, bm.getHeight()); 1226 } catch (IOException e) { 1227 fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e); 1228 } 1229 } 1230 1231 try { 1232 // Arbitrary square. 1233 l.width = 50; 1234 l.height = 50; 1235 ImageDecoder.Source src = f.apply(record.resId); 1236 Drawable drawable = ImageDecoder.decodeDrawable(src, l); 1237 assertEquals(50, drawable.getIntrinsicWidth()); 1238 assertEquals(50, drawable.getIntrinsicHeight()); 1239 1240 // Swap width and height, for different scales. 1241 l.height = record.width; 1242 l.width = record.height; 1243 src = f.apply(record.resId); 1244 drawable = ImageDecoder.decodeDrawable(src, l); 1245 assertEquals(record.height, drawable.getIntrinsicWidth()); 1246 assertEquals(record.width, drawable.getIntrinsicHeight()); 1247 } catch (IOException e) { 1248 fail("Failed with exception " + e); 1249 } 1250 } 1251 } 1252 } 1253 1254 @Test 1255 public void testResizeWebp() { 1256 // libwebp supports unpremultiplied for downscaled output 1257 class ResizeListener implements ImageDecoder.OnHeaderDecodedListener { 1258 public int width; 1259 public int height; 1260 @Override 1261 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 1262 ImageDecoder.Source src) { 1263 decoder.setTargetSize(width, height); 1264 decoder.setUnpremultipliedRequired(true); 1265 } 1266 }; 1267 ResizeListener l = new ResizeListener(); 1268 1269 float[] scales = new float[] { .0625f, .125f, .25f, .5f, .75f }; 1270 for (SourceCreator f : mCreators) { 1271 for (int j = 0; j < scales.length; ++j) { 1272 l.width = (int) (scales[j] * 240); 1273 l.height = (int) (scales[j] * 87); 1274 1275 ImageDecoder.Source src = f.apply(R.drawable.google_logo_2); 1276 try { 1277 Bitmap bm = ImageDecoder.decodeBitmap(src, l); 1278 assertEquals(l.width, bm.getWidth()); 1279 assertEquals(l.height, bm.getHeight()); 1280 assertTrue(bm.hasAlpha()); 1281 assertFalse(bm.isPremultiplied()); 1282 } catch (IOException e) { 1283 fail("Failed with exception " + e); 1284 } 1285 } 1286 } 1287 } 1288 1289 @Test(expected=IllegalStateException.class) 1290 public void testResizeWebpLarger() { 1291 // libwebp does not upscale, so there is no way to get unpremul. 1292 ImageDecoder.Source src = mCreators[0].apply(R.drawable.google_logo_2); 1293 try { 1294 ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 1295 Size size = info.getSize(); 1296 decoder.setTargetSize(size.getWidth() * 2, size.getHeight() * 2); 1297 decoder.setUnpremultipliedRequired(true); 1298 }); 1299 } catch (IOException e) { 1300 fail("Failed with exception " + e); 1301 } 1302 } 1303 1304 @Test(expected=IllegalStateException.class) 1305 public void testResizeUnpremul() { 1306 ImageDecoder.Source src = mCreators[0].apply(R.drawable.alpha); 1307 try { 1308 ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 1309 // Choose a width and height that cannot be achieved with sampling. 1310 Size size = info.getSize(); 1311 int width = size.getWidth() / 2 + 3; 1312 int height = size.getHeight() / 2 + 3; 1313 decoder.setTargetSize(width, height); 1314 decoder.setUnpremultipliedRequired(true); 1315 }); 1316 } catch (IOException e) { 1317 fail("Failed with exception " + e); 1318 } 1319 } 1320 1321 @Test 1322 public void testGetCrop() { 1323 final int resId = RECORDS[0].resId; 1324 ImageDecoder.Source src = mCreators[0].apply(resId); 1325 try { 1326 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1327 assertNull(decoder.getCrop()); 1328 1329 Rect r = new Rect(0, 0, info.getSize().getWidth() / 2, 5); 1330 decoder.setCrop(r); 1331 assertEquals(r, decoder.getCrop()); 1332 1333 r = new Rect(0, 0, 5, 10); 1334 decoder.setCrop(r); 1335 assertEquals(r, decoder.getCrop()); 1336 }); 1337 } catch (IOException e) { 1338 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 1339 } 1340 } 1341 1342 @Test 1343 public void testCrop() { 1344 class Listener implements ImageDecoder.OnHeaderDecodedListener { 1345 public boolean doScale; 1346 public boolean requireSoftware; 1347 public Rect cropRect; 1348 @Override 1349 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 1350 ImageDecoder.Source src) { 1351 int width = info.getSize().getWidth(); 1352 int height = info.getSize().getHeight(); 1353 if (doScale) { 1354 width /= 2; 1355 height /= 2; 1356 decoder.setTargetSize(width, height); 1357 } 1358 // Crop to the middle: 1359 int quarterWidth = width / 4; 1360 int quarterHeight = height / 4; 1361 cropRect = new Rect(quarterWidth, quarterHeight, 1362 quarterWidth * 3, quarterHeight * 3); 1363 decoder.setCrop(cropRect); 1364 1365 if (requireSoftware) { 1366 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 1367 } 1368 } 1369 }; 1370 Listener l = new Listener(); 1371 boolean trueFalse[] = new boolean[] { true, false }; 1372 for (Record record : RECORDS) { 1373 for (SourceCreator f : mCreators) { 1374 for (boolean doScale : trueFalse) { 1375 l.doScale = doScale; 1376 for (boolean requireSoftware : trueFalse) { 1377 l.requireSoftware = requireSoftware; 1378 ImageDecoder.Source src = f.apply(record.resId); 1379 1380 try { 1381 Drawable drawable = ImageDecoder.decodeDrawable(src, l); 1382 assertEquals(l.cropRect.width(), drawable.getIntrinsicWidth()); 1383 assertEquals(l.cropRect.height(), drawable.getIntrinsicHeight()); 1384 } catch (IOException e) { 1385 fail("Failed " + getAsResourceUri(record.resId) + 1386 " with exception " + e); 1387 } 1388 } 1389 } 1390 } 1391 } 1392 } 1393 1394 @Test(expected=IllegalArgumentException.class) 1395 public void testResizeZeroX() { 1396 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1397 try { 1398 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> 1399 decoder.setTargetSize(0, info.getSize().getHeight())); 1400 } catch (IOException e) { 1401 fail("Failed with exception " + e); 1402 } 1403 } 1404 1405 @Test(expected=IllegalArgumentException.class) 1406 public void testResizeZeroY() { 1407 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1408 try { 1409 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> 1410 decoder.setTargetSize(info.getSize().getWidth(), 0)); 1411 } catch (IOException e) { 1412 fail("Failed with exception " + e); 1413 } 1414 } 1415 1416 @Test(expected=IllegalArgumentException.class) 1417 public void testResizeNegativeX() { 1418 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1419 try { 1420 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> 1421 decoder.setTargetSize(-10, info.getSize().getHeight())); 1422 } catch (IOException e) { 1423 fail("Failed with exception " + e); 1424 } 1425 } 1426 1427 @Test(expected=IllegalArgumentException.class) 1428 public void testResizeNegativeY() { 1429 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1430 try { 1431 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> 1432 decoder.setTargetSize(info.getSize().getWidth(), -10)); 1433 } catch (IOException e) { 1434 fail("Failed with exception " + e); 1435 } 1436 } 1437 1438 @Test(expected=IllegalStateException.class) 1439 public void testStoreImageDecoder() { 1440 class CachingCallback implements ImageDecoder.OnHeaderDecodedListener { 1441 ImageDecoder cachedDecoder; 1442 @Override 1443 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 1444 ImageDecoder.Source src) { 1445 cachedDecoder = decoder; 1446 } 1447 }; 1448 CachingCallback l = new CachingCallback(); 1449 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1450 try { 1451 ImageDecoder.decodeDrawable(src, l); 1452 } catch (IOException e) { 1453 fail("Failed with exception " + e); 1454 } 1455 l.cachedDecoder.setTargetSampleSize(2); 1456 } 1457 1458 @Test(expected=IllegalStateException.class) 1459 public void testDecodeUnpremulDrawable() { 1460 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1461 try { 1462 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> 1463 decoder.setUnpremultipliedRequired(true)); 1464 } catch (IOException e) { 1465 fail("Failed with exception " + e); 1466 } 1467 } 1468 1469 @Test(expected=IllegalStateException.class) 1470 public void testCropNegativeLeft() { 1471 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1472 try { 1473 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1474 decoder.setCrop(new Rect(-1, 0, info.getSize().getWidth(), 1475 info.getSize().getHeight())); 1476 }); 1477 } catch (IOException e) { 1478 fail("Failed with exception " + e); 1479 } 1480 } 1481 1482 @Test(expected=IllegalStateException.class) 1483 public void testCropNegativeTop() { 1484 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1485 try { 1486 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1487 decoder.setCrop(new Rect(0, -1, info.getSize().getWidth(), 1488 info.getSize().getHeight())); 1489 }); 1490 } catch (IOException e) { 1491 fail("Failed with exception " + e); 1492 } 1493 } 1494 1495 @Test(expected=IllegalStateException.class) 1496 public void testCropTooWide() { 1497 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1498 try { 1499 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1500 decoder.setCrop(new Rect(1, 0, info.getSize().getWidth() + 1, 1501 info.getSize().getHeight())); 1502 }); 1503 } catch (IOException e) { 1504 fail("Failed with exception " + e); 1505 } 1506 } 1507 1508 @Test(expected=IllegalStateException.class) 1509 public void testCropTooTall() { 1510 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1511 try { 1512 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1513 decoder.setCrop(new Rect(0, 1, info.getSize().getWidth(), 1514 info.getSize().getHeight() + 1)); 1515 }); 1516 } catch (IOException e) { 1517 fail("Failed with exception " + e); 1518 } 1519 } 1520 1521 @Test(expected=IllegalStateException.class) 1522 public void testCropResize() { 1523 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 1524 try { 1525 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1526 Size size = info.getSize(); 1527 decoder.setTargetSize(size.getWidth() / 2, size.getHeight() / 2); 1528 decoder.setCrop(new Rect(0, 0, size.getWidth(), 1529 size.getHeight())); 1530 }); 1531 } catch (IOException e) { 1532 fail("Failed with exception " + e); 1533 } 1534 } 1535 1536 @Test 1537 public void testAlphaMaskNonGray() { 1538 // It is safe to call setDecodeAsAlphaMaskEnabled on a non-gray image. 1539 SourceCreator f = mCreators[0]; 1540 ImageDecoder.Source src = f.apply(R.drawable.png_test); 1541 assertNotNull(src); 1542 try { 1543 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 1544 decoder.setDecodeAsAlphaMaskEnabled(true); 1545 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 1546 }); 1547 assertNotNull(bm); 1548 assertNotEquals(Bitmap.Config.ALPHA_8, bm.getConfig()); 1549 } catch (IOException e) { 1550 fail("Failed with exception " + e); 1551 } 1552 1553 } 1554 1555 @Test(expected=IllegalStateException.class) 1556 public void testAlphaMaskPlusHardware() { 1557 SourceCreator f = mCreators[0]; 1558 ImageDecoder.Source src = f.apply(R.drawable.png_test); 1559 assertNotNull(src); 1560 try { 1561 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1562 decoder.setDecodeAsAlphaMaskEnabled(true); 1563 decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE); 1564 }); 1565 } catch (IOException e) { 1566 fail("Failed with exception " + e); 1567 } 1568 } 1569 1570 @Test 1571 public void testGetAlphaMask() { 1572 final int resId = R.drawable.grayscale_png; 1573 ImageDecoder.Source src = mCreators[0].apply(resId); 1574 try { 1575 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1576 assertFalse(decoder.isDecodeAsAlphaMaskEnabled()); 1577 1578 decoder.setDecodeAsAlphaMaskEnabled(true); 1579 assertTrue(decoder.isDecodeAsAlphaMaskEnabled()); 1580 1581 decoder.setDecodeAsAlphaMaskEnabled(false); 1582 assertFalse(decoder.isDecodeAsAlphaMaskEnabled()); 1583 }); 1584 } catch (IOException e) { 1585 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 1586 } 1587 } 1588 1589 @Test 1590 public void testAlphaMask() { 1591 class Listener implements ImageDecoder.OnHeaderDecodedListener { 1592 boolean doCrop; 1593 boolean doScale; 1594 boolean doPostProcess; 1595 @Override 1596 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 1597 ImageDecoder.Source src) { 1598 decoder.setDecodeAsAlphaMaskEnabled(true); 1599 Size size = info.getSize(); 1600 if (doScale) { 1601 decoder.setTargetSize(size.getWidth() / 2, 1602 size.getHeight() / 2); 1603 } 1604 if (doCrop) { 1605 decoder.setCrop(new Rect(0, 0, size.getWidth() / 4, 1606 size.getHeight() / 4)); 1607 } 1608 if (doPostProcess) { 1609 decoder.setPostProcessor((c) -> { 1610 c.drawColor(Color.BLACK); 1611 return PixelFormat.UNKNOWN; 1612 }); 1613 } 1614 } 1615 }; 1616 Listener l = new Listener(); 1617 // Both of these are encoded as single channel gray images. 1618 int resIds[] = new int[] { R.drawable.grayscale_png, R.drawable.grayscale_jpg }; 1619 boolean trueFalse[] = new boolean[] { true, false }; 1620 SourceCreator f = mCreators[0]; 1621 for (int resId : resIds) { 1622 // By default, this will decode to HARDWARE 1623 ImageDecoder.Source src = f.apply(resId); 1624 try { 1625 Bitmap bm = ImageDecoder.decodeBitmap(src); 1626 assertEquals(Bitmap.Config.HARDWARE, bm.getConfig()); 1627 } catch (IOException e) { 1628 fail("Failed with exception " + e); 1629 } 1630 1631 // Now set alpha mask, which is incompatible with HARDWARE 1632 for (boolean doCrop : trueFalse) { 1633 for (boolean doScale : trueFalse) { 1634 for (boolean doPostProcess : trueFalse) { 1635 l.doCrop = doCrop; 1636 l.doScale = doScale; 1637 l.doPostProcess = doPostProcess; 1638 src = f.apply(resId); 1639 try { 1640 Bitmap bm = ImageDecoder.decodeBitmap(src, l); 1641 assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig()); 1642 } catch (IOException e) { 1643 fail("Failed with exception " + e); 1644 } 1645 } 1646 } 1647 } 1648 } 1649 } 1650 1651 @Test 1652 public void testGetConserveMemory() { 1653 final int resId = RECORDS[0].resId; 1654 ImageDecoder.Source src = mCreators[0].apply(resId); 1655 try { 1656 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 1657 assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy()); 1658 1659 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM); 1660 assertEquals(ImageDecoder.MEMORY_POLICY_LOW_RAM, decoder.getMemorySizePolicy()); 1661 1662 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_DEFAULT); 1663 assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy()); 1664 }); 1665 } catch (IOException e) { 1666 fail("Failed " + getAsResourceUri(resId) + " with exception " + e); 1667 } 1668 } 1669 1670 @Test 1671 public void testConserveMemoryPlusHardware() { 1672 class Listener implements ImageDecoder.OnHeaderDecodedListener { 1673 int allocator; 1674 @Override 1675 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 1676 ImageDecoder.Source src) { 1677 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM); 1678 decoder.setAllocator(allocator); 1679 } 1680 }; 1681 Listener l = new Listener(); 1682 SourceCreator f = mCreators[0]; 1683 for (int resId : new int[] { R.drawable.png_test, R.raw.basi6a16 }) { 1684 Bitmap normal = null; 1685 try { 1686 normal = ImageDecoder.decodeBitmap(f.apply(resId)); 1687 } catch (IOException e) { 1688 fail("Failed with exception " + e); 1689 } 1690 assertNotNull(normal); 1691 int normalByteCount = normal.getAllocationByteCount(); 1692 for (int allocator : ALLOCATORS) { 1693 l.allocator = allocator; 1694 Bitmap test = null; 1695 try { 1696 test = ImageDecoder.decodeBitmap(f.apply(resId), l); 1697 } catch (IOException e) { 1698 fail("Failed with exception " + e); 1699 } 1700 assertNotNull(test); 1701 int byteCount = test.getAllocationByteCount(); 1702 1703 if (allocator == ImageDecoder.ALLOCATOR_HARDWARE 1704 || allocator == ImageDecoder.ALLOCATOR_DEFAULT) { 1705 if (resId == R.drawable.png_test) { 1706 // We do not support 565 in HARDWARE, so no RAM savings 1707 // are possible. 1708 assertEquals(normalByteCount, byteCount); 1709 } else { // R.raw.basi6a16 1710 // This image defaults to F16. MEMORY_POLICY_LOW_RAM 1711 // forces "test" to decode to 8888. But if the device 1712 // does not support F16 in HARDWARE, "normal" is also 1713 // 8888. Its Config is HARDWARE either way, but we can 1714 // detect its underlying pixel format by checking the 1715 // ColorSpace, which is always LINEAR_EXTENDED_SRGB for 1716 // F16. 1717 if (normal.getColorSpace().equals( 1718 ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB))) { 1719 // F16. "test" should be smaller. 1720 assertTrue(byteCount < normalByteCount); 1721 } else { 1722 // 8888. No RAM savings possible. 1723 assertEquals(normalByteCount, byteCount); 1724 } 1725 } 1726 } else { 1727 // Not decoding to HARDWARE, but |normal| was. Again, if basi6a16 1728 // was decoded to 8888, which we can detect by looking at the color 1729 // space, no savings are possible. 1730 if (resId == R.raw.basi6a16 && !normal.getColorSpace().equals( 1731 ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB))) { 1732 assertEquals(normalByteCount, byteCount); 1733 } else { 1734 assertTrue(byteCount < normalByteCount); 1735 } 1736 } 1737 } 1738 } 1739 } 1740 1741 @Test 1742 public void testConserveMemory() { 1743 class Listener implements ImageDecoder.OnHeaderDecodedListener { 1744 boolean doPostProcess; 1745 boolean preferRamOverQuality; 1746 @Override 1747 public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info, 1748 ImageDecoder.Source src) { 1749 if (preferRamOverQuality) { 1750 decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM); 1751 } 1752 if (doPostProcess) { 1753 decoder.setPostProcessor((c) -> { 1754 c.drawColor(Color.BLACK); 1755 return PixelFormat.TRANSLUCENT; 1756 }); 1757 } 1758 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 1759 } 1760 }; 1761 Listener l = new Listener(); 1762 // All of these images are opaque, so they can save RAM with 1763 // setConserveMemory. 1764 int resIds[] = new int[] { R.drawable.png_test, R.drawable.baseline_jpeg, 1765 // If this were stored in drawable/, it would 1766 // be converted from 16-bit to 8. FIXME: Is 1767 // behavior still desirable now that we have 1768 // F16? 1769 R.raw.basi6a16 }; 1770 // An opaque image can be converted to 565, but postProcess will promote 1771 // to 8888 in case alpha is added. The third image defaults to F16, so 1772 // even with postProcess it will only be promoted to 8888. 1773 boolean postProcessCancels[] = new boolean[] { true, true, false }; 1774 boolean trueFalse[] = new boolean[] { true, false }; 1775 SourceCreator f = mCreators[0]; 1776 for (int i = 0; i < resIds.length; ++i) { 1777 int resId = resIds[i]; 1778 l.doPostProcess = false; 1779 l.preferRamOverQuality = false; 1780 Bitmap normal = null; 1781 try { 1782 normal = ImageDecoder.decodeBitmap(f.apply(resId), l); 1783 } catch (IOException e) { 1784 fail("Failed with exception " + e); 1785 } 1786 int normalByteCount = normal.getAllocationByteCount(); 1787 for (boolean doPostProcess : trueFalse) { 1788 l.doPostProcess = doPostProcess; 1789 l.preferRamOverQuality = true; 1790 Bitmap saveRamOverQuality = null; 1791 try { 1792 saveRamOverQuality = ImageDecoder.decodeBitmap(f.apply(resId), l); 1793 } catch (IOException e) { 1794 fail("Failed with exception " + e); 1795 } 1796 int saveByteCount = saveRamOverQuality.getAllocationByteCount(); 1797 if (doPostProcess && postProcessCancels[i]) { 1798 // Promoted to normal. 1799 assertEquals(normalByteCount, saveByteCount); 1800 } else { 1801 assertTrue(saveByteCount < normalByteCount); 1802 } 1803 } 1804 } 1805 } 1806 1807 @Test 1808 public void testRespectOrientation() { 1809 // These 8 images test the 8 EXIF orientations. If the orientation is 1810 // respected, they all have the same dimensions: 100 x 80. 1811 // They are also identical (after adjusting), so compare them. 1812 Bitmap reference = null; 1813 for (int resId : new int[] { R.drawable.orientation_1, 1814 R.drawable.orientation_2, 1815 R.drawable.orientation_3, 1816 R.drawable.orientation_4, 1817 R.drawable.orientation_5, 1818 R.drawable.orientation_6, 1819 R.drawable.orientation_7, 1820 R.drawable.orientation_8, 1821 R.drawable.webp_orientation1, 1822 R.drawable.webp_orientation2, 1823 R.drawable.webp_orientation3, 1824 R.drawable.webp_orientation4, 1825 R.drawable.webp_orientation5, 1826 R.drawable.webp_orientation6, 1827 R.drawable.webp_orientation7, 1828 R.drawable.webp_orientation8, 1829 }) { 1830 if (resId == R.drawable.webp_orientation1) { 1831 // The webp files may not look exactly the same as the jpegs. 1832 // Recreate the reference. 1833 reference = null; 1834 } 1835 Uri uri = getAsResourceUri(resId); 1836 ImageDecoder.Source src = ImageDecoder.createSource(mContentResolver, uri); 1837 try { 1838 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 1839 // Use software allocator so we can compare. 1840 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 1841 }); 1842 assertNotNull(bm); 1843 assertEquals(100, bm.getWidth()); 1844 assertEquals(80, bm.getHeight()); 1845 1846 if (reference == null) { 1847 reference = bm; 1848 } else { 1849 BitmapUtils.compareBitmaps(bm, reference); 1850 } 1851 } catch (IOException e) { 1852 fail("Decoding " + uri.toString() + " yielded " + e); 1853 } 1854 } 1855 } 1856 1857 @Test(expected=IOException.class) 1858 public void testZeroLengthByteBuffer() throws IOException { 1859 Drawable drawable = ImageDecoder.decodeDrawable( 1860 ImageDecoder.createSource(ByteBuffer.wrap(new byte[10], 0, 0))); 1861 fail("should not have reached here!"); 1862 } 1863 1864 private interface ByteBufferSupplier extends Supplier<ByteBuffer> {}; 1865 1866 @Test 1867 public void testOffsetByteArray() { 1868 for (Record record : RECORDS) { 1869 int offset = 10; 1870 int extra = 15; 1871 byte[] array = getAsByteArray(record.resId, offset, extra); 1872 int length = array.length - extra - offset; 1873 // Used for SourceCreators that set both a position and an offset. 1874 int myOffset = 3; 1875 int myPosition = 7; 1876 assertEquals(offset, myOffset + myPosition); 1877 1878 ByteBufferSupplier[] suppliers = new ByteBufferSupplier[] { 1879 // Internally, this gives the buffer a position, but not an offset. 1880 () -> ByteBuffer.wrap(array, offset, length), 1881 // Same, but make it readOnly to ensure that we test the 1882 // ByteBufferSource rather than the ByteArraySource. 1883 () -> ByteBuffer.wrap(array, offset, length).asReadOnlyBuffer(), 1884 () -> { 1885 // slice() to give the buffer an offset. 1886 ByteBuffer buf = ByteBuffer.wrap(array, 0, array.length - extra); 1887 buf.position(offset); 1888 return buf.slice(); 1889 }, 1890 () -> { 1891 // Same, but make it readOnly to ensure that we test the 1892 // ByteBufferSource rather than the ByteArraySource. 1893 ByteBuffer buf = ByteBuffer.wrap(array, 0, array.length - extra); 1894 buf.position(offset); 1895 return buf.slice().asReadOnlyBuffer(); 1896 }, 1897 () -> { 1898 // Use both a position and an offset. 1899 ByteBuffer buf = ByteBuffer.wrap(array, myOffset, 1900 array.length - extra - myOffset); 1901 buf = buf.slice(); 1902 buf.position(myPosition); 1903 return buf; 1904 }, 1905 () -> { 1906 // Same, as readOnly. 1907 ByteBuffer buf = ByteBuffer.wrap(array, myOffset, 1908 array.length - extra - myOffset); 1909 buf = buf.slice(); 1910 buf.position(myPosition); 1911 return buf.asReadOnlyBuffer(); 1912 }, 1913 () -> { 1914 // Direct ByteBuffer with a position. 1915 ByteBuffer buf = ByteBuffer.allocateDirect(array.length); 1916 buf.put(array); 1917 buf.position(offset); 1918 return buf; 1919 }, 1920 () -> { 1921 // Sliced direct ByteBuffer, for an offset. 1922 ByteBuffer buf = ByteBuffer.allocateDirect(array.length); 1923 buf.put(array); 1924 buf.position(offset); 1925 return buf.slice(); 1926 }, 1927 () -> { 1928 // Direct ByteBuffer with position and offset. 1929 ByteBuffer buf = ByteBuffer.allocateDirect(array.length); 1930 buf.put(array); 1931 buf.position(myOffset); 1932 buf = buf.slice(); 1933 buf.position(myPosition); 1934 return buf; 1935 }, 1936 }; 1937 for (int i = 0; i < suppliers.length; ++i) { 1938 ByteBuffer buffer = suppliers[i].get(); 1939 final int position = buffer.position(); 1940 ImageDecoder.Source src = ImageDecoder.createSource(buffer); 1941 try { 1942 Drawable drawable = ImageDecoder.decodeDrawable(src); 1943 assertNotNull(drawable); 1944 } catch (IOException e) { 1945 fail("Failed with exception " + e); 1946 } 1947 assertEquals("Mismatch for supplier " + i, 1948 position, buffer.position()); 1949 } 1950 } 1951 } 1952 1953 @Test 1954 public void testResourceSource() { 1955 for (Record record : RECORDS) { 1956 ImageDecoder.Source src = ImageDecoder.createSource(mRes, record.resId); 1957 try { 1958 Drawable drawable = ImageDecoder.decodeDrawable(src); 1959 assertNotNull(drawable); 1960 } catch (IOException e) { 1961 fail("Failed " + getAsResourceUri(record.resId) + " with " + e); 1962 } 1963 } 1964 } 1965 1966 private BitmapDrawable decodeBitmapDrawable(int resId) { 1967 ImageDecoder.Source src = ImageDecoder.createSource(mRes, resId); 1968 try { 1969 Drawable drawable = ImageDecoder.decodeDrawable(src); 1970 assertNotNull(drawable); 1971 assertTrue(drawable instanceof BitmapDrawable); 1972 return (BitmapDrawable) drawable; 1973 } catch (IOException e) { 1974 fail("Failed " + getAsResourceUri(resId) + " with " + e); 1975 return null; 1976 } 1977 } 1978 1979 @Test 1980 public void testUpscale() { 1981 final int originalDensity = mRes.getDisplayMetrics().densityDpi; 1982 1983 try { 1984 for (Record record : RECORDS) { 1985 final int resId = record.resId; 1986 1987 // Set a high density. This will result in a larger drawable, but 1988 // not a larger Bitmap. 1989 mRes.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_XXXHIGH; 1990 BitmapDrawable drawable = decodeBitmapDrawable(resId); 1991 1992 Bitmap bm = drawable.getBitmap(); 1993 assertEquals(record.width, bm.getWidth()); 1994 assertEquals(record.height, bm.getHeight()); 1995 1996 assertTrue(drawable.getIntrinsicWidth() > record.width); 1997 assertTrue(drawable.getIntrinsicHeight() > record.height); 1998 1999 // Set a low density. This will result in a smaller drawable and 2000 // Bitmap. 2001 mRes.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_LOW; 2002 drawable = decodeBitmapDrawable(resId); 2003 2004 bm = drawable.getBitmap(); 2005 assertTrue(bm.getWidth() < record.width); 2006 assertTrue(bm.getHeight() < record.height); 2007 2008 assertEquals(bm.getWidth(), drawable.getIntrinsicWidth()); 2009 assertEquals(bm.getHeight(), drawable.getIntrinsicHeight()); 2010 } 2011 } finally { 2012 mRes.getDisplayMetrics().densityDpi = originalDensity; 2013 } 2014 } 2015 2016 private static class AssetRecord { 2017 public final String name; 2018 public final int width; 2019 public final int height; 2020 public final boolean isF16; 2021 private final ColorSpace mColorSpace; 2022 2023 AssetRecord(String name, int width, int height, boolean isF16, ColorSpace colorSpace) { 2024 this.name = name; 2025 this.width = width; 2026 this.height = height; 2027 this.isF16 = isF16; 2028 mColorSpace = colorSpace; 2029 } 2030 2031 public void checkColorSpace(ColorSpace requested, ColorSpace actual) { 2032 assertNotNull(actual); 2033 if (this.isF16) { 2034 assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB), actual); 2035 } else if (requested != null) { 2036 assertSame(requested, actual); 2037 } else if (mColorSpace == null) { 2038 assertEquals(this.name, "Unknown", actual.getName()); 2039 } else { 2040 assertSame(this.name, mColorSpace, actual); 2041 } 2042 } 2043 } 2044 2045 private static final AssetRecord[] ASSETS = new AssetRecord[] { 2046 // A null ColorSpace means that the color space is "Unknown". 2047 new AssetRecord("almost-red-adobe.png", 1, 1, false, null), 2048 new AssetRecord("green-p3.png", 64, 64, false, 2049 ColorSpace.get(ColorSpace.Named.DISPLAY_P3)), 2050 new AssetRecord("green-srgb.png", 64, 64, false, sSRGB), 2051 new AssetRecord("prophoto-rgba16f.png", 64, 64, true, 2052 ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB)), 2053 new AssetRecord("purple-cmyk.png", 64, 64, false, sSRGB), 2054 new AssetRecord("purple-displayprofile.png", 64, 64, false, null), 2055 new AssetRecord("red-adobergb.png", 64, 64, false, 2056 ColorSpace.get(ColorSpace.Named.ADOBE_RGB)), 2057 new AssetRecord("translucent-green-p3.png", 64, 64, false, 2058 ColorSpace.get(ColorSpace.Named.DISPLAY_P3)), 2059 }; 2060 2061 @Test 2062 public void testAssetSource() { 2063 AssetManager assets = mRes.getAssets(); 2064 for (AssetRecord record : ASSETS) { 2065 ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name); 2066 try { 2067 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 2068 if (record.isF16) { 2069 // CTS infrastructure fails to create F16 HARDWARE Bitmaps, so this 2070 // switches to using software. 2071 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 2072 } 2073 2074 record.checkColorSpace(null, info.getColorSpace()); 2075 }); 2076 assertEquals(record.name, record.width, bm.getWidth()); 2077 assertEquals(record.name, record.height, bm.getHeight()); 2078 record.checkColorSpace(null, bm.getColorSpace()); 2079 } catch (IOException e) { 2080 fail("Failed to decode asset " + record.name + " with " + e); 2081 } 2082 } 2083 } 2084 2085 @Test 2086 public void testTargetColorSpace() { 2087 AssetManager assets = mRes.getAssets(); 2088 for (AssetRecord record : ASSETS) { 2089 ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name); 2090 for (ColorSpace cs : new ColorSpace[] { 2091 sSRGB, 2092 ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB), 2093 ColorSpace.get(ColorSpace.Named.DISPLAY_P3), 2094 ColorSpace.get(ColorSpace.Named.ADOBE_RGB), 2095 ColorSpace.get(ColorSpace.Named.BT709), 2096 ColorSpace.get(ColorSpace.Named.BT2020), 2097 ColorSpace.get(ColorSpace.Named.DCI_P3), 2098 ColorSpace.get(ColorSpace.Named.NTSC_1953), 2099 ColorSpace.get(ColorSpace.Named.SMPTE_C), 2100 ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB), 2101 // FIXME: These will not match due to b/77276533. 2102 // ColorSpace.get(ColorSpace.Named.LINEAR_SRGB), 2103 // ColorSpace.get(ColorSpace.Named.ACES), 2104 // ColorSpace.get(ColorSpace.Named.ACESCG), 2105 }) { 2106 try { 2107 Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> { 2108 if (record.isF16) { 2109 // CTS infrastructure fails to create F16 HARDWARE Bitmaps, so this 2110 // switches to using software. 2111 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 2112 } 2113 decoder.setTargetColorSpace(cs); 2114 }); 2115 record.checkColorSpace(cs, bm.getColorSpace()); 2116 } catch (IOException e) { 2117 fail("Failed to decode asset " + record.name + " to " + cs + " with " + e); 2118 } 2119 } 2120 } 2121 } 2122 2123 @Test 2124 public void testTargetColorSpaceNonRGB() { 2125 ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test); 2126 for (ColorSpace cs : new ColorSpace[] { 2127 ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), 2128 ColorSpace.get(ColorSpace.Named.CIE_LAB), 2129 ColorSpace.get(ColorSpace.Named.CIE_XYZ), 2130 }) { 2131 try { 2132 ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 2133 decoder.setTargetColorSpace(cs); 2134 }); 2135 fail("Should have thrown an IllegalArgumentException for setTargetColorSpace(" 2136 + cs + ")!"); 2137 } catch (IOException e) { 2138 fail("Failed to decode png_test with " + e); 2139 } catch (IllegalArgumentException illegal) { 2140 // This is expected. 2141 } 2142 } 2143 } 2144 2145 private Bitmap drawToBitmap(Drawable dr) { 2146 Bitmap bm = Bitmap.createBitmap(dr.getIntrinsicWidth(), dr.getIntrinsicHeight(), 2147 Bitmap.Config.ARGB_8888); 2148 Canvas canvas = new Canvas(bm); 2149 dr.draw(canvas); 2150 return bm; 2151 } 2152 2153 private void testReuse(ImageDecoder.Source src, String name) { 2154 Drawable first = null; 2155 try { 2156 first = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 2157 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 2158 }); 2159 } catch (IOException e) { 2160 fail("Failed on first decode of " + name + " using " + src + "!"); 2161 } 2162 2163 Drawable second = null; 2164 try { 2165 second = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> { 2166 decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE); 2167 }); 2168 } catch (IOException e) { 2169 fail("Failed on second decode of " + name + " using " + src + "!"); 2170 } 2171 2172 assertEquals(first.getIntrinsicWidth(), second.getIntrinsicWidth()); 2173 assertEquals(first.getIntrinsicHeight(), second.getIntrinsicHeight()); 2174 2175 Bitmap bm1 = drawToBitmap(first); 2176 Bitmap bm2 = drawToBitmap(second); 2177 BitmapUtils.compareBitmaps(bm1, bm2); 2178 } 2179 2180 @Test 2181 @LargeTest 2182 public void testReuse() { 2183 for (Record record : RECORDS) { 2184 String name = getAsResourceUri(record.resId).toString(); 2185 for (SourceCreator f : mCreators) { 2186 ImageDecoder.Source src = f.apply(record.resId); 2187 testReuse(src, name); 2188 } 2189 2190 { 2191 ImageDecoder.Source src = ImageDecoder.createSource(mRes, record.resId); 2192 testReuse(src, name); 2193 } 2194 2195 for (UriCreator f : mUriCreators) { 2196 Uri uri = f.apply(record.resId); 2197 ImageDecoder.Source src = ImageDecoder.createSource(mContentResolver, uri); 2198 testReuse(src, uri.toString()); 2199 } 2200 2201 { 2202 ImageDecoder.Source src = ImageDecoder.createSource(getAsFile(record.resId)); 2203 testReuse(src, name); 2204 } 2205 } 2206 2207 AssetManager assets = mRes.getAssets(); 2208 for (AssetRecord record : ASSETS) { 2209 ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name); 2210 testReuse(src, record.name); 2211 } 2212 2213 2214 ImageDecoder.Source src = mCreators[0].apply(R.drawable.animated); 2215 testReuse(src, "animated.gif"); 2216 } 2217 2218 @Test 2219 public void testWarpedDng() { 2220 Context context = InstrumentationRegistry.getTargetContext(); 2221 ActivityManager activityManager = (ActivityManager) context 2222 .getSystemService(Context.ACTIVITY_SERVICE); 2223 ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo(); 2224 activityManager.getMemoryInfo(info); 2225 2226 // Decoding this image requires a lot of memory. Only attempt if the 2227 // device has a total memory of at least 2 Gigs. 2228 if (info.totalMem < 2 * 1024 * 1024 * 1024) { 2229 return; 2230 } 2231 2232 String name = "b78120086.dng"; 2233 ImageDecoder.Source src = ImageDecoder.createSource(mRes.getAssets(), name); 2234 try { 2235 ImageDecoder.decodeDrawable(src); 2236 } catch (IOException e) { 2237 fail("Failed to decode " + name + " with exception " + e); 2238 } 2239 } 2240 } 2241