exif thumbnail decoding: use raw image descriptor in Flutter on decoded bytes from Android

This commit is contained in:
Thibault Deckers 2025-03-02 19:47:32 +01:00
parent c1a99d9be5
commit 152b942f57
4 changed files with 72 additions and 15 deletions

View file

@ -10,7 +10,6 @@ import com.adobe.internal.xmp.XMPUtils
import com.bumptech.glide.load.resource.bitmap.TransformationUtils import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.metadata.xmp.XmpDirectory import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.Metadata import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.MultiPage import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
@ -23,7 +22,7 @@ import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ImageProvider import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getEncodedBytes import deckers.thibault.aves.utils.BitmapUtils.getDecodedBytes
import deckers.thibault.aves.utils.FileUtils.transferFrom import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
@ -47,7 +46,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) } "getExifThumbnails" -> ioScope.launch { safe(call, result, ::getExifThumbnails) }
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) } "extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
"extractJpegMpfItem" -> ioScope.launch { safe(call, result, ::extractJpegMpfItem) } "extractJpegMpfItem" -> ioScope.launch { safe(call, result, ::extractJpegMpfItem) }
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) } "extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
@ -58,7 +57,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
} }
} }
private suspend fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) { private fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.toUri() val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
@ -75,7 +74,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
exif.thumbnailBitmap?.let { bitmap -> exif.thumbnailBitmap?.let { bitmap ->
TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let { TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let {
it.getEncodedBytes(canHaveAlpha = false, recycle = false)?.let { bytes -> thumbnails.add(bytes) } it.getDecodedBytes(recycle = false)?.let { bytes -> thumbnails.add(bytes) }
} }
} }
} }

View file

@ -0,0 +1,45 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
@immutable
class DescriptorImageProvider extends ImageProvider<DescriptorImageProvider> {
const DescriptorImageProvider(this.descriptor, {this.scale = 1.0});
final ui.ImageDescriptor descriptor;
final double scale;
@override
Future<DescriptorImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<DescriptorImageProvider>(this);
}
@override
ImageStreamCompleter loadImage(DescriptorImageProvider key, ImageDecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode: decode),
scale: key.scale,
debugLabel: 'DescriptorImageProvider(${describeIdentity(key.descriptor)})',
);
}
Future<ui.Codec> _loadAsync(DescriptorImageProvider key, {required ImageDecoderCallback decode}) async {
assert(key == this);
return descriptor.instantiateCodec();
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is DescriptorImageProvider && other.descriptor == descriptor && other.scale == scale;
}
@override
int get hashCode => Object.hash(descriptor.hashCode, scale);
@override
String toString() => '${objectRuntimeType(this, 'DescriptorImageProvider')}(${describeIdentity(descriptor)}, scale: ${scale.toStringAsFixed(1)})';
}

View file

@ -1,10 +1,13 @@
import 'dart:ui' as ui;
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/decoding.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/text.dart'; import 'package:aves/theme/text.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
abstract class EmbeddedDataService { abstract class EmbeddedDataService {
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry); Future<List<ui.ImageDescriptor?>> getExifThumbnails(AvesEntry entry);
Future<Map> extractGoogleDeviceItem(AvesEntry entry, String dataUri); Future<Map> extractGoogleDeviceItem(AvesEntry entry, String dataUri);
@ -23,14 +26,20 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
static const _platform = MethodChannel('deckers.thibault/aves/embedded'); static const _platform = MethodChannel('deckers.thibault/aves/embedded');
@override @override
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async { Future<List<ui.ImageDescriptor?>> getExifThumbnails(AvesEntry entry) async {
try { try {
final result = await _platform.invokeMethod('getExifThumbnails', <String, dynamic>{ final result = await _platform.invokeMethod('getExifThumbnails', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,
'sizeBytes': entry.sizeBytes, 'sizeBytes': entry.sizeBytes,
}); });
if (result != null) return (result as List).cast<Uint8List>(); if (result != null) {
final descriptors = <ui.ImageDescriptor?>[];
await Future.forEach((result as List).cast<Uint8List>(), (bytes) async {
descriptors.add(await InteropDecoding.bytesToCodec(bytes));
});
return descriptors;
}
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:ui' as ui;
import 'package:aves/image_providers/descriptor_provider.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -18,7 +19,7 @@ class MetadataThumbnails extends StatefulWidget {
} }
class _MetadataThumbnailsState extends State<MetadataThumbnails> { class _MetadataThumbnailsState extends State<MetadataThumbnails> {
late Future<List<Uint8List>> _loader; late Future<List<ui.ImageDescriptor?>> _loader;
AvesEntry get entry => widget.entry; AvesEntry get entry => widget.entry;
@ -32,7 +33,7 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<List<Uint8List>>( return FutureBuilder<List<ui.ImageDescriptor?>>(
future: _loader, future: _loader,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data!.isNotEmpty) { if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data!.isNotEmpty) {
@ -40,10 +41,13 @@ class _MetadataThumbnailsState extends State<MetadataThumbnails> {
alignment: AlignmentDirectional.topStart, alignment: AlignmentDirectional.topStart,
padding: const EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4), padding: const EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 4),
child: Wrap( child: Wrap(
children: snapshot.data!.map((bytes) { children: snapshot.data!.map((descriptor) {
return Image.memory( if (descriptor == null) return const SizedBox();
bytes, return Image(
scale: MediaQuery.devicePixelRatioOf(context), image: DescriptorImageProvider(
descriptor,
scale: MediaQuery.devicePixelRatioOf(context),
),
); );
}).toList(), }).toList(),
), ),