Kumpu Kumpu - 2 months ago 7
Javascript Question

How to scale images on a html5 canvas with better interpolation?

First of all: what am I trying to do?

I have an application to view images. It uses the canvas element to render the image. You can zoom in, you can zoom out, and you can drag it around. This part works perfectly right now.

But let's say I have an image with a lot of text. It has a resolution of 1200x1700, and my canvas has 1200x900. Initially, when zoomed out, this leads to a rendered resolution of ~560x800.

My actual drawing looks like this:

drawImage(src, srcOffsetX, srcOffsetY, sourceViewWidth, sourceViewHeight,
destOffsetX, destOffsetY, destWidth, destHeight);


Small text on this image looks really, really bad, especially when compared to other image viewers (e.g. IrfanView), or even the html < img > element.

I figured out that the browsers interpolation algorithm is the cause of this problem. Comparing different browsers showed that Chrome renders scaled images the best, but still not good enough.

Well I searched in every corner of the Interwebs for 4-5 hours straight and did not find what I need. I found the "imageSmoothingEnabled" option, "image-rendering" CSS styles which you can not use on canvas, rendering at float positions and many JavaScript implementations of interpolation algorithms (those are far to slow for my purpose).

You may ask why I am telling you all of this: to save you the time to give me answers I already know :))

So: is there any good and fast way to have better interpolation? My current idea is to create an image object, resize this (because img has good interpolation when scaled!) and render it then. Unfortunately, applying img.width seems only to affect the displayed width...

Many thanks in advance for your answers!! :)

Update: Thanks to Simon, I could solve my problem.
Here is the dynamic scaling algorithm I used. Notice that it keeps the aspect ratio, the height parameter is only for avoiding more float computing. It only scales down right now.

scale(destWidth, destHeight){
var start = new Date().getTime();
var scalingSteps = 0;
var ctx = this._sourceImageCanvasContext;
var curWidth = this._sourceImageWidth;
var curHeight = this._sourceImageHeight;

var lastWidth = this._sourceImageWidth;
var lastHeight = this._sourceImageHeight;

var end = false;
var scale=0.75;
while(end==false){
scalingSteps +=1;
curWidth *= scale;
curHeight *= scale;
if(curWidth < destWidth){
curWidth = destWidth;
curHeight = destHeight;
end=true;
}
ctx.drawImage(this._sourceImageCanvas, 0, 0, Math.round(lastWidth), Math.round(lastHeight), 0, 0, Math.round(curWidth), Math.round(curHeight));
lastWidth = curWidth;
lastHeight = curHeight;
}
var endTime =new Date().getTime();
console.log("execution time: "+ ( endTime - start) + "ms. scale per frame: "+scale+ " scaling step count: "+scalingSteps);
}

Answer

You need to "step down" several times. Instead of scaling from a very large image to a very small, you need to re-scale it to intermediary sizes.

Consider an image you want to draw at 1/6 scale. You could do this:

var w = 1280;
var h = 853;

ctx.drawImage(img, 0, 0, w/6, h/6);   

Or you could draw it to an in-memory canvas at 1/2 scale, then 1/2 scale again, then 1/2 scale again. The result is a 1/6 scale image, but we use three steps:

var can2 = document.createElement('canvas');
can2.width = w/2;
can2.height = w/2;
var ctx2 = can2.getContext('2d');

ctx2.drawImage(img, 0, 0, w/2, h/2);
ctx2.drawImage(can2, 0, 0, w/2, h/2, 0, 0, w/4, h/4);
ctx2.drawImage(can2, 0, 0, w/4, h/4, 0, 0, w/6, h/6);

Then you can draw that back to your original context:

ctx.drawImage(can2, 0, 0, w/6, h/6, 0, 200, w/6, h/6);

You can see the difference live, here:

var can = document.getElementById('canvas1');
var ctx = can.getContext('2d');

var img = new Image();
var w = 1280;
var h = 853;
img.onload = function() {
    // step it down only once to 1/6 size:
    ctx.drawImage(img, 0, 0, w/6, h/6);   
    
    // Step it down several times
    var can2 = document.createElement('canvas');
    can2.width = w/2;
    can2.height = w/2;
    var ctx2 = can2.getContext('2d');
    
    // Draw it at 1/2 size 3 times (step down three times)
    
    ctx2.drawImage(img, 0, 0, w/2, h/2);
    ctx2.drawImage(can2, 0, 0, w/2, h/2, 0, 0, w/4, h/4);
    ctx2.drawImage(can2, 0, 0, w/4, h/4, 0, 0, w/6, h/6);
    ctx.drawImage(can2, 0, 0, w/6, h/6, 0, 200, w/6, h/6);
}



img.src = 'http://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/Equus_quagga_%28Namutoni%2C_2012%29.jpg/1280px-Equus_quagga_%28Namutoni%2C_2012%29.jpg'
canvas {
    border: 1px solid gray;
}
<canvas id="canvas1" width="400" height="400"></canvas>

View same snippet on jsfiddle.