Closed ember11498 closed 1 month ago
@hm21 after doing many tests i just think there is a bug when using standalone croprotateeditor.
if i open and close very fast and many times the ProImageEditor i never get exceptions it works fine.
now, if i open and close very fast and many times the CropRotateEditor it will eventually throw exceptions. You can try it yourself and see, I am sure you can reproduce
@hm21 here is more context to the exception:
https://github.com/user-attachments/assets/a226f1b7-750d-487d-bac3-b17ea97d2628
@hm21 so you have even more context:
my code looks like this (very simple):
https://github.com/user-attachments/assets/74356be8-3569-4b82-a83c-1514e2276fd9
now when I open and close the crop editor i get this printed in the debug console:
https://github.com/user-attachments/assets/72eb2936-18df-4c87-85d1-ed97ebe47a64
and in my emulator it looks like this:
https://github.com/user-attachments/assets/0bf072fa-1f3f-4a77-9e6a-f5c7f117b272
I attempted to reproduce the issue by rapidly pressing the open and close button, but I was unable to replicate it in the example. I tested this on both Android and Windows platforms using Asset-Image and Network-Image. One potential cause might be related to image caching; your code doesn't show whether you are precaching the image. If not, you might resolve the issue by doing so before opening the editor. Below is how you can precache the image:
await precacheImage(
AssetImage(ExampleConstants.of(context)!.demoAssetPath),
context,
);
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _buildEditor(),
),
);
If that didn't resolve the issue, it would be helpful if you could provide a complete example of how you're using the editor. To ensure I can reproduce the problem, please keep the example minimal and avoid using additional packages like go_router.
dart: 3.4.4 flutter: 3.22.3
emulator pixel 8, tiramisu
code:
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pro_image_editor/pro_image_editor.dart';
void main() {
runApp(const RootWidget());
}
class RootWidget extends StatelessWidget {
const RootWidget({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: ButtonPage(),
);
}
}
class ButtonPage extends StatefulWidget {
const ButtonPage({super.key});
@override
State<ButtonPage> createState() => _ButtonPageState();
}
class _ButtonPageState extends State<ButtonPage> {
List<AssetEntity> assets = [];
@override
void initState() {
super.initState();
loadAlbums(RequestType.image).then(
(albums) => loadAssets(albums[1]).then(
(loadedAssets) => setState(() {
assets = loadedAssets;
}),
),
);
}
Future<List<AssetPathEntity>> loadAlbums(RequestType requestType) async {
PermissionState permission = await PhotoManager.requestPermissionExtend();
List<AssetPathEntity> albumList = [];
if (permission.isAuth) {
albumList = await PhotoManager.getAssetPathList(type: requestType);
} else {
final action = await Permission.photos.request();
if (action.isGranted) {
albumList = await PhotoManager.getAssetPathList(type: requestType);
}
}
print('albums lenght: ${albumList.length}');
return albumList;
}
Future<List<AssetEntity>> loadAssets(AssetPathEntity selectedAlbum) async {
List<AssetEntity> assetList = await selectedAlbum.getAssetListRange(
start: 0, end: await selectedAlbum.assetCountAsync);
print('assets lenght: ${assetList.length}');
return assetList;
}
_onTap(AssetEntity asset) async {
final File? file = await asset.file;
final Uint8List? image = file?.readAsBytesSync();
if (context.mounted && image != null) {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CropRoateEditorTest(image: image),
));
}
}
@override
Widget build(BuildContext context) {
print(assets.length);
return Scaffold(
body: ((assets.isEmpty))
? const Center(
child: CupertinoActivityIndicator(),
)
: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4),
itemCount: assets.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_onTap(assets[index]);
},
child: AssetEntityImage(
assets[index],
isOriginal: false,
thumbnailSize: const ThumbnailSize.square(250),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Icon(Icons.error, color: Colors.red),
);
},
),
);
},
),
);
}
}
class CropRoateEditorTest extends StatelessWidget {
final Uint8List image;
const CropRoateEditorTest({super.key, required this.image});
@override
Widget build(BuildContext context) {
return CropRotateEditor.memory(image,
initConfigs: CropRotateEditorInitConfigs(theme: Theme.of(context)));
}
}
permissions (add to androidmanifest):
READ_MEDIA_IMAGES
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Add the permission here -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<application
android:label="pro_image_editor"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@hm21 obviously forgot to mention but in my previous code example we need to add images to the gallery, I downloaded 2 from google chrome randomly
Just to clarify, does this issue occur only when using the photo_manager
package? If so, could you please tell me which version of photo_manager you're using?
If the issue isn't related to photo_manager
or the way you're using it, we can try a simple example instead. Below, I've posted two examples that you can try. They work on my devices, although I haven't tested them on a Pixel 3
.
The first example converts some images directly to Uint8List because you mentioned earlier that you have a problem with memory and asset images. The second example uses only network images.
FYI: If you open the editor with assets like CropRotateEditor.asset
, remember that Flutter considers assets as only the static files located inside your asset folder, not files picked from the gallery. To open a file from the gallery, you should use CropRotateEditor.file(file)
or CropRotateEditor.file(File(filePath))
.
Example from memory
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:pro_image_editor/pro_image_editor.dart';
void main() {
runApp(const RootWidget());
}
class RootWidget extends StatelessWidget {
const RootWidget({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: ButtonPage(),
);
}
}
class ButtonPage extends StatefulWidget {
const ButtonPage({super.key});
@override
State<ButtonPage> createState() => _ButtonPageState();
}
class _ButtonPageState extends State<ButtonPage> {
late Future<List<Uint8List>> _fetchImagesFuture;
@override
void initState() {
super.initState();
_fetchImagesFuture = Future.wait([
_fetchImageAsUint8List('https://picsum.photos/id/130/2000'),
_fetchImageAsUint8List('https://picsum.photos/id/140/2000'),
_fetchImageAsUint8List('https://picsum.photos/id/180/2000'),
_fetchImageAsUint8List('https://picsum.photos/id/230/2000'),
]);
}
Future<Uint8List> _fetchImageAsUint8List(String imageUrl) async {
final response = await http.get(Uri.parse(imageUrl));
if (response.statusCode == 200) {
final Uint8List uint8List = Uint8List.fromList(response.bodyBytes);
return uint8List;
} else {
throw Exception('Failed to load image: $imageUrl');
}
}
_openEditor(Uint8List bytes) async {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CropRoateEditorTest(
image: bytes,
),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder<List<Uint8List>>(
future: _fetchImagesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
List<Uint8List> imgList = snapshot.data ?? [];
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: imgList.length,
itemBuilder: (context, index) {
Uint8List img = imgList[index];
return GestureDetector(
onTap: () => _openEditor(img),
child: Image.memory(img, fit: BoxFit.cover),
);
},
);
}),
);
}
}
class CropRoateEditorTest extends StatelessWidget {
final Uint8List image;
const CropRoateEditorTest({super.key, required this.image});
@override
Widget build(BuildContext context) {
return CropRotateEditor.memory(
image,
initConfigs: CropRotateEditorInitConfigs(
theme: Theme.of(context),
),
);
}
}
Example from network
import 'package:flutter/material.dart';
import 'package:pro_image_editor/pro_image_editor.dart';
void main() {
runApp(const RootWidget());
}
class RootWidget extends StatelessWidget {
const RootWidget({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: ButtonPage(),
);
}
}
class ButtonPage extends StatefulWidget {
const ButtonPage({super.key});
@override
State<ButtonPage> createState() => _ButtonPageState();
}
class _ButtonPageState extends State<ButtonPage> {
final List<String> _imgList = [
'https://picsum.photos/id/130/2000',
'https://picsum.photos/id/140/2000',
'https://picsum.photos/id/180/2000',
'https://picsum.photos/id/230/2000',
];
_openEditor(String url) async {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => CropRoateEditorTest(
url: url,
),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: _imgList.length,
itemBuilder: (context, index) {
String url = _imgList[index];
return GestureDetector(
onTap: () => _openEditor(url),
child: Image.network(url, fit: BoxFit.cover),
);
},
),
);
}
}
class CropRoateEditorTest extends StatelessWidget {
final String url;
const CropRoateEditorTest({super.key, required this.url});
@override
Widget build(BuildContext context) {
return CropRotateEditor.network(
url,
initConfigs: CropRotateEditorInitConfigs(
theme: Theme.of(context),
),
);
}
}
@hm21 I dont know why but I havent even noticed until now that you also had a file constructor so there is no need to convert the file to uint8list.
I just changed it and I am now passing the file to the croprotateeditor widget and I am not getting exceptions so maybe the error was in this pieace of code?
final Uint8List? image = file?.readAsBytesSync();
maybe there is some bug with this method. I just know that not using it is not throwing exceptions anymore...
Great that it works now :)
Reading a file synchronously can cause issues because it blocks the main thread, making the application unresponsive. To read files without blocking the UI, use asynchronous methods such as await file.readAsBytes()
. Asynchronous operations allow the main thread to remain responsive by not blocking it, but note that CPU-intensive tasks can still impact performance if not handled correctly. You can also take a look at my code here which shows you the best way to convert an image to Uint8List
.
For heavy computational tasks, such as complex image processing, consider using isolates or web workers to run tasks concurrently without affecting the UI thread. If you are curious, how to do that, you can take a loot at my code here. However, reading an image file asynchronously is typically not problematic in terms of performance.
FYI: When you use the file constructor, it converts the image to Uint8List format in the background. This conversion is necessary to manipulate the image data and generate edited versions.
Package Version
latest
Flutter Version
3.22.3
Platforms
Android
How to reproduce?
Just create the simplest of editors like:
and call it like:
The first time it never bugs, but if you keep closing and opening fast you will throw exceptions.
I am sorry to insist but I really think this is a bug and not me doing something wrong on the rest of my code.
I have test with memory, asset and network constructors all give me exceptions eventually. Although if i use network from the start it never bugs. but if i use memory or asset it will always eventually bug and then if i simply switch to network and hot restart will will keep throwing exceptions. it seems like once the editors gets bugged once it will always be bugged the next times i open it. the only time i dont get exceptions is if i start with network and stick with it always. asset and bytes are giving me errors even when i have images, it is not loading them properly for some reason.
Logs (optional)
D/EGL_emulation( 6292): app_time_stats: avg=215.07ms min=12.38ms max=3570.02ms count=18
════════ Exception caught by rendering library ═════════════════════════════════ Rect argument contained a NaN value. 'dart:ui/painting.dart': Failed assertion: line 26 pos 10: ''
The relevant error-causing widget was:
════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Unsupported operation: Infinity or NaN toInt The relevant error-causing widget was: ════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Rect argument contained a NaN value. 'dart:ui/painting.dart': Failed assertion: line 26 pos 10: ''
The relevant error-causing widget was:
The following RenderObject was being processed when the exception was fired: _RenderColoredBox#9b54b
RenderObject: _RenderColoredBox#9b54b
════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Rect argument contained a NaN value. 'dart:ui/painting.dart': Failed assertion: line 26 pos 10: ''
The relevant error-causing widget was:
════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by scheduler library ═════════════════════════════════ Unsupported operation: Infinity or NaN toInt ════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Unsupported operation: Infinity or NaN toInt The relevant error-causing widget was: ════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Unsupported operation: Infinity or NaN toInt The relevant error-causing widget was: ════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Rect argument contained a NaN value. 'dart:ui/painting.dart': Failed assertion: line 26 pos 10: ''
The relevant error-causing widget was:
The following RenderObject was being processed when the exception was fired: _RenderColoredBox#9b54b
RenderObject: _RenderColoredBox#9b54b
════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Rect argument contained a NaN value. 'dart:ui/painting.dart': Failed assertion: line 26 pos 10: ''
The relevant error-causing widget was:
════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Unsupported operation: Infinity or NaN toInt The relevant error-causing widget was: ════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Unsupported operation: Infinity or NaN toInt The relevant error-causing widget was: ════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Rect argument contained a NaN value. 'dart:ui/painting.dart': Failed assertion: line 26 pos 10: ''
The relevant error-causing widget was:
The following RenderObject was being processed when the exception was fired: _RenderColoredBox#9b54b
RenderObject: _RenderColoredBox#9b54b
════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by rendering library ═════════════════════════════════ Rect argument contained a NaN value. 'dart:ui/painting.dart': Failed assertion: line 26 pos 10: ''
The relevant error-causing widget was:
════════════════════════════════════════════════════════════════════════════════