heif/heic support

This commit is contained in:
Thibault Deckers 2020-03-24 09:33:40 +09:00
parent 39e41ae3d1
commit 3baaaa5877
8 changed files with 55 additions and 19 deletions

View file

@ -1,7 +1,11 @@
package deckers.thibault.aves.channelhandlers; package deckers.thibault.aves.channelhandlers;
import android.app.Activity; import android.app.Activity;
import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.ImageDecoder;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -15,6 +19,7 @@ import java.util.Map;
import deckers.thibault.aves.model.ImageEntry; import deckers.thibault.aves.model.ImageEntry;
import deckers.thibault.aves.model.provider.ImageProvider; import deckers.thibault.aves.model.provider.ImageProvider;
import deckers.thibault.aves.model.provider.ImageProviderFactory; import deckers.thibault.aves.model.provider.ImageProviderFactory;
import deckers.thibault.aves.utils.MimeTypes;
import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel; 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) { 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; 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) { if (is != null) {
data = getBytes(is); 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) { } catch (IOException ex) {
// ignore // ignore

View file

@ -3,6 +3,8 @@ package deckers.thibault.aves.utils;
public class MimeTypes { public class MimeTypes {
public static final String IMAGE = "image"; public static final String IMAGE = "image";
public static final String GIF = "image/gif"; 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 JPEG = "image/jpeg";
public static final String PNG = "image/png"; public static final String PNG = "image/png";
public static final String SVG = "image/svg+xml"; public static final String SVG = "image/svg+xml";

View file

@ -29,10 +29,11 @@ class ImageFileService {
return null; return null;
} }
static Future<Uint8List> readAsBytes(String uri) async { static Future<Uint8List> readAsBytes(String uri, String mimeType) async {
try { try {
final result = await platform.invokeMethod('readAsBytes', <String, dynamic>{ final result = await platform.invokeMethod('readAsBytes', <String, dynamic>{
'uri': uri, 'uri': uri,
'mimeType': mimeType,
}); });
return result as Uint8List; return result as Uint8List;
} on PlatformException catch (e) { } on PlatformException catch (e) {

View file

@ -85,7 +85,8 @@ class Thumbnail extends StatelessWidget {
height: extent, height: extent,
child: SvgPicture( child: SvgPicture(
UriPicture( UriPicture(
entry.uri, uri: entry.uri,
mimeType: entry.mimeType,
colorFilter: Constants.svgColorFilter, colorFilter: Constants.svgColorFilter,
), ),
width: extent, width: extent,

View file

@ -416,7 +416,10 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
} }
void _onImageChange() async { 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(); if (entry.path != null) await FileImage(File(entry.path)).evict();
// rebuild to refresh the Image inside ImagePage // rebuild to refresh the Image inside ImagePage
setState(() {}); setState(() {});

View file

@ -66,7 +66,8 @@ class ImageView extends StatelessWidget {
return PhotoView.customChild( return PhotoView.customChild(
child: SvgPicture( child: SvgPicture(
UriPicture( UriPicture(
entry.uri, uri: entry.uri,
mimeType: entry.mimeType,
colorFilter: Constants.svgColorFilter, colorFilter: Constants.svgColorFilter,
), ),
placeholderBuilder: placeholderBuilder, placeholderBuilder: placeholderBuilder,
@ -83,7 +84,10 @@ class ImageView extends StatelessWidget {
return PhotoView( return PhotoView(
// key includes size and orientation to refresh when the image is rotated // key includes size and orientation to refresh when the image is rotated
key: ValueKey('${entry.orientationDegrees}_${entry.width}_${entry.height}_${entry.path}'), 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), loadingBuilder: (context, event) => placeholderBuilder(context),
backgroundDecoration: backgroundDecoration, backgroundDecoration: backgroundDecoration,
heroAttributes: heroAttributes, heroAttributes: heroAttributes,

View file

@ -6,11 +6,14 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class UriImage extends ImageProvider<UriImage> { class UriImage extends ImageProvider<UriImage> {
const UriImage(this.uri, {this.scale = 1.0}) const UriImage({
: assert(uri != null), @required this.uri,
@required this.mimeType,
this.scale = 1.0,
}) : assert(uri != null),
assert(scale != null); assert(scale != null);
final String uri; final String uri, mimeType;
final double scale; final double scale;
@ -25,7 +28,7 @@ class UriImage extends ImageProvider<UriImage> {
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: key.scale, scale: key.scale,
informationCollector: () sync* { informationCollector: () sync* {
yield ErrorDescription('Uri: $uri'); yield ErrorDescription('uri=$uri, mimeType=$mimeType');
}, },
); );
} }
@ -33,7 +36,7 @@ class UriImage extends ImageProvider<UriImage> {
Future<ui.Codec> _loadAsync(UriImage key, DecoderCallback decode) async { Future<ui.Codec> _loadAsync(UriImage key, DecoderCallback decode) async {
assert(key == this); assert(key == this);
final Uint8List bytes = await ImageFileService.readAsBytes(uri); final Uint8List bytes = await ImageFileService.readAsBytes(uri, mimeType);
if (bytes.lengthInBytes == 0) { if (bytes.lengthInBytes == 0) {
return null; return null;
} }
@ -51,5 +54,5 @@ class UriImage extends ImageProvider<UriImage> {
int get hashCode => hashValues(uri, scale); int get hashCode => hashValues(uri, scale);
@override @override
String toString() => '${objectRuntimeType(this, 'UriImage')}("$uri", scale: $scale)'; String toString() => '${objectRuntimeType(this, 'UriImage')}(uri=$uri, mimeType=$mimeType, scale=$scale)';
} }

View file

@ -5,11 +5,14 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
class UriPicture extends PictureProvider<UriPicture> { class UriPicture extends PictureProvider<UriPicture> {
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; final ColorFilter colorFilter;
@override @override
@ -20,14 +23,14 @@ class UriPicture extends PictureProvider<UriPicture> {
@override @override
PictureStreamCompleter load(UriPicture key, {PictureErrorListener onError}) { PictureStreamCompleter load(UriPicture key, {PictureErrorListener onError}) {
return OneFramePictureStreamCompleter(_loadAsync(key, onError: onError), informationCollector: () sync* { return OneFramePictureStreamCompleter(_loadAsync(key, onError: onError), informationCollector: () sync* {
yield DiagnosticsProperty<String>('Uri', uri); yield DiagnosticsProperty<String>('uri', uri);
}); });
} }
Future<PictureInfo> _loadAsync(UriPicture key, {PictureErrorListener onError}) async { Future<PictureInfo> _loadAsync(UriPicture key, {PictureErrorListener onError}) async {
assert(key == this); assert(key == this);
final data = await ImageFileService.readAsBytes(uri); final data = await ImageFileService.readAsBytes(uri, mimeType);
if (data == null || data.isEmpty) { if (data == null || data.isEmpty) {
return null; return null;
} }
@ -51,5 +54,5 @@ class UriPicture extends PictureProvider<UriPicture> {
int get hashCode => hashValues(uri, colorFilter); int get hashCode => hashValues(uri, colorFilter);
@override @override
String toString() => '${objectRuntimeType(this, 'UriPicture')}("$uri", colorFilter: $colorFilter)'; String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType, colorFilter=$colorFilter)';
} }