You are currently viewing How to Crop and Compress Images in Dart/Flutter

How to Crop and Compress Images in Dart/Flutter

  • Post author:
  • Post category:Flutter
  • Reading time:8 mins read

Image cropping becomes necessary in apps when we want to save bandwidth by removing unnecessary image parts. For example we want to upload a thumbnail and we don’t want it to be very large is size and high quality image.

So I started looking for plugins to crop and compress images in Flutter and there are pretty good plugins to accomplish this, but I preferred another way to crop my image. Here I will be talking about cropping images in the background without a UI. We don’t always ask user to crop their image, sometimes we need to do it ourselves without their interaction or intention.

We will use image, a dart package to manipulate images in a variety of different file formats. With this package we already have copyCrop(), copyCropCircle(), copyResize() and copyResizeCropSquare()but we may also need to center crop. So let’s do a center crop.

Calculate the rectangle to crop

Android Crop Center of Bitmap - Stack Overflow

First we need to calculate the rectangle to crop of desired aspect ratio. We want the crop rectangle to always be inside the original image. We are trying to get similar result as in Image widget with fit: BoxFit.cover.


/// Get Crop Rectangle from given sizes
Rectangle<int> getCropRect({
  required int sourceWidth,
  required int sourceHeight,
  required double aspectRatio,
}) {
  var left = 0;
  var top = 0;
  var width = sourceWidth;
  var height = sourceHeight;

  if (aspectRatio < sourceWidth / sourceHeight) {
    // crop horizontally, from width
    width = (sourceHeight * aspectRatio).toInt(); // set new cropped width
    left = (sourceWidth - width) ~/ 2;
  } else if (aspectRatio > sourceWidth / sourceHeight) {
    // crop vertically, from height
    height = sourceWidth ~/ aspectRatio; // set new cropped height
    top = (sourceHeight - height) ~/ 2;
  }
  // else source and destination have same aspect ratio

  return Rectangle<int>(left, top, width, height);
}

Center crop

Now we can use this to center crop the image. Don’t forget to import image package.

import 'package:image/image.dart' as img;
/// Center crop the source image with given aspectRatio
img.Image centerCrop(img.Image source, double aspectRatio) {
  final rect = getCropRect(
      sourceWidth: source.width,
      sourceHeight: source.height,
      aspectRatio: aspectRatio);

  return img.copyCrop(source, rect.left, rect.top, rect.width, rect.height);
}

Resize

That was easy, but we also want to resize the image and want smaller image don’t we?

/// Crops and resize the image with given aspectRatio and width
img.Image cropAndResize(img.Image src, double aspectRatio, int width) {
  final cropped = centerCrop(src, aspectRatio);
  final croppedResized = img.copyResize(
    cropped,
    width: width,
    interpolation: img.Interpolation.average,
  );
  return croppedResized;
}

What if your input image size is smaller than desired image size? It will upscale the input image. If you don’t want this you can always modify the code like this;

    cropped,
    width: width,
    interpolation: img.Interpolation.average,
  );
  if (cropped.width <= width) {
    // do not resize if cropped image is smaller than desired image size
    // to avoid upscaling
    return cropped;
  }
  return croppedResized;
}

Here you can change the interpolation that fits best for you. I have used average since it is good if you scale down image by large factor. You can check the GitHub issue here for more detail. https://github.com/brendan-duncan/image/issues/23

Combining all these methods

Future<File> cropAndResizeFile({
  required File file,
  required double aspectRatio,
  required int width,
  int quality = 100,
}) async {
  final tempDir = await getTemporaryDirectory();
  final destPath = path.join(
    tempDir.path,
    path.basenameWithoutExtension(file.path) + '_compressed.jpg',
  );

  return compute<_CropResizeArgs, File>(
      _cropAndResizeFile,
      _CropResizeArgs(
        sourcePath: file.path,
        destPath: destPath,
        aspectRatio: aspectRatio,
        width: width,
        quality: quality,
      ));
}

class _CropResizeArgs {
  final String sourcePath;
  final String destPath;
  final double aspectRatio;
  final int width;
  final int quality;
  _CropResizeArgs({
    required this.sourcePath,
    required this.destPath,
    required this.aspectRatio,
    required this.width,
    required this.quality,
  });
}

Future<File> _cropAndResizeFile(_CropResizeArgs args) async {
  final image =
      await img.decodeImage(await File(args.sourcePath).readAsBytes());

  if (image == null) throw Exception('Unable to decode image from file');

  final croppedResized = cropAndResize(image, args.aspectRatio, args.width);
  // Encoding image to jpeg to compress the image. 
  final jpegBytes = img.encodeJpg(croppedResized, quality: args.quality);

  final croppedImageFile = await File(args.destPath).writeAsBytes(jpegBytes);
  return croppedImageFile;
}

You may wonder why using _CropResizeArgs? It is used to pass data to compute function. And why using compute function? If you process resource intensive task on main isolate then app will jank. If you don’t know term jank then it is lag and unresponsiveness in simpler term. In the case of image picking jank will cause a black screen after you picked your image. It is because main isolate have to process all the image decoding, cropping, resizing and encoding after you picked the image. You can see more about them here, https://dart.dev/guides/language/concurrency , https://docs.flutter.dev/cookbook/networking/background-parsing.

Use it

TextButton(
      onPressed: () async {
        // resizing image with image picker decreases quality
        final _pickedFile = await picker.pickImage(
          source: ImageSource.gallery,
        );

        // if user cancels file picking process do nothing
        if (_pickedFile == null) return;

        final length = await _pickedFile!.length() / 1024;
        log('Picked File Size in KB $length');

        await cropAndResizeFile(
          file: File(_pickedFile!.path),
          aspectRatio: 1.6,
          width: 800,
          quality: 80,
        ).then((value) async {
          _croppedFile = value;

          final length = await value.length() / 1024;
          log('Compressed File Size in KB $length');
        });
      },
      child: const Text('Pick an image'),
    )

Changing aspect ratio, width and quality we can get center cropped image with given quality and compression. We can also resize (maintaining aspect ratio) and compress images with image picker plugin. I didn’t use it here because I got terribly bad quality image after resizing and compressing. Also each time we decode and re-encode jpeg some data will be lost and it cause degraded image quality. To avoid this we decode the original image once and do all the stuffs then re-encode it to final image.