Skip to content

Commit

Permalink
PAINTROID - Implement JSON Serialization via Code Generation
Browse files Browse the repository at this point in the history
serialize catrobat image
  • Loading branch information
Lenkomotive committed Nov 12, 2023
1 parent bc991b2 commit 4b8ddb5
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 108 deletions.
5 changes: 3 additions & 2 deletions lib/command/src/command.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> toJson();

factory Command.fromJson(Map<String, dynamic> json) {
throw UnimplementedError(
'Subclasses must implement a factory constructor for deserialization.');
return DrawPathCommand.fromJson(json);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -22,7 +25,7 @@ class DrawPathCommand extends GraphicCommand {
}

@override
List<Object?> get props => [paint, path];
List<Object?> get props => [paint, path, type];

@override
Map<String, dynamic> toJson() => _$DrawPathCommandToJson(this);
Expand Down
28 changes: 25 additions & 3 deletions lib/io/src/entity/catrobat_image.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,13 +15,30 @@ class CatrobatImage {
final int width;
final int height;
final Iterable<Command> 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<String, dynamic> jsonMap = toJson();
String jsonString = json.encode(jsonMap);
return utf8.encode(jsonString) as Uint8List;
}

static CatrobatImage fromBytes(Uint8List bytes) {
String jsonString = utf8.decode(bytes);
Map<String, dynamic> jsonMap = json.decode(jsonString);
return CatrobatImage.fromJson(jsonMap);
}

Map<String, dynamic> toJson() => _$CatrobatImageToJson(this);

factory CatrobatImage.fromJson(Map<String, dynamic> json) =>
_$CatrobatImageFromJson(json);
}
39 changes: 39 additions & 0 deletions lib/io/src/json_serializer/converter/image_converter.dart
Original file line number Diff line number Diff line change
@@ -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<Image?, Map<String, dynamic>?> {
const ImageConverter();

@override
Image? fromJson(Map<String, dynamic>? 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<String, dynamic>? 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
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ class PathActionConverter
case 'CloseAction':
return createCloseAction(json);
default:
// hande error
throw Exception('Unknown PathAction type: ${json['type']}');
return const CloseAction();
}
}

Expand All @@ -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');
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ class PathWithActionHistoryConverter
PathWithActionHistory fromJson(Map<String, dynamic> json) {
var pathWithActionHistory = PathWithActionHistory();
var actionsJson = json['actions'] as List;
pathWithActionHistory.actions.addAll(actionsJson.map((actionJson) =>
const PathActionConverter().fromJson(actionJson as Map<String, dynamic>)));
for (var actionJson in actionsJson) {
var action = const PathActionConverter().fromJson(actionJson as Map<String, dynamic>);

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;
}

Expand Down
156 changes: 78 additions & 78 deletions lib/io/src/serialization/serializer/catrobat_image_serializer.dart
Original file line number Diff line number Diff line change
@@ -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<SerializableCatrobatImage> 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<CatrobatImage> deserializeWithLatestVersion(
SerializableCatrobatImage data) async {
final commands = <Command>[];
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<SerializableCatrobatImage> 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<CatrobatImage> deserializeWithLatestVersion(
// SerializableCatrobatImage data) async {
// final commands = <Command>[];
// 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;
// }
26 changes: 17 additions & 9 deletions lib/io/src/usecase/load_image_from_file_manager.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<Result<ImageFromFile, Failure>> call(
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 4b8ddb5

Please sign in to comment.