diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index f6275dac1..c1d21f51a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -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("mimeType") val uri = call.argument("uri")?.toUri() val sizeBytes = call.argument("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) } } } } diff --git a/lib/image_providers/descriptor_provider.dart b/lib/image_providers/descriptor_provider.dart new file mode 100644 index 000000000..75f6d93cf --- /dev/null +++ b/lib/image_providers/descriptor_provider.dart @@ -0,0 +1,45 @@ +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +@immutable +class DescriptorImageProvider extends ImageProvider { + const DescriptorImageProvider(this.descriptor, {this.scale = 1.0}); + + final ui.ImageDescriptor descriptor; + final double scale; + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage(DescriptorImageProvider key, ImageDecoderCallback decode) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode: decode), + scale: key.scale, + debugLabel: 'DescriptorImageProvider(${describeIdentity(key.descriptor)})', + ); + } + + Future _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)})'; +} diff --git a/lib/services/media/embedded_data_service.dart b/lib/services/media/embedded_data_service.dart index bb3a00aa5..fd6816261 100644 --- a/lib/services/media/embedded_data_service.dart +++ b/lib/services/media/embedded_data_service.dart @@ -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> getExifThumbnails(AvesEntry entry); + Future> getExifThumbnails(AvesEntry entry); Future extractGoogleDeviceItem(AvesEntry entry, String dataUri); @@ -23,14 +26,20 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { static const _platform = MethodChannel('deckers.thibault/aves/embedded'); @override - Future> getExifThumbnails(AvesEntry entry) async { + Future> getExifThumbnails(AvesEntry entry) async { try { final result = await _platform.invokeMethod('getExifThumbnails', { 'mimeType': entry.mimeType, 'uri': entry.uri, 'sizeBytes': entry.sizeBytes, }); - if (result != null) return (result as List).cast(); + if (result != null) { + final descriptors = []; + await Future.forEach((result as List).cast(), (bytes) async { + descriptors.add(await InteropDecoding.bytesToCodec(bytes)); + }); + return descriptors; + } } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } diff --git a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart index f4d3f2ba5..5eac1f1b1 100644 --- a/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart +++ b/lib/widgets/viewer/info/metadata/metadata_thumbnail.dart @@ -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 { - late Future> _loader; + late Future> _loader; AvesEntry get entry => widget.entry; @@ -32,7 +33,7 @@ class _MetadataThumbnailsState extends State { @override Widget build(BuildContext context) { - return FutureBuilder>( + return FutureBuilder>( future: _loader, builder: (context, snapshot) { if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done && snapshot.data!.isNotEmpty) { @@ -40,10 +41,13 @@ class _MetadataThumbnailsState extends State { 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, - scale: MediaQuery.devicePixelRatioOf(context), + children: snapshot.data!.map((descriptor) { + if (descriptor == null) return const SizedBox(); + return Image( + image: DescriptorImageProvider( + descriptor, + scale: MediaQuery.devicePixelRatioOf(context), + ), ); }).toList(), ),