extended_image icon indicating copy to clipboard operation
extended_image copied to clipboard

Crop on web improvement

Open SeriousMonk opened this issue 3 years ago • 18 comments

Using the image library on web to apply the transformations is slow and freezes the UI. Using the following method instead of "cropImageDataWithDartLibrary" takes significantly less time and doesn't freeze the UI:

Future<Uint8List?> cropImageDataWithHtmlCanvas(
      {required ExtendedImageEditorState state}) async {
    ///crop rect base on raw image
    final Rect? cropRect = state.getCropRect();

    final Uint8List image = state.rawImageData;

    final EditActionDetails editAction = state.editAction!;

    String? mimeType = lookupMimeType('', headerBytes: image);

    String img64 = base64Encode(image);
    html.ImageElement myImageElement = html.ImageElement();
    myImageElement.src = 'data:$mimeType;base64,$img64';

    await myImageElement.onLoad.first; // allow time for browser to render

    html.CanvasElement myCanvas;
    html.CanvasRenderingContext2D ctx;

    ///If cropping is needed create a canvas of the size of the cropped image
    ///else create a canvas of the size of the original image
    if(editAction.needCrop)
      myCanvas = html.CanvasElement(width: cropRect!.width.toInt(), height: cropRect.height.toInt());
    else
      myCanvas = html.CanvasElement(width: myImageElement.width, height: myImageElement.height);

    ctx = myCanvas.context2D;

    int drawWidth = myCanvas.width!, drawHeight = myCanvas.height!;

    ///This invert flag will be true if the image has been rotated 90 or 270 degrees
    ///if that happens draWidth and drawHeight will have to be inverted
    ///and Flip.vertical and Flip.horizontal will have to be swapped
    bool invert = false;
    if (editAction.hasRotateAngle) {
      if(editAction.rotateAngle == 90 || editAction.rotateAngle == 270){
        int tmp = myCanvas.width!;
        myCanvas.width = myCanvas.height;
        myCanvas.height = tmp;

        drawWidth = myCanvas.height!;
        drawHeight = myCanvas.width!;
        invert = true;
      }

      ctx.translate(myCanvas.width!/2, myCanvas.height!/2);
      ctx.rotate(editAction.rotateAngle * pi / 180);
    }else{
      ctx.translate(myCanvas.width!/2, myCanvas.height!/2);
    }

    ///By default extended_image associates
    ///editAction.flipY == true => Flip.horizontal and
    ///editAction.flipX == true => Flip.vertical
    if (editAction.needFlip) {
      late Flip mode;
      if (editAction.flipY && editAction.flipX) {
        mode = Flip.both;
      } else if (editAction.flipY) {
        if(invert)
          mode = Flip.vertical;
        else
          mode = Flip.horizontal;
      } else if (editAction.flipX) {
        if(invert)
          mode = Flip.horizontal;
        else
          mode = Flip.vertical;
      }

      ///ctx.scale() multiplicates its values to the drawWidth and drawHeight
      ///in ctx.drawImageScaledFromSource
      ///so applying ctx.scale(-1, 1) is like saying -drawWidth which means
      ///flip horizontal
      switch(mode){
        case Flip.horizontal:
          if(invert)
            ctx.scale(1, -1);
          else
            ctx.scale(-1, 1);
          break;
        case Flip.vertical:
          if(invert)
            ctx.scale(-1, 1);
          else
            ctx.scale(1, -1);
          break;
        case Flip.both:
          ctx.scale(-1, -1);
          break;
      }
    }

    ctx.drawImageScaledFromSource(
      myImageElement,
      cropRect!.left,
      cropRect.top,
      cropRect.width,
      cropRect.height,
      -drawWidth/2,
      -drawHeight/2,
      drawWidth,
      drawHeight,
    );

    return await imageService.getBlobData(await myCanvas.toBlob(mimeType ?? 'image/jpeg'));
  }

SeriousMonk avatar Sep 30 '21 13:09 SeriousMonk

Keep in mind this is not fully tested and may contain some bugs. But it applies the transformations in about 2 seconds and doesn't freeze UI.

SeriousMonk avatar Sep 30 '21 13:09 SeriousMonk

I'm not the author, but happy for any improvements. See related code: #394

Importantly, your decode logic can be simplified (and made generic) while keeping speed improvements

nwparker avatar Oct 03 '21 07:10 nwparker

@FRANIAZA what this function return ? :- getBlobData(await myCanvas.toBlob(mimeType ?? 'image/jpeg')) and how can I access imageService here.

am1tr0r avatar Oct 20 '21 16:10 am1tr0r

@FRANIAZA what this function return ? :- getBlobData(await myCanvas.toBlob(mimeType ?? 'image/jpeg')) and how can I access imageService here.

Right, sorry. getBlobData is the following function, which I happened to place in a class called ImageService.

Future<Uint8List> getBlobData(html.Blob blob) {
    final completer = Completer<Uint8List>();
    final reader = html.FileReader();
    reader.readAsArrayBuffer(blob);
    reader.onLoad.listen((_) => completer.complete(reader.result as Uint8List));
    return completer.future;
  }

SeriousMonk avatar Oct 20 '21 16:10 SeriousMonk

yeah Thanks! @FRANIAZA it takes comparatively very less time on web.

am1tr0r avatar Oct 20 '21 17:10 am1tr0r

Actually, this doesn't work on Web

final Uint8List image = state.rawImageData;

Right?

ViniciusSossela avatar Jan 10 '22 17:01 ViniciusSossela

I believe it does. What doesn't work on Web are file paths, maybe you're referring to that?

SeriousMonk avatar Jan 10 '22 19:01 SeriousMonk

I am not.

I'm using exactly the same code you wrote and running on the web... Has an issue with using rawImageData on WEB

// in web, we can't get rawImageData due to . https://github.com/fluttercandies/extended_image/blob/master/example/lib/common/utils/crop_editor_helper.dart

https://github.com/flutter/flutter/issues/44908

ViniciusSossela avatar Jan 10 '22 19:01 ViniciusSossela

I tested it just now and it works fine. I am using extended_image: ^5.1.2 and Flutter 2.5.3. It could be some version incompatibility. I'll try updating to check whether that's the problem

SeriousMonk avatar Jan 11 '22 09:01 SeriousMonk

Tested with flutter 2.8.1 and extended_image: ^6.0.1 -> still works

SeriousMonk avatar Jan 11 '22 09:01 SeriousMonk

If you would like I could send you the dart file of my cropping page via email to provide a complete usage example

SeriousMonk avatar Jan 11 '22 10:01 SeriousMonk

guys, is it me, or when we crop it becomes smaller and with smaller image quality, for the flutter candies way @SeriousMonk

BLUECALF avatar Jan 07 '23 16:01 BLUECALF

guys, is it me, or when we crop it becomes smaller and with smaller image quality, for the flutter candies way @SeriousMonk

Of course. That's how cropping works.

SeriousMonk avatar Jan 09 '23 13:01 SeriousMonk

but I cropped a 600 dpi image to a 500 * 500 pixels square. But when i get the data and save it I get my same image as a 382 *382 pixels and 96 dpi. main question is why is it not 500 * 500 pixels.

BLUECALF avatar Jan 09 '23 17:01 BLUECALF

but I cropped a 600 dpi image to a 500 * 500 pixels square. But when i get the data and save it I get my same image as a 382 *382 pixels and 96 dpi. main question is why is it not 500 * 500 pixels.

That seems like a problem with your integration of the image cropper. This function simply takes the data from the ExtendedImageEditorState and applies the changes. You could try to print the cropRect.width and cropRect.height to make sure you are passing the correct data to the method.

SeriousMonk avatar Jan 09 '23 18:01 SeriousMonk

yes crop rect,size keeps on changing

BLUECALF avatar Jan 09 '23 18:01 BLUECALF

@SeriousMonk thank you, I updated the code to XFile which makes process easier.

import 'dart:async';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui';

import 'package:cross_file/cross_file.dart';
import 'package:extended_image/extended_image.dart';
import 'package:image/image.dart' as img;

class ImageExport {
  const ImageExport();

  Future<html.ImageElement> createHtmlImageElement(String path) {
    final completer = Completer<html.ImageElement>();

    final imageElement = html.ImageElement();

    imageElement.onError.first.then((value) {
      completer.completeError(Exception('Could not load Image'));
    });

    imageElement.onLoad.first.then((value) {
      completer.complete(imageElement);
    });

    imageElement.src = path;

    return completer.future;
  }

  Future<Uint8List> export({
    required EditActionDetails editAction,
    required Rect cropRect,
    required Uint8List source,
    int quality = 100,
  }) async {
    final imageElement = await createHtmlImageElement(XFile.fromData(source).path);

    html.CanvasElement canvas;

    /// If cropping is needed create a canvas of the size of the cropped image
    /// else create a canvas of the size of the original image
    if (editAction.needCrop) {
      canvas = html.CanvasElement(width: cropRect.width.toInt(), height: cropRect.height.toInt());
    } else {
      canvas = html.CanvasElement(width: imageElement.width, height: imageElement.height);
    }

    final ctx = canvas.context2D;

    var drawWidth = canvas.width!;
    var drawHeight = canvas.height!;

    /// This invert flag will be true if the image has been rotated 90 or 270 degrees
    /// if that happens draWidth and drawHeight will have to be inverted
    /// and Flip.vertical and Flip.horizontal will have to be swapped
    var invert = false;
    if (editAction.hasRotateAngle) {
      if (editAction.rotateAngle == 90 || editAction.rotateAngle == 270) {
        final tmp = canvas.width!;
        canvas
          ..width = canvas.height
          ..height = tmp;

        drawWidth = canvas.height!;
        drawHeight = canvas.width!;
        invert = true;
      }

      ctx
        ..translate(canvas.width! / 2, canvas.height! / 2)
        ..rotate(editAction.rotateAngle * math.pi / 180);
    } else {
      ctx.translate(canvas.width! / 2, canvas.height! / 2);
    }

    /// By default extended_image associates
    /// editAction.flipY == true => img.FlipDirection.horizontal and
    /// editAction.flipX == true => img.FlipDirection.vertical
    if (editAction.needFlip) {
      late img.FlipDirection mode;
      if (editAction.flipY && editAction.flipX) {
        mode = img.FlipDirection.both;
      } else if (editAction.flipY) {
        if (invert) {
          mode = img.FlipDirection.vertical;
        } else {
          mode = img.FlipDirection.horizontal;
        }
      } else if (editAction.flipX) {
        if (invert) {
          mode = img.FlipDirection.horizontal;
        } else {
          mode = img.FlipDirection.vertical;
        }
      }

      /// ctx.scale() multiplicates its values to the drawWidth and drawHeight
      /// in ctx.drawImageScaledFromSource
      /// so applying ctx.scale(-1, 1) is like saying -drawWidth which means
      /// flip horizontal
      switch (mode) {
        case img.FlipDirection.horizontal:
          if (invert) {
            ctx.scale(1, -1);
          } else {
            ctx.scale(-1, 1);
          }
          break;
        case img.FlipDirection.vertical:
          if (invert) {
            ctx.scale(-1, 1);
          } else {
            ctx.scale(1, -1);
          }
          break;
        case img.FlipDirection.both:
          ctx.scale(-1, -1);
          break;
      }
    }

    ctx.drawImageScaledFromSource(
      imageElement,
      cropRect.left,
      cropRect.top,
      cropRect.width,
      cropRect.height,
      -drawWidth / 2,
      -drawHeight / 2,
      drawWidth,
      drawHeight,
    );

    final blob = await canvas.toBlob('image/jpeg', quality / 100);
    final path = html.Url.createObjectUrlFromBlob(blob);
    return XFile(path, mimeType: blob.type).readAsBytes();
  }
}

maRci002 avatar Apr 25 '23 11:04 maRci002