Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crop on web improvement #407

Open
SeriousMonk opened this issue Sep 30, 2021 · 17 comments
Open

Crop on web improvement #407

SeriousMonk opened this issue Sep 30, 2021 · 17 comments
Labels
help wanted Extra attention is needed

Comments

@SeriousMonk
Copy link

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
Copy link
Author

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.

@nwparker
Copy link

nwparker commented Oct 3, 2021

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

@zmtzawqlp zmtzawqlp added the enhancement New feature or request label Oct 7, 2021
@am1tr0r
Copy link

am1tr0r commented Oct 20, 2021

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

@SeriousMonk
Copy link
Author

@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;
  }

@am1tr0r
Copy link

am1tr0r commented Oct 20, 2021

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

@ViniciusSossela
Copy link

Actually, this doesn't work on Web

final Uint8List image = state.rawImageData;

Right?

@SeriousMonk
Copy link
Author

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

@ViniciusSossela
Copy link

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

flutter/flutter#44908

@zmtzawqlp zmtzawqlp added help wanted Extra attention is needed and removed enhancement New feature or request labels Jan 11, 2022
@SeriousMonk
Copy link
Author

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
Copy link
Author

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

@SeriousMonk
Copy link
Author

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

@BLUECALF
Copy link

BLUECALF commented Jan 7, 2023

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

@SeriousMonk
Copy link
Author

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.

@BLUECALF
Copy link

BLUECALF commented Jan 9, 2023

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.

@SeriousMonk
Copy link
Author

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.

@BLUECALF
Copy link

BLUECALF commented Jan 9, 2023

yes crop rect,size keeps on changing

@maRci002
Copy link

maRci002 commented Apr 25, 2023

@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();
  }
}

@zmtzawqlp zmtzawqlp pinned this issue Jun 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

7 participants