I am trying to scale a 1x image up to an arbitrary size using a canvas. However, the image always has some interpolation at the edges of each pixel. I expect the image provided to match how the look canvas exactly. No combination of different scales etc. has worked for me. Even right click -> save image on the canvas produces the same image.
I have also tried using toDataURL
on the canvas but I get the same result.
I see this on macOS with a 2x display. The same result in Safari, Chrome, and Firefox.
The 2 side-by-side:
The produced image:
// This function is unimportant; it is used as an example for this question.
function generateImage(width, height, digitOffset) {
const pixelsCount = width * height
const imageData = new ImageData(width, height)
const digits = "0123456789"
.repeat(30)
.substring(digitOffset, digitOffset + pixelsCount)
for (const [index, digit] of [...digits].entries()) {
const colours = {
0: [0x00, 0x12, 0x19],
1: [0x00, 0x5f, 0x73],
2: [0x0a, 0x93, 0x96],
3: [0x94, 0xd2, 0xbd],
4: [0xe9, 0xd8, 0xa6],
5: [0xee, 0x9b, 0x00],
6: [0xca, 0x67, 0x02],
7: [0xbb, 0x3e, 0x03],
8: [0xae, 0x20, 0x12],
9: [0x9b, 0x22, 0x26],
}
const dataOffset = index * 4
const color = colours[digit]
imageData.data[dataOffset] = color[0]
imageData.data[dataOffset + 1] = color[1]
imageData.data[dataOffset + 2] = color[2]
imageData.data[dataOffset + 3] = 0xff
}
return imageData
}
/** The width/height of the 1x image. */
const size = 16
const imageData = generateImage(size, size, 0)
const scale = 10
/** The canvas containing the 1x image. */
const unscaledCanvas = document.getElementById("unscaledCanvas")
unscaledCanvas.width = size
unscaledCanvas.height = size
unscaledCanvas.style.width = `${size}px`
unscaledCanvas.style.height = `${size}px`
const unscaledContext = unscaledCanvas.getContext("2d")
unscaledContext.putImageData(imageData, 0, 0)
const scaledCanvas = document.getElementById("increasedScale")
scaledCanvas.width = size * scale
scaledCanvas.height = size * scale
scaledCanvas.style.width = `${size * scale}px`
scaledCanvas.style.height = `${size * scale}px`
scaledCanvas.style.imageRendering = "pixelated"
const scaledContext = scaledCanvas.getContext("2d")
scaledContext.imageSmoothingEnabled = false
scaledContext.mozImageSmoothingEnabled = false
scaledContext.webkitImageSmoothingEnabled = false
scaledContext.drawImage(unscaledCanvas, 0, 0, size * scale, size * scale)
const image = document.getElementById("output")
image.width = size * scale
image.height = size * scale
scaledCanvas.toBlob((blob) => {
const dataURL = URL.createObjectURL(blob)
image.onload = () => {
URL.revokeObjectURL(dataURL)
}
image.src = dataURL
})
<canvas id="unscaledCanvas"></canvas>
<canvas id="increasedScale"></canvas>
<img id="output" />
The image does not have the antialiasing artifacts you're seeing encoded in it, your browser creates these when upscaling the image to match your 2x monitor's pixel density.
We can prove that by drawing this same image upscaled again on another <canvas>
:
(async () => {
// using OP's produced image
const resp = await fetch("https://i.sstatic.net/7oZk5oeK.png");
const blob = await resp.blob();
const bmp = await createImageBitmap(blob);
const canvas = document.querySelector("canvas");
// Make the canvas the correct density
canvas.width *= devicePixelRatio;
canvas.height *= devicePixelRatio;
const ctx = canvas.getContext("2d");
ctx.scale(devicePixelRatio, devicePixelRatio);
// no smoothing
ctx.imageSmoothingEnabled = false;
// upscale
ctx.scale(10, 10);
ctx.drawImage(bmp, 0, 0);
})().catch(console.error);
canvas { width: 300px; height: 150px; }
<canvas></canvas>
Note that for this canvas I took care of avoiding the resizing issue by resizing the <canvas>
's width
and height
based on the current dPR, while downscaling it through CSS.
To render your image correctly in an <img>
, you could prepare multiple versions with different density and then use the srcset
attribute of your <img>
:
<img
width=160 height=160
srcset="
https://i.sstatic.net/7oZk5oeK.png,
https://i.sstatic.net/OlFcowA1.png 2x,
https://i.sstatic.net/QsWfkvSn.png 3x,
">
Or you could use the same image-rendering
CSS rule you used on your <canvas>
:
img { image-rendering: pixelated }
<img src="https://i.sstatic.net/7oZk5oeK.png">