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