Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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...

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);     } 
like image 601
Kumpu Avatar asked Sep 12 '13 10:09

Kumpu


People also ask

How do you scale on canvas?

You can scale your canvas with content by "bouncing" the content off a temporary canvas while you resize the original canvas. This save+redraw process is necessary because canvas content is automatically cleared when you resize the canvas width or height.

How do I stretch an image in canvas?

To stretch the image and canvas simultaneously, use the Image Size command. To stretch the canvas separately, use the Canvas Size command. If your image doesn't touch the edges of the canvas, it will create a frame around the image that represents the empty space on the canvas not covered by the image.


1 Answers

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.

like image 76
Simon Sarris Avatar answered Oct 13 '22 02:10

Simon Sarris