Search code examples
imagecanvasfabricjs

Applying filters to large images using FabricJS


I'm trying to apply a filter to a large image in FabricJS.

The image is about 3000 x 4000 which exceeds the default 2048 texture size. I could increase this to 4096 and it would work for this image.

Currently when the filter is applied the image is cut. I have tried reducing the size of the image using the following:

let obj = this.canvas.getActiveObject();
if (Math.max(obj.width, obj.height) > 2048) {
  let scale = 2048 / Math.max(obj.width, obj.height);
  obj.filters.push(
    new fabric.Image.filters.Resize({ scaleX: scale, scaleY: scale })
  );
}
obj.filters.push(new fabric.Image.filters.Grayscale());
obj.applyFilters();
this.canvas.renderAll();

This however did not work for me. The image is still cut and rotated.

This is how I am adding my image to the canvas:

//in a parent component
let url = URL.createObjectURL(e.target.files[0]);
fabric.Image.fromURL(url, (image) => {
  this.setState({
    image,
  });
});

//following is in a child component
let canvasWidth = this.canvasWrapperRef.current.clientWidth;
let canvasHeight = this.canvasWrapperRef.current.clientHeight;
//create canvas
this.canvas = new fabric.Canvas("canvas", {
  selection: false,
  backgroundColor: "black",
  hoverCursor: "context-menu",
  height: canvasHeight,
  width: canvasWidth,
});
let image = this.props.image;
image.set({ selectable: false });
this.canvas.centerObject(image);
this.canvas.setActiveObject(image);
this.canvas.add(image);
this.canvas.renderAll();

Is there a way I can reduce the size/quality of the original image so I can apply filters to it?


Solution

  • It took me a few days but I finally figured out a solution to the issue I was having. It seems that when you resize the image using fabric, it retains the original size. To get around this, I added the image to a canvas of a size that can handle the filters, then exported that canvas as an image and added it to fabric in the usual way.

    This seems like a bit of a hacky solution to me, it works but if anyone knows of a better solution please let me know.

    //handle the image uploading process
    handleImageUpload = async (e) => {
      if (e.target.files.length < 1) return;
      let objectUrl = URL.createObjectURL(e.target.files[0]);
      //resize if needed
      let imageUrl = await this.resizeImage(this.textureSize, objectUrl);
      fabric.Image.fromURL(imageUrl, (image) => {
        this.setState({
          image,
        });
      });
    };
    
    //resize the image if one of the sides of the image exceeds the max texture size
    //if the image requires resizing, create a canvas element but don't attach to DOM
    //size the canvas with the largest size being equal to the max texture size
    //then scale the image down to the correct size when adding to the canvas
    resizeImage = (maxSize, imageUrl) => {
      return new Promise((resolve) => {
        let image = new Image();
        image.src = imageUrl;
        image.onload = (img) => {
          //check if resizing is required
          if (Math.max(img.target.width, img.target.height) > maxSize) {
            //create canvas
            let canvas = document.createElement("canvas");
            //scale image
            if (img.target.height >= img.target.width) {
              canvas.height = maxSize;
              canvas.width = (maxSize / img.target.height) * img.target.width;
            } else {
              canvas.width = maxSize;
              canvas.height = (maxSize / img.target.width) * img.target.height;
            }
            //draw to canvas
            let context = canvas.getContext("2d");
            context.drawImage(img.target, 0, 0, canvas.width, canvas.height);
            //assign new image url
            resolve(context.canvas.toDataURL());
          }
          resolve(imageUrl);
        };
      });
    };
    

    I am using 2048 as my max texture size. Anything above this and the filters start to cut and rotate the image. I think this is determined by hardware, I am using 2048 as a relatively safe limit. See here for further information: http://fabricjs.com/fabric-filters

    When you are scaling down the image using the above method you can lose quality in your image. A better solution might be to use fabrics built in method of scaling the image down (I have not tested this but I would imagine fabric would handle the rescaling better than the above method), then exporting and re-importing similar to the above. However as this is just a personal project with the purpose of learning react I am happy to take the hit on the image quality and display a warning that large image sizes are resized and may have a loss in quality.