[Bug]: Crop Editor returns zero bytes
Package Version
4.2.5
Flutter Version
3.22.2
Platforms
Android, iOS
How to reproduce?
Hi @hm21. I'm using the standalone Crop Editor, but it returns zero bytes (I guess the issue occurs with images that have a vertical ratio). It's working with images that have a horizontal ratio.
This is the video of the error (Image size: 1.7MB)
https://github.com/user-attachments/assets/98992dc0-a2c7-49e3-be46-9e3424a087c0
This is a video showing it working (Image size: 2.8MB)
https://github.com/user-attachments/assets/b2489ee6-9d32-47f9-8bfa-1792f43ce1ca
Original Image URL: https://images.unsplash.com/photo-1534794420636-dbc13b8a48d2?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fGJlYXV0aWZ1bCUyMGdpcmx8ZW58MHx8MHx8fDA%3D
Image file:
Logs (optional)
No response
Example code (optional)
// Flutter imports:
import 'dart:async';
import 'dart:io';
import 'dart:math';
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:image_picker/image_picker.dart';
// Package imports:
import 'package:pro_image_editor/pro_image_editor.dart';
// Project imports:
import '../utils/example_helper.dart';
class StandaloneExample extends StatefulWidget {
const StandaloneExample({super.key});
@override
State<StandaloneExample> createState() => _StandaloneExampleState();
}
class _StandaloneExampleState extends State<StandaloneExample>
with ExampleHelperState<StandaloneExample> {
void _openPicker(ImageSource source) async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: source);
if (image == null) return;
String? path;
Uint8List? bytes;
if (kIsWeb) {
bytes = await image.readAsBytes();
if (!mounted) return;
await precacheImage(MemoryImage(bytes), context);
} else {
path = image.path;
if (!mounted) return;
await precacheImage(FileImage(File(path)), context);
}
if (!mounted) return;
if (kIsWeb ||
(!Platform.isWindows && !Platform.isLinux && !Platform.isMacOS)) {
Navigator.pop(context);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StandaloneCropRotateScreen(
imagePath: path!,
),
),
);
}
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),
),
],
),
),
),
);
},
);
}
}
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
showModalBottomSheet(
context: context,
builder: (BuildContext _) {
return ListView(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 20),
children: <Widget>[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('Editor',
style:
TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
),
ListTile(
leading: const Icon(Icons.crop_rotate_rounded),
title: const Text('Crop-Rotate-Editor'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
_chooseCameraOrGallery();
},
),
],
);
},
);
},
leading: const Icon(Icons.view_in_ar_outlined),
title: const Text('Standalone Sub-Editor'),
trailing: const Icon(Icons.chevron_right),
);
}
}
class StandaloneCropRotateScreen extends StatefulWidget {
final String imagePath;
const StandaloneCropRotateScreen({
super.key,
required this.imagePath,
});
@override
State<StandaloneCropRotateScreen> createState() =>
_StandaloneCropRotateScreenState();
}
class _StandaloneCropRotateScreenState
extends State<StandaloneCropRotateScreen> {
final _cropRotateEditorKey = GlobalKey<CropRotateEditorState>();
late StreamController _updateUIStream;
Future<void> _onImageEditingComplete(bytes) async {
Navigator.of(context).pop(bytes);
}
/// Undo icon button
Widget _undoButton({
required bool? canUndo,
required VoidCallback? onPressed,
}) =>
IconButton(
tooltip: 'Undo',
icon: const Icon(Icons.undo_outlined),
color: (canUndo ?? false)
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primary.withAlpha(80),
onPressed: onPressed,
);
/// Redo icon button
Widget _redoButton({
required bool? canUndo,
required VoidCallback? onPressed,
}) =>
IconButton(
tooltip: 'Redo',
icon: const Icon(Icons.redo_outlined),
color: (canUndo ?? false)
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.primary.withAlpha(80),
onPressed: onPressed,
);
/// Back icon button
Widget _backButton({
required VoidCallback? onPressed,
}) =>
IconButton(
tooltip: 'Cancel',
icon: Icon(Platform.isAndroid
? Icons.arrow_back_outlined
: Icons.arrow_back_ios_new_outlined),
color: Theme.of(context).colorScheme.primary,
onPressed: onPressed,
);
/// Done icon button
Widget _doneButton({
required VoidCallback? onPressed,
IconData icon = Icons.done,
}) =>
IconButton(
tooltip: 'Done',
icon: Icon(icon),
color: Theme.of(context).colorScheme.primary,
onPressed: onPressed,
);
/// AppBar Crop Editor
AppBar _buildAppBarCropEditor(CropRotateEditorState cropRotateEditorState) {
return AppBar(
automaticallyImplyLeading: false,
backgroundColor: Theme.of(context).colorScheme.surface,
foregroundColor: Theme.of(context).colorScheme.surface,
actions: [
StreamBuilder(
stream: _updateUIStream.stream,
builder: (_, __) {
return _backButton(
onPressed: cropRotateEditorState.close,
);
},
),
const Spacer(),
StreamBuilder(
stream: _updateUIStream.stream,
builder: (_, __) {
return _undoButton(
canUndo: cropRotateEditorState.canUndo,
onPressed: cropRotateEditorState.undoAction,
);
}),
StreamBuilder(
stream: _updateUIStream.stream,
builder: (_, __) {
return _redoButton(
canUndo: cropRotateEditorState.canRedo,
onPressed: cropRotateEditorState.redoAction,
);
},
),
StreamBuilder(
stream: _updateUIStream.stream,
builder: (_, __) {
return _doneButton(onPressed: cropRotateEditorState.done);
},
),
],
);
}
/// BottomBar Crop Editor
Widget _buildBottomBarCropEditor(
CropRotateEditorState cropRotateEditorState) {
return StreamBuilder(
stream: _updateUIStream.stream,
builder: (_, __) {
return BottomAppBar(
height: kBottomNavigationBarHeight,
color: Theme.of(context).colorScheme.surface,
padding: EdgeInsets.zero,
child: ListView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(
horizontal: 12,
),
shrinkWrap: true,
children: [
/// Rotate
FlatIconTextButton(
key: const ValueKey('crop-rotate-editor-rotate-btn'),
label: const Text('Rotate'),
icon: const Icon(Icons.rotate_90_degrees_ccw_outlined),
onPressed: () {
cropRotateEditorState.rotate();
},
),
/// Flip
FlatIconTextButton(
key: const ValueKey('crop-rotate-editor-flip-btn'),
label: const Text('Flip'),
icon: const Icon(Icons.flip_outlined),
onPressed: () {
cropRotateEditorState.flip();
},
),
/// Ratio
FlatIconTextButton(
key: const ValueKey('crop-rotate-editor-ratio-btn'),
label: const Text('Ratio'),
icon: const Icon(Icons.crop_outlined),
onPressed: () {
cropRotateEditorState.openAspectRatioOptions();
},
),
/// Reset
FlatIconTextButton(
key: const ValueKey('crop-rotate-editor-reset-btn'),
label: const Text('Reset'),
icon: const Icon(Icons.replay_outlined),
onPressed: () {
cropRotateEditorState.reset();
},
),
],
),
);
},
);
}
@override
void initState() {
super.initState();
_updateUIStream = StreamController.broadcast();
}
@override
void dispose() {
_updateUIStream.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CropRotateEditor.file(
File(widget.imagePath),
key: _cropRotateEditorKey,
initConfigs: CropRotateEditorInitConfigs(
convertToUint8List: true,
configs: ProImageEditorConfigs(
icons: ImageEditorIcons(
backButton:
Platform.isIOS ? Icons.arrow_back_ios : Icons.arrow_back,
),
customWidgets: ImageEditorCustomWidgets(
cropRotateEditor: CustomWidgetsCropRotateEditor(
appBar: (cropRotateEditor, rebuildStream) => ReactiveCustomAppbar(
stream: rebuildStream,
builder: (_) => _buildAppBarCropEditor(cropRotateEditor),
),
bottomBar: (cropRotateEditor, rebuildStream) =>
ReactiveCustomWidget(
stream: rebuildStream,
builder: (_) => _buildBottomBarCropEditor(cropRotateEditor),
),
),
),
),
onImageEditingComplete: _onImageEditingComplete,
theme: ThemeData.dark(),
),
);
}
}
Device Model (optional)
No response
I just tried your code, but I can't reproduce it. I used many different image ratios, but in my case it always works. Actually, it should not matter what kind of ratio the image has. However, can you try for me with an image that is in “JPEG” format that has the same ratio as the one that is causing a problem?
The point is, currently I think there is an issue with decoding your image. I saw in the example URL which you posted it automatically choose the format. Keep in mind that Flutter can make problems by decoding more modern image formats like “AVIF” so I recommend you to use jpeg/png. Alternative you can download it in a modern format, but before you insert it in the image editor you use an external package that converts it in a format that Flutter can decode.
In the case, the same issue is also with the format “JPEG” do you get any error in the console when you open the image? And on which exactly device you test it? Happen the issue also on other devices and also on the platforms windows and web?
You're right. The image ratio doesn't matter. I've tried several images with different ratios, and sometimes it works, sometimes it doesn't. The image format is .jpg. I've tested on both an iPhone 15 simulator (iOS 17) and a Samsung Galaxy A71 (Android 13. Real device). The error occurs on both devices. Currently, I haven't found a pattern because sometimes the same image works without any error. It also doesn't throw any exceptions.
Sometimes the captureFinalScreenshot method in content_recorder_controller.dart returns null.
if (activeScreenshotGeneration) {
// Get screenshot from isolated generated thread.
bytes = await backgroundScreenshot.completer.future;
} else {
// Capture a new screenshot if the current screenshot is broken or
// doesn't exist.
bytes = widget == null
? await _capture(
id: id,
imageInfos: imageInfos,
)
: await captureFromWidget(
widget,
id: id,
targetSize: targetSize,
imageInfos: imageInfos,
);
}
Sometimes it goes to the 'if' block, sometimes to the 'else' block, and returns null. So in the following code:
Uint8List? bytes = await screenshotCtrl.captureFinalScreenshot(
imageInfos: imageInfos!,
context: context,
widget: _screenshotWidget(transformC),
targetSize:
_rotated90deg ? imageInfos!.rawSize.flipped : imageInfos!.rawSize,
backgroundScreenshot:
screenshotHistoryPosition >= screenshotHistory.length
? null
: screenshotHistory[screenshotHistoryPosition],
);
bytes is null in the done() method in crop_rotate_editor.dart. That's why it returns zero bytes.
Here's the corrected version:
STEP is the same video. Just pick an image and then click the Done button on the top right. Don't do anything else. Don't click Flip, Ratio
Thank you for this information, that's good to know and help me to fix it. Can you also answer me the questions below that will help me to fix it.
- When you do any interaction like flip it always works correctly, right?
- When you open the editor and wait a longer time (about 10 seconds) before you press “done” it also works correctly, right?
- In the case you answered question 2 with yes, can you check here if this method is always called when you open the editor? And if yes, can you check if this method is also triggered?
Currently, from that what you describe, it sounds to me that the editor didn't capture the image. I'm not sure if the problem is that the editor didn't await to finish the capture progress, or it never started, but I think more that the problem is the editor didn't start to capture it.
Thanks for your help.
- Yes
- If you wait a longer time (> 10 seconds), it works more than 90%. Sometimes it doesn't work.
- Both methods you mentioned
hideFakeHeroandtakeScreenshotare called.
Okay, got it. When you print the parameter screenshotHistoryPosition and screenshotHistory.length here which values does it show you?
Its always are screenshotHistoryPosition = 0 and screenshotHistory.length = 1
This is screenshotHistory value when its working.
This is screenshotHistory value when its error (case return zero bytes).
Ah okay, that's interesting, something seems to fail in the background generation that the state broken is true. Actually, in this case the editor should re-capture an image when you press “done”, but it seems like that also fails. I'm not sure, but maybe something failed in the generation of the plain-transformed image. Can you check here if the size is correct when you press done?
The size is correct even if it returns data or returns zero bytes.
Okay, that's interesting. The problem is that I still can't reproduce it even after trying it on many mobile devices, so I don't think I can resolve this problem until I can reproduce it by myself.
I hope you can fix it soon. Thank you!
Hi @hm21. I can not reopen this issue. Please help reopen.
This issue needs time to fix.
@thanglq1
I have occasionally been able to reproduce the issue, though it happens very rarely. It appears that there is a problem, particularly in debug mode, where the widget tree isn't correctly built to capture the background. I've added a loop that retries a few times if the widget tree is not ready in version 5.1.3. When you have time, could you please test it to see if this resolves the issue on your end as well?
Hi @hm21. Will check when I have time. Thank you so much for your effort.
Hi @thanglq1, I will close this issue now as I believe it has been resolved. If the issue reappears after your testing, please feel free to reopen it.