Read Exif orientation data in DataSourceBitmapLoader
PiperOrigin-RevId: 537258424
This commit is contained in:
parent
fa990714fe
commit
04d8edf19e
@ -5,6 +5,7 @@
|
||||
* Common Library:
|
||||
* ExoPlayer:
|
||||
* Transformer:
|
||||
* Parse EXIF rotation data for image inputs.
|
||||
* Track Selection:
|
||||
* Extractors:
|
||||
* Audio:
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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 |
Loading…
x
Reference in New Issue
Block a user