import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; import 'package:aves/model/entry.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/image_op_events.dart'; import 'package:aves/services/service_policy.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:streams_channel/streams_channel.dart'; class ImageFileService { static const platform = MethodChannel('deckers.thibault/aves/image'); static final StreamsChannel _byteStreamChannel = StreamsChannel('deckers.thibault/aves/imagebytestream'); static final StreamsChannel _opStreamChannel = StreamsChannel('deckers.thibault/aves/imageopstream'); static const double thumbnailDefaultSize = 64.0; static Map _toPlatformEntryMap(AvesEntry entry) { return { 'uri': entry.uri, 'path': entry.path, 'pageId': entry.pageId, 'mimeType': entry.mimeType, 'width': entry.width, 'height': entry.height, 'rotationDegrees': entry.rotationDegrees, 'isFlipped': entry.isFlipped, 'dateModifiedSecs': entry.dateModifiedSecs, }; } static Future getEntry(String uri, String mimeType) async { try { final result = await platform.invokeMethod('getEntry', { 'uri': uri, 'mimeType': mimeType, }) as Map; return AvesEntry.fromMap(result); } on PlatformException catch (e) { debugPrint('getEntry failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return null; } static Future getSvg( String uri, String mimeType, { int expectedContentLength, BytesReceivedCallback onBytesReceived, }) => getImage( uri, mimeType, 0, false, expectedContentLength: expectedContentLength, onBytesReceived: onBytesReceived, ); static Future getImage( String uri, String mimeType, int rotationDegrees, bool isFlipped, { int pageId, int expectedContentLength, BytesReceivedCallback onBytesReceived, }) { try { final completer = Completer.sync(); final sink = _OutputBuffer(); var bytesReceived = 0; _byteStreamChannel.receiveBroadcastStream({ 'uri': uri, 'mimeType': mimeType, 'rotationDegrees': rotationDegrees ?? 0, 'isFlipped': isFlipped ?? false, 'pageId': pageId, }).listen( (data) { final chunk = data as Uint8List; sink.add(chunk); if (onBytesReceived != null) { bytesReceived += chunk.length; try { onBytesReceived(bytesReceived, expectedContentLength); } catch (error, stack) { completer.completeError(error, stack); return; } } }, onError: completer.completeError, onDone: () { sink.close(); completer.complete(sink.bytes); }, cancelOnError: true, ); return completer.future; } on PlatformException catch (e) { debugPrint('getImage failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return Future.sync(() => null); } // `rect`: region to decode, with coordinates in reference to `imageSize` static Future getRegion( String uri, String mimeType, int rotationDegrees, bool isFlipped, int sampleSize, Rectangle regionRect, Size imageSize, { int pageId, Object taskKey, int priority, }) { return servicePolicy.call( () async { try { final result = await platform.invokeMethod('getRegion', { 'uri': uri, 'mimeType': mimeType, 'pageId': pageId, 'sampleSize': sampleSize, 'regionX': regionRect.left, 'regionY': regionRect.top, 'regionWidth': regionRect.width, 'regionHeight': regionRect.height, 'imageWidth': imageSize.width.toInt(), 'imageHeight': imageSize.height.toInt(), }); return result as Uint8List; } on PlatformException catch (e) { debugPrint('getRegion failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return null; }, priority: priority ?? ServiceCallPriority.getRegion, key: taskKey, ); } static Future getThumbnail({ @required String uri, @required String mimeType, @required int rotationDegrees, @required int pageId, @required bool isFlipped, @required int dateModifiedSecs, @required double extent, Object taskKey, int priority, }) { if (mimeType == MimeTypes.svg) { return Future.sync(() => null); } return servicePolicy.call( () async { try { final result = await platform.invokeMethod('getThumbnail', { 'uri': uri, 'mimeType': mimeType, 'dateModifiedSecs': dateModifiedSecs, 'rotationDegrees': rotationDegrees, 'isFlipped': isFlipped, 'widthDip': extent, 'heightDip': extent, 'pageId': pageId, 'defaultSizeDip': thumbnailDefaultSize, }); return result as Uint8List; } on PlatformException catch (e) { debugPrint('getThumbnail failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return null; }, priority: priority ?? (extent == 0 ? ServiceCallPriority.getFastThumbnail : ServiceCallPriority.getSizedThumbnail), key: taskKey, ); } static Future clearSizedThumbnailDiskCache() async { try { return platform.invokeMethod('clearSizedThumbnailDiskCache'); } on PlatformException catch (e) { debugPrint('clearSizedThumbnailDiskCache failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } } static bool cancelRegion(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getRegion]); static bool cancelThumbnail(Object taskKey) => servicePolicy.pause(taskKey, [ServiceCallPriority.getFastThumbnail, ServiceCallPriority.getSizedThumbnail]); static Future resumeLoading(Object taskKey) => servicePolicy.resume(taskKey); static Stream delete(Iterable entries) { try { return _opStreamChannel.receiveBroadcastStream({ 'op': 'delete', 'entries': entries.map(_toPlatformEntryMap).toList(), }).map((event) => ImageOpEvent.fromMap(event)); } on PlatformException catch (e) { debugPrint('delete failed with code=${e.code}, exception=${e.message}, details=${e.details}'); return Stream.error(e); } } static Stream move( Iterable entries, { @required bool copy, @required String destinationAlbum, }) { try { return _opStreamChannel.receiveBroadcastStream({ 'op': 'move', 'entries': entries.map(_toPlatformEntryMap).toList(), 'copy': copy, 'destinationPath': destinationAlbum, }).map((event) => MoveOpEvent.fromMap(event)); } on PlatformException catch (e) { debugPrint('move failed with code=${e.code}, exception=${e.message}, details=${e.details}'); return Stream.error(e); } } static Stream export( Iterable entries, { String mimeType = MimeTypes.jpeg, @required String destinationAlbum, }) { try { return _opStreamChannel.receiveBroadcastStream({ 'op': 'export', 'entries': entries.map(_toPlatformEntryMap).toList(), 'mimeType': mimeType, 'destinationPath': destinationAlbum, }).map((event) => ExportOpEvent.fromMap(event)); } on PlatformException catch (e) { debugPrint('export failed with code=${e.code}, exception=${e.message}, details=${e.details}'); return Stream.error(e); } } static Future rename(AvesEntry entry, String newName) async { try { // return map with: 'contentId' 'path' 'title' 'uri' (all optional) final result = await platform.invokeMethod('rename', { 'entry': _toPlatformEntryMap(entry), 'newName': newName, }) as Map; return result; } on PlatformException catch (e) { debugPrint('rename failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return {}; } static Future rotate(AvesEntry entry, {@required bool clockwise}) async { try { // return map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('rotate', { 'entry': _toPlatformEntryMap(entry), 'clockwise': clockwise, }) as Map; return result; } on PlatformException catch (e) { debugPrint('rotate failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return {}; } static Future flip(AvesEntry entry) async { try { // return map with: 'rotationDegrees' 'isFlipped' final result = await platform.invokeMethod('flip', { 'entry': _toPlatformEntryMap(entry), }) as Map; return result; } on PlatformException catch (e) { debugPrint('flip failed with code=${e.code}, exception=${e.message}, details=${e.details}'); } return {}; } } // cf flutter/foundation `consolidateHttpClientResponseBytes` typedef BytesReceivedCallback = void Function(int cumulative, int total); // cf flutter/foundation `consolidateHttpClientResponseBytes` class _OutputBuffer extends ByteConversionSinkBase { List> _chunks = >[]; int _contentLength = 0; Uint8List _bytes; @override void add(List chunk) { assert(_bytes == null); _chunks.add(chunk); _contentLength += chunk.length; } @override void close() { if (_bytes != null) { // We've already been closed; this is a no-op return; } _bytes = Uint8List(_contentLength); var offset = 0; for (final chunk in _chunks) { _bytes.setRange(offset, offset + chunk.length, chunk); offset += chunk.length; } _chunks = null; } Uint8List get bytes { assert(_bytes != null); return _bytes; } }