Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"Appbar",
"bbox",
"Bezier",
"Cooldown",
Expand All @@ -25,5 +26,6 @@
"plaintext": false,
"markdown": false,
"scminput": false
}
},
"cSpell.diagnosticLevel": "Hint"
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 12.1.0
- **FEAT**(layers): Add layer export API to capture individual layers as PNG images. Use `Layer.captureAsPng()` for single layers or `Layer.captureAllLayers()` for batch export with shared isolate reuse. The main editor exposes `captureAllLayers()` and `captureAllLayersWithMeta()` convenience methods.
- **FEAT**(layers): Add `ExportedLayer` model containing the source layer, encoded image bytes, and logical size metadata.

## 12.0.13
- **FEAT**(text-editor): Add `composingTextDecoration` to `TextEditorConfigs` to control the text decoration of the IME composing region. Defaults to `TextDecoration.none` to remove the underline shown when `enableSuggestions` is active.

Expand Down
2 changes: 2 additions & 0 deletions example/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ analyzer:
strict-raw-types: true
errors:
close_sinks: ignore
exclude:
- build/**

linter:
rules:
Expand Down
251 changes: 251 additions & 0 deletions example/lib/features/layer/layer_export_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
// Dart imports:
import 'dart:math';

// Flutter imports:
import 'package:flutter/material.dart';

// Package imports:
import 'package:pro_image_editor/pro_image_editor.dart';

// Project imports:
import '/core/constants/example_constants.dart';
import '/core/mixin/example_helper.dart';

/// Demonstrates how to export individual layers as PNG images.
///
/// Each layer has a [RepaintBoundary] with a [GlobalKey] attached, accessible
/// via [Layer.repaintBoundaryKey]. Use [Layer.captureAsPng] to capture the
/// visual content of any mounted layer.
class LayerExportExample extends StatefulWidget {
/// Creates a new [LayerExportExample] widget.
const LayerExportExample({super.key});

@override
State<LayerExportExample> createState() => _LayerExportExampleState();
}

class _LayerExportExampleState extends State<LayerExportExample>
with ExampleHelperState<LayerExportExample> {
@override
void initState() {
super.initState();
preCacheImage(assetPath: kImageEditorExampleAssetPath);
}

Future<void> _exportLayers(
ProImageEditorState editor, {
required bool overlay,
}) async {
final layers = editor.activeLayers;
if (layers.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No layers to export.')),
);
return;
}

final bodySize = editor.sizesManager.bodySize;
final exported = await editor.captureAllLayersWithMeta(
applyTransforms: false,
);

if (!mounted) return;

if (overlay) {
await Navigator.of(context).push(
PageRouteBuilder(
opaque: false,
pageBuilder: (_, __, ___) => _ExportedLayersOverlay(
layers: exported,
editorBodySize: bodySize,
),
),
);
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => _ExportedLayersPreview(
layers: exported,
editorBodySize: bodySize,
),
),
);
}
}

@override
Widget build(BuildContext context) {
if (!isPreCached) return const PrepareImageWidget();

return ProImageEditor.asset(
kImageEditorExampleAssetPath,
key: editorKey,
callbacks: ProImageEditorCallbacks(
onImageEditingStarted: onImageEditingStarted,
onImageEditingComplete: onImageEditingComplete,
onCloseEditor: (editorMode) => onCloseEditor(
editorMode: editorMode,
enablePop: !isDesktopMode(context),
),
mainEditorCallbacks: MainEditorCallbacks(
helperLines: HelperLinesCallbacks(onLineHit: vibrateLineHit),
),
),
configs: ProImageEditorConfigs(
designMode: platformDesignMode,
mainEditor: MainEditorConfigs(
enableCloseButton: !isDesktopMode(context),
widgets: MainEditorWidgets(
appBar: (editor, rebuildStream) => ReactiveAppbar(
stream: rebuildStream,
builder: (_) => AppBar(
automaticallyImplyLeading: false,
foregroundColor:
Theme.of(context).appBarTheme.foregroundColor ??
Colors.white,
backgroundColor: Colors.black,
title: const Text('Layer Export'),
actions: [
PopupMenuButton<bool>(
icon: const Icon(Icons.visibility),
tooltip: 'Export all layers as PNG',
onSelected: (overlay) =>
_exportLayers(editor, overlay: overlay),
itemBuilder: (_) => const [
PopupMenuItem(
value: false,
child: Text('Preview (new page)'),
),
PopupMenuItem(
value: true,
child: Text('Overlay (50% opacity)'),
),
],
),
],
),
),
),
),
imageGeneration: const ImageGenerationConfigs(
processorConfigs: ProcessorConfigs(
processorMode: ProcessorMode.auto,
),
),
),
);
}
}

class _ExportedLayersOverlay extends StatelessWidget {
const _ExportedLayersOverlay({
required this.layers,
required this.editorBodySize,
});
final List<ExportedLayer> layers;
final Size editorBodySize;

@override
Widget build(BuildContext context) {
final halfWidth = editorBodySize.width / 2;
final halfHeight = editorBodySize.height / 2;

return GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Scaffold(
backgroundColor: Colors.transparent,
body: Center(
child: Opacity(
opacity: 0.5,
child: SizedBox(
width: editorBodySize.width,
height: editorBodySize.height,
child: Stack(
clipBehavior: Clip.none,
children: [
for (final item in layers)
Positioned(
left: item.layer.offset.dx + halfWidth,
top: item.layer.offset.dy + halfHeight,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: Transform(
transform: Matrix4.identity()
..rotateX(item.layer.flipY ? pi : 0)
..rotateY(item.layer.flipX ? pi : 0)
..rotateZ(item.layer.rotation),
alignment: Alignment.center,
child: Image.memory(
item.bytes,
width: item.logicalSize.width,
height: item.logicalSize.height,
),
),
),
),
],
),
),
),
),
),
);
}
}

class _ExportedLayersPreview extends StatelessWidget {
const _ExportedLayersPreview({
required this.layers,
required this.editorBodySize,
});
final List<ExportedLayer> layers;
final Size editorBodySize;

@override
Widget build(BuildContext context) {
final halfWidth = editorBodySize.width / 2;
final halfHeight = editorBodySize.height / 2;

return Scaffold(
appBar: AppBar(title: const Text('Exported Layers')),
body: Center(
child: SizedBox(
width: editorBodySize.width,
height: editorBodySize.height,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: Image.asset(
kImageEditorExampleAssetPath,
fit: BoxFit.contain,
),
),
for (final item in layers)
Positioned(
left: item.layer.offset.dx + halfWidth,
top: item.layer.offset.dy + halfHeight,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: Transform(
transform: Matrix4.identity()
..rotateX(item.layer.flipY ? pi : 0)
..rotateY(item.layer.flipX ? pi : 0)
..rotateZ(item.layer.rotation),
alignment: Alignment.center,
child: Image.memory(
item.bytes,
width: item.logicalSize.width,
height: item.logicalSize.height,
),
),
),
),
],
),
),
),
);
}
}
7 changes: 7 additions & 0 deletions example/lib/features/layer/layer_group_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';

import '/features/layer/layer_export_example.dart';
import '/features/layer/layer_grouping_example.dart';
import '/features/layer/layer_select_design_example.dart';
import '/features/layer/selectable_layer_example.dart';
Expand Down Expand Up @@ -42,6 +43,12 @@ class _LayerGroupPageState extends State<LayerGroupPage> {
trailing: const Icon(Icons.chevron_right),
onTap: () => _openExample(const SelectableLayerExample()),
),
ListTile(
leading: const Icon(Icons.image_outlined),
title: const Text('Export Layers as PNG'),
trailing: const Icon(Icons.chevron_right),
onTap: () => _openExample(const LayerExportExample()),
),
],
),
);
Expand Down
25 changes: 18 additions & 7 deletions example/lib/features/video_examples/mixins/video_editor_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -385,11 +385,18 @@ mixin VideoEditorMixin<T extends StatefulWidget> on State<T> {

var exportModel = VideoRenderData(
id: taskId,
video: useSegments ? null : video,
videoSegments: useSegments ? videoSegments : null,
imageBytes: parameters.layers.isNotEmpty ? parameters.image : null,
videoSegments: useSegments
? videoSegments
.map((video) =>
video.copyWith(volume: audioVolumes.originalVolume))
.toList()
: [VideoSegment(video: video, volume: audioVolumes.originalVolume)],
imageLayers: [
if (parameters.layers.isNotEmpty)
ImageLayer(image: EditorLayerImage.memory(parameters.image))
],
blur: parameters.blur,
colorMatrixList: [parameters.colorFiltersCombined],
colorFilters: [ColorFilter(matrix: parameters.colorFiltersCombined)],
startTime: useSegments ? null : parameters.startTime,
endTime: useSegments ? null : parameters.endTime,
transform: parameters.isTransformed
Expand All @@ -406,9 +413,13 @@ mixin VideoEditorMixin<T extends StatefulWidget> on State<T> {
enableAudio: proVideoController?.isAudioEnabled ?? true,
outputFormat: outputFormat,
bitrate: videoMetadata.bitrate,
customAudioPath: customAudioPath,
originalAudioVolume: audioVolumes.originalVolume,
customAudioVolume: audioVolumes.customVolume,
audioTracks: [
if (customAudioPath != null)
VideoAudioTrack(
path: customAudioPath,
volume: audioVolumes.customVolume,
)
],
);

final now = DateTime.now().millisecondsSinceEpoch;
Expand Down
2 changes: 0 additions & 2 deletions example/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import gal
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
import path_provider_foundation
import pro_image_editor
import pro_video_editor
import shared_preferences_foundation
Expand All @@ -39,7 +38,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ProImageEditorPlugin.register(with: registry.registrar(forPlugin: "ProImageEditorPlugin"))
ProVideoEditorPlugin.register(with: registry.registrar(forPlugin: "ProVideoEditorPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading