diff --git a/lib/command/src/command.dart b/lib/command/src/command.dart index 72e43668..6a841a13 100644 --- a/lib/command/src/command.dart +++ b/lib/command/src/command.dart @@ -1,12 +1,13 @@ import 'package:equatable/equatable.dart'; +import 'package:paintroid/command/src/implementation/command/graphic/draw_path_command.dart'; + abstract class Command with EquatableMixin { const Command(); Map toJson(); factory Command.fromJson(Map json) { - throw UnimplementedError( - 'Subclasses must implement a factory constructor for deserialization.'); + return DrawPathCommand.fromJson(json); } } diff --git a/lib/command/src/implementation/command/graphic/draw_path_command.dart b/lib/command/src/implementation/command/graphic/draw_path_command.dart index 6daf5aee..da32d768 100644 --- a/lib/command/src/implementation/command/graphic/draw_path_command.dart +++ b/lib/command/src/implementation/command/graphic/draw_path_command.dart @@ -13,6 +13,9 @@ part 'draw_path_command.g.dart'; class DrawPathCommand extends GraphicCommand { const DrawPathCommand(this.path, super.paint); + @JsonKey(ignore: true) + final String type = 'DrawPathCommand'; + @PathWithActionHistoryConverter() final PathWithActionHistory path; @@ -22,7 +25,7 @@ class DrawPathCommand extends GraphicCommand { } @override - List get props => [paint, path]; + List get props => [paint, path, type]; @override Map toJson() => _$DrawPathCommandToJson(this); diff --git a/lib/io/src/entity/catrobat_image.dart b/lib/io/src/entity/catrobat_image.dart index 67e6a703..ec2d71a6 100644 --- a/lib/io/src/entity/catrobat_image.dart +++ b/lib/io/src/entity/catrobat_image.dart @@ -1,7 +1,12 @@ -import 'dart:ui'; +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:paintroid/command/command.dart' show Command; +part 'catrobat_image.g.dart'; + +@JsonSerializable() class CatrobatImage { static const magicValue = 'CATROBAT'; static const latestVersion = 1; @@ -10,13 +15,30 @@ class CatrobatImage { final int width; final int height; final Iterable commands; - final Image? backgroundImage; + final String backgroundImage; const CatrobatImage( this.commands, this.width, this.height, this.backgroundImage, { - this.version = latestVersion, + this.version = 1, }); + + Uint8List toBytes() { + Map jsonMap = toJson(); + String jsonString = json.encode(jsonMap); + return utf8.encode(jsonString) as Uint8List; + } + + static CatrobatImage fromBytes(Uint8List bytes) { + String jsonString = utf8.decode(bytes); + Map jsonMap = json.decode(jsonString); + return CatrobatImage.fromJson(jsonMap); + } + + Map toJson() => _$CatrobatImageToJson(this); + + factory CatrobatImage.fromJson(Map json) => + _$CatrobatImageFromJson(json); } diff --git a/lib/io/src/json_serializer/converter/image_converter.dart b/lib/io/src/json_serializer/converter/image_converter.dart new file mode 100644 index 00000000..ebaf8bf1 --- /dev/null +++ b/lib/io/src/json_serializer/converter/image_converter.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:image/image.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'package:paintroid/io/src/json_serializer/serializer_versions.dart'; + +class ImageConverter implements JsonConverter?> { + const ImageConverter(); + + @override + Image? fromJson(Map? json) { + if (json == null) { + return null; + } + + if (json['bytes'] != null) { + final bytesAsString = json['bytes'] as String; + final bytes = base64Decode(bytesAsString); + return decodeImage(bytes); + } + + return null; + } + + @override + Map? toJson(Image? image) { + if (image == null) { + return null; + } + final bytes = encodePng(image); + final encodedBytes = base64Encode(bytes); + + return { + 'version': SerializerVersions.PATH_ACTION_SERIALIZER_VERSION, + 'bytes': encodedBytes + }; + } +} \ No newline at end of file diff --git a/lib/io/src/json_serializer/converter/path_action_converter.dart b/lib/io/src/json_serializer/converter/path_action_converter.dart index b8107ae1..ac91abad 100644 --- a/lib/io/src/json_serializer/converter/path_action_converter.dart +++ b/lib/io/src/json_serializer/converter/path_action_converter.dart @@ -17,8 +17,7 @@ class PathActionConverter case 'CloseAction': return createCloseAction(json); default: - // hande error - throw Exception('Unknown PathAction type: ${json['type']}'); + return const CloseAction(); } } @@ -38,13 +37,11 @@ class PathActionConverter 'x': action.x, 'y': action.y }; - } else if (action is CloseAction) { + } else { return { 'version': SerializerVersions.PATH_ACTION_SERIALIZER_VERSION, 'type': 'CloseAction' }; - } else { - throw Exception('Unknown PathAction type'); } } diff --git a/lib/io/src/json_serializer/converter/path_with_action_history_converter.dart b/lib/io/src/json_serializer/converter/path_with_action_history_converter.dart index 90a2416f..2d38addc 100644 --- a/lib/io/src/json_serializer/converter/path_with_action_history_converter.dart +++ b/lib/io/src/json_serializer/converter/path_with_action_history_converter.dart @@ -11,9 +11,17 @@ class PathWithActionHistoryConverter PathWithActionHistory fromJson(Map json) { var pathWithActionHistory = PathWithActionHistory(); var actionsJson = json['actions'] as List; - pathWithActionHistory.actions.addAll(actionsJson.map((actionJson) => - const PathActionConverter().fromJson(actionJson as Map))); + for (var actionJson in actionsJson) { + var action = const PathActionConverter().fromJson(actionJson as Map); + if (action is MoveToAction) { + pathWithActionHistory.moveTo(action.x, action.y); + } else if (action is LineToAction) { + pathWithActionHistory.lineTo(action.x, action.y); + } else if (action is CloseAction) { + pathWithActionHistory.close(); + } + } return pathWithActionHistory; } diff --git a/lib/io/src/serialization/serializer/catrobat_image_serializer.dart b/lib/io/src/serialization/serializer/catrobat_image_serializer.dart index bdbed0e0..a67b35d0 100644 --- a/lib/io/src/serialization/serializer/catrobat_image_serializer.dart +++ b/lib/io/src/serialization/serializer/catrobat_image_serializer.dart @@ -1,78 +1,78 @@ -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:flutter_riverpod/flutter_riverpod.dart' show Provider; -import 'package:paintroid/command/command.dart' show Command, DrawPathCommand; -import 'package:paintroid/io/io.dart' show CatrobatImage, IImageService; -import 'package:paintroid/io/serialization.dart'; - -class CatrobatImageSerializer extends ProtoSerializerWithVersioning< - CatrobatImage, SerializableCatrobatImage> { - final DrawPathCommandSerializer _drawPathCommandSerializer; - final IImageService _imageService; - - const CatrobatImageSerializer( - super.version, this._imageService, this._drawPathCommandSerializer); - - static final provider = Provider.family( - (ref, int ver) => CatrobatImageSerializer( - ver, - ref.watch(IImageService.provider), - ref.watch(DrawPathCommandSerializer.provider(ver)), - ), - ); - - @override - Future serializeWithLatestVersion( - CatrobatImage object) async { - Uint8List? backgroundImageData; - if (object.backgroundImage != null) { - final result = await _imageService.exportAsPng(object.backgroundImage!); - backgroundImageData = - result.unwrapOrElse((failure) => throw failure.message); - } - return SerializableCatrobatImage() - ..magicValue = CatrobatImage.magicValue - ..version = CatrobatImage.latestVersion - ..width = object.width - ..height = object.height - ..backgroundImage = - (backgroundImageData != null) ? backgroundImageData : Uint8List(0) - ..commands.addAll(await Future.wait(object.commands.map((command) async { - if (command is DrawPathCommand) { - return Any.pack( - await _drawPathCommandSerializer - .serializeWithLatestVersion(command), - typeUrlPrefix: ProtoSerializerWithVersioning.urlPrefix, - ); - } else { - throw 'Invalid command type'; - } - }))); - } - - @override - Future deserializeWithLatestVersion( - SerializableCatrobatImage data) async { - final commands = []; - for (final cmd in data.commands) { - if (cmd.canUnpackInto(SerializableDrawPathCommand.getDefault())) { - final unpacked = cmd.unpackInto(SerializableDrawPathCommand()); - commands.add(await _drawPathCommandSerializer.deserialize(unpacked)); - } else { - throw 'Invalid command type'; - } - } - Image? image; - if (data.hasBackgroundImage()) { - final result = - await _imageService.import(Uint8List.fromList(data.backgroundImage)); - image = result.unwrapOrElse((failure) => throw failure.message); - } - return CatrobatImage(commands, data.width, data.height, image, - version: data.version); - } - - @override - final fromBytesToSerializable = SerializableCatrobatImage.fromBuffer; -} +// import 'dart:typed_data'; +// import 'dart:ui'; +// +// import 'package:flutter_riverpod/flutter_riverpod.dart' show Provider; +// import 'package:paintroid/command/command.dart' show Command, DrawPathCommand; +// import 'package:paintroid/io/io.dart' show CatrobatImage, IImageService; +// import 'package:paintroid/io/serialization.dart'; +// +// class CatrobatImageSerializer extends ProtoSerializerWithVersioning< +// CatrobatImage, SerializableCatrobatImage> { +// final DrawPathCommandSerializer _drawPathCommandSerializer; +// final IImageService _imageService; +// +// const CatrobatImageSerializer( +// super.version, this._imageService, this._drawPathCommandSerializer); +// +// static final provider = Provider.family( +// (ref, int ver) => CatrobatImageSerializer( +// ver, +// ref.watch(IImageService.provider), +// ref.watch(DrawPathCommandSerializer.provider(ver)), +// ), +// ); +// +// @override +// Future serializeWithLatestVersion( +// CatrobatImage object) async { +// Uint8List? backgroundImageData; +// if (object.backgroundImage != null) { +// final result = await _imageService.exportAsPng(object.backgroundImage!); +// backgroundImageData = +// result.unwrapOrElse((failure) => throw failure.message); +// } +// return SerializableCatrobatImage() +// ..magicValue = CatrobatImage.magicValue +// ..version = CatrobatImage.latestVersion +// ..width = object.width +// ..height = object.height +// ..backgroundImage = +// (backgroundImageData != null) ? backgroundImageData : Uint8List(0) +// ..commands.addAll(await Future.wait(object.commands.map((command) async { +// if (command is DrawPathCommand) { +// return Any.pack( +// await _drawPathCommandSerializer +// .serializeWithLatestVersion(command), +// typeUrlPrefix: ProtoSerializerWithVersioning.urlPrefix, +// ); +// } else { +// throw 'Invalid command type'; +// } +// }))); +// } +// +// @override +// Future deserializeWithLatestVersion( +// SerializableCatrobatImage data) async { +// final commands = []; +// for (final cmd in data.commands) { +// if (cmd.canUnpackInto(SerializableDrawPathCommand.getDefault())) { +// final unpacked = cmd.unpackInto(SerializableDrawPathCommand()); +// commands.add(await _drawPathCommandSerializer.deserialize(unpacked)); +// } else { +// throw 'Invalid command type'; +// } +// } +// Image? image; +// if (data.hasBackgroundImage()) { +// final result = +// await _imageService.import(Uint8List.fromList(data.backgroundImage)); +// image = result.unwrapOrElse((failure) => throw failure.message); +// } +// return CatrobatImage(commands, data.width, data.height, image, +// version: data.version); +// } +// +// @override +// final fromBytesToSerializable = SerializableCatrobatImage.fromBuffer; +// } diff --git a/lib/io/src/usecase/load_image_from_file_manager.dart b/lib/io/src/usecase/load_image_from_file_manager.dart index 328489c2..d82dd5e7 100644 --- a/lib/io/src/usecase/load_image_from_file_manager.dart +++ b/lib/io/src/usecase/load_image_from_file_manager.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:oxidized/oxidized.dart'; @@ -18,19 +21,16 @@ class LoadImageFromFileManager with LoggableMixin { final IFileService fileService; final IImageService imageService; final IPermissionService permissionService; - final CatrobatImageSerializer catrobatImageSerializer; LoadImageFromFileManager(this.fileService, this.imageService, - this.permissionService, this.catrobatImageSerializer); + this.permissionService); static final provider = Provider((ref) { final imageService = ref.watch(IImageService.provider); final fileService = ref.watch(IFileService.provider); final permissionService = ref.watch(IPermissionService.provider); - const ver = CatrobatImage.latestVersion; - final serializer = ref.watch(CatrobatImageSerializer.provider(ver)); return LoadImageFromFileManager( - fileService, imageService, permissionService, serializer); + fileService, imageService, permissionService); }); Future> call( @@ -52,11 +52,19 @@ class LoadImageFromFileManager with LoggableMixin { .import(await file.readAsBytes()) .map((img) => ImageFromFile.rasterImage(img)); case 'catrobat-image': - final image = await catrobatImageSerializer - .fromBytes(await file.readAsBytes()); + // final image = await catrobatImageSerializer + // .fromBytes(await file.readAsBytes()); + Uint8List bytes = await file.readAsBytes(); + CatrobatImage catrobatImage = CatrobatImage.fromBytes(bytes); + Image? backgroundImage; + if (catrobatImage.backgroundImage.isNotEmpty) { + Uint8List? backgroundImageData = base64Decode(catrobatImage.backgroundImage); + final result = await imageService.import(Uint8List.fromList(backgroundImageData)); + backgroundImage = result.unwrapOrElse((failure) => throw failure.message); + } return Result.ok(ImageFromFile.catrobatImage( - image, - backgroundImage: image.backgroundImage, + catrobatImage, + backgroundImage: backgroundImage, )); default: return const Result.err(LoadImageFailure.invalidImage); diff --git a/lib/io/src/usecase/save_as_catrobat_image.dart b/lib/io/src/usecase/save_as_catrobat_image.dart index 376ad31a..d95e8ba7 100644 --- a/lib/io/src/usecase/save_as_catrobat_image.dart +++ b/lib/io/src/usecase/save_as_catrobat_image.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:oxidized/oxidized.dart'; @@ -8,7 +9,6 @@ import 'package:paintroid/io/io.dart' show CatrobatImage, CatrobatImageMetaData, - CatrobatImageSerializer, IFileService, SaveImageFailure; import 'package:paintroid/io/src/service/permission_service.dart'; @@ -16,17 +16,14 @@ import 'package:paintroid/io/src/service/permission_service.dart'; class SaveAsCatrobatImage with LoggableMixin { final IFileService _fileService; final IPermissionService permissionService; - final CatrobatImageSerializer _catrobatImageSerializer; SaveAsCatrobatImage( - this._fileService, this.permissionService, this._catrobatImageSerializer); + this._fileService, this.permissionService); static final provider = Provider((ref) { final fileService = ref.watch(IFileService.provider); final permissionService = ref.watch(IPermissionService.provider); - const ver = CatrobatImage.latestVersion; - final serializer = ref.watch(CatrobatImageSerializer.provider(ver)); - return SaveAsCatrobatImage(fileService, permissionService, serializer); + return SaveAsCatrobatImage(fileService, permissionService); }); Future> call( @@ -36,7 +33,7 @@ class SaveAsCatrobatImage with LoggableMixin { } final nameWithExt = '${data.name}.${data.format.extension}'; try { - final bytes = await _catrobatImageSerializer.toBytes(image); + Uint8List bytes = image.toBytes(); if (isAProject) { return _fileService.saveToApplicationDirectory(nameWithExt, bytes); } diff --git a/lib/ui/io_handler.dart b/lib/ui/io_handler.dart index 4b7ef0e8..353c374c 100644 --- a/lib/ui/io_handler.dart +++ b/lib/ui/io_handler.dart @@ -1,4 +1,6 @@ +import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -203,8 +205,16 @@ class IOHandler { final canvasState = ref.read(canvasStateProvider); final imgWidth = canvasState.size.width.toInt(); final imgHeight = canvasState.size.height.toInt(); + Uint8List? backgroundImageData; + if (canvasState.backgroundImage != null) { + final result = await ref.read(IImageService.provider).exportAsPng(canvasState.backgroundImage!); + backgroundImageData = + result.unwrapOrElse((failure) => throw failure.message); + } + + final String backgroundImageAsString = backgroundImageData != null? base64Encode(backgroundImageData) : ''; final catrobatImage = CatrobatImage( - commands, imgWidth, imgHeight, canvasState.backgroundImage); + commands, imgWidth, imgHeight, backgroundImageAsString); final saveAsCatrobatImage = ref.read(SaveAsCatrobatImage.provider); final result = await saveAsCatrobatImage(imageData, catrobatImage, isAProject);