heif/heic support
This commit is contained in:
parent
39e41ae3d1
commit
3baaaa5877
8 changed files with 55 additions and 19 deletions
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(() {});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue