extended_image
extended_image copied to clipboard
Crop on web improvement
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'));
}
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.
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
@FRANIAZA what this function return ? :- getBlobData(await myCanvas.toBlob(mimeType ?? 'image/jpeg')) and how can I access imageService here.
@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;
}
yeah Thanks! @FRANIAZA it takes comparatively very less time on web.
Actually, this doesn't work on Web
final Uint8List image = state.rawImageData;
Right?
I believe it does. What doesn't work on Web are file paths, maybe you're referring to that?
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
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
Tested with flutter 2.8.1 and extended_image: ^6.0.1 -> still works
If you would like I could send you the dart file of my cropping page via email to provide a complete usage example
guys, is it me, or when we crop it becomes smaller and with smaller image quality, for the flutter candies way @SeriousMonk
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.
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.
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.
yes crop rect,size keeps on changing
@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();
}
}