I'd like to draw a red rectangle and an image on a HTML canvas. My goal is to completely cover the rectangle with the image, in combination with scaling all content using setTransform
so the image fits the canvas. The issue I'm having is that the rectangle is still visible on the edges of the image.
I tried experimenting with the scale and it seems this is caused by rounding, as I'm scaling all content using a single setTransform
call.
I can fix the issue by applying adjusted dimensions to both the rectangle and the image, so the resulting dimensions the rectangle and image are integer values. That works in Safari and Chrome, but it doesn't in Firefox.
Here's the reproduction code:
const DPR = window.devicePixelRatio || 1;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const checkbox = document.querySelector('input');
// Logical canvas size (matches physical pixels)
const container = document.getElementById('container');
const width = container.clientWidth * DPR;
const height = container.clientHeight * DPR;
// Desired content size
const contentWidth = 3508;
const contentHeight = 2480;
// Compute scale to fit content inside canvas
const scale = Math.min(width / contentWidth, height / contentHeight);
console.log(scale);
// Compute adjusted input size so output aligns to integers
const targetOutputWidth = Math.round(contentWidth * scale);
const targetOutputHeight = Math.round(contentHeight * scale);
const adjustedWidth = targetOutputWidth / scale;
const adjustedHeight = targetOutputHeight / scale;
function draw() {
canvas.width = width;
canvas.height = height;
// Set transform and draw
ctx.setTransform(scale, 0, 0, scale, 0, 0);
// Determine to fix issue
const fixIssue = checkbox.checked;
// Draw background rect
ctx.fillStyle = 'red';
if (!fixIssue) {
ctx.fillRect(0, 0, contentWidth, contentHeight);
} else {
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
}
// Load and draw image
const img = new Image();
img.onload = () => {
// ctx.imageSmoothingEnabled = false; // Uncommenting this doesn't help
if (!fixIssue) {
ctx.drawImage(img, 0, 0, contentWidth, contentHeight);
} else {
ctx.drawImage(img, 0, 0, adjustedWidth, adjustedHeight);
}
};
img.src = `https://placehold.co/${contentWidth}x${contentHeight}`;
}
checkbox.oninput = draw;
draw();
<label>Fix issue: <input type="checkbox"></label>
<div id="container" style="width: 838px; height: 446px;">
<canvas id="canvas" style="width: 100%; height: 100%; background: white; display: block;"></canvas>
</div>
When fixIssue
is set to false
, this is what browsers display (make sure to view these screenshots at 100% to see the red rectangle at the edge of the image):
Chrome | Safari | Firefox |
---|---|---|
![]() |
![]() |
![]() |
When setting fixIssue
to true
, the issue is fixed in Chrome and Safari but not in Firefox:
Chrome | Safari | Firefox |
---|---|---|
![]() |
![]() |
![]() |
You can see that the rectangle is still visible at the top of the image in Firefox.
How can I prevent this from happening? I tried disabling image smoothing which doesn't help. I'd like to keep the single setTransform
call as it's convenient for scaling all content.
The issue is that your image service actually returns an SVG image, and that Firefox and Chrome do not treat the SVG image's preserveAspectRatio="xMidYMid meet"
(the default) the same way. Firefox assumes the SVG author should be the one that wins, and thus will treat the drawImage
output rectangle as the resizing source for this aspect-ratio. Chrome on the other hand assumes the canvas author should win, and will apply the aspect-ratio factor before the SVG image is even passed to drawImage
, making the stretching inherent with drawImage(src, x, y, width, height)
actually not preserve the SVG's preserveAspectRatio
attribute value.
I did open a specs issue some times ago, but it's still not resolved.
Here is a snippet showing what's happening:
(async () => {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = `https://placehold.co/800x600`;
await img.decode();
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
})().catch(console.error)
canvas { background: red }
<canvas></canvas>
Resulting, in Chrome:
,
while in Firefox:
To get Chrome's behavior in Firefox, you can convert your <img>
to an ImageBitmap
before drawing it on your context.
const DPR = window.devicePixelRatio || 1;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const checkbox = document.querySelector('input');
// Logical canvas size (matches physical pixels)
const container = document.getElementById('container');
const width = container.clientWidth * DPR;
const height = container.clientHeight * DPR;
// Desired content size
const contentWidth = 3508;
const contentHeight = 2480;
// Compute scale to fit content inside canvas
const scale = Math.min(width / contentWidth, height / contentHeight);
console.log(scale);
// Compute adjusted input size so output aligns to integers
const targetOutputWidth = Math.round(contentWidth * scale);
const targetOutputHeight = Math.round(contentHeight * scale);
const adjustedWidth = targetOutputWidth / scale;
const adjustedHeight = targetOutputHeight / scale;
function draw() {
canvas.width = width;
canvas.height = height;
// Set transform and draw
ctx.setTransform(scale, 0, 0, scale, 0, 0);
// Determine to fix issue
const fixIssue = checkbox.checked;
// Draw background rect
ctx.fillStyle = 'red';
if (!fixIssue) {
ctx.fillRect(0, 0, contentWidth, contentHeight);
} else {
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
}
// Load and draw image
const img = new Image();
img.onload = async () => {
// Make it a bitmap
const bmp = await createImageBitmap(img);
if (!fixIssue) {
ctx.drawImage(bmp, 0, 0, contentWidth, contentHeight);
} else {
ctx.drawImage(bmp, 0, 0, adjustedWidth, adjustedHeight);
}
};
img.src = `https://placehold.co/${contentWidth}x${contentHeight}`;
}
checkbox.oninput = draw;
draw();
<label>Fix issue: <input type="checkbox"></label>
<div id="container" style="width: 838px; height: 446px;">
<canvas id="canvas" style="width: 100%; height: 100%; background: white; display: block;"></canvas>
</div>