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.drew.metadata.xmp.XmpDirectory
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.MultiPage
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.ImageProviderFactory.getProvider
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.LogUtils
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) {
when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
"getExifThumbnails" -> ioScope.launch { safe(call, result, ::getExifThumbnails) }
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
"extractJpegMpfItem" -> ioScope.launch { safe(call, result, ::extractJpegMpfItem) }
"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 uri = call.argument<String>("uri")?.toUri()
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)
exif.thumbnailBitmap?.let { bitmap ->
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/services/common/decoding.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/text.dart';
import 'package:flutter/services.dart';
abstract class EmbeddedDataService {
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry);
Future<List<ui.ImageDescriptor?>> getExifThumbnails(AvesEntry entry);
Future<Map> extractGoogleDeviceItem(AvesEntry entry, String dataUri);
@ -23,14 +26,20 @@ class PlatformEmbeddedDataService implements EmbeddedDataService {
static const _platform = MethodChannel('deckers.thibault/aves/embedded');
@override
Future<List<Uint8List>> getExifThumbnails(AvesEntry entry) async {
Future<List<ui.ImageDescriptor?>> getExifThumbnails(AvesEntry entry) async {
try {
final result = await _platform.invokeMethod('getExifThumbnails', <String, dynamic>{
'mimeType': entry.mimeType,
'uri': entry.uri,
'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) {
await reportService.recordError(e, stack);
}

View file

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