hm21 / pro_image_editor

The pro_image_editor is a Flutter widget designed for image editing within your application. It provides a flexible and convenient way to integrate image editing capabilities into your Flutter project.
https://hm21.github.io/pro_image_editor/
BSD 3-Clause "New" or "Revised" License
94 stars 59 forks source link

[Bug]: If you open and close a standalone editor like CropRotateEditor many times it will eventually bug out and the next times you open it it will always bug. #183

Closed ember11498 closed 1 month ago

ember11498 commented 1 month ago

Package Version

latest

Flutter Version

3.22.3

Platforms

Android

How to reproduce?

Just create the simplest of editors like:

  class TestEditor extends StatelessWidget {
    const TestEditor({super.key});

    @override
    Widget build(BuildContext context) {
      return CropRotateEditor.asset(
        'assets/sun.png',
        initConfigs: CropRotateEditorInitConfigs(
          onCloseEditor: () {
            context.pop();
          },
          theme: Theme.of(context).copyWith(scaffoldBackgroundColor: Colors.red),
        ),
      );
    }
  }

and call it like:

                      Navigator.of(context).push(MaterialPageRoute(
                        builder: (context) => TestEditor(),
                      ));

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: ════════════════════════════════════════════════════════════════════════════════



### Example code (optional)

_No response_

### Device Model (optional)

pixel 3 and pixel 8, both throw the same exceptions
ember11498 commented 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

ember11498 commented 1 month ago

@hm21 here is more context to the exception:

https://github.com/user-attachments/assets/a226f1b7-750d-487d-bac3-b17ea97d2628

ember11498 commented 1 month ago

@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

hm21 commented 1 month ago

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.

ember11498 commented 1 month ago

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>
ember11498 commented 1 month ago

@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

hm21 commented 1 month ago

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),
      ),
    );
  }
}
ember11498 commented 1 month ago

@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...

hm21 commented 1 month ago

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.