Canvas通过toBlob导出图片时边缘出现插值问题
解决Canvas放大导出图片模糊(高DPI屏)问题
问题根源
在macOS的2倍DPI显示屏上,浏览器会自动将Canvas的实际渲染尺寸乘以设备像素比(devicePixelRatio)。你的代码仅设置了Canvas的CSS尺寸和绘图上下文的平滑开关,但drawImage跨Canvas绘制时,会受设备像素比影响,导致实际缩放比例并非严格整数倍,即便关闭了图像平滑,非整数倍缩放仍会触发插值模糊,最终导出的图片和Canvas显示效果不一致。
两种可行解决方案
方案1:适配设备像素比,确保整数倍缩放
通过获取设备像素比,调整Canvas的实际宽高与绘制参数,让drawImage执行严格的整数倍缩放:
<canvas id="unscaledCanvas"></canvas> <canvas id="increasedScale"></canvas> <img id="output" />
// 生成图片的函数保持不变 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 } const size = 16 const imageData = generateImage(size, size, 0) const scale = 10 // 获取当前设备像素比 const dpr = window.devicePixelRatio || 1 // 处理原始Canvas,适配DPI const unscaledCanvas = document.getElementById("unscaledCanvas") unscaledCanvas.width = size * dpr unscaledCanvas.height = size * dpr unscaledCanvas.style.width = `${size}px` unscaledCanvas.style.height = `${size}px` const unscaledContext = unscaledCanvas.getContext("2d") unscaledContext.scale(dpr, dpr) unscaledContext.putImageData(imageData, 0, 0) // 处理缩放后的Canvas,适配DPI并确保整数倍缩放 const scaledSize = size * scale const scaledCanvas = document.getElementById("increasedScale") scaledCanvas.width = scaledSize * dpr scaledCanvas.height = scaledSize * dpr scaledCanvas.style.width = `${scaledSize}px` scaledCanvas.style.height = `${scaledSize}px` scaledCanvas.style.imageRendering = "pixelated" const scaledContext = scaledCanvas.getContext("2d") scaledContext.imageSmoothingEnabled = false scaledContext.mozImageSmoothingEnabled = false scaledContext.webkitImageSmoothingEnabled = false // 按DPI缩放上下文,保证绘制尺寸为整数倍 scaledContext.scale(dpr, dpr) scaledContext.drawImage(unscaledCanvas, 0, 0, scaledSize, scaledSize) // 导出图片 const image = document.getElementById("output") image.width = scaledSize image.height = scaledSize scaledCanvas.toBlob((blob) => { const dataURL = URL.createObjectURL(blob) image.onload = () => URL.revokeObjectURL(dataURL) image.src = dataURL })
方案2:手动像素复制,彻底绕开浏览器缩放逻辑
直接读取原始图片的像素数据,手动将每个像素复制scale次,生成放大后的ImageData再绘制,完全避免浏览器的自动插值:
<canvas id="increasedScale"></canvas> <img id="output" />
// 生成图片的函数保持不变 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 } const size = 16 const imageData = generateImage(size, size, 0) const scale = 10 const scaledSize = size * scale // 手动生成放大后的像素数据 const scaledImageData = new ImageData(scaledSize, scaledSize) for (let y = 0; y < size; y++) { for (let x = 0; x < size; x++) { const srcIndex = (y * size + x) * 4 // 获取原像素的RGBA值 const r = imageData.data[srcIndex] const g = imageData.data[srcIndex + 1] const b = imageData.data[srcIndex + 2] const a = imageData.data[srcIndex + 3] // 将原像素复制scale*scale次 for (let sy = 0; sy < scale; sy++) { for (let sx = 0; sx < scale; sx++) { const destIndex = ((y * scale + sy) * scaledSize + (x * scale + sx)) * 4 scaledImageData.data[destIndex] = r scaledImageData.data[destIndex + 1] = g scaledImageData.data[destIndex + 2] = b scaledImageData.data[destIndex + 3] = a } } } } // 绘制并导出图片 const scaledCanvas = document.getElementById("increasedScale") scaledCanvas.width = scaledSize scaledCanvas.height = scaledSize scaledCanvas.style.width = `${scaledSize}px` scaledCanvas.style.height = `${scaledSize}px` const scaledContext = scaledCanvas.getContext("2d") scaledContext.putImageData(scaledImageData, 0, 0) const image = document.getElementById("output") image.width = scaledSize image.height = scaledSize scaledCanvas.toBlob((blob) => { const dataURL = URL.createObjectURL(blob) image.onload = () => URL.revokeObjectURL(dataURL) image.src = dataURL })
效果说明
方案1通过适配设备像素比,让drawImage执行严格的整数倍缩放,关闭平滑后即可得到无插值的清晰图片;方案2手动复制像素,完全规避了浏览器的自动缩放逻辑,适合对像素精度要求极高的场景。两种方案导出的图片均会与Canvas显示效果完全一致。
内容的提问来源于stack exchange,提问作者Joseph Duffy
相关产品推荐
相关产品推荐

