crop_view

pub version pub points likes license: MIT
platforms Flutter >= 3.22 style: flutter_lints

An image cropper for Flutter with a draggable crop box.
The image pans, zooms and rotates behind the box, and the box is always kept inside the image — so a crop can never include anything that isn't part of the picture.


Why crop_view?

  • 🎯 Draggable, resizable crop box with corner and edge handles.
  • 🔒 Locked aspect ratio (square, 16:9, …) or free-form — your choice.
  • 🔄 Pan, zoom and 90° rotation, with the box always clamped to the image.
  • 🖼️ JPEG (with a quality factor) or PNG output.
  • 🧩 Bring your own toolbar. No buttons, no strings, no icons baked in — it drops into any design system and needs no localization.
  • 🪶 Pure Dart & Flutter. No platform channels, no JavaScript. It behaves identically on Android, iOS, web, Windows, macOS and Linux — and, unlike croppers built on a browser library, its crop maths are unit-tested.

Installation

dependencies:
  crop_view: ^0.1.0
import 'package:crop_view/crop_view.dart';

Quick start

class EditPhoto extends StatefulWidget {
  const EditPhoto({super.key, required this.bytes});
  final Uint8List bytes;

  @override
  State<EditPhoto> createState() => _EditPhotoState();
}

class _EditPhotoState extends State<EditPhoto> {
  // Pass an aspectRatio to lock the shape (1 = square); omit it for a free crop.
  final controller = CropController(aspectRatio: 1);

  @override
  void dispose() {
    controller.dispose(); // always dispose the controller
    super.dispose();
  }

  Future<void> _done() async {
    final Uint8List? jpeg = await controller.crop(
      maxEdge: 1080,
      format: const CropFormat.jpeg(quality: 85),
    );
    if (jpeg != null && mounted) Navigator.pop(context, jpeg);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: CropView(imageBytes: widget.bytes, controller: controller),
          ),
          // Your toolbar — style it however you like.
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              IconButton(onPressed: controller.rotateLeft,  icon: const Icon(Icons.rotate_left)),
              IconButton(onPressed: controller.rotateRight, icon: const Icon(Icons.rotate_right)),
              IconButton(onPressed: controller.zoomIn,      icon: const Icon(Icons.zoom_in)),
              IconButton(onPressed: controller.zoomOut,     icon: const Icon(Icons.zoom_out)),
              IconButton(onPressed: controller.reset,       icon: const Icon(Icons.restore)),
              FilledButton(onPressed: _done, child: const Text('Done')),
            ],
          ),
        ],
      ),
    );
  }
}

💡 The widget draws only the image, the crop box and its handles. Everything else — buttons, labels, layout — is yours, so it matches your app instead of fighting it.

Recipes

Free-form crop (any shape)

final controller = CropController(); // no aspectRatio

A specific ratio

CropController(aspectRatio: 16 / 9); // widescreen
CropController(aspectRatio: 3 / 4);  // portrait

PNG output (keeps transparency)

final png = await controller.crop(
  maxEdge: 2048,
  format: const CropFormat.png(),
);

JPEG has no alpha channel, so transparent pixels are composited onto white; PNG keeps them transparent.

Get a dart:ui image instead of bytes

final ui.Image image = (await controller.cropToImage(maxEdge: 1024))!;
// …use it, then image.dispose();

React to state (enable/disable your buttons)

CropController is a ChangeNotifier:

AnimatedBuilder(
  animation: controller,
  builder: (_, __) => IconButton(
    onPressed: controller.isTransformed ? controller.reset : null,
    icon: const Icon(Icons.restore),
  ),
);

Supply an already-decoded image, or decode your own way

CropView(
  imageBytes: bytes,
  controller: controller,
  decoder: (bytes) async => myAlreadyDecodedUiImage,
);

API at a glance

CropView The crop surface. Props for colours, maxDecodedDimension, loadingBuilder, errorBuilder, decoder.
CropController rotateLeft · rotateRight · zoomIn · zoomOut · reset · crop · cropToImage · isReady · isTransformed.
CropFormat CropFormat.jpeg(quality: 85) · CropFormat.png().

crop's result has its longest side capped at maxEdge and is never upscaled past the cropped region's own resolution. Large sources are downscaled to CropView.maxDecodedDimension (2160px default) while decoding, to bound memory.

Notes

  • CropController.aspectRatio is fixed for the controller's life — create a new controller for a different ratio.
  • Always dispose() the controller with your widget.

Example

A complete, runnable example lives in example/.

License

MIT © srad

Libraries

crop_view
An image cropper for Flutter with a draggable crop box.