Read Exif orientation data in DataSourceBitmapLoader

PiperOrigin-RevId: 537258424
This commit is contained in:
kimvde 2023-06-02 09:39:23 +00:00 committed by Tofunmi Adigun-Hameed
parent fa990714fe
commit 04d8edf19e
6 changed files with 104 additions and 42 deletions

View File

@ -5,6 +5,7 @@
* Common Library:
* ExoPlayer:
* Transformer:
* Parse EXIF rotation data for image inputs.
* Track Selection:
* Extractors:
* Audio:

View File

@ -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'

View File

@ -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

View File

@ -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<Bitmap> future;
try (MockWebServer mockWebServer = new MockWebServer()) {
mockWebServer.enqueue(new MockResponse().setResponseCode(404));
ListenableFuture<Bitmap> 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);

View File

@ -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();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB