From be3867039105a9398262d3b9b032e00d26a2db00 Mon Sep 17 00:00:00 2001 From: tofunmi Date: Wed, 21 Jun 2023 19:02:03 +0000 Subject: [PATCH] Transformer: Decode image in sRGB The effects pipeline must receive images in the sRGB colorspace due to the color transfers applied in the shaders. Currently the burden to making sure images are in the right colorspaces falls onto apps. This CL ensures that this is not the case anymore. PiperOrigin-RevId: 542323613 --- .../media3/common/util/BitmapLoader.java | 8 ++++++- .../DataSourceBitmapLoaderTest.java | 18 ++++++++++++++++ .../datasource/DataSourceBitmapLoader.java | 21 ++++++++++++------- .../media3/session/CacheBitmapLoader.java | 5 +++-- .../media3/session/SimpleBitmapLoader.java | 17 ++++++++------- .../media3/transformer/ImageAssetLoader.java | 10 ++++++++- 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java b/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java index 44b7cadb28..118d2cd575 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/BitmapLoader.java @@ -16,6 +16,7 @@ package androidx.media3.common.util; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.MediaMetadata; @@ -28,7 +29,12 @@ public interface BitmapLoader { ListenableFuture decodeBitmap(byte[] data); /** Loads an image from {@code uri}. */ - ListenableFuture loadBitmap(Uri uri); + default ListenableFuture loadBitmap(Uri uri) { + return loadBitmap(uri, /* options= */ null); + } + + /** Loads an image from {@code uri} with the given {@link BitmapFactory.Options}. */ + ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options); /** * Loads an image from {@link MediaMetadata}. Returns null if {@code metadata} doesn't contain diff --git a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java index 32d366bb8b..5c81878f1e 100644 --- a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java +++ b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java @@ -198,6 +198,24 @@ public class DataSourceBitmapLoaderTest { .isTrue(); } + @Test + public void loadBitmap_withFileUriAndOptions_loadsDataWithRespectToOptions() throws Exception { + byte[] imageData = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH); + File file = tempFolder.newFile(); + Files.write(Paths.get(file.getAbsolutePath()), imageData); + Uri uri = Uri.fromFile(file); + DataSourceBitmapLoader bitmapLoader = + new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + + Bitmap bitmap = bitmapLoader.loadBitmap(uri, options).get(); + + assertThat(bitmap.isMutable()).isTrue(); + } + @Test public void loadBitmap_fileUriWithFileNotExisting_throws() { DataSourceBitmapLoader bitmapLoader = diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java index 0f60321b5d..6c0b547874 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java @@ -78,17 +78,21 @@ public final class DataSourceBitmapLoader implements BitmapLoader { @Override public ListenableFuture decodeBitmap(byte[] data) { - return listeningExecutorService.submit(() -> decode(data)); + return listeningExecutorService.submit(() -> decode(data, /* options= */ null)); } - /** Loads an image from a {@link Uri}. */ @Override - public ListenableFuture loadBitmap(Uri uri) { - return listeningExecutorService.submit(() -> load(dataSourceFactory.createDataSource(), uri)); + public ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) { + return listeningExecutorService.submit( + () -> load(dataSourceFactory.createDataSource(), uri, options)); } - private static Bitmap decode(byte[] data) throws IOException { - @Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length); + // BitmapFactory's options parameter is null-ok. + @SuppressWarnings("nullness:argument.type.incompatible") + private static Bitmap decode(byte[] data, @Nullable BitmapFactory.Options options) + throws IOException { + @Nullable + Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length, options); checkArgument(bitmap != null, "Could not decode image data"); ExifInterface exifInterface; try (InputStream inputStream = new ByteArrayInputStream(data)) { @@ -111,12 +115,13 @@ public final class DataSourceBitmapLoader implements BitmapLoader { return bitmap; } - private static Bitmap load(DataSource dataSource, Uri uri) throws IOException { + private static Bitmap load( + DataSource dataSource, Uri uri, @Nullable BitmapFactory.Options options) throws IOException { try { DataSpec dataSpec = new DataSpec(uri); dataSource.open(dataSpec); byte[] readData = DataSourceUtil.readToEnd(dataSource); - return decode(readData); + return decode(readData, options); } finally { dataSource.close(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java b/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java index 4fc3f5717f..5426d670a2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java +++ b/libraries/session/src/main/java/androidx/media3/session/CacheBitmapLoader.java @@ -18,6 +18,7 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkStateNotNull; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.util.BitmapLoader; @@ -59,11 +60,11 @@ public final class CacheBitmapLoader implements BitmapLoader { } @Override - public ListenableFuture loadBitmap(Uri uri) { + public ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) { if (lastBitmapLoadRequest != null && lastBitmapLoadRequest.matches(uri)) { return lastBitmapLoadRequest.getFuture(); } - ListenableFuture future = bitmapLoader.loadBitmap(uri); + ListenableFuture future = bitmapLoader.loadBitmap(uri, options); lastBitmapLoadRequest = new BitmapLoadRequest(uri, future); return future; } diff --git a/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java b/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java index 5a74e4ae38..76d42b608e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java +++ b/libraries/session/src/main/java/androidx/media3/session/SimpleBitmapLoader.java @@ -76,21 +76,24 @@ public final class SimpleBitmapLoader implements BitmapLoader { @Override public ListenableFuture decodeBitmap(byte[] data) { - return executorService.submit(() -> decode(data)); + return executorService.submit(() -> decode(data, /* options= */ null)); } @Override - public ListenableFuture loadBitmap(Uri uri) { - return executorService.submit(() -> load(uri)); + public ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) { + return executorService.submit(() -> load(uri, options)); } - private static Bitmap decode(byte[] data) { - @Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length); + // BitmapFactory's options parameter is null-ok. + @SuppressWarnings("nullness:argument.type.incompatible") + private static Bitmap decode(byte[] data, @Nullable BitmapFactory.Options options) { + @Nullable + Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length, options); checkArgument(bitmap != null, "Could not decode image data"); return bitmap; } - private static Bitmap load(Uri uri) throws IOException { + private static Bitmap load(Uri uri, @Nullable BitmapFactory.Options options) throws IOException { if ("file".equals(uri.getScheme())) { @Nullable String path = uri.getPath(); if (path == null) { @@ -113,7 +116,7 @@ public final class SimpleBitmapLoader implements BitmapLoader { throw new IOException("Invalid response status code: " + responseCode); } try (InputStream inputStream = httpConnection.getInputStream()) { - return decode(ByteStreams.toByteArray(inputStream)); + return decode(ByteStreams.toByteArray(inputStream), options); } } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java index 805eb42cb2..4ad4a57bcf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ImageAssetLoader.java @@ -26,6 +26,8 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.ColorSpace; import android.os.Looper; import androidx.annotation.Nullable; import androidx.media3.common.C; @@ -35,6 +37,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.datasource.DefaultDataSource; @@ -109,7 +112,12 @@ public final class ImageAssetLoader implements AssetLoader { MoreExecutors.listeningDecorator(scheduledExecutorService), dataSourceFactory); MediaItem.LocalConfiguration localConfiguration = checkNotNull(editedMediaItem.mediaItem.localConfiguration); - ListenableFuture future = bitmapLoader.loadBitmap(localConfiguration.uri); + @Nullable BitmapFactory.Options options = null; + if (Util.SDK_INT >= 26) { + options = new BitmapFactory.Options(); + options.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB); + } + ListenableFuture future = bitmapLoader.loadBitmap(localConfiguration.uri, options); Futures.addCallback( future, new FutureCallback() {