You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 07:58:12