Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
deckerst committed Sep 22, 2020
2 parents 8815a79 + 42287f5 commit 440d6da
Show file tree
Hide file tree
Showing 66 changed files with 1,498 additions and 429 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
case "rotate":
new Thread(() -> rotate(call, new MethodResultWrapper(result))).start();
break;
case "renameDirectory":
new Thread(() -> renameDirectory(call, new MethodResultWrapper(result))).start();
break;
default:
result.notImplemented();
break;
Expand Down Expand Up @@ -179,4 +182,26 @@ public void onFailure(Throwable throwable) {
}
});
}

private void renameDirectory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
String dirPath = call.argument("path");
String newName = call.argument("newName");
if (dirPath == null || newName == null) {
result.error("renameDirectory-args", "failed because of missing arguments", null);
return;
}

ImageProvider provider = new MediaStoreImageProvider();
provider.renameDirectory(activity, dirPath, newName, new ImageProvider.AlbumRenameOpCallback() {
@Override
public void onSuccess(List<Map<String, Object>> fieldsByEntry) {
result.success(fieldsByEntry);
}

@Override
public void onFailure(Throwable throwable) {
result.error("renameDirectory-failure", "failed to rename directory", throwable.getMessage());
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,36 @@
import com.adobe.internal.xmp.XMPException;
import com.adobe.internal.xmp.XMPIterator;
import com.adobe.internal.xmp.XMPMeta;
import com.adobe.internal.xmp.XMPUtils;
import com.adobe.internal.xmp.properties.XMPProperty;
import com.adobe.internal.xmp.properties.XMPPropertyInfo;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
import com.drew.imaging.jpeg.JpegSegmentType;
import com.drew.lang.GeoLocation;
import com.drew.lang.Rational;
import com.drew.lang.annotations.NotNull;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifReader;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.ExifThumbnailDirectory;
import com.drew.metadata.exif.GpsDirectory;
import com.drew.metadata.file.FileTypeDirectory;
import com.drew.metadata.gif.GifAnimationDirectory;
import com.drew.metadata.webp.WebpDirectory;
import com.drew.metadata.xmp.XmpDirectory;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TimeZone;
Expand Down Expand Up @@ -70,9 +82,15 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {

// XMP
private static final String XMP_DC_SCHEMA_NS = "http://purl.org/dc/elements/1.1/";
private static final String XMP_XMP_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/";
private static final String XMP_IMG_SCHEMA_NS = "http://ns.adobe.com/xap/1.0/g/img/";

private static final String XMP_SUBJECT_PROP_NAME = "dc:subject";
private static final String XMP_TITLE_PROP_NAME = "dc:title";
private static final String XMP_DESCRIPTION_PROP_NAME = "dc:description";
private static final String XMP_THUMBNAIL_PROP_NAME = "xmp:Thumbnails";
private static final String XMP_THUMBNAIL_IMAGE_PROP_NAME = "xmpGImg:image";

private static final String XMP_GENERIC_LANG = "";
private static final String XMP_SPECIFIC_LANG = "en-US";

Expand Down Expand Up @@ -108,6 +126,49 @@ public class MetadataHandler implements MethodChannel.MethodCallHandler {
// "+51.3328-000.7053+113.474/" (Apple)
private static final Pattern VIDEO_LOCATION_PATTERN = Pattern.compile("([+-][.0-9]+)([+-][.0-9]+).*");

private static int TAG_THUMBNAIL_DATA = 0x10000;

// modify metadata-extractor readers to store EXIF thumbnail data
// cf https://github.com/drewnoakes/metadata-extractor/issues/276#issuecomment-677767368
static {
List<JpegSegmentMetadataReader> allReaders = (List<JpegSegmentMetadataReader>) JpegMetadataReader.ALL_READERS;
for (int n = 0, cnt = allReaders.size(); n < cnt; n++) {
if (allReaders.get(n).getClass() != ExifReader.class) {
continue;
}

allReaders.set(n, new ExifReader() {
@Override
public void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType) {
super.readJpegSegments(segments, metadata, segmentType);

for (byte[] segmentBytes : segments) {
// Filter any segments containing unexpected preambles
if (!startsWithJpegExifPreamble(segmentBytes)) {
continue;
}

// Extract the thumbnail
try {
ExifThumbnailDirectory tnDirectory = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class);
if (tnDirectory != null && tnDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET)) {
int offset = tnDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
int length = tnDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);

byte[] tnData = new byte[length];
System.arraycopy(segmentBytes, JPEG_SEGMENT_PREAMBLE.length() + offset, tnData, 0, length);
tnDirectory.setObject(TAG_THUMBNAIL_DATA, tnData);
}
} catch (MetadataException e) {
e.printStackTrace();
}
}
}
});
break;
}
}

private Context context;

public MetadataHandler(Context context) {
Expand All @@ -129,6 +190,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
case "getContentResolverMetadata":
new Thread(() -> getContentResolverMetadata(call, new MethodResultWrapper(result))).start();
break;
case "getExifThumbnails":
new Thread(() -> getExifThumbnails(call, new MethodResultWrapper(result))).start();
break;
case "getXmpThumbnails":
new Thread(() -> getXmpThumbnails(call, new MethodResultWrapper(result))).start();
break;
default:
result.notImplemented();
break;
Expand Down Expand Up @@ -463,6 +530,50 @@ private void getContentResolverMetadata(MethodCall call, MethodChannel.Result re
}
}

private void getExifThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Uri uri = Uri.parse(call.argument("uri"));
List<byte[]> thumbnails = new ArrayList<>();
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
for (ExifThumbnailDirectory dir : metadata.getDirectoriesOfType(ExifThumbnailDirectory.class)) {
byte[] data = (byte[]) dir.getObject(TAG_THUMBNAIL_DATA);
if (data != null) {
thumbnails.add(data);
}
}
} catch (IOException | ImageProcessingException | NoClassDefFoundError e) {
Log.w(LOG_TAG, "failed to extract exif thumbnail", e);
}
result.success(thumbnails);
}

private void getXmpThumbnails(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Uri uri = Uri.parse(call.argument("uri"));
List<byte[]> thumbnails = new ArrayList<>();
try (InputStream is = StorageUtils.openInputStream(context, uri)) {
Metadata metadata = ImageMetadataReader.readMetadata(is);
for (XmpDirectory dir : metadata.getDirectoriesOfType(XmpDirectory.class)) {
XMPMeta xmpMeta = dir.getXMPMeta();
try {
if (xmpMeta.doesPropertyExist(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME)) {
int count = xmpMeta.countArrayItems(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME);
for (int i = 1; i < count + 1; i++) {
XMPProperty image = xmpMeta.getStructField(XMP_XMP_SCHEMA_NS, XMP_THUMBNAIL_PROP_NAME + "[" + i + "]", XMP_IMG_SCHEMA_NS, XMP_THUMBNAIL_IMAGE_PROP_NAME);
if (image != null) {
thumbnails.add(XMPUtils.decodeBase64(image.getValue()));
}
}
}
} catch (XMPException e) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=" + uri, e);
}
}
} catch (IOException | ImageProcessingException | NoClassDefFoundError e) {
Log.w(LOG_TAG, "failed to extract xmp thumbnail", e);
}
result.success(thumbnails);
}

// convenience methods

private static <T extends Directory> void putDateFromDirectoryTag(Map<String, Object> metadataMap, String key, Metadata metadata, Class<T> dirClass, int tag) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ private void endOfStream() {
handler.post(() -> eventSink.endOfStream());
}

// Supported image formats:
// - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
private void getImage() {
if (mimeType != null && mimeType.startsWith(MimeTypes.VIDEO)) {
RequestOptions options = new RequestOptions()
Expand All @@ -91,42 +95,40 @@ private void getImage() {
} finally {
Glide.with(activity).clear(target);
}
} else if (MimeTypes.DNG.equals(mimeType) || MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType)) {
// as of Flutter v1.20, Dart Image.memory cannot decode DNG/HEIC/HEIF images
// so we convert the image on platform side first
FutureTarget<Bitmap> target = Glide.with(activity)
.asBitmap()
.load(uri)
.submit();
try {
Bitmap bitmap = target.get();
if (bitmap != null) {
bitmap = TransformationUtils.rotateImage(bitmap, orientationDegrees);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
success(stream.toByteArray());
} else {
error("getImage-image-decode-null", "failed to get image from uri=" + uri, null);
}
} catch (Exception e) {
error("getImage-image-decode-exception", "failed to get image from uri=" + uri, e.getMessage());
} finally {
Glide.with(activity).clear(target);
}
} else {
ContentResolver cr = activity.getContentResolver();
if (MimeTypes.DNG.equals(mimeType) || MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType)) {
// as of Flutter v1.20, Dart Image.memory cannot decode DNG/HEIC/HEIF images
// so we convert the image on platform side first
FutureTarget<Bitmap> target = Glide.with(activity)
.asBitmap()
.load(uri)
.submit();
try {
Bitmap bitmap = target.get();
if (bitmap != null) {
bitmap = TransformationUtils.rotateImage(bitmap, orientationDegrees);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
// we compress the bitmap because Dart Image.memory cannot decode the raw bytes
// Bitmap.CompressFormat.PNG is slower than JPEG
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
success(stream.toByteArray());
} else {
error("getImage-image-decode-null", "failed to get image from uri=" + uri, null);
}
} catch (Exception e) {
error("getImage-image-decode-exception", "failed to get image from uri=" + uri, e.getMessage());
} finally {
Glide.with(activity).clear(target);
}
} else {
try (InputStream is = cr.openInputStream(uri)) {
if (is != null) {
streamBytes(is);
} else {
error("getImage-image-read-null", "failed to get image from uri=" + uri, null);
}
} catch (IOException e) {
error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage());
try (InputStream is = cr.openInputStream(uri)) {
if (is != null) {
streamBytes(is);
} else {
error("getImage-image-read-null", "failed to get image from uri=" + uri, null);
}
} catch (IOException e) {
error("getImage-image-read-exception", "failed to get image from uri=" + uri, e.getMessage());
}
}
endOfStream();
Expand Down
Loading

0 comments on commit 440d6da

Please sign in to comment.