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