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 0000000000..296724d1c9 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/jpeg/non-motion-photo-shortened-no-exif.jpg differ