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