157

I'm trying to resize some images with canvas but I'm clueless on how to smoothen them. On photoshop, browsers etc.. there are a few algorithms they use (e.g. bicubic, bilinear) but I don't know if these are built into canvas or not.

Here's my fiddle: http://jsfiddle.net/EWupT/

var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width=300
canvas.height=234
ctx.drawImage(img, 0, 0, 300, 234);
document.body.appendChild(canvas);

The first one is a normal resized image tag, and the second one is canvas. Notice how the canvas one is not as smooth. How can I achieve 'smoothness'?

2
  • Is the problem in blurred result in canvas compared to <img />? If so it's likely you have retina screen (with DPR >= 2) and you can scale up canvas to achieve a similar result as seen in <img />, for example like this — jsfiddle.net/c8uo3pda/4 Commented Mar 17, 2021 at 9:06
  • See also "Resizing an image in an HTML5 canvas": stackoverflow.com/q/2303690/1066234 Commented Apr 17, 2023 at 6:43

14 Answers 14

181

You can use down-stepping to achieve better results. Most browsers seem to use linear interpolation rather than bi-cubic when resizing images.

(Update There has been added a quality property to the specs, imageSmoothingQuality which is currently available in Chrome only.)

Unless one chooses no smoothing or nearest neighbor the browser will always interpolate the image after down-scaling it as this function as a low-pass filter to avoid aliasing.

Bi-linear uses 2x2 pixels to do the interpolation while bi-cubic uses 4x4 so by doing it in steps you can get close to bi-cubic result while using bi-linear interpolation as seen in the resulting images.

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var img = new Image();

img.onload = function () {

    // set size proportional to image
    canvas.height = canvas.width * (img.height / img.width);

    // step 1 - resize to 50%
    var oc = document.createElement('canvas'),
        octx = oc.getContext('2d');

    oc.width = img.width * 0.5;
    oc.height = img.height * 0.5;
    octx.drawImage(img, 0, 0, oc.width, oc.height);

    // step 2
    octx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5);

    // step 3, resize to final size
    ctx.drawImage(oc, 0, 0, oc.width * 0.5, oc.height * 0.5,
    0, 0, canvas.width, canvas.height);
}
img.src = "//i.imgur.com/SHo6Fub.jpg";
<img src="//i.imgur.com/SHo6Fub.jpg" width="300" height="234">
<canvas id="canvas" width=300></canvas>

Depending on how drastic your resize is you can might skip step 2 if the difference is less.

In the demo you can see the new result is now much similar to the image element.

Sign up to request clarification or add additional context in comments.

9 Comments

@steve heh, sometimes these things happen :) For images you can usually override this by setting a css BTW.
Ken, the first result worked great but when I change the images, you can see it being too blurry jsfiddle.net/kcHLG What can be done in this case and others?
@steve you can reduce the number of steps to just 1 or none (for some images this works fine). See also this answer which is similar to this but here I added a sharpen convolution to it so you can make the resulting image sharper after it has been downscaled.
@steve here is an modified fiddle with Bill using only one extra step: jsfiddle.net/AbdiasSoftware/kcHLG/1
@neaumusic the code is a continuation of OPs code. If you open the fiddle you'll see ctx being defined. I have inlined it here to avoid misunderstandings.
|
88

Since Trung Le Nguyen Nhat's fiddle isn't correct at all (it just uses the original image in the last step)
I wrote my own general fiddle with performance comparison:

FIDDLE

Basically it's:

img.onload = function() {
   var canvas = document.createElement('canvas'),
       ctx = canvas.getContext("2d"),
       oc = document.createElement('canvas'),
       octx = oc.getContext('2d');

   canvas.width = width; // destination canvas size
   canvas.height = canvas.width * img.height / img.width;

   var cur = {
     width: Math.floor(img.width * 0.5),
     height: Math.floor(img.height * 0.5)
   }

   oc.width = cur.width;
   oc.height = cur.height;

   octx.drawImage(img, 0, 0, cur.width, cur.height);

   while (cur.width * 0.5 > width) {
     cur = {
       width: Math.floor(cur.width * 0.5),
       height: Math.floor(cur.height * 0.5)
     };
     octx.drawImage(oc, 0, 0, cur.width * 2, cur.height * 2, 0, 0, cur.width, cur.height);
   }

   ctx.drawImage(oc, 0, 0, cur.width, cur.height, 0, 0, canvas.width, canvas.height);
}

5 Comments

Most underrated answer ever seen.
This should be the accepted answer. Just make sure you replace "width" with your desired width value.
Note also that this method will create artifacts with transparent images, as later calls to octx.drawImage will not completely override the pixels in the smaller rectangle
i have question so here, at the end you end up with a canvas which has these overlapping images drawn one over the other. and when you try to download it this whole overlapping thing gets download as all of this is on one canvas. i want to know how can i use this optimization and endup with a canvas of just the final target image size.
@SaumyaRanjanNayak you probably need to do this before calling drawImage() in the while loop: stackoverflow.com/a/2142549/2336212
19

I created a reusable Angular service to handle high quality resizing of images / canvases for anyone who's interested: https://gist.github.com/transitive-bullshit/37bac5e741eaec60e983

The service includes two solutions because they both have their own pros / cons. The lanczos convolution approach is higher quality at the cost of being slower, whereas the step-wise downscaling approach produces reasonably antialiased results and is significantly faster.

Example usage:

angular.module('demo').controller('ExampleCtrl', function (imageService) {
  // EXAMPLE USAGE
  // NOTE: it's bad practice to access the DOM inside a controller, 
  // but this is just to show the example usage.

  // resize by lanczos-sinc filter
  imageService.resize($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })

  // resize by stepping down image size in increments of 2x
  imageService.resizeStep($('#myimg')[0], 256, 256)
    .then(function (resizedImage) {
      // do something with resized image
    })
})

2 Comments

Sorry about that - I had changed my github username. Just updated the link gist.github.com/transitive-bullshit/37bac5e741eaec60e983
I saw the word angular, I got that funny feeling
16

I don't understand why nobody is suggesting createImageBitmap.

createImageBitmap(
    document.getElementById('image'), 
    { resizeWidth: 300, resizeHeight: 234, resizeQuality: 'high' }
)
.then(imageBitmap => 
    document.getElementById('canvas').getContext('2d').drawImage(imageBitmap, 0, 0)
);

works beautifully (assuming you set ids for image and canvas).

3 Comments

Because It's not widely supported caniuse.com/#search=createImageBitmap
createImageBitmap is supported for 73% of all users. Depending on your use case, it may be good enough. It's just Safari that refuses to add support for it. I think it's worth mentioning as a possible solution.
Nice solution, but it doesn't work on firefox unfortunately
14

While some of those code-snippets are short and working, they aren't trivial to follow and understand.

As i am not a fan of "copy-paste" from stack-overflow, i would like developers to understand the code they are push into they software, hope you'll find the below useful.

DEMO: Resizing images with JS and HTML Canvas Demo fiddler.

You may find 3 different methods to do this resize, that will help you understand how the code is working and why.

https://jsfiddle.net/1b68eLdr/93089/

Full code of both demo, and TypeScript method that you may want to use in your code, can be found in the GitHub project.

https://github.com/eyalc4/ts-image-resizer

This is the final code:

export class ImageTools {
base64ResizedImage: string = null;

constructor() {
}

ResizeImage(base64image: string, width: number = 1080, height: number = 1080) {
    let img = new Image();
    img.src = base64image;

    img.onload = () => {

        // Check if the image require resize at all
        if(img.height <= height && img.width <= width) {
            this.base64ResizedImage = base64image;

            // TODO: Call method to do something with the resize image
        }
        else {
            // Make sure the width and height preserve the original aspect ratio and adjust if needed
            if(img.height > img.width) {
                width = Math.floor(height * (img.width / img.height));
            }
            else {
                height = Math.floor(width * (img.height / img.width));
            }

            let resizingCanvas: HTMLCanvasElement = document.createElement('canvas');
            let resizingCanvasContext = resizingCanvas.getContext("2d");

            // Start with original image size
            resizingCanvas.width = img.width;
            resizingCanvas.height = img.height;


            // Draw the original image on the (temp) resizing canvas
            resizingCanvasContext.drawImage(img, 0, 0, resizingCanvas.width, resizingCanvas.height);

            let curImageDimensions = {
                width: Math.floor(img.width),
                height: Math.floor(img.height)
            };

            let halfImageDimensions = {
                width: null,
                height: null
            };

            // Quickly reduce the dize by 50% each time in few iterations until the size is less then
            // 2x time the target size - the motivation for it, is to reduce the aliasing that would have been
            // created with direct reduction of very big image to small image
            while (curImageDimensions.width * 0.5 > width) {
                // Reduce the resizing canvas by half and refresh the image
                halfImageDimensions.width = Math.floor(curImageDimensions.width * 0.5);
                halfImageDimensions.height = Math.floor(curImageDimensions.height * 0.5);

                resizingCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                    0, 0, halfImageDimensions.width, halfImageDimensions.height);

                curImageDimensions.width = halfImageDimensions.width;
                curImageDimensions.height = halfImageDimensions.height;
            }

            // Now do final resize for the resizingCanvas to meet the dimension requirments
            // directly to the output canvas, that will output the final image
            let outputCanvas: HTMLCanvasElement = document.createElement('canvas');
            let outputCanvasContext = outputCanvas.getContext("2d");

            outputCanvas.width = width;
            outputCanvas.height = height;

            outputCanvasContext.drawImage(resizingCanvas, 0, 0, curImageDimensions.width, curImageDimensions.height,
                0, 0, width, height);

            // output the canvas pixels as an image. params: format, quality
            this.base64ResizedImage = outputCanvas.toDataURL('image/jpeg', 0.85);

            // TODO: Call method to do something with the resize image
        }
    };
}}

Comments

5

I created a library that allows you to downstep any percentage while keeping all the color data.

https://github.com/danschumann/limby-resize/blob/master/lib/canvas_resize.js

That file you can include in the browser. The results will look like photoshop or image magick, preserving all the color data, averaging pixels, rather than taking nearby ones and dropping others. It doesn't use a formula to guess the averages, it takes the exact average.

1 Comment

I would probably use webgl to resize now
5

Based on K3N answer, I rewrite code generally for anyone wants

var oc = document.createElement('canvas'), octx = oc.getContext('2d');
    oc.width = img.width;
    oc.height = img.height;
    octx.drawImage(img, 0, 0);
    while (oc.width * 0.5 > width) {
       oc.width *= 0.5;
       oc.height *= 0.5;
       octx.drawImage(oc, 0, 0, oc.width, oc.height);
    }
    oc.width = width;
    oc.height = oc.width * img.height / img.width;
    octx.drawImage(img, 0, 0, oc.width, oc.height);

UPDATE JSFIDDLE DEMO

Here is my ONLINE DEMO

1 Comment

This won't work : each time you resize the canvas, it will clear its context. You need 2 canvases. Here it's just the same as directly calling drawImage with the final dimensions.
5

I solved this by using scale for canvas and the image quality in my case becomes really good.

So first I scale the content inside of canvas:

ctx.scale(2, 2)

And then scale out the canvas tag with css:

#myCanvas { transform: scale(0.5); }

Comments

2
export const resizeImage = (imageFile, size = 80) => {
    
    let resolver = ()=>{};

    let reader = new FileReader();

    reader.onload = function (e) {
        let img = document.createElement("img");
        img.onload = function (event) {
            // Dynamically create a canvas element
            let canvas = document.createElement("canvas");

            canvas.width=size;
            canvas.height=size;

            // let canvas = document.getElementById("canvas");
            let ctx = canvas.getContext("2d");

            // Actual resizing
            ctx.drawImage(img, 0, 0, size, size);

            // Show resized image in preview element
            let dataurl = canvas.toDataURL(imageFile.type);

            resolver(dataurl);
        }
        img.src = e.target.result;
    }

    reader.readAsDataURL(imageFile);

    
    return new Promise((resolve, reject) => {
        resolver = resolve;
    })
};

Comments

1

I wrote small js-utility to crop and resize image on front-end. Here is link on GitHub project. Also you can get blob from final image to send it.

import imageSqResizer from './image-square-resizer.js'

let resizer = new imageSqResizer(
    'image-input',
    300,
    (dataUrl) => 
        document.getElementById('image-output').src = dataUrl;
);
//Get blob
let formData = new FormData();
formData.append('files[0]', resizer.blob);

//get dataUrl
document.getElementById('image-output').src = resizer.dataUrl;

Comments

1

Here is my code, which I hope may be useful for someone out there in the SO community:

You can include your target image dimension as a param in your script call. That will be the result value of your image width or height, whichever is bigger. The smaller dimension is resized keeping your image aspect ratio unchanged. You can also hard-code your default target size in the script.

You can easily change the script to suit your specific needs, such as the image type you want (default is "image/png") for an output and decide in how many steps percentwise you want to resize your image for a finer result (see const percentStep in code).

   const ResizeImage = ( _ => {

const MAX_LENGTH = 260;     // default target size of largest dimension, either witdth or height
const percentStep = .3;     // resizing steps until reaching target size in percents (30% default)
const canvas = document.createElement("canvas");
const canvasContext = canvas.getContext("2d");
const image = new Image();

const doResize = (callback, maxLength) => {

    // abort with error if image has a dimension equal to zero
    if(image.width == 0 || image.height == 0) {
        return {blob: null, error: "either image width or height was zero "};
    }

    // use caller dimension or default length if none provided
    const length = maxLength == null  ? MAX_LENGTH : maxLength;

    canvas.width = image.width;
    canvas.height = image.height;
    canvasContext.drawImage(image, 0, 0, image.width, image.height);
    // if image size already within target size, just copy and return blob
    if(image.width <= length && image.height <= length) {
        canvas.toBlob( blob => {
            callback({ blob: blob, error: null });
        }, "image/png", 1);
        return;
    }

    var startDim = Math.max(image.width, image.height);
    var startSmallerDim = Math.min(image.width, image.height);

    // gap to decrease in size until we reach the target size,
    // be it by decreasing the image width or height,
    // whichever is largest
    const gap = startDim - length;
    // step length of each resizing iteration
    const step = parseInt(percentStep*gap);
    //  no. of iterations
    var nSteps = 0;
    if(step == 0) {
        step = 1;
    } else {
        nSteps = parseInt(gap/step);
    }
    // length of last additional resizing step, if needed
    const lastStep = gap % step;
    // aspect ratio = value by which we'll  multiply the smaller dimension
    // in order to keep the aspect ratio unchanged in each iteration
    const ratio = startSmallerDim/startDim;

    var newDim;          // calculated new length for the bigger dimension of the image, be it image width or height
    var smallerDim;     // length along the smaller dimension of the image, width or height
    for(var i = 0; i < nSteps; i++) {
        // decrease longest dimension one step in pixels
        newDim = startDim - step;
        // decrease shortest dimension proportionally, so as to keep aspect ratio
        smallerDim = parseInt(ratio*newDim);
        // assign calculated vars to their corresponding canvas dimension, width or height
        if(image.width > image.height) {
            [canvas.width, canvas.height]  = [newDim, smallerDim];
        } else {
            [canvas.width, canvas.height] = [smallerDim, newDim];
        }
        // draw image one step smaller
        canvasContext.drawImage(canvas, 0, 0, canvas.width, canvas.height);
        // cycle var startDim for new loop
        startDim = newDim;
    }

    // do last missing resizing step to finally reach target image size
    if(lastStep > 0) {
        if(image.width > image.height) {
            [canvas.width, canvas.height]  = [startDim - lastStep, parseInt(ratio*(startDim - lastStep))];
        } else {
            [canvas.width, canvas.height] = [parseInt(ratio*(startDim -lastStep)), startDim - lastStep];
        }
        canvasContext.drawImage(image, 0, 0, canvas.width, canvas.height);
    }

    // send blob to caller
    canvas.toBlob( blob => {
        callback({blob: blob, error: null});
    }, "image/png", 1);

};

const resize = async (imgSrc, callback, maxLength) => {
    image.src = imgSrc;
    image.onload = _ => {
       doResize(callback, maxLength);
    };
};

return { resize: resize }

})();

Usage:

ResizeImage.resize("./path/to/image/or/blob/bytes/to/resize", imageObject => {
    if(imageObject.error != null) {
      // handle errors here
      console.log(imageObject.error);
      return;
    }
    // do whatever you want with the blob, like assinging it to
    // an img element, or uploading it to a database
    // ...
    document.querySelector("#my-image").src = imageObject.blob;
    // ...
}, 300);

Comments

0

With OffscreenCanvas and Blob: arrayBuffer() it is possible to generate a thumbnail without the DOM (this may be offloaded to web workers):

async function generateThumbnail(photo: File, size = 256): Promise<Blob> {
  const buffer = Buffer.from(await photo.arrayBuffer());
  const image = new Image();

  image.src = `data:${photo.type};base64,${buffer.toString("base64")}`;
  await new Promise((imageLoaded) => (image.onload = imageLoaded));

  const deltaWidth =
    image.width > image.height ? (image.width / image.height) * size : size;
  const deltaHeight =
    image.height > image.width ? (image.height / image.width) * size : size;

  const canvas = new OffscreenCanvas(size, size);
  const ctx = canvas.getContext("2d");
  ctx?.drawImage(
    image,
    (size - deltaWidth) / 2,
    (size - deltaHeight) / 2,
    deltaWidth,
    deltaHeight
  );
  return canvas.convertToBlob();
}

Comments

0

There are many answers added here. Yet i find this solution really compact and easy.

  • scales down directly when user uploads an image
  • convert to base64 format which i find easier to save in databases
    <input type="file" id="file"/>
const resizeAndConvertToBase64 = () => {
    const file = document.getElementById("input").files[0];
    const reader = new FileReader();
    reader.onload = function (e) {
        const img = new Image();
        img.src = e.target.result;
        img.onload = function () {
            const canvas = document.createElement("canvas");
            const ctx = canvas.getContext("2d");
            //maybe u need to change (canvas.width, canvas.height) 
            //if you want to scale down to different size
            canvas.width = 250;
            canvas.height = 250;
            ctx.drawImage(img, 0, 0, 250, 250);
            // you can change "image/png" to "image/jpeg"
            // if "jpeg" image needed
            const resizedBase64 = canvas.toDataURL("image/png");
            document.getElementById("scaled").src = resizedBase64;
          };
        };
        reader.readAsDataURL(file);
      };
const fileInput = document.getElementById("input");
fileInput.addEventListener("change", resizeAndConvertToBase64)

Comments

0

Even though I am still relatively new to JavaScript programming, I have achieved a simple and effective solution to resize images dynamically. This approach is relevant for anyone looking for an easy way to accomplish similar tasks, regardless of the age of the original post.

The key features of my code snippet

Automatic Image Resizing: You only need to specify the desired image size in the tag (using the size attribute). The code handles the resizing process automatically.

Selective Resizing: The code targets images using the img[data-src] selector. This approach eliminates the need to assign unique IDs to each image. It ensures that only images with the data-src attribute are resized, avoiding unintended changes to other images.

Customization: If you want to target a specific image, you can replace img[data-src] with a unique id. This allows the resizing function to work only on that image, providing additional flexibility.

Advantages

Ease of Use: Just specify the size attribute and let the code do the rest.

Selective Application: The function only applies to images explicitly marked with data-src, avoiding blanket resizing.

Customizability: Supports both broad and targeted resizing based on your needs.

Disadvantages

It resizes any image, whether smaller or larger size than the original image, therefore stretching the smaller images to bigger sizes.

This solution is useful for maintaining consistent shapes across multiple images or ensuring specific dimensions for individual images, making it versatile for a variety of use cases.

Here is how it goes

Usage

 <img data-src="path/to/your/image" size="(310, 310)" alt="speaker image missing">

and add the event listener at the end of your document

  <script>
  document.addEventListener("DOMContentLoaded", function () {
            const images = document.querySelectorAll("img[data-src]");

  images.forEach((img) => {
       const imagePath = img.getAttribute("data-src");
       const sizeAttr = img.getAttribute("size");
       const sizeMatch = sizeAttr.match(/\((\d+),\s*(\d+)\)/);

       if (sizeMatch) {
          const targetWidth = parseInt(sizeMatch[1]);
          const targetHeight = parseInt(sizeMatch[2]);

          resizeImageToExactSize(imagePath, targetWidth, targetHeight, img);
              } else {
                console.error("Invalid size format. Use (width, height).");
                    }
                });
            });

   function resizeImageToExactSize(imagePath, targetWidth, targetHeight, imgElement) {
        const img = new Image();
        img.src = imagePath;

        img.onload = function () {
        const canvas = document.createElement("canvas");

        // Set the canvas size to the target width and height
        canvas.width = targetWidth;
        canvas.height = targetHeight;

        const ctx = canvas.getContext("2d");

        // Draw the image onto the canvas, scaled to the target size
        ctx.drawImage(img, 0, 0, targetWidth, targetHeight);

        // Convert the canvas to a data URL and update the img element's src
        const resizedImage = canvas.toDataURL("image/jpeg");
        imgElement.src = resizedImage;

        console.log(`Resized image dimensions: ${targetWidth}x${targetHeight}`);
                };

        img.onerror = function () {
           console.error("Failed to load image:", imagePath);
                };
            }

  </script>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.