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