mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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
This commit is contained in:
parent
1831220a53
commit
be38670391
@ -16,6 +16,7 @@
|
|||||||
package androidx.media3.common.util;
|
package androidx.media3.common.util;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
@ -28,7 +29,12 @@ public interface BitmapLoader {
|
|||||||
ListenableFuture<Bitmap> decodeBitmap(byte[] data);
|
ListenableFuture<Bitmap> decodeBitmap(byte[] data);
|
||||||
|
|
||||||
/** Loads an image from {@code uri}. */
|
/** Loads an image from {@code uri}. */
|
||||||
ListenableFuture<Bitmap> loadBitmap(Uri uri);
|
default ListenableFuture<Bitmap> loadBitmap(Uri uri) {
|
||||||
|
return loadBitmap(uri, /* options= */ null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Loads an image from {@code uri} with the given {@link BitmapFactory.Options}. */
|
||||||
|
ListenableFuture<Bitmap> loadBitmap(Uri uri, @Nullable BitmapFactory.Options options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads an image from {@link MediaMetadata}. Returns null if {@code metadata} doesn't contain
|
* Loads an image from {@link MediaMetadata}. Returns null if {@code metadata} doesn't contain
|
||||||
|
@ -198,6 +198,24 @@ public class DataSourceBitmapLoaderTest {
|
|||||||
.isTrue();
|
.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
|
@Test
|
||||||
public void loadBitmap_fileUriWithFileNotExisting_throws() {
|
public void loadBitmap_fileUriWithFileNotExisting_throws() {
|
||||||
DataSourceBitmapLoader bitmapLoader =
|
DataSourceBitmapLoader bitmapLoader =
|
||||||
|
@ -78,17 +78,21 @@ public final class DataSourceBitmapLoader implements BitmapLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
|
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
|
||||||
return listeningExecutorService.submit(() -> decode(data));
|
return listeningExecutorService.submit(() -> decode(data, /* options= */ null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads an image from a {@link Uri}. */
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
|
public ListenableFuture<Bitmap> loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) {
|
||||||
return listeningExecutorService.submit(() -> load(dataSourceFactory.createDataSource(), uri));
|
return listeningExecutorService.submit(
|
||||||
|
() -> load(dataSourceFactory.createDataSource(), uri, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Bitmap decode(byte[] data) throws IOException {
|
// BitmapFactory's options parameter is null-ok.
|
||||||
@Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length);
|
@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");
|
checkArgument(bitmap != null, "Could not decode image data");
|
||||||
ExifInterface exifInterface;
|
ExifInterface exifInterface;
|
||||||
try (InputStream inputStream = new ByteArrayInputStream(data)) {
|
try (InputStream inputStream = new ByteArrayInputStream(data)) {
|
||||||
@ -111,12 +115,13 @@ public final class DataSourceBitmapLoader implements BitmapLoader {
|
|||||||
return bitmap;
|
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 {
|
try {
|
||||||
DataSpec dataSpec = new DataSpec(uri);
|
DataSpec dataSpec = new DataSpec(uri);
|
||||||
dataSource.open(dataSpec);
|
dataSource.open(dataSpec);
|
||||||
byte[] readData = DataSourceUtil.readToEnd(dataSource);
|
byte[] readData = DataSourceUtil.readToEnd(dataSource);
|
||||||
return decode(readData);
|
return decode(readData, options);
|
||||||
} finally {
|
} finally {
|
||||||
dataSource.close();
|
dataSource.close();
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package androidx.media3.session;
|
|||||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.util.BitmapLoader;
|
import androidx.media3.common.util.BitmapLoader;
|
||||||
@ -59,11 +60,11 @@ public final class CacheBitmapLoader implements BitmapLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
|
public ListenableFuture<Bitmap> loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) {
|
||||||
if (lastBitmapLoadRequest != null && lastBitmapLoadRequest.matches(uri)) {
|
if (lastBitmapLoadRequest != null && lastBitmapLoadRequest.matches(uri)) {
|
||||||
return lastBitmapLoadRequest.getFuture();
|
return lastBitmapLoadRequest.getFuture();
|
||||||
}
|
}
|
||||||
ListenableFuture<Bitmap> future = bitmapLoader.loadBitmap(uri);
|
ListenableFuture<Bitmap> future = bitmapLoader.loadBitmap(uri, options);
|
||||||
lastBitmapLoadRequest = new BitmapLoadRequest(uri, future);
|
lastBitmapLoadRequest = new BitmapLoadRequest(uri, future);
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
@ -76,21 +76,24 @@ public final class SimpleBitmapLoader implements BitmapLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
|
public ListenableFuture<Bitmap> decodeBitmap(byte[] data) {
|
||||||
return executorService.submit(() -> decode(data));
|
return executorService.submit(() -> decode(data, /* options= */ null));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<Bitmap> loadBitmap(Uri uri) {
|
public ListenableFuture<Bitmap> loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) {
|
||||||
return executorService.submit(() -> load(uri));
|
return executorService.submit(() -> load(uri, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Bitmap decode(byte[] data) {
|
// BitmapFactory's options parameter is null-ok.
|
||||||
@Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length);
|
@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");
|
checkArgument(bitmap != null, "Could not decode image data");
|
||||||
return bitmap;
|
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())) {
|
if ("file".equals(uri.getScheme())) {
|
||||||
@Nullable String path = uri.getPath();
|
@Nullable String path = uri.getPath();
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
@ -113,7 +116,7 @@ public final class SimpleBitmapLoader implements BitmapLoader {
|
|||||||
throw new IOException("Invalid response status code: " + responseCode);
|
throw new IOException("Invalid response status code: " + responseCode);
|
||||||
}
|
}
|
||||||
try (InputStream inputStream = httpConnection.getInputStream()) {
|
try (InputStream inputStream = httpConnection.getInputStream()) {
|
||||||
return decode(ByteStreams.toByteArray(inputStream));
|
return decode(ByteStreams.toByteArray(inputStream), options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.ColorSpace;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
@ -35,6 +37,7 @@ import androidx.media3.common.MediaItem;
|
|||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.BitmapLoader;
|
import androidx.media3.common.util.BitmapLoader;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.datasource.DataSource;
|
import androidx.media3.datasource.DataSource;
|
||||||
import androidx.media3.datasource.DataSourceBitmapLoader;
|
import androidx.media3.datasource.DataSourceBitmapLoader;
|
||||||
import androidx.media3.datasource.DefaultDataSource;
|
import androidx.media3.datasource.DefaultDataSource;
|
||||||
@ -109,7 +112,12 @@ public final class ImageAssetLoader implements AssetLoader {
|
|||||||
MoreExecutors.listeningDecorator(scheduledExecutorService), dataSourceFactory);
|
MoreExecutors.listeningDecorator(scheduledExecutorService), dataSourceFactory);
|
||||||
MediaItem.LocalConfiguration localConfiguration =
|
MediaItem.LocalConfiguration localConfiguration =
|
||||||
checkNotNull(editedMediaItem.mediaItem.localConfiguration);
|
checkNotNull(editedMediaItem.mediaItem.localConfiguration);
|
||||||
ListenableFuture<Bitmap> 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<Bitmap> future = bitmapLoader.loadBitmap(localConfiguration.uri, options);
|
||||||
Futures.addCallback(
|
Futures.addCallback(
|
||||||
future,
|
future,
|
||||||
new FutureCallback<Bitmap>() {
|
new FutureCallback<Bitmap>() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user