-
After editing, I manually replaced the text in layer['history'], but the screenshot still shows the layer as it was before the replacement. Uint8List? imgBytes = await _editorKey.currentState?.captureEditorImage(); I would like to captureEditorImage of the layers after I have made replacements. Here is the example code: Since our project requires replacing these #instructions and printing them out, I need this feature. If it can be implemented, I would be very grateful! void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'SUPABASE_URL',
anonKey: 'SUPABASE_ANON_KEY',
debug: false,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Pro-Image-Editor',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue.shade800),
useMaterial3: true,
),
debugShowCheckedModeBanner: false,
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: GestureDetector(
onTap: () async {
const Size backgroundCanvasSize = Size(2480.0, 3508.0);
const Color backgroundCanvasColor = Colors.white;
double width = backgroundCanvasSize.width;
double height = backgroundCanvasSize.height;
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder, Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()));
final paint = Paint()..color = backgroundCanvasColor;
canvas.drawRect(Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()), paint);
final picture = recorder.endRecording();
final img = await picture.toImage(width.toInt(), height.toInt());
final pngBytes = await img.toByteData(format: ui.ImageByteFormat.png);
final transparentBytes = pngBytes!.buffer.asUint8List();
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MoveableBackgroundImageExample(transparentBytes),
),
);
},
child: const Text('go to editor'),
),
)),
);
}
} // Dart imports:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
// Flutter imports:
import 'package:example/pages/pick_image_example.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
// Package imports:
import 'package:pro_image_editor/pro_image_editor.dart';
class MoveableBackgroundImageExample extends StatefulWidget {
final Uint8List transparentBytes;
const MoveableBackgroundImageExample(this.transparentBytes, {super.key});
@override
State<MoveableBackgroundImageExample> createState() => _MoveableBackgroundImageExampleState();
}
class _MoveableBackgroundImageExampleState extends State<MoveableBackgroundImageExample> {
final _editorKey = GlobalKey<ProImageEditorState>();
late final StreamController<bool> _openEditorStreamCtrl;
late final StreamController _updateUIStreamCtrl;
late final ScrollController _bottomBarScrollCtrl;
/// Better sense of scale when we start with a large number
final double _initScale = 20;
final _bottomTextStyle = const TextStyle(fontSize: 10.0, color: Colors.white);
final Size _backgroundCanvasSize = const Size(1000, 1000);
Uint8List? _currentImageBytes;
@override
void initState() {
_openEditorStreamCtrl = StreamController.broadcast();
_updateUIStreamCtrl = StreamController.broadcast();
_bottomBarScrollCtrl = ScrollController();
super.initState();
}
@override
void dispose() {
_openEditorStreamCtrl.close();
_updateUIStreamCtrl.close();
_bottomBarScrollCtrl.dispose();
super.dispose();
}
void _openPicker(ImageSource source) async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: source);
if (image == null) return;
Uint8List? bytes;
bytes = await image.readAsBytes();
if (!mounted) return;
await precacheImage(MemoryImage(bytes), context);
var decodedImage = await decodeImageFromList(bytes);
if (!mounted) return;
if (kIsWeb || (!Platform.isWindows && !Platform.isLinux && !Platform.isMacOS)) {
Navigator.pop(context);
}
_editorKey.currentState?.addLayer(
StickerLayerData(
offset: Offset.zero,
scale: _initScale * 0.5,
sticker: Image.memory(
bytes,
width: decodedImage.width.toDouble(),
height: decodedImage.height.toDouble(),
),
),
);
setState(() {});
}
void _chooseCameraOrGallery() async {
/// Open directly the gallery if the camera is not supported
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
_openPicker(ImageSource.gallery);
return;
}
if (!kIsWeb && Platform.isIOS) {
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) => CupertinoTheme(
data: const CupertinoThemeData(),
child: CupertinoActionSheet(
actions: <CupertinoActionSheetAction>[
CupertinoActionSheetAction(
onPressed: () => _openPicker(ImageSource.camera),
child: const Wrap(
spacing: 7,
runAlignment: WrapAlignment.center,
children: [
Icon(CupertinoIcons.photo_camera),
Text('Camera'),
],
),
),
CupertinoActionSheetAction(
onPressed: () => _openPicker(ImageSource.gallery),
child: const Wrap(
spacing: 7,
runAlignment: WrapAlignment.center,
children: [
Icon(CupertinoIcons.photo),
Text('Gallery'),
],
),
),
],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () {
Navigator.pop(context);
},
child: const Text('Cancel'),
),
),
),
);
} else {
showModalBottomSheet(
context: context,
showDragHandle: true,
constraints: BoxConstraints(
minWidth: min(MediaQuery.of(context).size.width, 360),
),
builder: (context) {
return Material(
color: Colors.transparent,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 24, left: 16, right: 16),
child: Wrap(
spacing: 45,
runSpacing: 30,
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
alignment: WrapAlignment.spaceAround,
children: [
MaterialIconActionButton(
primaryColor: const Color(0xFFEC407A),
secondaryColor: const Color(0xFFD3396D),
icon: Icons.photo_camera,
text: 'Camera',
onTap: () => _openPicker(ImageSource.camera),
),
MaterialIconActionButton(
primaryColor: const Color(0xFFBF59CF),
secondaryColor: const Color(0xFFAC44CF),
icon: Icons.image,
text: 'Gallery',
onTap: () => _openPicker(ImageSource.gallery),
),
],
),
),
),
);
},
);
}
}
Size get _editorSize => Size(
MediaQuery.of(context).size.width - MediaQuery.of(context).padding.horizontal,
MediaQuery.of(context).size.height -
kToolbarHeight -
kBottomNavigationBarHeight -
MediaQuery.of(context).padding.vertical,
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: _buildEditor(),
),
);
}
Widget _buildEditor() {
return Stack(
children: [
LayoutBuilder(builder: (context, constraints) {
return ProImageEditor.memory(
widget.transparentBytes,
key: _editorKey,
callbacks: ProImageEditorCallbacks(
onImageEditingComplete: (bytes) async {
print('Done ${bytes.length}');
},
mainEditorCallbacks: MainEditorCallbacks(
onOpenSubEditor: (editor) => _openEditorStreamCtrl.add(true),
onCloseSubEditor: (editor) => _openEditorStreamCtrl.add(false),
onUpdateUI: () {
_updateUIStreamCtrl.add(null);
},
),
),
configs: ProImageEditorConfigs(
designMode: platformDesignMode,
imageGenerationConfigs: ImageGeneratioConfigs(
captureOnlyDrawingBounds: true,
/// Set the pixel ratio manually. You can also set this value higher than the device pixel ratio for higher quality.
customPixelRatio: max(
_editorSize.width / MediaQuery.of(context).size.width,
_editorSize.height / MediaQuery.of(context).size.height,
),
),
/// Crop-Rotate, Filter and Blur editors are not supported
cropRotateEditorConfigs: const CropRotateEditorConfigs(enabled: false),
filterEditorConfigs: const FilterEditorConfigs(enabled: false),
blurEditorConfigs: const BlurEditorConfigs(enabled: false),
customWidgets: ImageEditorCustomWidgets(
bottomNavigationBar: _bottomNavigationBar(constraints),
),
imageEditorTheme: const ImageEditorTheme(
uiOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.black,
),
background: Colors.transparent,
paintingEditor: PaintingEditorTheme(background: Colors.transparent),
/// Optionally remove background
/// cropRotateEditor: CropRotateEditorTheme(background: Colors.transparent),
/// filterEditor: FilterEditorTheme(background: Colors.transparent),
/// blurEditor: BlurEditorTheme(background: Colors.transparent),
),
stickerEditorConfigs: StickerEditorConfigs(
enabled: false,
initWidth: (_editorSize.aspectRatio > _backgroundCanvasSize.aspectRatio
? _editorSize.height
: _editorSize.width) /
_initScale,
buildStickers: (setLayer, scrollController) {
// Optionally your code to pick layers
return const SizedBox();
},
)),
);
}),
_buildImportButton(),
_buildConvertButton(),
_buildCaptureButton(),
if (_currentImageBytes != null) _buildPreview(),
],
);
}
_buildImportButton() {
return Positioned(
bottom: 3 * kBottomNavigationBarHeight,
left: -8,
child: Container(
decoration: const BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.only(
topRight: Radius.circular(100),
bottomRight: Radius.circular(100),
),
),
child: IconButton(
onPressed: () async {
String jsonStr =
'{"version":"2.0.0","position":0,"history":[{"layers":[{"x":-57.420249938964844,"y":-70.95330810546875,"rotation":0.0,"scale":1.0,"flipX":false,"flipY":false,"type":"text","text":"#1","colorMode":"backgroundAndColor","color":4278190080,"background":4294967295,"colorPickerPosition":0.0,"align":"center","fontScale":1.0,"fontFamily":null},{"x":-62.41331481933594,"y":-111.92637634277344,"rotation":0.0,"scale":1.0,"flipX":false,"flipY":false,"type":"text","text":"hello","colorMode":"backgroundAndColor","color":4278190080,"background":4294967295,"colorPickerPosition":0.0,"align":"center","fontScale":1.0,"fontFamily":null}],"blur":0.0}],"imgSize":{"width":360.0,"height":360.0}}';
_editorKey.currentState!.importStateHistory(ImportStateHistory.fromJson(jsonStr));
},
icon: const Icon(
Icons.import_export_outlined,
color: Colors.white,
),
),
),
);
}
_buildConvertButton() {
return Positioned(
bottom: 2 * kBottomNavigationBarHeight,
left: -8,
child: Container(
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.only(
topRight: Radius.circular(100),
bottomRight: Radius.circular(100),
),
),
child: IconButton(
onPressed: () async {
var history = await _editorKey.currentState!.exportStateHistory(
configs: const ExportEditorConfigs(historySpan: ExportHistorySpan.current),
);
String? jsonStr = await history.toJson();
Map<String, dynamic> data = jsonDecode(jsonStr);
// replace
_replaceTextInJson(data);
String updatedJsonData = jsonEncode(data);
_editorKey.currentState!.importStateHistory(ImportStateHistory.fromJson(updatedJsonData));
},
icon: const Icon(
Icons.change_circle_outlined,
color: Colors.white,
),
),
),
);
}
_buildCaptureButton() {
return Positioned(
bottom: 1 * kBottomNavigationBarHeight,
left: -8,
child: Container(
decoration: const BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.only(
topRight: Radius.circular(100),
bottomRight: Radius.circular(100),
),
),
child: IconButton(
onPressed: () async {
Uint8List? imgBytes = await _editorKey.currentState?.captureEditorImage();
if (imgBytes == null) {
return;
}
setState(() {
_currentImageBytes = imgBytes;
});
},
icon: const Icon(
Icons.camera,
color: Colors.white,
),
),
),
);
}
_buildPreview() {
return Positioned(
bottom: 80,
right: 0,
width: 100,
child: Container(
color: Colors.yellow,
child: Image.memory(_currentImageBytes!),
),
);
}
Widget _bottomNavigationBar(BoxConstraints constraints) {
return StreamBuilder(
stream: _updateUIStreamCtrl.stream,
builder: (_, __) {
return Scrollbar(
controller: _bottomBarScrollCtrl,
scrollbarOrientation: ScrollbarOrientation.top,
thickness: isDesktop ? null : 0,
child: BottomAppBar(
/// kBottomNavigationBarHeight is important that helperlines will work
height: kBottomNavigationBarHeight,
color: Colors.black,
padding: EdgeInsets.zero,
child: Center(
child: SingleChildScrollView(
controller: _bottomBarScrollCtrl,
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: min(constraints.maxWidth, 500),
maxWidth: 500,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatIconTextButton(
label: Text('Add Image', style: _bottomTextStyle),
icon: const Icon(
Icons.image_outlined,
size: 22.0,
color: Colors.white,
),
onPressed: _chooseCameraOrGallery,
),
FlatIconTextButton(
label: Text('Paint', style: _bottomTextStyle),
icon: const Icon(
Icons.edit_rounded,
size: 22.0,
color: Colors.white,
),
onPressed: _editorKey.currentState?.openPaintingEditor,
),
FlatIconTextButton(
label: Text('Text', style: _bottomTextStyle),
icon: const Icon(
Icons.text_fields,
size: 22.0,
color: Colors.white,
),
onPressed: _editorKey.currentState?.openTextEditor,
),
FlatIconTextButton(
label: Text('Emoji', style: _bottomTextStyle),
icon: const Icon(
Icons.sentiment_satisfied_alt_rounded,
size: 22.0,
color: Colors.white,
),
onPressed: _editorKey.currentState?.openEmojiEditor,
),
],
),
),
),
),
),
),
);
},
);
}
void _replaceTextInJson(Map<String, dynamic> data) {
Map replacements = {'#1': 'world'};
for (var historyItem in data['history']) {
for (var layer in historyItem['layers']) {
var layers = historyItem['layers'];
if (layers == null) break;
if (layer['type'] == 'text') {
String text = layer['text'];
for (var entry in replacements.entries) {
String key = entry.key;
String value = entry.value;
if (text.contains(key)) {
text = text.replaceAll(key, value);
}
}
layer['text'] = text;
}
}
}
}
}
|
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 6 replies
-
Thanks for the detailed description of the problem with the example, that helped me a lot to resolve this issue. I released version Btw, in version >= 4.0.0 I made some changes in customWidgets. Below is an example of how you can update your code with the new way to handle custom widgets. customWidgets: ImageEditorCustomWidgets(
mainEditor: CustomWidgetsMainEditor(
bottomBar: (editor, rebuildStream, key) {
return ReactiveCustomWidget(
key: key,
stream: rebuildStream,
builder: (_) {
if (editor.selectedLayerIndex >= 0) {
return const SizedBox.shrink();
}
return Scrollbar(
controller: _bottomBarScrollCtrl,
scrollbarOrientation: ScrollbarOrientation.top,
thickness: isDesktop ? null : 0,
child: BottomAppBar(
/// kBottomNavigationBarHeight is important that helperlines will work
height: kBottomNavigationBarHeight,
color: Colors.black,
padding: EdgeInsets.zero,
child: Center(
child: SingleChildScrollView(
controller: _bottomBarScrollCtrl,
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: min(constraints.maxWidth, 500),
maxWidth: 500,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FlatIconTextButton(
label: Text('Add Image',
style: _bottomTextStyle),
icon: const Icon(
Icons.image_outlined,
size: 22.0,
color: Colors.white,
),
onPressed: _chooseCameraOrGallery,
),
FlatIconTextButton(
label: Text('Paint',
style: _bottomTextStyle),
icon: const Icon(
Icons.edit_rounded,
size: 22.0,
color: Colors.white,
),
onPressed:
editor.openPaintingEditor,
),
FlatIconTextButton(
label: Text('Text',
style: _bottomTextStyle),
icon: const Icon(
Icons.text_fields,
size: 22.0,
color: Colors.white,
),
onPressed: editor.openTextEditor,
),
FlatIconTextButton(
label: Text('Emoji',
style: _bottomTextStyle),
icon: const Icon(
Icons
.sentiment_satisfied_alt_rounded,
size: 22.0,
color: Colors.white,
),
onPressed: editor.openEmojiEditor,
),
],
),
),
),
),
),
),
);
},
);
},
),
) |
Beta Was this translation helpful? Give feedback.
You can create a new editor behind the
NewPage
and import the state history there, which will also allow you to capture it without your users seeing that you updated something. Below is an example where I updated your code above with this function: