HTML5 canvas: is there a way to resize image with "nearest neighbour" resampling?
Asked Answered
T

7

14

I have some JS that makes some manipulations with images. I want to have pixelart-like graphics, so I had to enlarge original images in graphics editor. But I think it'd be good idea to make all the manipulations with the small image and then enlarge it with html5 functionality. This will save bunch of processing time (because now my demo (warning: domain-name may cause some issues at work etc) loads extremely long in Firefox, for example). But when I try to resize the image, it gets resampled bicubically. How to make it resize image without resampling? Is there any crossbrowser solution?

Tommy answered 18/10, 2011 at 14:57 Comment(6)
Many of us have jobs - please warn users here of any link to something NSFW like "anal-slavery".Vitriol
There's nothing NSFW. That's just my test server with such weird domain name.Tommy
The link itself is the problem, and it is extremely NSFW.Vitriol
Oh boy, now my address bar auto completion looks pretty good with this URL. thanks ABTOMAT, you know you can use jsfiddle.net or dropbox don't you?Hillaryhillbilly
I create test to compare results: jsperf.com/image-linear-resizeValediction
That URL is why we can't have nice things.Farrahfarrand
R
14
image-rendering: -webkit-optimize-contrast; /* webkit */
image-rendering: -moz-crisp-edges /* Firefox */

http://phrogz.net/tmp/canvas_image_zoom.html can provide a fallback case using canvas and getImageData. In short:

// Create an offscreen canvas, draw an image to it, and fetch the pixels
var offtx = document.createElement('canvas').getContext('2d');
offtx.drawImage(img1,0,0);
var imgData = offtx.getImageData(0,0,img1.width,img1.height).data;

// Draw the zoomed-up pixels to a different canvas context
for (var x=0;x<img1.width;++x){
  for (var y=0;y<img1.height;++y){
    // Find the starting index in the one-dimensional image data
    var i = (y*img1.width + x)*4;
    var r = imgData[i  ];
    var g = imgData[i+1];
    var b = imgData[i+2];
    var a = imgData[i+3];
    ctx2.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";
    ctx2.fillRect(x*zoom,y*zoom,zoom,zoom);
  }
}

More: MDN docs on image-rendering

Routine answered 18/10, 2011 at 14:58 Comment(1)
A comment for future readers: CSS image optimising will round to the nearest pixel, which may not always be ideal for your images. Your space invaders may become lumpy and Mario may be deformed. Best to use the code method detailed above - or enlarge your images x3 so that the effects of binomial image-rendering are negligable.Ashelman
B
5

I wrote a NN resizing script a while ago using ImageData (around line 1794)

https://github.com/arahaya/ImageFilters.js/blob/master/imagefilters.js

You can see a demo here

http://www.arahaya.com/imagefilters/

unfortunately the builtin resizing should be slightly faster.

Bushwa answered 20/10, 2011 at 23:29 Comment(0)
C
4

This CSS on the canvas element works:

image-rendering: pixelated;

This works in Chrome 93, as of September 2021.

Comstockery answered 10/9, 2021 at 14:23 Comment(1)
> pixelatedFrith
B
4

You can simply set context.imageSmoothingEnabled to false. This will make everything drawn with context.drawImage() resize using nearest neighbor.

// the canvas to resize
const canvas = document.createElement("canvas");

// the canvas to output to
const canvas2 = document.createElement("canvas");
const context2 = canvas2.getContext("2d");

// disable image smoothing
context2.imageSmoothingEnabled = false;

// draw image from the canvas
context2.drawImage(canvas, 0, 0, canvas2.width, canvas2.height);

This has better support than using image-rendering: pixelated.

Brainless answered 24/8, 2022 at 1:11 Comment(1)
There are some browser inconsistencies. Firefox ignores imageSmoothingEnabled = false if downscaling an image.Essene
A
0

I'll echo what others have said and tell you it's not a built-in function. After running into the same issue, I've made one below.

It uses fillRect() instead of looping through each pixel and painting it. Everything is commented to help you better understand how it works.

//img is the original image, scale is a multiplier. It returns the resized image.
function Resize_Nearest_Neighbour( img, scale ){
    //make shortcuts for image width and height
    var w = img.width;
    var h = img.height;

    //---------------------------------------------------------------
    //draw the original image to a new canvas
    //---------------------------------------------------------------

    //set up the canvas
    var c = document.createElement("CANVAS");
    var ctx = c.getContext("2d");
    //disable antialiasing on the canvas
    ctx.imageSmoothingEnabled = false;
    //size the canvas to match the input image
    c.width = w;
    c.height = h;
    //draw the input image
    ctx.drawImage( img, 0, 0 );
    //get the input image as image data
    var inputImg = ctx.getImageData(0,0,w,h);
    //get the data array from the canvas image data
    var data = inputImg.data;

    //---------------------------------------------------------------
    //resize the canvas to our bigger output image
    //---------------------------------------------------------------
    c.width = w * scale;
    c.height = h * scale;
    //---------------------------------------------------------------
    //loop through all the data, painting each pixel larger
    //---------------------------------------------------------------
    for ( var i = 0; i < data.length; i+=4 ){

        //find the colour of this particular pixel
        var colour = "#";

        //---------------------------------------------------------------
        //convert the RGB numbers into a hex string. i.e. [255, 10, 100]
        //into "FF0A64"
        //---------------------------------------------------------------
        function _Dex_To_Hex( number ){
            var out = number.toString(16);
            if ( out.length < 2 ){
                out = "0" + out;
            }
            return out;
        }
        for ( var colourIndex = 0; colourIndex < 3; colourIndex++ ){
            colour += _Dex_To_Hex( data[ i+colourIndex ] );
        }
        //set the fill colour
        ctx.fillStyle = colour;

        //---------------------------------------------------------------
        //convert the index in the data array to x and y coordinates
        //---------------------------------------------------------------
        var index = i/4;
        var x = index % w;
        //~~ is a faster way to do 'Math.floor'
        var y = ~~( index / w );
        //---------------------------------------------------------------
        //draw an enlarged rectangle on the enlarged canvas
        //---------------------------------------------------------------
        ctx.fillRect( x*scale, y*scale, scale, scale );
    }

    //get the output image from the canvas
    var output = c.toDataURL("image/png");
    //returns image data that can be plugged into an img tag's src
    return output;
}

Below is an example of it in use.

Your image would appear in the HTML like this:

<img id="pixel-image" src="" data-src="pixel-image.png"/>

The data-src tag contains the URL for the image you want to enlarge. This is a custom data tag. The code below will take the image URL from the data tag and put it through the resizing function, returning a larger image (30x the original size) which then gets injected into the src attribute of the img tag.

Remember to put the function Resize_Nearest_Neighbour (above) into the <script> tag before you include the following.

function Load_Image( element ){
    var source = element.getAttribute("data-src");
    var img = new Image();

    img.addEventListener("load", function(){

        var bigImage = Resize_Nearest_Neighbour( this, 30 );
        element.src = bigImage;

    });

    img.src = source;
}

Load_Image( document.getElementById("pixel-image") );
Ashelman answered 15/1, 2018 at 23:37 Comment(0)
M
-1

There is no built-in way. You have to do it yourself with getImageData.

Masonry answered 18/10, 2011 at 15:3 Comment(2)
Thought about that, but won't it be very slow?Tommy
In short, yeah. It will be very slow. There's another way I can think of involving an in-memory canvas and a large amount of drawImage calls but it may not be consistent across browser because of anti-aliasing differences and may not be much faster.Masonry
I
-1

Based on Paul Irish's comment:

function resizeBase64(base64, zoom) {
    return new Promise(function(resolve, reject) {
        var img = document.createElement("img");

        // once image loaded, resize it
        img.onload = function() {
            // get image size
            var imageWidth = img.width;
            var imageHeight = img.height;

            // create and draw image to our first offscreen canvas
            var canvas1 = document.createElement("canvas");
            canvas1.width = imageWidth;
            canvas1.height = imageHeight;
            var ctx1 = canvas1.getContext("2d");
            ctx1.drawImage(this, 0, 0, imageWidth, imageHeight);

            // get pixel data from first canvas
            var imgData = ctx1.getImageData(0, 0, imageWidth, imageHeight).data;

            // create second offscreen canvas at the zoomed size
            var canvas2 = document.createElement("canvas");
            canvas2.width = imageWidth * zoom;
            canvas2.height = imageHeight * zoom;
            var ctx2 = canvas2.getContext("2d");

            // draw the zoomed-up pixels to a the second canvas
            for (var x = 0; x < imageWidth; ++x) {
                for (var y = 0; y < imageHeight; ++y) {
                    // find the starting index in the one-dimensional image data
                    var i = (y * imageWidth + x) * 4;
                    var r = imgData[i];
                    var g = imgData[i + 1];
                    var b = imgData[i + 2];
                    var a = imgData[i + 3];
                    ctx2.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a / 255 + ")";
                    ctx2.fillRect(x * zoom, y * zoom, zoom, zoom);
                }
            }

            // resolve promise with the zoomed base64 image data
            var dataURI = canvas2.toDataURL();
            resolve(dataURI);
        };
        img.onerror = function(error) {
            reject(error);
        };
        // set the img soruce
        img.src = base64;
    });
}

resizeBase64(src, 4).then(function(zoomedSrc) {
    console.log(zoomedSrc);
});

https://jsfiddle.net/djhyquon/69/

Incubator answered 2/5, 2019 at 12:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.