From 04d8edf19efa11671f409c4b079d912dbea528c2 Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 2 Jun 2023 09:39:23 +0000 Subject: [PATCH] Read Exif orientation data in DataSourceBitmapLoader PiperOrigin-RevId: 537258424 --- RELEASENOTES.md | 1 + constants.gradle | 1 + libraries/datasource/build.gradle | 1 + .../DataSourceBitmapLoaderTest.java | 107 ++++++++++++------ .../datasource/DataSourceBitmapLoader.java | 36 +++++- .../non-motion-photo-shortened-no-exif.jpg | Bin 0 -> 5043 bytes 6 files changed, 104 insertions(+), 42 deletions(-) create mode 100644 libraries/test_data/src/test/assets/media/jpeg/non-motion-photo-shortened-no-exif.jpg diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6fcade8582..243b23ac8c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,7 @@ * Common Library: * ExoPlayer: * Transformer: + * Parse EXIF rotation data for image inputs. * Track Selection: * Extractors: * Audio: diff --git a/constants.gradle b/constants.gradle index c44c813a8f..a0ff19d7e8 100644 --- a/constants.gradle +++ b/constants.gradle @@ -45,6 +45,7 @@ project.ext { androidxConstraintLayoutVersion = '2.1.4' // Updating this to 1.9.0+ will import Kotlin stdlib [internal ref: b/277891049]. androidxCoreVersion = '1.8.0' + androidxExifInterfaceVersion = '1.3.6' androidxFuturesVersion = '1.1.0' androidxMediaVersion = '1.6.0' androidxMedia2Version = '1.2.1' diff --git a/libraries/datasource/build.gradle b/libraries/datasource/build.gradle index d2ca01a333..1001800f4b 100644 --- a/libraries/datasource/build.gradle +++ b/libraries/datasource/build.gradle @@ -38,6 +38,7 @@ dependencies { implementation project(modulePrefix + 'lib-common') implementation project(modulePrefix + 'lib-database') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.exifinterface:exifinterface:' + androidxExifInterfaceVersion compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion 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 c67a047088..32d366bb8b 100644 --- a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java +++ b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/DataSourceBitmapLoaderTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertThrows; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Matrix; import android.net.Uri; import androidx.media3.common.MediaMetadata; import androidx.media3.test.utils.TestUtil; @@ -52,7 +53,9 @@ public class DataSourceBitmapLoaderTest { @Rule public final TemporaryFolder tempFolder = new TemporaryFolder(); - private static final String TEST_IMAGE_PATH = "media/jpeg/non-motion-photo-shortened.jpg"; + private static final String TEST_IMAGE_FOLDER = "media/jpeg/"; + private static final String TEST_IMAGE_PATH = + TEST_IMAGE_FOLDER + "non-motion-photo-shortened-no-exif.jpg"; private DataSource.Factory dataSourceFactory; @@ -76,6 +79,33 @@ public class DataSourceBitmapLoaderTest { .isTrue(); } + @Test + public void decodeBitmap_withExifRotation_loadsCorrectData() throws Exception { + DataSourceBitmapLoader bitmapLoader = + new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); + byte[] imageData = + TestUtil.getByteArray( + ApplicationProvider.getApplicationContext(), + TEST_IMAGE_FOLDER + "non-motion-photo-shortened.jpg"); + Bitmap bitmapWithoutRotation = + BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length); + Matrix rotationMatrix = new Matrix(); + rotationMatrix.postRotate(/* degrees= */ 90); + Bitmap expectedBitmap = + Bitmap.createBitmap( + bitmapWithoutRotation, + /* x= */ 0, + /* y= */ 0, + bitmapWithoutRotation.getWidth(), + bitmapWithoutRotation.getHeight(), + rotationMatrix, + /* filter= */ false); + + Bitmap actualBitmap = bitmapLoader.decodeBitmap(imageData).get(); + + assertThat(actualBitmap.sameAs(expectedBitmap)).isTrue(); + } + @Test public void decodeBitmap_withInvalidData_throws() { DataSourceBitmapLoader bitmapLoader = @@ -93,14 +123,15 @@ public class DataSourceBitmapLoaderTest { public void loadBitmap_withHttpUri_loadsCorrectData() throws Exception { DataSourceBitmapLoader bitmapLoader = new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); - MockWebServer mockWebServer = new MockWebServer(); byte[] imageData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH); Buffer responseBody = new Buffer().write(imageData); - mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody)); + Bitmap bitmap; + try (MockWebServer mockWebServer = new MockWebServer()) { + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody)); - Bitmap bitmap = - bitmapLoader.loadBitmap(Uri.parse(mockWebServer.url("test_path").toString())).get(); + bitmap = bitmapLoader.loadBitmap(Uri.parse(mockWebServer.url("test_path").toString())).get(); + } assertThat( bitmap.sameAs( @@ -109,14 +140,15 @@ public class DataSourceBitmapLoaderTest { } @Test - public void loadBitmap_httpUriAndServerError_throws() { + public void loadBitmap_httpUriAndServerError_throws() throws Exception { DataSourceBitmapLoader bitmapLoader = new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); - MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.enqueue(new MockResponse().setResponseCode(404)); + ListenableFuture future; + try (MockWebServer mockWebServer = new MockWebServer()) { + mockWebServer.enqueue(new MockResponse().setResponseCode(404)); - ListenableFuture future = - bitmapLoader.loadBitmap(Uri.parse(mockWebServer.url("test_path").toString())); + future = bitmapLoader.loadBitmap(Uri.parse(mockWebServer.url("test_path").toString())); + } assertException( future::get, HttpDataSource.InvalidResponseCodeException.class, /* messagePart= */ "404"); @@ -138,7 +170,7 @@ public class DataSourceBitmapLoaderTest { } @Test - public void loadBitmap_assetUriWithAssetNotExisting_throws() throws Exception { + public void loadBitmap_assetUriWithAssetNotExisting_throws() { DataSourceBitmapLoader bitmapLoader = new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); @@ -167,7 +199,7 @@ public class DataSourceBitmapLoaderTest { } @Test - public void loadBitmap_fileUriWithFileNotExisting_throws() throws Exception { + public void loadBitmap_fileUriWithFileNotExisting_throws() { DataSourceBitmapLoader bitmapLoader = new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); @@ -182,49 +214,50 @@ public class DataSourceBitmapLoaderTest { throws Exception { byte[] imageData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH); - MockWebServer mockWebServer = new MockWebServer(); - Uri uri = Uri.parse(mockWebServer.url("test_path").toString()); - MediaMetadata metadata = - new MediaMetadata.Builder() - .setArtworkData(imageData, MediaMetadata.PICTURE_TYPE_FRONT_COVER) - .setArtworkUri(uri) - .build(); DataSourceBitmapLoader bitmapLoader = new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); + try (MockWebServer mockWebServer = new MockWebServer()) { + Uri uri = Uri.parse(mockWebServer.url("test_path").toString()); + MediaMetadata metadata = + new MediaMetadata.Builder() + .setArtworkData(imageData, MediaMetadata.PICTURE_TYPE_FRONT_COVER) + .setArtworkUri(uri) + .build(); - Bitmap bitmap = bitmapLoader.loadBitmapFromMetadata(metadata).get(); + Bitmap bitmap = bitmapLoader.loadBitmapFromMetadata(metadata).get(); - assertThat( - bitmap.sameAs( - BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length))) - .isTrue(); - assertThat(mockWebServer.getRequestCount()).isEqualTo(0); + assertThat( + bitmap.sameAs( + BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length))) + .isTrue(); + assertThat(mockWebServer.getRequestCount()).isEqualTo(0); + } } @Test public void loadBitmapFromMetadata_withArtworkUriSet_loadFromArtworkUri() throws Exception { byte[] imageData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TEST_IMAGE_PATH); - MockWebServer mockWebServer = new MockWebServer(); Buffer responseBody = new Buffer().write(imageData); - mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody)); - Uri uri = Uri.parse(mockWebServer.url("test_path").toString()); - MediaMetadata metadata = new MediaMetadata.Builder().setArtworkUri(uri).build(); DataSourceBitmapLoader bitmapLoader = new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); + try (MockWebServer mockWebServer = new MockWebServer()) { + mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(responseBody)); + Uri uri = Uri.parse(mockWebServer.url("test_path").toString()); + MediaMetadata metadata = new MediaMetadata.Builder().setArtworkUri(uri).build(); - Bitmap bitmap = bitmapLoader.loadBitmapFromMetadata(metadata).get(); + Bitmap bitmap = bitmapLoader.loadBitmapFromMetadata(metadata).get(); - assertThat( - bitmap.sameAs( - BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length))) - .isTrue(); - assertThat(mockWebServer.getRequestCount()).isEqualTo(1); + assertThat( + bitmap.sameAs( + BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length))) + .isTrue(); + assertThat(mockWebServer.getRequestCount()).isEqualTo(1); + } } @Test - public void loadBitmapFromMetadata_withArtworkDataAndArtworkUriUnset_returnNull() - throws Exception { + public void loadBitmapFromMetadata_withArtworkDataAndArtworkUriUnset_returnNull() { MediaMetadata metadata = new MediaMetadata.Builder().build(); DataSourceBitmapLoader bitmapLoader = new DataSourceBitmapLoader(MoreExecutors.newDirectExecutorService(), dataSourceFactory); 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 9d0723a8b1..0f60321b5d 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceBitmapLoader.java @@ -21,8 +21,10 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Matrix; import android.net.Uri; import androidx.annotation.Nullable; +import androidx.exifinterface.media.ExifInterface; import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.UnstableApi; import com.google.common.base.Supplier; @@ -30,7 +32,9 @@ import com.google.common.base.Suppliers; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.util.concurrent.Executors; /** @@ -83,16 +87,38 @@ public final class DataSourceBitmapLoader implements BitmapLoader { return listeningExecutorService.submit(() -> load(dataSourceFactory.createDataSource(), uri)); } - private static Bitmap decode(byte[] data) { + private static Bitmap decode(byte[] data) throws IOException { @Nullable Bitmap bitmap = BitmapFactory.decodeByteArray(data, /* offset= */ 0, data.length); checkArgument(bitmap != null, "Could not decode image data"); + ExifInterface exifInterface; + try (InputStream inputStream = new ByteArrayInputStream(data)) { + exifInterface = new ExifInterface(inputStream); + } + int rotationDegrees = exifInterface.getRotationDegrees(); + if (rotationDegrees != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(rotationDegrees); + bitmap = + Bitmap.createBitmap( + bitmap, + /* x= */ 0, + /* y= */ 0, + bitmap.getWidth(), + bitmap.getHeight(), + matrix, + /* filter= */ false); + } return bitmap; } private static Bitmap load(DataSource dataSource, Uri uri) throws IOException { - DataSpec dataSpec = new DataSpec(uri); - dataSource.open(dataSpec); - byte[] readData = DataSourceUtil.readToEnd(dataSource); - return decode(readData); + try { + DataSpec dataSpec = new DataSpec(uri); + dataSource.open(dataSpec); + byte[] readData = DataSourceUtil.readToEnd(dataSource); + return decode(readData); + } finally { + dataSource.close(); + } } } diff --git a/libraries/test_data/src/test/assets/media/jpeg/non-motion-photo-shortened-no-exif.jpg b/libraries/test_data/src/test/assets/media/jpeg/non-motion-photo-shortened-no-exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..296724d1c94ee13399f80b273c109facd76ba3cc GIT binary patch literal 5043 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 PQHjh%iWUoc3nTC!=DJ;X literal 0 HcmV?d00001