I am trying to draw a 300dpi image to a canvas object but in Chrome it shows in very poor quality. When I used the below code, it did not improve but that was because devicePixelRatio
was the same as backingStoreRatio
(both were 1
).
I then tried to force some ratio changes and found the following:
ratio
to be 2
and forced the scaling code to run, then it draws to the canvas in a better resolution. ratio
to anything greater than 2
(e.g. 3
, 4
, 5
, 6
, etc) then it had poor resolution!This was all done on a desktop computer.
How can I ensure the canvas draws with a high resolution?
(Code from: http://www.html5rocks.com/en/tutorials/canvas/hidpi/ )
/**
* Writes an image into a canvas taking into
* account the backing store pixel ratio and
* the device pixel ratio.
*
* @author Paul Lewis
* @param {Object} opts The params for drawing an image to the canvas
*/
function drawImage(opts) {
if(!opts.canvas) {
throw("A canvas is required");
}
if(!opts.image) {
throw("Image is required");
}
// get the canvas and context
var canvas = opts.canvas,
context = canvas.getContext('2d'),
image = opts.image,
// now default all the dimension info
srcx = opts.srcx || 0,
srcy = opts.srcy || 0,
srcw = opts.srcw || image.naturalWidth,
srch = opts.srch || image.naturalHeight,
desx = opts.desx || srcx,
desy = opts.desy || srcy,
desw = opts.desw || srcw,
desh = opts.desh || srch,
auto = opts.auto,
// finally query the various pixel ratios
devicePixelRatio = window.devicePixelRatio || 1,
backingStoreRatio = context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1,
ratio = devicePixelRatio / backingStoreRatio;
// ensure we have a value set for auto.
// If auto is set to false then we
// will simply not upscale the canvas
// and the default behaviour will be maintained
if (typeof auto === 'undefined') {
auto = true;
}
// upscale the canvas if the two ratios don't match
if (auto && devicePixelRatio !== backingStoreRatio) {
var oldWidth = canvas.width;
var oldHeight = canvas.height;
canvas.width = oldWidth * ratio;
canvas.height = oldHeight * ratio;
canvas.style.width = oldWidth + 'px';
canvas.style.height = oldHeight + 'px';
// now scale the context to counter
// the fact that we've manually scaled
// our canvas element
context.scale(ratio, ratio);
}
context.drawImage(pic, srcx, srcy, srcw, srch, desx, desy, desw, desh);
}
Making only the below changes results in high resolution canvas images (why?):
//WE FORCE RATIO TO BE 2
ratio = 2;
//WE FORCE IT TO UPSCALE (event though they're equal)
if (auto && devicePixelRatio === backingStoreRatio) {
If we change the above to be a ratio of 3
, it is no longer high resolution!
EDIT: One additional observation - even with the 2x ratio, while it is noticeably better resolution, it's still not as sharp as showing the image in an img
tag)
The HTML5Rocks article linked from the question makes things more difficult than they need to be, but it makes the same basic mistake as other resources I have seen (1, 2, 3, 4). Those references give some variation on this formula:
var rect = canvas.getBoundingClientRect();
canvas.width = Math.round (devicePixelRatio * rect.width); // WRONG!
The formula is wrong. A better formula is
var rect = canvas.getBoundingClientRect();
canvas.width = Math.round (devicePixelRatio * rect.right)
- Math.round (devicePixelRatio * rect.left);
The point is, it doesn't make sense to scale a width or height (i.e., the difference of two positions) by devicePixelRatio. You should only ever scale an absolute position. I can't find a reference for this exact point but I think it is obvious, once you get it.
There is no way to calculate the physical width and height of a rectangle (in device pixels) from its CSS width and height (in device-independent pixels).
Suppose you have two elements whose bounding rectangles in device-independent pixels are
{ left: 0, top: 10, right: 8, bottom: 20, width: 8, height: 10 },
{ left: 1, top: 20, right: 9, bottom: 30, width: 8, height: 10 }.
Now assuming devicePixelRatio is 1.4 the elements will cover these device-pixel rectangles:
{ left: 0, top: 14, right: 11, bottom: 28, width: 11, height: 14 },
{ left: 1, top: 28, right: 13, bottom: 42, width: 12, height: 14 },
where left, top, right and bottom have been multiplied by devicePixelRatio and rounded to the nearest integer (using Math.round()).
You will notice that the two rectangles have the same width in device-independent pixels but different widths in device pixels. ▯
Here is a code sample for testing. Load it in a browser, then zoom in and out with the mouse. The last canvas should always have sharp lines. The other three will be blurry at some resolutions.
Tested on desktop Firefox, IE, Edge, Chrome and Android Chrome and Firefox. (Note, this doesn't work on JSfiddle because getBoundingClientRect returns incorrect values there.)
<!DOCTYPE html>
<html>
<head>
<script>
function resize() {
var canvases = document.getElementsByTagName("canvas");
var i, j;
for (i = 0; i != canvases.length; ++ i) {
var canvas = canvases[i];
var method = canvas.getAttribute("method");
var dipRect = canvas.getBoundingClientRect();
var context = canvas.getContext("2d");
switch (method) {
case "0":
// Incorrect:
canvas.width = devicePixelRatio * dipRect.width;
canvas.height = devicePixelRatio * dipRect.height;
break;
case "1":
// Incorrect:
canvas.width = Math.round(devicePixelRatio * dipRect.width);
canvas.height = Math.round(devicePixelRatio * dipRect.height);
break;
case "2":
// Incorrect:
canvas.width = Math.floor(devicePixelRatio * dipRect.width);
canvas.height = Math.floor(devicePixelRatio * dipRect.height);
break;
case "3":
// Correct:
canvas.width = Math.round(devicePixelRatio * dipRect.right)
- Math.round(devicePixelRatio * dipRect.left);
canvas.height = Math.round(devicePixelRatio * dipRect.bottom)
- Math.round(devicePixelRatio * dipRect.top);
break;
}
console.log("method " + method
+ ", devicePixelRatio " + devicePixelRatio
+ ", client rect (DI px) (" + dipRect.left + ", " + dipRect.top + ")"
+ ", " + dipRect.width + " x " + dipRect.height
+ ", canvas width, height (logical px) " + canvas.width + ", " + canvas.height);
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "cyan";
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "black";
for (j = 0; j != Math.floor (canvas.width / 2); ++ j) {
context.fillRect(2 * j, 0, 1, canvas.height);
}
}
};
addEventListener("DOMContentLoaded", resize);
addEventListener("resize", resize);
</script>
</head>
<body>
<canvas method="0" style="position: absolute; left: 1px; top: 10px; width: 8px; height: 10px"></canvas>
<canvas method="1" style="position: absolute; left: 1px; top: 25px; width: 8px; height: 10px"></canvas>
<canvas method="2" style="position: absolute; left: 1px; top: 40px; width: 8px; height: 10px"></canvas>
<canvas method="3" style="position: absolute; left: 1px; top: 55px; width: 8px; height: 10px"></canvas>
</body>
</html>
One trick you could use is to actually set the canvas width and height to the pixel height and width of the photo then use css to resize the image. Below is a sudo-code example.
canvasElement.width = imgWidth;
canvasElement.height = imgHeight;
canvasElement.getContext("2d").drawImage(ARGS);
Then you can either use width & height setting or a 3d transform.
canvasElement.style.transform = "scale3d(0.5,0.5,0)";
or
canvasElement.style.width = newWidth;
canvasElement.style.height = newWidth * imgHeight / imgWidth;
I suggest you use the 3d transform because when you use 3d transforms the image data of the element gets copied to the GPU so you don't really have to worry about any quality degradation.
I hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With