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