Skip to content

Commit

Permalink
feat(import-export): add state history minification and optimize memo…
Browse files Browse the repository at this point in the history
…ry usage (#322)

- Added `import_history_5_0_0_minified.dart` for example state history import with minified format
- Implemented key minifier in `key_minifier.dart` to reduce export data size
- Enhanced state history handling to consume less RAM during in-app usage
- Updated affected models, layers, and editors for compatibility with the minified export format
- Reorganized test structure, moving modules into `features` and `shared` directories
- Introduced new tests for minified keys to ensure reliability
  • Loading branch information
hm21 authored Jan 10, 2025
1 parent c77f1b5 commit 9c334f6
Show file tree
Hide file tree
Showing 45 changed files with 2,019 additions and 271 deletions.
457 changes: 457 additions & 0 deletions example/lib/core/constants/history_demo/import_history_5_0_0.dart

Large diffs are not rendered by default.

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions example/lib/features/import_export_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:pro_image_editor/pro_image_editor.dart';

import '/core/constants/import_history_demo_data.dart';
import '/core/constants/history_demo/import_history_5_0_0_minified.dart';
import '/core/mixin/example_helper.dart';

/// The import export example
Expand Down Expand Up @@ -66,6 +66,8 @@ class _ImportExportExampleState extends State<ImportExportExample>
onPressed: () async {
var history = await editor.exportStateHistory(
configs: const ExportEditorConfigs(
historySpan:
ExportHistorySpan.currentAndBackward
// configs...
),
);
Expand Down Expand Up @@ -101,8 +103,8 @@ class _ImportExportExampleState extends State<ImportExportExample>

/// The `widgetLoader` is optional and only required if you
/// add `exportConfigs` with an id to the widget layers.
/// Refer to the [sticker-example] for details on how this
/// works in the sticker editor.
/// Refer to the [sticker-example](https://github.com/hm21/pro_image_editor/blob/stable/example/lib/features/stickers_example.dart)
/// for details on how this works in the sticker editor.
///
/// If you add widget layers directly to the editor,
/// you can pass the parameters as shown below:
Expand Down
8 changes: 8 additions & 0 deletions lib/core/models/editor_configs/text_editor_configs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class TextEditorConfigs {
this.minScale = double.negativeInfinity,
this.maxScale = double.infinity,
this.customTextStyles,
this.defaultTextStyle = const TextStyle(),
this.initialBackgroundColorMode = LayerBackgroundMode.backgroundAndColor,
this.safeArea = const EditorSafeArea(),
this.style = const TextEditorStyle(),
Expand Down Expand Up @@ -94,6 +95,11 @@ class TextEditorConfigs {
/// Allow users to select a different font style
final List<TextStyle>? customTextStyles;

/// The default text style to be used in the text editor.
///
/// This style will be applied to the text if no other style is specified.
final TextStyle defaultTextStyle;

/// The minimum scale factor from the layer.
final double minScale;

Expand Down Expand Up @@ -145,6 +151,7 @@ class TextEditorConfigs {
double? minFontScale,
LayerBackgroundMode? initialBackgroundColorMode,
List<TextStyle>? customTextStyles,
TextStyle? defaultTextStyle,
double? minScale,
double? maxScale,
bool? enableSuggestions,
Expand All @@ -171,6 +178,7 @@ class TextEditorConfigs {
initialBackgroundColorMode:
initialBackgroundColorMode ?? this.initialBackgroundColorMode,
customTextStyles: customTextStyles ?? this.customTextStyles,
defaultTextStyle: defaultTextStyle ?? this.defaultTextStyle,
minScale: minScale ?? this.minScale,
maxScale: maxScale ?? this.maxScale,
enableSuggestions: enableSuggestions ?? this.enableSuggestions,
Expand Down
14 changes: 7 additions & 7 deletions lib/core/models/history/state_history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ class EditorStateHistory {
///
/// All parameters are required.
EditorStateHistory({
required this.blur,
required this.layers,
required this.filters,
required this.tuneAdjustments,
required this.transformConfigs,
this.blur,
this.layers = const [],
this.filters = const [],
this.tuneAdjustments = const [],
this.transformConfigs,
});

/// The blur factor.
final double blur;
final double? blur;

/// The list of layers.
final List<Layer> layers;
Expand All @@ -33,5 +33,5 @@ class EditorStateHistory {
final List<TuneAdjustmentMatrix> tuneAdjustments;

/// The transformation from the crop/ rotate editor.
TransformConfigs transformConfigs;
TransformConfigs? transformConfigs;
}
20 changes: 18 additions & 2 deletions lib/core/models/layers/emoji_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,24 @@ class EmojiLayer extends Layer {

/// Factory constructor for creating an EmojiLayer instance from a Layer
/// and a map.
factory EmojiLayer.fromMap(Layer layer, Map<String, dynamic> map) {
factory EmojiLayer.fromMap(
Layer layer,
Map<String, dynamic> map, {
Function(String key)? keyConverter,
}) {
keyConverter ??= (String key) => key;

/// Constructs and returns an EmojiLayer instance with properties
/// derived from the layer and map.
return EmojiLayer(
id: layer.id,
flipX: layer.flipX,
flipY: layer.flipY,
enableInteraction: layer.enableInteraction,
offset: layer.offset,
rotation: layer.rotation,
scale: layer.scale,
emoji: map['emoji'],
emoji: map[keyConverter('emoji')],
);
}

Expand All @@ -57,6 +64,14 @@ class EmojiLayer extends Layer {
'type': 'emoji',
};
}

@override
Map<String, dynamic> toMapFromReference(Layer layer) {
return {
...super.toMapFromReference(layer),
if ((layer as EmojiLayer).emoji != emoji) 'emoji': emoji,
};
}
}

// TODO: Remove in version 8.0.0
Expand All @@ -81,6 +96,7 @@ class EmojiLayerData extends EmojiLayer {
/// Constructs and returns an EmojiLayerData instance with properties
/// derived from the layer and map.
return EmojiLayerData(
id: layer.id,
flipX: layer.flipX,
flipY: layer.flipY,
enableInteraction: layer.enableInteraction,
Expand Down
78 changes: 69 additions & 9 deletions lib/core/models/layers/layer.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Flutter imports:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:pro_image_editor/shared/services/import_export/utils/key_minifier.dart';

import '/shared/services/import_export/types/widget_loader.dart';
import '../../utils/parser/double_parser.dart';
Expand Down Expand Up @@ -38,6 +39,7 @@ class Layer {
bool? flipX,
bool? flipY,
bool? enableInteraction,
bool? isDeleted,
}) {
key = GlobalKey();
// Initialize properties with provided values or defaults.
Expand All @@ -48,6 +50,7 @@ class Layer {
this.flipX = flipX ?? false;
this.flipY = flipY ?? false;
this.enableInteraction = enableInteraction ?? true;
this.isDeleted = isDeleted ?? false;
}

/// Factory constructor for creating a Layer instance from a map and a list
Expand All @@ -56,31 +59,36 @@ class Layer {
Map<String, dynamic> map, {
List<Uint8List>? widgetRecords,
WidgetLoader? widgetLoader,
String? id,
Function(EditorImage editorImage)? requirePrecache,
EditorKeyMinifier? minifier,
}) {
var keyConverter = minifier?.convertLayerKey ?? (String key) => key;

/// Creates a base Layer instance with default or map-provided properties.
Layer layer = Layer(
flipX: map['flipX'] ?? false,
flipY: map['flipY'] ?? false,
enableInteraction: map['enableInteraction'] ?? true,
id: id,
flipX: map[keyConverter('flipX')] ?? false,
flipY: map[keyConverter('flipY')] ?? false,
enableInteraction: map[keyConverter('enableInteraction')] ?? true,
offset: Offset(safeParseDouble(map['x']), safeParseDouble(map['y'])),
rotation: safeParseDouble(map['rotation']),
scale: safeParseDouble(map['scale'], fallback: 1),
rotation: safeParseDouble(map[keyConverter('rotation')]),
scale: safeParseDouble(map[keyConverter('scale')], fallback: 1),
);

/// Determines the layer type from the map and returns the appropriate
/// LayerData subclass.
switch (map['type']) {
switch (map[keyConverter('type')]) {
case 'text':
// Returns a TextLayer instance when type is 'text'.
return TextLayer.fromMap(layer, map);
return TextLayer.fromMap(layer, map, keyConverter: keyConverter);
case 'emoji':
// Returns an EmojiLayer instance when type is 'emoji'.
return EmojiLayer.fromMap(layer, map);
return EmojiLayer.fromMap(layer, map, keyConverter: keyConverter);
case 'paint':
case 'painting':
// Returns a PaintLayer instance when type is 'paint'.
return PaintLayer.fromMap(layer, map);
return PaintLayer.fromMap(layer, map, minifier: minifier);
case 'sticker':
case 'widget':
// Returns a WidgetLayer instance when type is 'widget' or 'sticker',
Expand All @@ -91,6 +99,7 @@ class Layer {
widgetRecords: widgetRecords ?? [],
widgetLoader: widgetLoader,
requirePrecache: requirePrecache,
keyConverter: keyConverter,
);
default:
// Returns the base Layer instance when type is unrecognized.
Expand All @@ -114,6 +123,9 @@ class Layer {
/// Flag to enable or disable the user interaction with the layer.
late bool enableInteraction;

/// Flag which indicates to the history that the layer is removed.
late bool isDeleted;

/// A unique identifier for the layer.
late String id;

Expand All @@ -130,8 +142,56 @@ class Layer {
'scale': scale,
'flipX': flipX,
'flipY': flipY,
if (isDeleted) 'isDeleted': isDeleted,
'enableInteraction': enableInteraction,
'type': 'default',
};
}

/// Converts the current layer to a map representation, comparing it with a
/// reference layer.
///
/// The resulting map will contain only the properties that differ from the
/// reference layer.
Map<String, dynamic> toMapFromReference(Layer layer) {
return {
'id': layer.id,
if (layer.offset.dx != offset.dx) 'x': offset.dx,
if (layer.offset.dy != offset.dy) 'y': offset.dy,
if (layer.rotation != rotation) 'rotation': rotation,
if (layer.scale != scale) 'scale': scale,
if (layer.flipX != flipX) 'flipX': flipX,
if (layer.flipY != flipY) 'flipY': flipY,
if (isDeleted) 'isDeleted': isDeleted,
if (layer.enableInteraction != enableInteraction)
'enableInteraction': enableInteraction,
};
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;

return other is Layer &&
other.id == id &&
other.offset == offset &&
other.rotation == rotation &&
other.scale == scale &&
other.flipX == flipX &&
other.flipY == flipY &&
other.enableInteraction == enableInteraction &&
other.isDeleted == isDeleted;
}

@override
int get hashCode {
return id.hashCode ^
offset.hashCode ^
rotation.hashCode ^
scale.hashCode ^
flipX.hashCode ^
flipY.hashCode ^
enableInteraction.hashCode ^
isDeleted.hashCode;
}
}
37 changes: 32 additions & 5 deletions lib/core/models/layers/paint_layer.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/widgets.dart';

import '../../../shared/services/import_export/utils/key_minifier.dart';
import '../../utils/parser/double_parser.dart';
import '../paint_editor/painted_model.dart';
import 'layer.dart';
Expand Down Expand Up @@ -41,22 +42,32 @@ class PaintLayer extends Layer {

/// Factory constructor for creating a PaintLayer instance from a
/// Layer and a map.
factory PaintLayer.fromMap(Layer layer, Map<String, dynamic> map) {
factory PaintLayer.fromMap(
Layer layer,
Map<String, dynamic> map, {
EditorKeyMinifier? minifier,
}) {
var keyConverter = minifier?.convertLayerKey ?? (String key) => key;

/// Constructs and returns a PaintLayer instance with properties
/// derived from the layer and map.
return PaintLayer(
id: layer.id,
flipX: layer.flipX,
flipY: layer.flipY,
enableInteraction: layer.enableInteraction,
offset: layer.offset,
rotation: layer.rotation,
scale: layer.scale,
opacity: safeParseDouble(map['opacity'], fallback: 1.0),
opacity: safeParseDouble(map[keyConverter('opacity')], fallback: 1.0),
rawSize: Size(
safeParseDouble(map['rawSize']?['w'], fallback: 0),
safeParseDouble(map['rawSize']?['h'], fallback: 0),
safeParseDouble(map[keyConverter('rawSize')]?['w'], fallback: 0),
safeParseDouble(map[keyConverter('rawSize')]?['h'], fallback: 0),
),
item: PaintedModel.fromMap(
map[keyConverter('item')] ?? {},
keyConverter: minifier?.convertPaintKey,
),
item: PaintedModel.fromMap(map['item'] ?? {}),
);
}

Expand Down Expand Up @@ -85,6 +96,21 @@ class PaintLayer extends Layer {
'type': 'paint',
};
}

@override
Map<String, dynamic> toMapFromReference(Layer layer) {
var paintLayer = layer as PaintLayer;
return {
...super.toMapFromReference(layer),
if (paintLayer.item != item) 'item': item.toMap(),
if (paintLayer.rawSize != rawSize)
'rawSize': {
'w': rawSize.width,
'h': rawSize.height,
},
if (paintLayer.opacity != opacity) 'opacity': opacity,
};
}
}

// TODO: Remove in version 8.0.0
Expand All @@ -109,6 +135,7 @@ class PaintLayerData extends PaintLayer {
/// Layer and a map.
factory PaintLayerData.fromMap(Layer layer, Map<String, dynamic> map) {
return PaintLayerData(
id: layer.id,
flipX: layer.flipX,
flipY: layer.flipY,
enableInteraction: layer.enableInteraction,
Expand Down
Loading

0 comments on commit 9c334f6

Please sign in to comment.