From 68ad23682b337be1b9a9b77a33a95c9850c5caa1 Mon Sep 17 00:00:00 2001 From: vishad-tyagi <78733360+vishad-tyagi@users.noreply.github.com> Date: Mon, 18 Mar 2024 15:46:06 +0530 Subject: [PATCH 1/5] Paintroid-457 : Flutter: Add .ora support while Save/Load --- ios/Podfile.lock | 2 +- packages/io_library/lib/io_library.dart | 3 + .../lib/src/enums/image_format.dart | 3 +- packages/io_library/lib/src/io_handler.dart | 61 +++++++++++++++++++ .../lib/src/models/image_from_file.dart | 10 ++- .../lib/src/models/image_meta_data.dart | 5 ++ .../io_library/lib/src/models/ora_image.dart | 35 +++++++++++ .../lib/src/models/process_ora.dart | 42 +++++++++++++ .../lib/src/ui/image_format_info.dart | 5 ++ .../lib/src/ui/save_image_dialog.dart | 3 + .../usecase/load_image_from_file_manager.dart | 21 +++++-- .../lib/src/usecase/save_as_ora_image.dart | 26 ++++++++ packages/io_library/pubspec.yaml | 1 + pubspec.lock | 4 +- 14 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 packages/io_library/lib/src/models/ora_image.dart create mode 100644 packages/io_library/lib/src/models/process_ora.dart create mode 100644 packages/io_library/lib/src/usecase/save_as_ora_image.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 62cb12ec..b124104e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -140,4 +140,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: a62623f56f2d1d0e85a4a3c73509cd2832d5c86f -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.0 diff --git a/packages/io_library/lib/io_library.dart b/packages/io_library/lib/io_library.dart index beda5731..00960e89 100644 --- a/packages/io_library/lib/io_library.dart +++ b/packages/io_library/lib/io_library.dart @@ -32,3 +32,6 @@ export 'src/usecase/load_image_from_file_manager.dart'; export 'src/usecase/load_image_from_photo_library.dart'; export 'src/usecase/save_as_catrobat_image.dart'; export 'src/usecase/save_as_raster_image.dart'; + +export 'src/models/ora_image.dart'; +export 'src/models/process_ora.dart'; diff --git a/packages/io_library/lib/src/enums/image_format.dart b/packages/io_library/lib/src/enums/image_format.dart index b0f81771..4ae018e8 100644 --- a/packages/io_library/lib/src/enums/image_format.dart +++ b/packages/io_library/lib/src/enums/image_format.dart @@ -1,7 +1,8 @@ enum ImageFormat { png('png'), jpg('jpg'), - catrobatImage('catrobat-image'); + catrobatImage('catrobat-image'), + ora('ora'); const ImageFormat(this.extension); diff --git a/packages/io_library/lib/src/io_handler.dart b/packages/io_library/lib/src/io_handler.dart index 72c0a782..7022f516 100644 --- a/packages/io_library/lib/src/io_handler.dart +++ b/packages/io_library/lib/src/io_handler.dart @@ -7,9 +7,13 @@ import 'package:component_library/component_library.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:io_library/io_library.dart'; +import 'package:io_library/src/usecase/save_as_ora_image.dart'; import 'package:oxidized/oxidized.dart'; import 'package:workspace_screen/workspace_screen.dart'; +import 'dart:ui' as ui; +import 'package:image/image.dart' as img; + class IOHandler { final Ref ref; @@ -152,10 +156,67 @@ class IOHandler { } else if (imageData is CatrobatImageMetaData) { final savedFile = await _saveAsCatrobatImage(imageData, false); isImageSaved = (savedFile != null); + } else if (imageData is OraMetaData) { + isImageSaved = await _saveAsOraImage(imageData); } return isImageSaved; } + Future convertUiImageToImgImage(ui.Image uiImage) async { + final byteData = + await uiImage.toByteData(format: ui.ImageByteFormat.rawRgba); + final buffer = byteData!.buffer.asUint8List(); + + return img.Image.fromBytes( + uiImage.width, + uiImage.height, + buffer, + format: img.Format.rgba, + ); + } + + String generateXmlMetadataForOra(List layers) { + var buffer = StringBuffer(); + buffer.writeln(''); + + for (int i = 0; i < layers.length; i++) { + buffer.writeln( + ''); + } + + buffer.writeln(''); + return buffer.toString(); + } + + Future _saveAsOraImage(OraMetaData imageData) async { + final canvasState = ref.read(canvasStateProvider); + final oraImageService = ref.read(SaveAsOraImage.provider); + + if (canvasState.cachedImage == null) { + return false; + } + + final imgWidth = canvasState.size.width.toInt(); + final imgHeight = canvasState.size.height.toInt(); + + img.Image layer = await convertUiImageToImgImage(canvasState.cachedImage!); + + final oraImage = OraImage( + width: imgWidth, + height: imgHeight, + layers: [layer], // Single layer based on cachedImage + xmlMetadata: generateXmlMetadataForOra([layer]), + ); + + final fileName = '${imageData.name}.ora'; + final result = await oraImageService.call(oraImage, fileName); + + return result.match( + (file) => true, + (error) => false, + ); + } + Future _saveAsRasterImage(ImageMetaData imageData) async { final image = await ref .read(RenderImageForExport.provider) diff --git a/packages/io_library/lib/src/models/image_from_file.dart b/packages/io_library/lib/src/models/image_from_file.dart index 7183b72c..b430599b 100644 --- a/packages/io_library/lib/src/models/image_from_file.dart +++ b/packages/io_library/lib/src/models/image_from_file.dart @@ -5,14 +5,22 @@ import 'package:io_library/io_library.dart'; class ImageFromFile { final Image? rasterImage; final CatrobatImage? catrobatImage; + final List? oraImageLayers; const ImageFromFile.catrobatImage( CatrobatImage image, { Image? backgroundImage, }) : catrobatImage = image, - rasterImage = backgroundImage; + rasterImage = backgroundImage, + oraImageLayers = null; const ImageFromFile.rasterImage(Image image) : rasterImage = image, + catrobatImage = null, + oraImageLayers = null; + + const ImageFromFile.oraImage(List layers) + : oraImageLayers = layers, + rasterImage = null, catrobatImage = null; } diff --git a/packages/io_library/lib/src/models/image_meta_data.dart b/packages/io_library/lib/src/models/image_meta_data.dart index ee9385c3..fa66e77a 100644 --- a/packages/io_library/lib/src/models/image_meta_data.dart +++ b/packages/io_library/lib/src/models/image_meta_data.dart @@ -28,3 +28,8 @@ class CatrobatImageMetaData extends ImageMetaData { const CatrobatImageMetaData(String name) : super(name, ImageFormat.catrobatImage); } + + +class OraMetaData extends ImageMetaData { + const OraMetaData(String name) : super(name, ImageFormat.ora); +} \ No newline at end of file diff --git a/packages/io_library/lib/src/models/ora_image.dart b/packages/io_library/lib/src/models/ora_image.dart new file mode 100644 index 00000000..672cfa37 --- /dev/null +++ b/packages/io_library/lib/src/models/ora_image.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:image/image.dart' as img; +import 'package:archive/archive.dart'; + +class OraImage { + final int width; + final int height; + final List layers; + final String xmlMetadata; + + OraImage({ + required this.width, + required this.height, + required this.layers, + required this.xmlMetadata, + }); + + Uint8List toBytes() { + final archive = Archive(); + + for (int i = 0; i < layers.length; i++) { + final layer = layers[i]; + final encoder = img.PngEncoder(); + final layerData = encoder.encodeImage(layer); + archive.addFile(ArchiveFile('layer_$i.png', layerData.length, layerData)); + } + + final encodedXml = utf8.encode(xmlMetadata); + archive.addFile(ArchiveFile('stack.xml', encodedXml.length, encodedXml)); + + final zipEncoder = ZipEncoder(); + return Uint8List.fromList(zipEncoder.encode(archive)!); + } +} diff --git a/packages/io_library/lib/src/models/process_ora.dart b/packages/io_library/lib/src/models/process_ora.dart new file mode 100644 index 00000000..7ca0de9d --- /dev/null +++ b/packages/io_library/lib/src/models/process_ora.dart @@ -0,0 +1,42 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:archive/archive.dart'; +import 'package:image/image.dart' as img; + +class ProcessOra { + Future> processOraFile(Archive archive) async { + List layers = []; + + for (var file in archive) { + if (file.isFile && (file.name.endsWith('.png') || file.name.endsWith('.jpg')) || file.name.endsWith('.ora')) { + // Decode the image using the image package + print("Processing layer: ${file.name}"); + img.Image? decodedImage = img.decodeImage(file.content as List); + + if (decodedImage != null) { + // Convert the img.Image to ui.Image + print("Decoded layer: ${file.name}"); + ui.Image layer = await convertImgImageToUiImage(decodedImage); + layers.add(layer); + print("Converted layer: ${file.name}"); + } + else { + print("Failed to decode layer: ${file.name}"); + } + } + } + + return layers; + } + + Future convertImgImageToUiImage(img.Image image) async { + // This function converts an img.Image object to a ui.Image object. + // Encode the image to a PNG + List pngBytes = img.encodePng(image); + + // Use a codec to decode the PNG bytes to a ui.Image + final codec = await ui.instantiateImageCodec(Uint8List.fromList(pngBytes)); + final frame = await codec.getNextFrame(); + return frame.image; + } +} diff --git a/packages/io_library/lib/src/ui/image_format_info.dart b/packages/io_library/lib/src/ui/image_format_info.dart index 78fbcd8e..c1f9e587 100644 --- a/packages/io_library/lib/src/ui/image_format_info.dart +++ b/packages/io_library/lib/src/ui/image_format_info.dart @@ -22,6 +22,11 @@ extension on ImageFormat { return const TextSpan( text: 'Pocket Paint\'s native image format. ' 'This format remembers commands and layers.'); + + case ImageFormat.ora: + return const TextSpan( + text: + 'OpenRaster format. Supports layers and various attributes like opacity and visibility for each layer.'); } } } diff --git a/packages/io_library/lib/src/ui/save_image_dialog.dart b/packages/io_library/lib/src/ui/save_image_dialog.dart index 104f4bb5..e3b95f77 100644 --- a/packages/io_library/lib/src/ui/save_image_dialog.dart +++ b/packages/io_library/lib/src/ui/save_image_dialog.dart @@ -49,6 +49,9 @@ class _SaveImageDialogState extends State { case ImageFormat.catrobatImage: data = CatrobatImageMetaData(nameFieldController.text); break; + case ImageFormat.ora: + data = OraMetaData(nameFieldController.text); + break; } Navigator.of(context).pop(data); } diff --git a/packages/io_library/lib/src/usecase/load_image_from_file_manager.dart b/packages/io_library/lib/src/usecase/load_image_from_file_manager.dart index aaa4d2fd..2bf63ea9 100644 --- a/packages/io_library/lib/src/usecase/load_image_from_file_manager.dart +++ b/packages/io_library/lib/src/usecase/load_image_from_file_manager.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'dart:ui'; - +import 'dart:ui' as ui; +import 'package:archive/archive.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:io_library/io_library.dart'; import 'package:oxidized/oxidized.dart'; @@ -52,12 +52,25 @@ class LoadImageFromFileManager with LoggableMixin { case 'catrobat-image': Uint8List bytes = await file.readAsBytes(); CatrobatImage catrobatImage = CatrobatImage.fromBytes(bytes); - Image? backgroundImage = + ui.Image? backgroundImage = await rebuildBackgroundImage(catrobatImage); return Result.ok(ImageFromFile.catrobatImage( catrobatImage, backgroundImage: backgroundImage, )); + + case 'ora': + Uint8List bytes = await file.readAsBytes(); + Archive archive = ZipDecoder().decodeBytes(bytes); + ProcessOra processOra = ProcessOra(); + List layers = await processOra.processOraFile(archive); + + if (layers.isNotEmpty) { + return Result.ok(ImageFromFile.rasterImage(layers.first)); + } else { + return const Result.err(LoadImageFailure.invalidImage); + } + default: return const Result.err(LoadImageFailure.invalidImage); } @@ -71,7 +84,7 @@ class LoadImageFromFileManager with LoggableMixin { }); } - Future rebuildBackgroundImage(CatrobatImage catrobatImage) async { + Future rebuildBackgroundImage(CatrobatImage catrobatImage) async { if (catrobatImage.backgroundImage.isNotEmpty) { final backgroundImageData = base64Decode(catrobatImage.backgroundImage); final result = diff --git a/packages/io_library/lib/src/usecase/save_as_ora_image.dart b/packages/io_library/lib/src/usecase/save_as_ora_image.dart new file mode 100644 index 00000000..4dc9db78 --- /dev/null +++ b/packages/io_library/lib/src/usecase/save_as_ora_image.dart @@ -0,0 +1,26 @@ +import 'dart:io'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:io_library/io_library.dart'; +import 'package:oxidized/oxidized.dart'; + +class SaveAsOraImage { + final IFileService _fileService; + final IPermissionService _permissionService; + + SaveAsOraImage(this._fileService, this._permissionService); + + static final provider = Provider((ref) { + final fileService = ref.watch(IFileService.provider); + final permissionService = ref.watch(IPermissionService.provider); + return SaveAsOraImage(fileService, permissionService); + }); + + Future> call(OraImage image, String fileName) async { + if (!(await _permissionService.requestAccessToSharedFileStorage())) { + return const Result.err(SaveImageFailure.permissionDenied); + } + + final bytes = image.toBytes(); + return _fileService.save(fileName, bytes); + } +} diff --git a/packages/io_library/pubspec.yaml b/packages/io_library/pubspec.yaml index 33e1fe2b..4016bac1 100644 --- a/packages/io_library/pubspec.yaml +++ b/packages/io_library/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: oxidized: ^5.2.0 intl: ^0.18.0 json_annotation: ^4.8.1 + archive: ^3.4.10 # Internal packages: component_library: diff --git a/pubspec.lock b/pubspec.lock index 23ee001e..9dbe75d2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: From 49e85c3e34f1103f0536901ccb4b4c3708aabbf1 Mon Sep 17 00:00:00 2001 From: vishad-tyagi <78733360+vishad-tyagi@users.noreply.github.com> Date: Sat, 23 Mar 2024 00:57:03 +0530 Subject: [PATCH 2/5] Delete comments --- packages/io_library/lib/src/models/process_ora.dart | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/io_library/lib/src/models/process_ora.dart b/packages/io_library/lib/src/models/process_ora.dart index 7ca0de9d..7bb03b72 100644 --- a/packages/io_library/lib/src/models/process_ora.dart +++ b/packages/io_library/lib/src/models/process_ora.dart @@ -9,19 +9,11 @@ class ProcessOra { for (var file in archive) { if (file.isFile && (file.name.endsWith('.png') || file.name.endsWith('.jpg')) || file.name.endsWith('.ora')) { - // Decode the image using the image package - print("Processing layer: ${file.name}"); img.Image? decodedImage = img.decodeImage(file.content as List); if (decodedImage != null) { - // Convert the img.Image to ui.Image - print("Decoded layer: ${file.name}"); ui.Image layer = await convertImgImageToUiImage(decodedImage); layers.add(layer); - print("Converted layer: ${file.name}"); - } - else { - print("Failed to decode layer: ${file.name}"); } } } @@ -30,11 +22,9 @@ class ProcessOra { } Future convertImgImageToUiImage(img.Image image) async { - // This function converts an img.Image object to a ui.Image object. - // Encode the image to a PNG List pngBytes = img.encodePng(image); - // Use a codec to decode the PNG bytes to a ui.Image + final codec = await ui.instantiateImageCodec(Uint8List.fromList(pngBytes)); final frame = await codec.getNextFrame(); return frame.image; From 938ab24af2d58cd88e226036897dc149ea0a1849 Mon Sep 17 00:00:00 2001 From: vishad-tyagi <78733360+vishad-tyagi@users.noreply.github.com> Date: Sun, 14 Apr 2024 23:54:29 +0530 Subject: [PATCH 3/5] Changes 1 --- packages/io_library/lib/io_library.dart | 1 + packages/io_library/lib/src/io_handler.dart | 3 +-- packages/io_library/lib/src/models/process_ora.dart | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/io_library/lib/io_library.dart b/packages/io_library/lib/io_library.dart index 00960e89..aaae0188 100644 --- a/packages/io_library/lib/io_library.dart +++ b/packages/io_library/lib/io_library.dart @@ -32,6 +32,7 @@ export 'src/usecase/load_image_from_file_manager.dart'; export 'src/usecase/load_image_from_photo_library.dart'; export 'src/usecase/save_as_catrobat_image.dart'; export 'src/usecase/save_as_raster_image.dart'; +export 'src/usecase/save_as_ora_image.dart'; export 'src/models/ora_image.dart'; export 'src/models/process_ora.dart'; diff --git a/packages/io_library/lib/src/io_handler.dart b/packages/io_library/lib/src/io_handler.dart index 7022f516..f52a6607 100644 --- a/packages/io_library/lib/src/io_handler.dart +++ b/packages/io_library/lib/src/io_handler.dart @@ -7,7 +7,6 @@ import 'package:component_library/component_library.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:io_library/io_library.dart'; -import 'package:io_library/src/usecase/save_as_ora_image.dart'; import 'package:oxidized/oxidized.dart'; import 'package:workspace_screen/workspace_screen.dart'; @@ -204,7 +203,7 @@ class IOHandler { final oraImage = OraImage( width: imgWidth, height: imgHeight, - layers: [layer], // Single layer based on cachedImage + layers: [layer], xmlMetadata: generateXmlMetadataForOra([layer]), ); diff --git a/packages/io_library/lib/src/models/process_ora.dart b/packages/io_library/lib/src/models/process_ora.dart index 7bb03b72..d941c3b5 100644 --- a/packages/io_library/lib/src/models/process_ora.dart +++ b/packages/io_library/lib/src/models/process_ora.dart @@ -8,7 +8,9 @@ class ProcessOra { List layers = []; for (var file in archive) { - if (file.isFile && (file.name.endsWith('.png') || file.name.endsWith('.jpg')) || file.name.endsWith('.ora')) { + if (file.isFile && + (file.name.endsWith('.png') || file.name.endsWith('.jpg')) || + file.name.endsWith('.ora')) { img.Image? decodedImage = img.decodeImage(file.content as List); if (decodedImage != null) { @@ -24,7 +26,6 @@ class ProcessOra { Future convertImgImageToUiImage(img.Image image) async { List pngBytes = img.encodePng(image); - final codec = await ui.instantiateImageCodec(Uint8List.fromList(pngBytes)); final frame = await codec.getNextFrame(); return frame.image; From 0d10ee11f3dc341793af31e52a4641cbd1be5494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20=C5=A0e=C5=A1ko?= Date: Wed, 5 Jun 2024 18:06:00 +0200 Subject: [PATCH 4/5] Fix conflicts --- lib/core/models/ora_image.dart | 5 ++++- lib/core/models/process_ora.dart | 3 +++ lib/core/providers/object/io_handler.dart | 7 ++++--- lib/core/providers/object/save_as_ora_image.dart | 11 ++++++++++- pubspec.lock | 6 +++--- pubspec.yaml | 2 +- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/core/models/ora_image.dart b/lib/core/models/ora_image.dart index 672cfa37..5c995da6 100644 --- a/lib/core/models/ora_image.dart +++ b/lib/core/models/ora_image.dart @@ -1,7 +1,10 @@ +// Dart imports: import 'dart:convert'; import 'dart:typed_data'; -import 'package:image/image.dart' as img; + +// Package imports: import 'package:archive/archive.dart'; +import 'package:image/image.dart' as img; class OraImage { final int width; diff --git a/lib/core/models/process_ora.dart b/lib/core/models/process_ora.dart index d941c3b5..b7aacb3c 100644 --- a/lib/core/models/process_ora.dart +++ b/lib/core/models/process_ora.dart @@ -1,5 +1,8 @@ +// Dart imports: import 'dart:typed_data'; import 'dart:ui' as ui; + +// Package imports: import 'package:archive/archive.dart'; import 'package:image/image.dart' as img; diff --git a/lib/core/providers/object/io_handler.dart b/lib/core/providers/object/io_handler.dart index a169cd1b..0734bc8e 100644 --- a/lib/core/providers/object/io_handler.dart +++ b/lib/core/providers/object/io_handler.dart @@ -2,12 +2,14 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui' as ui; // Flutter imports: import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image/image.dart' as img; import 'package:oxidized/oxidized.dart'; // Project imports: @@ -16,12 +18,14 @@ import 'package:paintroid/core/enums/image_format.dart'; import 'package:paintroid/core/enums/image_location.dart'; import 'package:paintroid/core/models/catrobat_image.dart'; import 'package:paintroid/core/models/image_meta_data.dart'; +import 'package:paintroid/core/models/ora_image.dart'; import 'package:paintroid/core/providers/object/file_service.dart'; import 'package:paintroid/core/providers/object/image_service.dart'; import 'package:paintroid/core/providers/object/load_image_from_file_manager.dart'; import 'package:paintroid/core/providers/object/load_image_from_photo_library.dart'; import 'package:paintroid/core/providers/object/render_image_for_export.dart'; import 'package:paintroid/core/providers/object/save_as_catrobat_image.dart'; +import 'package:paintroid/core/providers/object/save_as_ora_image.dart'; import 'package:paintroid/core/providers/object/save_as_raster_image.dart'; import 'package:paintroid/core/providers/state/canvas_state_provider.dart'; import 'package:paintroid/core/providers/state/workspace_state_notifier.dart'; @@ -32,9 +36,6 @@ import 'package:paintroid/ui/shared/dialogs/load_image_dialog.dart'; import 'package:paintroid/ui/shared/dialogs/save_image_dialog.dart'; import 'package:paintroid/ui/utils/toast_utils.dart'; -import 'dart:ui' as ui; -import 'package:image/image.dart' as img; - class IOHandler { final Ref ref; diff --git a/lib/core/providers/object/save_as_ora_image.dart b/lib/core/providers/object/save_as_ora_image.dart index 4dc9db78..c6384c9e 100644 --- a/lib/core/providers/object/save_as_ora_image.dart +++ b/lib/core/providers/object/save_as_ora_image.dart @@ -1,8 +1,17 @@ +// Dart imports: import 'dart:io'; + +// Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:io_library/io_library.dart'; import 'package:oxidized/oxidized.dart'; +// Project imports: +import 'package:paintroid/core/models/ora_image.dart'; +import 'package:paintroid/core/providers/object/file_service.dart'; +import 'package:paintroid/core/providers/object/permission_service.dart'; +import 'package:paintroid/core/utils/failure.dart'; +import 'package:paintroid/core/utils/save_image_failure.dart'; + class SaveAsOraImage { final IFileService _fileService; final IPermissionService _permissionService; diff --git a/pubspec.lock b/pubspec.lock index ab13e332..8ed4f499 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -26,13 +26,13 @@ packages: source: hosted version: "0.11.3" archive: - dependency: transitive + dependency: "direct main" description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.6.1" args: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cc0a8055..f5cfba61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: file_picker: ^5.3.1 floor: ^1.2.0 sqflite: ^2.3.0 + archive: ^3.6.1 dev_dependencies: flutter_test: @@ -59,7 +60,6 @@ dev_dependencies: import_sorter: ^4.6.0 json_serializable: ^6.7.1 - flutter: uses-material-design: true From 4e16d7a660739de07b3c84b2a9ea9a76eb7556d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20=C5=A0e=C5=A1ko?= Date: Sat, 15 Jun 2024 23:45:33 +0200 Subject: [PATCH 5/5] Add initial test work for ora --- ios/Podfile.lock | 2 +- .../graphic/draw_path_command.g.dart | 23 --------- lib/core/commands/command_painter.dart | 1 + lib/core/providers/object/image_service.dart | 48 +++++++++++++++++++ lib/core/tools/line_tool/line_tool.dart | 6 ++- lib/core/tools/line_tool/vertex.dart | 2 + lib/core/utils/distance_calculator.dart | 1 + lib/ui/pages/landing_page/landing_page.dart | 4 +- .../drawing_surface/canvas_painter.dart | 2 + .../components/top_bar/top_app_bar.dart | 2 + lib/ui/utils/top_bar_action_data.dart | 2 +- ...d_image_from_photo_library_test.mocks.dart | 18 +++++++ .../provider/save_as_raster_image_test.dart | 26 ++++++++++ .../save_as_raster_image_test.mocks.dart | 18 +++++++ test/utils/canvas_positions.dart | 1 + test/utils/ui_interaction.dart | 3 +- test/utils/widget_finder.dart | 5 ++ .../landing_page/landing_page_test.mocks.dart | 18 +++++++ .../bottom_control_navigation_bar_test.dart | 3 +- .../workspace_page/eraser_tool_test.dart | 3 +- .../widget/workspace_page/hand_tool_test.dart | 3 +- .../widget/workspace_page/line_tool_test.dart | 3 +- 22 files changed, 161 insertions(+), 33 deletions(-) delete mode 100644 lib/core/commands/command_implementation/graphic/draw_path_command.g.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9adb1728..feaf5061 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -135,4 +135,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 303789365c3a8d7bc562e5e65d7e8e15218ec5c6 -COCOAPODS: 1.15.0 +COCOAPODS: 1.14.3 diff --git a/lib/core/commands/command_implementation/graphic/draw_path_command.g.dart b/lib/core/commands/command_implementation/graphic/draw_path_command.g.dart deleted file mode 100644 index 5711dc2f..00000000 --- a/lib/core/commands/command_implementation/graphic/draw_path_command.g.dart +++ /dev/null @@ -1,23 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'path_command.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PathCommand _$PathCommandFromJson(Map json) => PathCommand( - const PathWithActionHistoryConverter() - .fromJson(json['path'] as Map), - const PaintConverter().fromJson(json['paint'] as Map), - type: json['type'] as String? ?? SerializerType.PATH_COMMAND, - version: (json['version'] as num?)?.toInt(), - ); - -Map _$PathCommandToJson(PathCommand instance) => - { - 'paint': const PaintConverter().toJson(instance.paint), - 'type': instance.type, - 'version': instance.version, - 'path': const PathWithActionHistoryConverter().toJson(instance.path), - }; diff --git a/lib/core/commands/command_painter.dart b/lib/core/commands/command_painter.dart index 59c3195d..0b33883c 100644 --- a/lib/core/commands/command_painter.dart +++ b/lib/core/commands/command_painter.dart @@ -1,5 +1,6 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Project imports: import 'package:paintroid/core/commands/command_manager/command_manager.dart'; import 'package:paintroid/core/enums/tool_types.dart'; diff --git a/lib/core/providers/object/image_service.dart b/lib/core/providers/object/image_service.dart index a49a2bdb..b97487f9 100644 --- a/lib/core/providers/object/image_service.dart +++ b/lib/core/providers/object/image_service.dart @@ -8,11 +8,13 @@ import 'package:flutter/painting.dart'; // Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image/image.dart' as img; import 'package:image/image.dart'; import 'package:oxidized/oxidized.dart'; // Project imports: import 'package:paintroid/core/models/loggable_mixin.dart'; +import 'package:paintroid/core/models/ora_image.dart'; import 'package:paintroid/core/utils/failure.dart'; import 'package:paintroid/core/utils/load_image_failure.dart'; import 'package:paintroid/core/utils/save_image_failure.dart'; @@ -25,6 +27,8 @@ abstract class IImageService { Future> exportAsPng(ui.Image image); + Future> exportAsOra(ui.Image image); + Result getProjectPreview(String? path); static final provider = Provider((ref) => ImageService()); @@ -70,6 +74,50 @@ class ImageService with LoggableMixin implements IImageService { } } + @override + Future> exportAsOra(ui.Image image) async { + try { + final img.Image layer = await convertUiImageToImgImage(image); + final oraImage = OraImage( + width: image.width, + height: image.height, + layers: [layer], + xmlMetadata: generateXmlMetadataForOra([layer]), + ); + final bytes = oraImage.toBytes(); + return Result.ok(bytes); + } catch (err, stacktrace) { + logger.severe('Could not export to Ora', err, stacktrace); + return const Result.err(SaveImageFailure.unidentified); + } + } + + Future convertUiImageToImgImage(ui.Image uiImage) async { + final byteData = + await uiImage.toByteData(format: ui.ImageByteFormat.rawRgba); + final buffer = byteData!.buffer.asUint8List(); + + return img.Image.fromBytes( + uiImage.width, + uiImage.height, + buffer, + format: img.Format.rgba, + ); + } + + String generateXmlMetadataForOra(List layers) { + var buffer = StringBuffer(); + buffer.writeln(''); + + for (int i = 0; i < layers.length; i++) { + buffer.writeln( + ''); + } + + buffer.writeln(''); + return buffer.toString(); + } + @override Result getProjectPreview(String? path) { try { diff --git a/lib/core/tools/line_tool/line_tool.dart b/lib/core/tools/line_tool/line_tool.dart index 51eadd58..3422b213 100644 --- a/lib/core/tools/line_tool/line_tool.dart +++ b/lib/core/tools/line_tool/line_tool.dart @@ -1,10 +1,12 @@ // Dart imports: import 'dart:ui'; -// Package imports: -import 'package:equatable/equatable.dart'; // Flutter imports: import 'package:flutter/material.dart'; + +// Package imports: +import 'package:equatable/equatable.dart'; + // Project imports: import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart'; import 'package:paintroid/core/commands/graphic_factory/graphic_factory.dart'; diff --git a/lib/core/tools/line_tool/vertex.dart b/lib/core/tools/line_tool/vertex.dart index 933a8b77..bfda7ed9 100644 --- a/lib/core/tools/line_tool/vertex.dart +++ b/lib/core/tools/line_tool/vertex.dart @@ -1,6 +1,8 @@ // Dart imports: + // Flutter imports: import 'package:flutter/material.dart'; + // Project imports: import 'package:paintroid/core/commands/command_implementation/graphic/line_command.dart'; import 'package:paintroid/core/utils/distance_calculator.dart'; diff --git a/lib/core/utils/distance_calculator.dart b/lib/core/utils/distance_calculator.dart index cf8dfcda..34f71bae 100644 --- a/lib/core/utils/distance_calculator.dart +++ b/lib/core/utils/distance_calculator.dart @@ -1,3 +1,4 @@ +// Dart imports: import 'dart:math'; class DistanceCalculator { diff --git a/lib/ui/pages/landing_page/landing_page.dart b/lib/ui/pages/landing_page/landing_page.dart index aa184cbc..d0816827 100644 --- a/lib/ui/pages/landing_page/landing_page.dart +++ b/lib/ui/pages/landing_page/landing_page.dart @@ -1,8 +1,11 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:oxidized/oxidized.dart'; +import 'package:toast/toast.dart'; + // Project imports: import 'package:paintroid/core/database/project_database.dart'; import 'package:paintroid/core/models/database/project.dart'; @@ -22,7 +25,6 @@ import 'package:paintroid/ui/pages/landing_page/components/project_overflow_menu import 'package:paintroid/ui/shared/icon_svg.dart'; import 'package:paintroid/ui/theme/theme.dart'; import 'package:paintroid/ui/utils/toast_utils.dart'; -import 'package:toast/toast.dart'; class LandingPage extends ConsumerStatefulWidget { final String title; diff --git a/lib/ui/pages/workspace_page/components/drawing_surface/canvas_painter.dart b/lib/ui/pages/workspace_page/components/drawing_surface/canvas_painter.dart index af697446..4faa2e41 100644 --- a/lib/ui/pages/workspace_page/components/drawing_surface/canvas_painter.dart +++ b/lib/ui/pages/workspace_page/components/drawing_surface/canvas_painter.dart @@ -1,7 +1,9 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; + // Project imports: import 'package:paintroid/core/commands/command_manager/command_manager_provider.dart'; import 'package:paintroid/core/commands/command_painter.dart'; diff --git a/lib/ui/pages/workspace_page/components/top_bar/top_app_bar.dart b/lib/ui/pages/workspace_page/components/top_bar/top_app_bar.dart index c1cd3e21..f4a9a2c6 100644 --- a/lib/ui/pages/workspace_page/components/top_bar/top_app_bar.dart +++ b/lib/ui/pages/workspace_page/components/top_bar/top_app_bar.dart @@ -1,7 +1,9 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; + // Project imports: import 'package:paintroid/core/enums/tool_types.dart'; import 'package:paintroid/core/providers/state/checkmark_clicked_state.dart'; diff --git a/lib/ui/utils/top_bar_action_data.dart b/lib/ui/utils/top_bar_action_data.dart index 05360676..62b23d87 100644 --- a/lib/ui/utils/top_bar_action_data.dart +++ b/lib/ui/utils/top_bar_action_data.dart @@ -1,4 +1,4 @@ -// Project imports: +// Flutter imports: import 'package:flutter/material.dart'; class TopBarActionData { diff --git a/test/unit/provider/load_image_from_photo_library_test.mocks.dart b/test/unit/provider/load_image_from_photo_library_test.mocks.dart index 11ab4975..3307a2a5 100644 --- a/test/unit/provider/load_image_from_photo_library_test.mocks.dart +++ b/test/unit/provider/load_image_from_photo_library_test.mocks.dart @@ -115,6 +115,24 @@ class MockIImageService extends _i1.Mock implements _i3.IImageService { )), ) as _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>>); + @override + _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>> exportAsOra( + _i5.Image? image) => + (super.noSuchMethod( + Invocation.method( + #exportAsOra, + [image], + ), + returnValue: _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>>.value( + _FakeResult_0<_i7.Uint8List, _i6.Failure>( + this, + Invocation.method( + #exportAsOra, + [image], + ), + )), + ) as _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>>); + @override _i2.Result<_i7.Uint8List, _i6.Failure> getProjectPreview(String? path) => (super.noSuchMethod( diff --git a/test/unit/provider/save_as_raster_image_test.dart b/test/unit/provider/save_as_raster_image_test.dart index cb7dffac..02ed13b4 100644 --- a/test/unit/provider/save_as_raster_image_test.dart +++ b/test/unit/provider/save_as_raster_image_test.dart @@ -104,6 +104,32 @@ void main() { verifyNoMoreInteractions(mockImageService); verifyNoMoreInteractions(mockPhotoLibraryService); }); + + // test('When format is ora', () async { + // final expectedFilename = '$testName.ora'; + // when(mockImageService.exportAsPng(any)) + // .thenAnswer((_) async => Ok(fakeBytes)); + // when(mockImageService.exportAsJpg(any, any)) + // .thenAnswer((_) async => Ok(fakeBytes)); + // when(mockImageService.exportAsOra(any)) + // .thenAnswer((_) async => Ok(fakeBytes)); + // when(mockPermissionService.requestAccessForSavingToPhotos()) + // .thenAnswer((_) async => true); + // when(mockPhotoLibraryService.save(any, any)) + // .thenAnswer((_) async => const Ok(unit)); + // final testMetaData = OraMetaData(testName); + // final result = await sut(testMetaData, fakeImage); + + // expect(result, const Ok(unit)); + + // verify(mockPermissionService.requestAccessForSavingToPhotos()); + // verify(mockImageService.exportAsOra(fakeImage)); + // verify(mockPhotoLibraryService.save(expectedFilename, fakeBytes)); + + // verifyNoMoreInteractions(mockPermissionService); + // verifyNoMoreInteractions(mockImageService); + // verifyNoMoreInteractions(mockPhotoLibraryService); + // }); }, ); diff --git a/test/unit/provider/save_as_raster_image_test.mocks.dart b/test/unit/provider/save_as_raster_image_test.mocks.dart index 588d1a84..e0ccf037 100644 --- a/test/unit/provider/save_as_raster_image_test.mocks.dart +++ b/test/unit/provider/save_as_raster_image_test.mocks.dart @@ -118,6 +118,24 @@ class MockIImageService extends _i1.Mock implements _i3.IImageService { )), ) as _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>>); + @override + _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>> exportAsOra( + _i5.Image? image) => + (super.noSuchMethod( + Invocation.method( + #exportAsOra, + [image], + ), + returnValue: _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>>.value( + _FakeResult_0<_i7.Uint8List, _i6.Failure>( + this, + Invocation.method( + #exportAsOra, + [image], + ), + )), + ) as _i4.Future<_i2.Result<_i7.Uint8List, _i6.Failure>>); + @override _i2.Result<_i7.Uint8List, _i6.Failure> getProjectPreview(String? path) => (super.noSuchMethod( diff --git a/test/utils/canvas_positions.dart b/test/utils/canvas_positions.dart index 57bad567..d2736417 100644 --- a/test/utils/canvas_positions.dart +++ b/test/utils/canvas_positions.dart @@ -1,3 +1,4 @@ +// Flutter imports: import 'package:flutter/cupertino.dart'; class CanvasPosition { diff --git a/test/utils/ui_interaction.dart b/test/utils/ui_interaction.dart index 7e51b7d1..249d6def 100644 --- a/test/utils/ui_interaction.dart +++ b/test/utils/ui_interaction.dart @@ -1,14 +1,15 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image/image.dart' as img; + // Project imports: import 'package:paintroid/app.dart'; import 'package:paintroid/core/providers/state/canvas_state_provider.dart'; import 'package:paintroid/core/providers/state/tools/toolbox/toolbox_state_provider.dart'; - import 'canvas_positions.dart'; import 'widget_finder.dart'; diff --git a/test/utils/widget_finder.dart b/test/utils/widget_finder.dart index ae2c736c..04b095e2 100644 --- a/test/utils/widget_finder.dart +++ b/test/utils/widget_finder.dart @@ -1,5 +1,10 @@ +// Flutter imports: import 'package:flutter/cupertino.dart'; + +// Package imports: import 'package:flutter_test/flutter_test.dart'; + +// Project imports: import 'package:paintroid/core/utils/widget_identifier.dart'; import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/bottom_nav_bar_items.dart'; import 'package:paintroid/ui/utils/top_bar_action_data.dart'; diff --git a/test/widget/landing_page/landing_page_test.mocks.dart b/test/widget/landing_page/landing_page_test.mocks.dart index 2937a373..94658a88 100644 --- a/test/widget/landing_page/landing_page_test.mocks.dart +++ b/test/widget/landing_page/landing_page_test.mocks.dart @@ -293,6 +293,24 @@ class MockIImageService extends _i1.Mock implements _i9.IImageService { )), ) as _i3.Future<_i5.Result<_i11.Uint8List, _i10.Failure>>); + @override + _i3.Future<_i5.Result<_i11.Uint8List, _i10.Failure>> exportAsOra( + _i6.Image? image) => + (super.noSuchMethod( + Invocation.method( + #exportAsOra, + [image], + ), + returnValue: _i3.Future<_i5.Result<_i11.Uint8List, _i10.Failure>>.value( + _FakeResult_3<_i11.Uint8List, _i10.Failure>( + this, + Invocation.method( + #exportAsOra, + [image], + ), + )), + ) as _i3.Future<_i5.Result<_i11.Uint8List, _i10.Failure>>); + @override _i5.Result<_i11.Uint8List, _i10.Failure> getProjectPreview(String? path) => (super.noSuchMethod( diff --git a/test/widget/workspace_page/bottom_control_navigation_bar_test.dart b/test/widget/workspace_page/bottom_control_navigation_bar_test.dart index d026f8c4..e3c04eb9 100644 --- a/test/widget/workspace_page/bottom_control_navigation_bar_test.dart +++ b/test/widget/workspace_page/bottom_control_navigation_bar_test.dart @@ -1,10 +1,12 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; + // Project imports: import 'package:paintroid/core/localization/app_localizations.dart'; import 'package:paintroid/core/tools/tool_data.dart'; @@ -12,7 +14,6 @@ import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_opt import 'package:paintroid/ui/pages/workspace_page/components/bottom_bar/tool_options/stroke_width_tool_option.dart'; import 'package:paintroid/ui/pages/workspace_page/workspace_page.dart'; import 'package:paintroid/ui/theme/theme.dart'; - import '../../utils/bottom_nav_bar_interactions.dart'; void main() { diff --git a/test/widget/workspace_page/eraser_tool_test.dart b/test/widget/workspace_page/eraser_tool_test.dart index ba6698f4..9468fe68 100644 --- a/test/widget/workspace_page/eraser_tool_test.dart +++ b/test/widget/workspace_page/eraser_tool_test.dart @@ -1,17 +1,18 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; + // Project imports: import 'package:paintroid/core/localization/app_localizations.dart'; import 'package:paintroid/core/providers/object/device_service.dart'; import 'package:paintroid/core/tools/tool_data.dart'; import 'package:paintroid/ui/pages/workspace_page/workspace_page.dart'; import 'package:paintroid/ui/theme/theme.dart'; - import '../../utils/test_utils.dart'; void main() { diff --git a/test/widget/workspace_page/hand_tool_test.dart b/test/widget/workspace_page/hand_tool_test.dart index ab0ec7f7..1d402152 100644 --- a/test/widget/workspace_page/hand_tool_test.dart +++ b/test/widget/workspace_page/hand_tool_test.dart @@ -1,17 +1,18 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; + // Project imports: import 'package:paintroid/core/localization/app_localizations.dart'; import 'package:paintroid/core/providers/object/device_service.dart'; import 'package:paintroid/core/tools/tool_data.dart'; import 'package:paintroid/ui/pages/workspace_page/workspace_page.dart'; import 'package:paintroid/ui/theme/theme.dart'; - import '../../utils/test_utils.dart'; void main() { diff --git a/test/widget/workspace_page/line_tool_test.dart b/test/widget/workspace_page/line_tool_test.dart index 8458cc5b..0abd0f89 100644 --- a/test/widget/workspace_page/line_tool_test.dart +++ b/test/widget/workspace_page/line_tool_test.dart @@ -1,13 +1,14 @@ // Flutter imports: import 'package:flutter/material.dart'; + // Package imports: import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; + // Project imports: import 'package:paintroid/app.dart'; import 'package:paintroid/core/tools/tool_data.dart'; - import '../../utils/test_utils.dart'; void main() {