Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAINTROID-457: Add .ora support while Save/Load #77

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 303789365c3a8d7bc562e5e65d7e8e15218ec5c6

COCOAPODS: 1.15.0
COCOAPODS: 1.14.3

This file was deleted.

1 change: 1 addition & 0 deletions lib/core/commands/command_painter.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
3 changes: 2 additions & 1 deletion lib/core/enums/image_format.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
enum ImageFormat {
png('png'),
jpg('jpg'),
catrobatImage('catrobat-image');
catrobatImage('catrobat-image'),
ora('ora');

const ImageFormat(this.extension);

Expand Down
10 changes: 9 additions & 1 deletion lib/core/models/image_from_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@ import 'package:paintroid/core/models/catrobat_image.dart';
class ImageFromFile {
final Image? rasterImage;
final CatrobatImage? catrobatImage;
final List<Image>? 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<Image> layers)
: oraImageLayers = layers,
rasterImage = null,
catrobatImage = null;
}
5 changes: 5 additions & 0 deletions lib/core/models/image_meta_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,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);
}
38 changes: 38 additions & 0 deletions lib/core/models/ora_image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Dart imports:
import 'dart:convert';
import 'dart:typed_data';

// Package imports:
import 'package:archive/archive.dart';
import 'package:image/image.dart' as img;

class OraImage {
final int width;
final int height;
final List<img.Image> 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)!);
}
}
36 changes: 36 additions & 0 deletions lib/core/models/process_ora.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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;

class ProcessOra {
Future<List<ui.Image>> processOraFile(Archive archive) async {
List<ui.Image> layers = [];

for (var file in archive) {
if (file.isFile &&
(file.name.endsWith('.png') || file.name.endsWith('.jpg')) ||
file.name.endsWith('.ora')) {
img.Image? decodedImage = img.decodeImage(file.content as List<int>);

if (decodedImage != null) {
ui.Image layer = await convertImgImageToUiImage(decodedImage);
layers.add(layer);
}
}
}

return layers;
}

Future<ui.Image> convertImgImageToUiImage(img.Image image) async {
List<int> pngBytes = img.encodePng(image);

final codec = await ui.instantiateImageCodec(Uint8List.fromList(pngBytes));
final frame = await codec.getNextFrame();
return frame.image;
}
}
48 changes: 48 additions & 0 deletions lib/core/providers/object/image_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,6 +27,8 @@ abstract class IImageService {

Future<Result<Uint8List, Failure>> exportAsPng(ui.Image image);

Future<Result<Uint8List, Failure>> exportAsOra(ui.Image image);

Result<Uint8List, Failure> getProjectPreview(String? path);

static final provider = Provider<IImageService>((ref) => ImageService());
Expand Down Expand Up @@ -70,6 +74,50 @@ class ImageService with LoggableMixin implements IImageService {
}
}

@override
Future<Result<Uint8List, Failure>> 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<img.Image> 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<img.Image> layers) {
var buffer = StringBuffer();
buffer.writeln('<image>');

for (int i = 0; i < layers.length; i++) {
buffer.writeln(
'<layer name="Layer $i" src="data/layer_$i.png" x="0" y="0" opacity="1.0"/>');
}

buffer.writeln('</image>');
return buffer.toString();
}

@override
Result<Uint8List, Failure> getProjectPreview(String? path) {
try {
Expand Down
61 changes: 61 additions & 0 deletions lib/core/providers/object/io_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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';
Expand Down Expand Up @@ -182,10 +186,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<img.Image> 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<img.Image> layers) {
var buffer = StringBuffer();
buffer.writeln('<image>');

for (int i = 0; i < layers.length; i++) {
buffer.writeln(
'<layer name="Layer $i" src="data/layer_$i.png" x="0" y="0" opacity="1.0"/>');
}

buffer.writeln('</image>');
return buffer.toString();
}

Future<bool> _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],
xmlMetadata: generateXmlMetadataForOra([layer]),
);

final fileName = '${imageData.name}.ora';
final result = await oraImageService.call(oraImage, fileName);

return result.match(
(file) => true,
(error) => false,
);
}

Future<bool> _saveAsRasterImage(ImageMetaData imageData) async {
final image = await ref
.read(RenderImageForExport.provider)
Expand Down
35 changes: 35 additions & 0 deletions lib/core/providers/object/save_as_ora_image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Dart imports:
import 'dart:io';

// Package imports:
import 'package:flutter_riverpod/flutter_riverpod.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;

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<Result<File, Failure>> 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);
}
}
6 changes: 4 additions & 2 deletions lib/core/tools/line_tool/line_tool.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 2 additions & 0 deletions lib/core/tools/line_tool/vertex.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
1 change: 1 addition & 0 deletions lib/core/utils/distance_calculator.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Dart imports:
import 'dart:math';

class DistanceCalculator {
Expand Down
Loading