improved support for raw formats

This commit is contained in:
Thibault Deckers 2020-10-03 09:12:06 +09:00
parent 71d7262b74
commit 8ca648b94a
12 changed files with 142 additions and 58 deletions

View file

@ -198,12 +198,14 @@ public class ImageDecodeTask extends AsyncTask<ImageDecodeTask.Params, Void, Ima
@Override
protected void onPostExecute(Result result) {
MethodChannel.Result r = result.params.result;
String uri = result.params.entry.uri.toString();
Params params = result.params;
MethodChannel.Result r = params.result;
AvesImageEntry entry = params.entry;
String uri = entry.uri.toString();
if (result.data != null) {
r.success(result.data);
} else {
r.error("getThumbnail-null", "failed to get thumbnail for uri=" + uri, null);
r.error("getThumbnail-null", "failed to get thumbnail for uri=" + uri + ", path=" + entry.path, null);
}
}
}

View file

@ -16,6 +16,8 @@ import com.bumptech.glide.request.RequestOptions;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import deckers.thibault.aves.decoder.VideoThumbnail;
@ -32,6 +34,17 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
private EventChannel.EventSink eventSink;
private Handler handler;
private static final List<String> flutterSupportedTypes = Arrays.asList(
MimeTypes.JPEG,
MimeTypes.PNG,
MimeTypes.GIF,
MimeTypes.WEBP,
MimeTypes.BMP,
MimeTypes.WBMP,
MimeTypes.ICO,
MimeTypes.SVG
);
@SuppressWarnings("unchecked")
public ImageByteStreamHandler(Activity activity, Object arguments) {
this.activity = activity;
@ -95,9 +108,8 @@ public class ImageByteStreamHandler implements EventChannel.StreamHandler {
} 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
} else if (!flutterSupportedTypes.contains(mimeType)) {
// we convert the image on platform side first, when Dart Image.memory does not support it
FutureTarget<Bitmap> target = Glide.with(activity)
.asBitmap()
.load(uri)

View file

@ -59,7 +59,7 @@ public class SourceImageEntry {
this.sourceMimeType = (String) map.get("sourceMimeType");
this.width = (int) map.get("width");
this.height = (int) map.get("height");
this.rotationDegrees = (int) map.get("orientationDegrees");
this.rotationDegrees = (int) map.get("rotationDegrees");
this.sizeBytes = toLong(map.get("sizeBytes"));
this.title = (String) map.get("title");
this.dateModifiedSecs = toLong(map.get("dateModifiedSecs"));

View file

@ -163,7 +163,7 @@ public class MediaStoreImageProvider extends ImageProvider {
put("uri", itemUri.toString());
put("path", path);
put("sourceMimeType", mimeType);
put("orientationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0);
put("rotationDegrees", orientationColumn != -1 ? cursor.getInt(orientationColumn) : 0);
put("sizeBytes", cursor.getLong(sizeColumn));
put("title", cursor.getString(titleColumn));
put("dateModifiedSecs", dateModifiedSecs);

View file

@ -2,16 +2,25 @@ package deckers.thibault.aves.utils
object MimeTypes {
const val IMAGE = "image"
const val DNG = "image/x-adobe-dng" // .dng
const val BMP = "image/bmp"
const val GIF = "image/gif"
const val HEIC = "image/heic"
const val HEIF = "image/heif"
const val ICO = "image/x-icon"
const val JPEG = "image/jpeg"
const val PNG = "image/png"
const val PSD = "image/x-photoshop" // .psd
const val SVG = "image/svg+xml" // .svg
const val WBMP = "image/vnd.wap.wbmp"
const val WEBP = "image/webp"
const val HEIC = "image/heic"
const val HEIF = "image/heif"
const val PSD = "image/x-photoshop" // .psd
const val DNG = "image/x-adobe-dng" // .dng
const val SVG = "image/svg+xml" // .svg
const val VIDEO = "video"
const val AVI = "video/avi"
const val MP2T = "video/mp2t" // .m2ts
const val MP4 = "video/mp4"

View file

@ -52,9 +52,9 @@ class MimeFilter extends CollectionFilter {
static String displayType(String mime) {
final patterns = [
RegExp('.*/'), // remove type, keep subtype
RegExp('(X-|VND.)'), // noisy prefixes
RegExp('(X-|VND.(WAP.)?)'), // noisy prefixes
'+XML', // noisy suffix
RegExp('ADOBE[-\.]'), // for DNG, PSD...
RegExp('ADOBE\.'), // for PSD
];
mime = mime.toUpperCase();
patterns.forEach((pattern) => mime = mime.replaceFirst(pattern, ''));

View file

@ -90,7 +90,7 @@ class ImageEntry {
sourceMimeType: map['sourceMimeType'] as String,
width: map['width'] as int ?? 0,
height: map['height'] as int ?? 0,
orientationDegrees: map['orientationDegrees'] as int,
orientationDegrees: map['orientationDegrees'] as int ?? 0,
sizeBytes: map['sizeBytes'] as int,
sourceTitle: map['title'] as String,
dateModifiedSecs: map['dateModifiedSecs'] as int,
@ -165,7 +165,7 @@ class ImageEntry {
// guess whether this is a photo, according to file type (used as a hint to e.g. display megapixels)
bool get isPhoto => [MimeTypes.heic, MimeTypes.heif, MimeTypes.jpeg].contains(mimeType) || isRaw;
bool get isRaw => [MimeTypes.dng].contains(mimeType);
bool get isRaw => MimeTypes.rawImages.contains(mimeType);
bool get isVideo => mimeType.startsWith('video');

View file

@ -1,6 +1,6 @@
class MimeTypes {
static const String anyImage = 'image/*';
static const String dng = 'image/x-adobe-dng';
static const String gif = 'image/gif';
static const String heic = 'image/heic';
static const String heif = 'image/heif';
@ -9,8 +9,33 @@ class MimeTypes {
static const String svg = 'image/svg+xml';
static const String webp = 'image/webp';
static const String arw = 'image/x-sony-arw';
static const String cr2 = 'image/x-canon-cr2';
static const String crw = 'image/x-canon-crw';
static const String dcr = 'image/x-kodak-dcr';
static const String dng = 'image/x-adobe-dng';
static const String erf = 'image/x-epson-erf';
static const String k25 = 'image/x-kodak-k25';
static const String kdc = 'image/x-kodak-kdc';
static const String mrw = 'image/x-minolta-mrw';
static const String nef = 'image/x-nikon-nef';
static const String nrw = 'image/x-nikon-nrw';
static const String orf = 'image/x-olympus-orf';
static const String pef = 'image/x-pentax-pef';
static const String raf = 'image/x-fuji-raf';
static const String raw = 'image/x-panasonic-raw';
static const String rw2 = 'image/x-panasonic-rw2';
static const String sr2 = 'image/x-sony-sr2';
static const String srf = 'image/x-sony-srf';
static const String srw = 'image/x-samsung-srw';
static const String x3f = 'image/x-sigma-x3f';
static const String anyVideo = 'video/*';
static const String avi = 'video/avi';
static const String mp2t = 'video/mp2t'; // .m2ts
static const String mp4 = 'video/mp4';
// groups
static const List<String> rawImages = [arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f];
}

View file

@ -20,6 +20,8 @@ class Constants {
static const pointNemo = Tuple2(-48.876667, -123.393333);
static const int infoGroupMaxValueLength = 140;
static const List<Dependency> androidDependencies = [
Dependency(
name: 'CWAC-Document',

View file

@ -0,0 +1,41 @@
import 'package:aves/widgets/common/highlight_title.dart';
import 'package:expansion_tile_card/expansion_tile_card.dart';
import 'package:flutter/material.dart';
class AvesExpansionTile extends StatelessWidget {
final String title;
final List<Widget> children;
final ValueNotifier<String> expandedNotifier;
const AvesExpansionTile({
@required this.title,
@required this.children,
this.expandedNotifier,
});
@override
Widget build(BuildContext context) {
return Theme(
data: Theme.of(context).copyWith(
// color used by the `ExpansionTileCard` for selected text and icons
accentColor: Colors.white,
),
child: ExpansionTileCard(
key: Key('tilecard-$title'),
value: title,
expandedNotifier: expandedNotifier,
title: HighlightTitle(
title,
fontSize: 18,
),
children: [
Divider(thickness: 1, height: 1),
SizedBox(height: 4),
...children,
],
baseColor: Colors.grey[900],
expandedColor: Colors.grey[850],
),
);
}
}

View file

@ -5,11 +5,12 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_db.dart';
import 'package:aves/services/metadata_service.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/aves_expansion_tile.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/settings/settings_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tuple/tuple.dart';
@ -260,7 +261,7 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
static const millisecondTimestampKeys = ['datetaken', 'datetime'];
Widget _buildContentResolverTabView() {
Widget builder(BuildContext context, AsyncSnapshot<Map> snapshot) {
Widget builder(BuildContext context, AsyncSnapshot<Map> snapshot, String title) {
if (snapshot.hasError) return Text(snapshot.error.toString());
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
final data = SplayTreeMap.of(snapshot.data.map((k, v) {
@ -277,21 +278,30 @@ class _FullscreenDebugPageState extends State<FullscreenDebugPage> {
}
return MapEntry(key, value);
}));
return InfoRowGroup(data);
return AvesExpansionTile(
title: title,
children: [
Padding(
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(
data,
maxValueLength: Constants.infoGroupMaxValueLength,
),
)
],
);
}
return ListView(
padding: EdgeInsets.all(16),
padding: EdgeInsets.all(8),
children: [
SectionTitle('Content Resolver (Media Store)'),
FutureBuilder<Map>(
future: _contentResolverMetadataLoader,
builder: builder,
builder: (context, snapshot) => builder(context, snapshot, 'Content Resolver'),
),
SectionTitle('Exif Interface'),
FutureBuilder<Map>(
future: _exifInterfaceMetadataLoader,
builder: builder,
builder: (context, snapshot) => builder(context, snapshot, 'Exif Interface'),
),
],
);

View file

@ -3,12 +3,12 @@ import 'dart:collection';
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/metadata_service.dart';
import 'package:aves/widgets/common/highlight_title.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/aves_expansion_tile.dart';
import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/fullscreen/info/metadata_thumbnail.dart';
import 'package:collection/collection.dart';
import 'package:expansion_tile_card/expansion_tile_card.dart';
import 'package:flutter/material.dart';
class MetadataSectionSliver extends StatefulWidget {
@ -33,8 +33,6 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
bool get isVisible => widget.visibleNotifier.value;
static const int maxValueLength = 140;
// directory names from metadata-extractor
static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor
static const xmpDirectory = 'XMP'; // from metadata-extractor
@ -86,37 +84,22 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
}
if (index < untitledDirectoryCount + 1) {
final dir = directoriesWithoutTitle[index - 1];
return InfoRowGroup(dir.tags, maxValueLength: maxValueLength);
return InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength);
}
final dir = directoriesWithTitle[index - 1 - untitledDirectoryCount];
return Theme(
data: Theme.of(context).copyWith(
// color used by the `ExpansionTileCard` for selected text and icons
accentColor: Colors.white,
),
child: ExpansionTileCard(
key: Key('tilecard-${dir.name}'),
value: dir.name,
expandedNotifier: _expandedDirectoryNotifier,
title: HighlightTitle(
dir.name,
fontSize: 18,
return AvesExpansionTile(
title: dir.name,
expandedNotifier: _expandedDirectoryNotifier,
children: [
if (dir.name == exifThumbnailDirectory) MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry),
if (dir.name == xmpDirectory) MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry),
if (dir.name == videoDirectory) MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry),
Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength),
),
children: [
Divider(thickness: 1, height: 1),
SizedBox(height: 4),
if (dir.name == exifThumbnailDirectory) MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry),
if (dir.name == xmpDirectory) MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry),
if (dir.name == videoDirectory) MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry),
Container(
alignment: Alignment.topLeft,
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(dir.tags, maxValueLength: maxValueLength),
),
],
baseColor: Colors.grey[900],
expandedColor: Colors.grey[850],
),
],
);
},
childCount: 1 + _metadata.length,