crop_your_image Flutter Plugin

repository·main·Indexed 42 days ago

https://github.com/chooyan-eng/crop_your_image

A flexible, UI-agnostic Flutter plugin providing a customizable Crop widget and CropController for image cropping. It supports rectangular and circular cropping, undo/redo history, interactive zooming and panning, and custom image processing backends. Developers can control the cropping process via the CropController and handle outcomes using the CropResult sealed class.

Tokens
3.8K
Snippets
7
Records
14
Agent score
49%

What's inside crop_your_image

  1. How the Crop widget and CropController work together

    main

    The crop_your_image package is built around two primary components: the Crop widget and the CropController.

    • Crop: A widget designed to be used anywhere (e.g., in a Dialog or BottomSheet). It handles the UI for zooming and panning via a GestureDetector.
    • CropController: An imperative controller used to manage the Crop widget, similar to how TextEditingController works for a TextField. You use it to trigger actions like crop().

    To use them together, you must provide the CropController to the Crop widget. The widget will then initialize the controller and attach its internal delegates.

  2. How CropController works

    main

    The CropController is the primary way to interact with the Crop widget from outside its immediate scope. It allows you to:

    1. Trigger Actions: Call .crop() to start the cropping process or .cropCircle() to perform a circular crop.
    2. Manage History: Call .undo() and .redo() to navigate through previous crop rect states.
    3. Modify Configuration: Dynamically update the aspectRatio of the crop rect.

    Note that calling .crop() does not return the image data directly; instead, the result is emitted through the onCropped callback of the Crop widget.

  3. Customize image processing backends

    main

    The Crop widget provides a mechanism to interchange backend logic for image processing. This allows you to replace the default behavior (which uses the image package) with your own implementation, such as delegating processing to a server or using a different Dart package.

    You can customize the following logic:

    • Determining the given image format.
    • Parsing the image from Uint8List to your required data type.
    • Cropping the image using a given Rect.
  4. Implement Undo and Redo functionality

    main

    The CropController manages the history of crop rect changes. You can trigger undo and redo actions using _controller.undo() and _controller.redo(). To update your UI based on whether these actions are available, use the onHistoryChanged callback on the Crop widget, which provides a History object containing undoCount and redoCount.

    // Triggering actions
    ElevatedButton(
      child: Text('Undo'),
      onPressed: () => _cropController.undo(),
    ),
    
    // Updating UI based on history
    Crop(
      onHistoryChanged: (history) {
        setState(() {
          _undoEnabled = history.undoCount > 0;
          _redoEnabled = history.redoCount > 0;
        });
      },
    )
  5. Provide viewport constraints to the Crop widget

    main

    The Crop widget determines its viewport size by overriding MediaQueryData.size. Because of this mechanism, you must explicitly supply the size of the viewport to the Crop widget.

    If you do not wrap the Crop widget in a widget that provides explicit constraints, it may not function correctly. Common ways to provide these constraints include:

    • SizedBox
    • AspectRatio
    • Expanded
  6. Basic usage of the Crop widget

    main

    To implement image cropping, place the Crop widget in your widget tree and provide the image data as a Uint8List. Use a CropController to trigger the cropping action from external UI elements (like buttons). The result of the cropping operation is returned via the onCropped callback, which provides a CropResult object containing either the success image data or an error.

    final _controller = CropController();
    
    @override
    Widget build(BuildContext context) {
      return Crop(
        image: _imageData, 
        controller: _controller,
        onCropped: (result) {
          switch(result) {
            case CropResult.success(:final croppedImage):
              // do something with cropped image data 
            case CropResult.error(:final error):
              // do something with error
          }
        }
      );
    }
    
    // To trigger cropping from a button:
    ElevatedButton(
      child: Text('Crop it!'),
      onPressed: () => _controller.crop(),
    ),
  7. Configure the initial cropping rectangle with InitialRectBuilder

    main

    Instead of legacy arguments, use initialRectBuilder to define the starting state of the cropping area. There are three ways to initialize the rect:

    1. InitialRectBuilder.withSizeAndRatio: Sets an initial size (as a fraction of the viewport, 0.0 to 1.0) and an initial aspect ratio. If an aspect ratio is provided, user interactions will maintain this ratio.
    2. InitialRectBuilder.withBuilder: Provides full control by allowing you to build a ViewportBasedRect using both the viewportRect (the widget's size) and the imageRect (the original image's dimensions).
    3. InitialRectBuilder.withArea: Sets an initial area based on ImageBasedRect. Note that ImageBasedRect refers to coordinates in the original image data, not the screen pixels. Crop will automatically convert this to a ViewportBasedRect.

    Note: If aspectRatio is also provided to the Crop widget, the widget will prioritize the aspect ratio during user interaction.

    // Example: Initializing with a specific size and aspect ratio
    Crop(
      image: myImage,
      onCropped: (result) => print(result),
      initialRectBuilder: InitialRectBuilder.withSizeAndRatio(
        size: 0.5, // 50% of viewport
        aspectRatio: 1.0,
      ),
    )
  8. Customize cropping backend logic

    main

    For advanced use cases, you can override the default logic for image processing by providing custom functions to the following Crop arguments:

    • formatDetector: A function ImageFormat Function(Uint8List)? to detect the image format.
    • imageParser: A function ImageDetail<T> Function(Uint8List, {ImageFormat})? to parse the Uint8List into an ImageDetail object.
    • imageCropper: An implementation of the ImageCropper<T> interface to define how the actual pixel cropping is performed.
  9. Configure the Crop widget

    main

    The Crop widget accepts various configuration arguments to customize the UI and behavior. Key options include:

    • aspectRatio: Sets the initial aspect ratio. Can be updated dynamically via CropController.aspectRatio.
    • initialRectBuilder: Defines the starting crop area using InitialRectBuilder.withBuilder, InitialRectBuilder.withArea, or InitialRectBuilder.withSizeAndRatio.
    • withCircleUi: If true, the UI shows a circular crop area (sets aspectRatio to 1.0). Note: To get a circular result, use CropController.cropCircle() instead of crop().
    • interactive: Enables zooming and panning of the image.
    • fixCropRect: If true, the crop rect remains fixed while the image is moved/zoomed in interactive mode.
    • maskColor & baseColor: Customize the colors of the cropping editor overlay.
    Crop(
      image: _imageData,
      controller: _controller,
      aspectRatio: 4 / 3,
      initialRectBuilder: InitialRectBuilder.withBuilder((viewportRect, imageRect) {
        return Rect.fromLTRB(
          viewportRect.left + 24,
          viewportRect.top + 32,
          viewportRect.right - 24,
          viewportRect.bottom - 32,
        );
      }),
      withCircleUi: true,
      maskColor: Colors.white.withAlpha(100),
      baseColor: Colors.blue.shade900,
      interactive: true,
      onMoved: (newRect) {
        // Handle crop rect movement
      },
    )
  10. Implement CropControllerDelegate to handle controller actions

    main

    The CropControllerDelegate is a required interface used to respond to commands issued by a CropController. You must initialize the following late callbacks to prevent runtime errors when the controller is used:

    CallbackTriggered byDescription
    onCrop.crop(), .cropCircle()ValueChanged<bool> where true indicates a circular crop.
    onImageChanged.image = valueValueChanged<Uint8List> containing the new image data.
    onChangeAspectRatio.aspectRatio = valueValueChanged<double?> where null enables free aspect ratio.
    onChangeWithCircleUi.withCircleUi = valueValueChanged<bool> indicating if circular UI is active.
    onChangeCropRect.cropRect = valueValueChanged<ViewportBasedRect> for viewport-relative rects.
    onChangeArea.area = valueValueChanged<ImageBasedRect> for image-relative rects.
    onUndo.undo()VoidCallback to trigger undo.
    onRedo.redo()VoidCallback to trigger redo.
  11. Programmatically control cropping with CropController

    main

    Use CropController to trigger cropping actions, change the image, adjust aspect ratios, or manipulate the crop area programmatically. To make the controller functional, you must assign a CropControllerDelegate to its delegate property. The delegate acts as the bridge, providing callbacks that respond to the controller's commands.

    Key Capabilities:

    • Cropping: Trigger a standard crop via .crop() or a circular crop via .cropCircle().
    • Image Management: Update the source image using the .image setter (this resets the current crop rect).
    • Aspect Ratio: Set a fixed aspect ratio using .aspectRatio. Passing null allows free movement of the cropping area.
    • UI Mode: Toggle circular UI mode using .withCircleUi. Setting this to true automatically fixes the aspect ratio to 1.
    • Area Manipulation: Adjust the crop area using .cropRect (viewport-based) or .area (image-based).
    • History: Control undo/redo actions via .undo() and .redo().
    // 1. Initialize the controller
    final CropController controller = CropController();
    
    // 2. Implement the delegate to handle the controller's commands
    final delegate = CropControllerDelegate()
      ..onCrop = (isCircle) {
        print("Cropping! Circle: $isCircle");
      }
      ..onImageChanged = (imageBytes) {
        print("New image loaded");
      }
      ..onChangeAspectRatio = (ratio) {
        print("Aspect ratio changed to: $ratio");
      };
    
    // 3. Connect them
    controller.delegate = delegate;
    
    // 4. Use the controller
    controller.aspectRatio = 16 / 9;
    controller.crop();
  12. Use the Crop widget to implement image cropping

    main

    The Crop widget is the primary entry point for the crop_your_image package. It provides a complete UI for users to select a cropping area, zoom, and pan an image.

    Key Features:

    • Cropping Modes: Supports rectangular cropping (default) and circular cropping (via withCircleUi).
    • Interactivity: When interactive is set to true, users can zoom and pan the image. When false, they can only move the cropping rectangle.
    • Aspect Ratio: You can enforce a fixed aspect ratio by providing an aspectRatio value.
    • Initial State: Use initialRectBuilder to define how the cropping area is initially positioned or sized.
    • Callbacks:
      • onCropped: Triggered when the user completes the cropping action. Returns a CropResult (either CropSuccess with the new image or CropFailure).
      • onStatusChanged: Notifies you of lifecycle changes like CropStatus.loading, ready, or cropping.
      • onMoved: Notifies you when the cropping rectangle or the image position changes.

    Basic Usage Example:

    Crop(
      image: myUint8List,
      aspectRatio: 16 / 9,
      onCropped: (result) {
        if (result is CropSuccess) {
          // Use result.image
        }
      },
      onStatusChanged: (status) {
        print('Current status: $status');
      },
    )
    Crop(
      image: myUint8List,
      onCropped: (result) {
        if (result is CropSuccess) {
          // Use result.image
        }
      },
    )