diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java index f93c6c6a0..324262abe 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java @@ -1,7 +1,11 @@ package deckers.thibault.aves.channelhandlers; import android.app.Activity; +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.ImageDecoder; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -15,6 +19,7 @@ import java.util.Map; import deckers.thibault.aves.model.ImageEntry; import deckers.thibault.aves.model.provider.ImageProvider; import deckers.thibault.aves.model.provider.ImageProviderFactory; +import deckers.thibault.aves.utils.MimeTypes; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -66,12 +71,26 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { } private void readAsBytes(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - String uri = call.argument("uri"); + String mimeType = call.argument("mimeType"); + String uriString = call.argument("uri"); byte[] data = null; - try (InputStream is = activity.getContentResolver().openInputStream(Uri.parse(uri))) { + ContentResolver cr = activity.getContentResolver(); + Uri uri = Uri.parse(uriString); + try (InputStream is = cr.openInputStream(uri)) { if (is != null) { data = getBytes(is); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && (MimeTypes.HEIC.equals(mimeType) || MimeTypes.HEIF.equals(mimeType))) { + // as of Flutter v1.15.17, Dart Image.memory cannot decode HEIF/HEIC images + // so we convert the image using Android native decoder + ImageDecoder.Source source = ImageDecoder.createSource(cr, uri); + Bitmap bitmap = ImageDecoder.decodeBitmap(source); + 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); + data = stream.toByteArray(); + } } } catch (IOException ex) { // ignore diff --git a/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java b/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java index cffb48a54..5b89a7337 100644 --- a/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java +++ b/android/app/src/main/java/deckers/thibault/aves/utils/MimeTypes.java @@ -3,6 +3,8 @@ package deckers.thibault.aves.utils; public class MimeTypes { public static final String IMAGE = "image"; public static final String GIF = "image/gif"; + public static final String HEIC = "image/heic"; + public static final String HEIF = "image/heif"; public static final String JPEG = "image/jpeg"; public static final String PNG = "image/png"; public static final String SVG = "image/svg+xml"; diff --git a/lib/model/image_file_service.dart b/lib/model/image_file_service.dart index 14b5f07cc..52f5484fc 100644 --- a/lib/model/image_file_service.dart +++ b/lib/model/image_file_service.dart @@ -29,10 +29,11 @@ class ImageFileService { return null; } - static Future readAsBytes(String uri) async { + static Future readAsBytes(String uri, String mimeType) async { try { final result = await platform.invokeMethod('readAsBytes', { 'uri': uri, + 'mimeType': mimeType, }); return result as Uint8List; } on PlatformException catch (e) { diff --git a/lib/widgets/album/thumbnail.dart b/lib/widgets/album/thumbnail.dart index 145f75971..c47bfa6a2 100644 --- a/lib/widgets/album/thumbnail.dart +++ b/lib/widgets/album/thumbnail.dart @@ -85,7 +85,8 @@ class Thumbnail extends StatelessWidget { height: extent, child: SvgPicture( UriPicture( - entry.uri, + uri: entry.uri, + mimeType: entry.mimeType, colorFilter: Constants.svgColorFilter, ), width: extent, diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index 355a89334..6b91c65b5 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -416,7 +416,10 @@ class _FullscreenVerticalPageViewState extends State } void _onImageChange() async { - await UriImage(entry.uri).evict(); + await UriImage( + uri: entry.uri, + mimeType: entry.mimeType, + ).evict(); if (entry.path != null) await FileImage(File(entry.path)).evict(); // rebuild to refresh the Image inside ImagePage setState(() {}); diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index 9ab77964d..4265a139b 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -66,7 +66,8 @@ class ImageView extends StatelessWidget { return PhotoView.customChild( child: SvgPicture( UriPicture( - entry.uri, + uri: entry.uri, + mimeType: entry.mimeType, colorFilter: Constants.svgColorFilter, ), placeholderBuilder: placeholderBuilder, @@ -83,7 +84,10 @@ class ImageView extends StatelessWidget { return PhotoView( // key includes size and orientation to refresh when the image is rotated key: ValueKey('${entry.orientationDegrees}_${entry.width}_${entry.height}_${entry.path}'), - imageProvider: UriImage(entry.uri), + imageProvider: UriImage( + uri: entry.uri, + mimeType: entry.mimeType, + ), loadingBuilder: (context, event) => placeholderBuilder(context), backgroundDecoration: backgroundDecoration, heroAttributes: heroAttributes, diff --git a/lib/widgets/fullscreen/uri_image_provider.dart b/lib/widgets/fullscreen/uri_image_provider.dart index 17504325a..7ef3257dd 100644 --- a/lib/widgets/fullscreen/uri_image_provider.dart +++ b/lib/widgets/fullscreen/uri_image_provider.dart @@ -6,11 +6,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class UriImage extends ImageProvider { - const UriImage(this.uri, {this.scale = 1.0}) - : assert(uri != null), + const UriImage({ + @required this.uri, + @required this.mimeType, + this.scale = 1.0, + }) : assert(uri != null), assert(scale != null); - final String uri; + final String uri, mimeType; final double scale; @@ -25,7 +28,7 @@ class UriImage extends ImageProvider { codec: _loadAsync(key, decode), scale: key.scale, informationCollector: () sync* { - yield ErrorDescription('Uri: $uri'); + yield ErrorDescription('uri=$uri, mimeType=$mimeType'); }, ); } @@ -33,7 +36,7 @@ class UriImage extends ImageProvider { Future _loadAsync(UriImage key, DecoderCallback decode) async { assert(key == this); - final Uint8List bytes = await ImageFileService.readAsBytes(uri); + final Uint8List bytes = await ImageFileService.readAsBytes(uri, mimeType); if (bytes.lengthInBytes == 0) { return null; } @@ -51,5 +54,5 @@ class UriImage extends ImageProvider { int get hashCode => hashValues(uri, scale); @override - String toString() => '${objectRuntimeType(this, 'UriImage')}("$uri", scale: $scale)'; + String toString() => '${objectRuntimeType(this, 'UriImage')}(uri=$uri, mimeType=$mimeType, scale=$scale)'; } diff --git a/lib/widgets/fullscreen/uri_picture_provider.dart b/lib/widgets/fullscreen/uri_picture_provider.dart index 624496a4e..93cfcc516 100644 --- a/lib/widgets/fullscreen/uri_picture_provider.dart +++ b/lib/widgets/fullscreen/uri_picture_provider.dart @@ -5,11 +5,14 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:pedantic/pedantic.dart'; class UriPicture extends PictureProvider { - const UriPicture(this.uri, {this.colorFilter}) : assert(uri != null); + const UriPicture({ + @required this.uri, + @required this.mimeType, + this.colorFilter, + }) : assert(uri != null); - final String uri; + final String uri, mimeType; - /// The [ColorFilter], if any, to use when drawing this picture. final ColorFilter colorFilter; @override @@ -20,14 +23,14 @@ class UriPicture extends PictureProvider { @override PictureStreamCompleter load(UriPicture key, {PictureErrorListener onError}) { return OneFramePictureStreamCompleter(_loadAsync(key, onError: onError), informationCollector: () sync* { - yield DiagnosticsProperty('Uri', uri); + yield DiagnosticsProperty('uri', uri); }); } Future _loadAsync(UriPicture key, {PictureErrorListener onError}) async { assert(key == this); - final data = await ImageFileService.readAsBytes(uri); + final data = await ImageFileService.readAsBytes(uri, mimeType); if (data == null || data.isEmpty) { return null; } @@ -51,5 +54,5 @@ class UriPicture extends PictureProvider { int get hashCode => hashValues(uri, colorFilter); @override - String toString() => '${objectRuntimeType(this, 'UriPicture')}("$uri", colorFilter: $colorFilter)'; + String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType, colorFilter=$colorFilter)'; }