Optimisation Required / Feature Request
Hi, sorry this post has become very large, but I've spent all evening digging into this and would love to see improvements in this brilliantly useful library.
My issue mostly pertains to the fact that the library appears to not use byte arrays for images, and instead handles things mostly using data urls i.e. throwing lots of very large base64 strings around.
I apologise if I'm wrong with that analysis - I have looked into a lot of the code, and have been trying a lot of things with it in my own project, but it seems to have optimisation issues that I was unable to resolve with any of the existing Cropper library methods.
So this is what I've noticed / would like to suggest:
GetCroppedCanvasDataURLAsync() is far too slow.
Tested with image size >2MB, dimensions 1900 x 700
Not sure what's causing this. I've found the window.getPolygonImage JS method which appears to be where the work is done. Something there is presumably in need of optimisation.
Currently I'm using my own code to handle the raw byte data, and it is a LOT faster.
It takes the raw image bytes, along with Cropper.Blazor.Models.CropBoxData and Cropper.Blazor.Models.ContainerData, then crops the image almost instantly.
export async function cropImage(
imageBytes: Uint8Array,
cropBox: CropBoxData,
container: ContainerData
): Promise<Uint8Array> {
try {
const blob = new Blob([imageBytes], { type: "image/png" });
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.src = imageUrl;
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
const croppingCanvas = document.createElement("canvas");
const context = croppingCanvas.getContext("2d");
if (!context) {
throw new Error("Unable to get canvas context");
}
// Calculate uniform scale factor based on image fit within container
const scale = Math.min(container.width / img.naturalWidth, container.height / img.naturalHeight);
// Calculate the offsets for centering the image within the container
const offsetX = (container.width - img.naturalWidth * scale) / 2;
const offsetY = (container.height - img.naturalHeight * scale) / 2;
// Adjust crop box coordinates from container space to image space
const imageCropBox = {
left: (cropBox.left - offsetX) / scale,
top: (cropBox.top - offsetY) / scale,
width: cropBox.width / scale,
height: cropBox.height / scale,
};
// Set the cropping canvas size to match the cropped region
croppingCanvas.width = imageCropBox.width;
croppingCanvas.height = imageCropBox.height;
// Draw the cropped portion of the image onto the cropping canvas
context.drawImage(
img,
imageCropBox.left,
imageCropBox.top,
imageCropBox.width,
imageCropBox.height,
0,
0,
imageCropBox.width,
imageCropBox.height
);
const resizedBlob = await new Promise<Blob | null>((resolve) => {
croppingCanvas?.toBlob(
(blob) => resolve(blob),
'image/jpeg',
1
);
});
if (resizedBlob) {
return new Uint8Array(await resizedBlob.arrayBuffer());
}
else {
throw new Error("Resized blob empty");
}
} catch (error) {
console.error("Error cropping image:", error);
return new Uint8Array();
}
}
Return image as a byte array
To accompany the above, it would be good to have a C# method in the library that returns the canvas data as a Uint8Array object so it can be handled as a byte array in C#.
I've tried searching but can't find one.
So currently I'm using my own custom code for this too, i.e.
public async Task<byte[]> CropImage(byte[] image, CropBoxData cropBox, ContainerData container)
{
var module = await moduleTask.Value;
return await module.InvokeAsync<byte[]>("cropImage", image, cropBox, container);
}
GetCroppedCanvasDataURLAsync() locks the UI.
This even happens in the demo page so I'm sure it's not just an issue I'm facing.
Would it be possible to perform the image processing in the the background? i.e. not any await on the main function, then use DotNet.invokeMethodAsync to send the completed data back.
You could expose an event handler service for this so users can handle it without locking the UI, i.e.
// Cropper JsInterop
async function getCroppedCanvasDataURL() : Promise<void> {
const dataUrl = await doStufftoGetDataUrl();
DotNet.invokeMethodAsync('[Cropper Assembly Name]', 'DataUrlProcessingComplete', dataUrl );
}
// A Cropper message handler
public class CropperService : ICropperService
{
private event Action<string> OnDataUrlProcessingComplete;
[JSInvokable]
public static async Task DataUrlProcessingComplete(string dataUrl)
{
this.OnDataUrlProcessingComplete?.Invoke(dataUrl);
}
}
/// MyComponent.razor
[Inject]
public ICropperService CropperService { get; set; }
protect override OnInitalized()
{
CropperService.OnDataUrlProcessingComplete += DoStuffWithDataUrl;
CropperService.OnImageBytesProcessingComplete += DoStuffWithImageBytes;
}
Task DoStuffWithDataUrl(string dataUrl)
{
// stuff
}
Task DoStuffWithImageBytes(byte[] imageBytes)
{
// stuff
}
Thanks! And sorry for the wall of text, and sorry if I've missed something and all of my commentary is incorrect!
Hi @Stuart88, I'll look into your approach, thanks!
@Stuart88 Have you ever wondered how to handle a case where the image is still being prepared in 'getCroppedCanvasDataURL' method but the cropper component has been destroyed?
@Stuart88 Have you ever wondered how to handle a case where the image is still being prepared in 'getCroppedCanvasDataURL' method but the cropper component has been destroyed?
I haven't looked back at the Cropper code in my project since I made this post, so I don't have any specific thoughts sorry!
However in general I think a way to prevent data being lost if the component is destroyed, is to use a separate service (probably a singleton) to hold onto any data so it can persist a little longer if needed.
Or maybe if the component implements IDisposableAsync it might be possible to prevent disposal until the canvas data processing is complete. I'm not 100% sure about it though.
Hi @Stuart88 could you provided your opinion regarding based on my PoC: https://github.com/CropperBlazor/Cropper.Blazor/pull/402? It seems that the UI does not block during image processing and the performance is several times better on the working machine. I only see UI blocking when I want to draw the processed image in the mudblazor image component. Now we can get a picture over 100mb based on my tests!
Button for testing:
@MaxymGorn if the UI isn't locked, and the performance is faster, that sounds like a win to me!