H5页面生成图片方案总结

h5页面图片生成是现代 Web 应用中比较常见的需求,有着广泛的应用场景:

  • 分享海报生成:将页面内容转化为图片用于社交媒体分享(QQ、微信朋友圈分享)。
  • 内容保存:允许用户保存页面内容为图片格式。
  • 证书/票据生成:自动生成证书、票据或凭证图片。
  • 数据可视化导出:将图表、报表导出为图片。
  • 截图功能:实现网页截图功能。
  • 水印添加:为页面内容添加水印并导出。
  • 长图生成:将长页面内容合并为一张长图。

html2canvas

html2canvas 生成图片是通过读取 DOM 和应用于元素的不同样式,渲染为 canvas 图像。因此,其生成的图片,可能跟设备(手机、电脑)屏幕上看起来的效果有差异,无法保证 100% 准确。

实现原理

html2canvas 将 DOM 转换为 canvas 图像:

  • 遍历目标 DOM 元素及其子元素。
  • 收集元素的样式、尺寸、位置等信息。
  • 在 Canvas 中重建 DOM 的视觉表现。
  • 处理图片、背景图等资源。
  • 将 Canvas 转换为图片数据 URL。

优点

  • 兼容性好,支持大部分 CSS 属性。
  • 支持跨域图片,但需要正确配置。

缺点

  • 性能较差,处理复杂 DOM 时速度慢。
  • 对某些 CSS3 属性支持不完整。
  • 字体渲染可能不一致。
  • 不支持 Shadow DOM。

真机测试(Lizhao)

iPhone 14 Pro 为例:Canvas 宽高为 594 * 2400px 时,canvas.toDataURL() 会返回结果 data:,,且是非必现**的,有时又能获取正确结果。

无法确定浏览器限制的规则是什么?!

html2canvas(dom).then(canvas => {
    canvas.toDataURL() // data:,
});

另外,据说这是 html2canvas@1.4.1 的 bug,@1.3.4 没有问题,未进一步尝试。

实测总结

  • Android 运行正常,iOS 运行,兼容性较差。

  • 无法绕过浏览器设置的内容策略限制:绘制跨源的图像会污染 canvas,导致 canvas 无法读取。

其他常见问题

  • 模糊问题(需设置合适的 scale)。
  • 滚动内容截取不完整。

dom-to-image

dom-to-image 是一个用 JavaScript 编写的库,能够将任意的 DOM 节点转换为矢量(SVG)格式或位图(PNG 或 JPEG)格式的图像。它基于 Paul Bakaus 的domvas 库,并且已经进行了彻底的重写,修复了一些错误,并添加了一些新功能(如网页字体和图像支持)。

实现原理

dom-to-image 使用 SVG 的 foreignObject 元素实现,SVG 允许 foreignObject 标签内嵌入任意 HTML 内容,实现步骤:

  • 递归克隆目标 DOM。
  • 计算节点及其每个子节点的样式,并将其复制到相应的克隆对象中(重新创建伪元素,因为它们是不会被任何方式复制的)。
  • 嵌入网页字体:找出 @font-face 声明,下载字体文件,进行base64 编码并将内容作为 data: 嵌入,将所有处理后的 CSS 规则连接起来,并将它们放入一个 style 元素中,然后将其附加到克隆对象上。
  • 嵌入图片:img 元素中的URL 和 css 中 background 属性使用的图像。
  • 将克隆的节点序列化为为 XML。
  • 将序列化内容放入 SVG 的 foreignObject 标签。
  • 使用 Canvas 绘制 SVG 内容。
  • 将 Canvas 导出为图片格式。
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
  <foreignObject width="100%" height="100%">
    <body xmlns="http://www.w3.org/1999/xhtml">
      <!-- DOM 内容 -->
    </body>
  </foreignObject>
</svg>

优点

  • 渲染质量较高。
  • 支持更多 CSS 特性。
  • 字体渲染更准确。

缺点

  • 兼容性较差(某些浏览器 foreignObject 支持不完整)。
  • 外部资源加载限制。
  • 样式内联处理复杂。

真机测试(Lizhao)

在 IOS 上,生成图片时,可能 DOM 中的 img 标签的 src 未加载成功,最终生成的图片丢失 img 标签的图像(dom-to-image 也一样)。

根据 html-to-image issues 讨论区的方案 Not all images are loading when saving image to jpeg on iOS,ios 调用 2 次生成图片的方法,可以解决该问题。但实际测试发现:调用 2 次只是减少图像丢失的概率,不能保证一定成功

根本原因可能是 webkit 的 BUG:

HTML + JS 创建SVG,渲染到图像和绘制到画布,使用包含嵌入字体的 SVG 图像的 href 来呈现 img 标记会导致 WebKit 在 img 准备好之前触发 img onload 函数。

我通过创建带有嵌入式字体的 SVG 的 Blob URL 来验证这一点,将其加载到 img 标记并将该 img 绘制到画布——画布仍然是空白的。

这只在第一次加载图像时发生。在随后的尝试和页面刷新中,图像加载正常,嵌入相同字体的其他图像也是如此。重新启动浏览器时,问题再次出现。

这是一个类似于 39059 的问题(canvas drawImage does not render SVG with embedded images correctly),但那个错误专门涉及嵌入图像。

来自——SVG with embedded font triggers img.onload before font is available

import uap from 'ua-parser-js'
import canvasSize from 'canvas-size';
import domToImage from 'dom-to-image'

export async function calcImageMaxScale(element) {
  const userAgent = _get(window, 'navigator.userAgent') || ''
  const ua = userAgent ? uap(userAgent) : ''
  const osVersion = _get(ua, 'os.version') && (typeof Number(_get(ua, 'os.version')) === 'number') ? Number(_get(ua, 'os.version')) : null
  // 旧安卓机限制最多放大2倍,其他机型最大3倍
  const maxScale = isAndroid && (!osVersion || osVersion < 13) ? 2 : 3
  let scale = maxScale
  if (element) {
    const canvasSizeData = await canvasSize.maxArea({
      max: 18000,
      min: 1,
      step: 100
    }).catch(() => {});
    const rect = element.getBoundingClientRect();
    const safetyFactor = isAndroid ? 0.8 : 0.95
    const maxWidth = _get(canvasSizeData, 'width') ? _get(canvasSizeData, 'width') * safetyFactor : 4096 * safetyFactor;
    const maxHeight = _get(canvasSizeData, 'height') ? _get(canvasSizeData, 'height') * safetyFactor : 4096 * safetyFactor;
    scale = Math.min(maxScale, maxWidth / rect.width, maxHeight / rect.height);
    console.log('osVersion: ', osVersion)
    console.log(`canvas: ${canvasSizeData.width}, ${canvasSizeData.height};rect: ${rect.width}, ${rect.height};safe: ${maxWidth}, ${maxHeight};scale: ${scale}`)
  }
  return scale
}

export async function domToImageToPng(element) {
  if (element) {
    const scale = await calcImageMaxScale(element)
    const rect = element.getBoundingClientRect()
    const width = rect.width * scale
    const height = rect.height * scale
    const config = {
      width,
      height,
      style: {
        transform: `scale(${scale})`,
        transformOrigin: 'top left'
      }
    }
    console.log(`dom-to-image: ${width}, ${height}`)
    return domToImage
      .toPng(element, config)
      .catch(error => {
        console.error('导出图片: ', error)
        Toast('生成图片失败!')
        throw error
      })
  }
  return Promise.resolve('')
}

实现总结: 支持 Andorid、iOS 上生成图片,但浏览器必需支持 Promise、SVG 的 foreignObject 标签。

备注: dom-to-image 基本不再维护,最新更新时间是 8 年前(2017年)。

html-to-image

html-to-image 使用 HTML5 Canvas 和 SVG 从 DOM 节点生成图像,该包是从 dom-to-image 派生的,使用更易维护的代码和一些新特性。

与 dom-to-image 的差异

  • 新增了一些特性:pixelRatio(图版像素比率)、cacheBust(清除缓存)。
  • 新增输出方法和格式:toCanvas(返回 Canvas 元素)、toPixelData(返回像素数据)。

实现原理

html-to-image 是基于 dom-to-image 的升级版/改进版,实现原理类似,其优缺点也类似。

注意:html-to-image 也会出现图片丢失的问题。

真机测试(Lizhao)

页面上一个包含图片的 DOM,用 dom-to-image 可以正常生成图片,改用 html-to-image,生成图片时报错。

export async function htmlToImageToPng(element) {
  if (element) {
    const scale = await calcImageMaxScale(element)
    const config = {
      pixelRatio: scale,
      skipFonts: true
    }
    console.log(`html-to-image`)
    return toPng(element, config)
      .catch(error => {
        console.error('导出图片: ', error)
        Toast('生成图片失败!')
        throw error
      })
  }
  return Promise.resolve('')
}
Event {isTrusted: true, type: 'error', target: img, currentTarget: img, eventPhase: 2, …}
  isTrusted: true
  __sentry_captured__: true
  bubbles: false
  cancelBubble: false
  cancelable: false
  composed: false
  currentTarget: null
  defaultPrevented: false
  eventPhase: 0
  returnValue: true
  srcElement: null
  target: null
  timeStamp: 6319.699999999255
  type: "error"

实现总结:没有找到具体原因,大致定位是 html-to-image 内部图片处理问题(也有可能跟跨域有关,未确定),解决方案是:生成图片前,先将 DOM 内的需要嵌入的图片转成 base64。

export async function imageUrlToBase64(url) {
  if (!url) {
    return ''
  }
  const response = await fetch(url, { mode: 'cors' })
  if (!response.ok) {
    throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`)
  }
  const blob = await response.blob()

  return await new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = () => resolve(reader.result)
    reader.onerror = reject
    reader.readAsDataURL(blob)
  })
}

modern-screenshot

modern-screenshot 是 html-to-image 的一个分支,或者说继承与发展,它在 html-to-image 的基础上,整合了其他类似库(如,dom-to-image-more)的一些优化,并引入了更现代的浏览器特性,旨在提供一个更强大、更高效的解决方案。

与 html-to-image 差异

特性维度 html-to-image modern-screenshot
核心渲染策略 主要依赖 SVG foreignObject` 技术 多渲染策略:优先尝试 SVG 的 foreignObject ,失败时可能回退到 Canvas 2D API 绘制数据URL方式
资源处理与跨域 存在跨域资源处理问题 整合了 dom-to-image-more 的优化,能更好地处理跨域资源
性能优化 - 支持 Web WorkerOffscreenCanvas,允许在后台线程进行渲染,避免阻塞主线程。
API 与现代特性 - 应用现代 Web API(Web Worker、OffscreenCanvas),并可能使用 createImageBitmap 处理图像。

OffscreenCanvas:允许将 Canvas 的绘制工作从主线程移至 Web Worker,以避免阻塞主线程

createImageBitmap:可以异步地解码图像数据(如从 Blob 或 URL),并生成一个低延迟ImageBitmap 对象,该对象可以高效地绘制到 Canvas 上。这对于在 Worker 中预处理图片资源非常有用。

总的来说:modern-screenshot 可以看作是集大成者的现代化 DOM 截图方案。它在继承 html-to-image 的基础上,通过多渲染策略、离屏渲染优化和对现代Web API的充分利用,在兼容性、性能和开发体验上都有了显著的进步。

实现原理(多渲染策略)

  • SVG foreignObject(最高质量):核心且质量最高的方法。它允许将完整的 DOM 元素嵌入 SVG 图片中,能很好地保留 CSS 样式。

  • Canvas 2D API绘制:将 DOM 元素及其样式手动解析并通过 Canvas 的绘图指令(如绘制矩形、文本等)重新绘制到Canvas上。虽然控制更精细,但可能无法 100% 还原所有 CSS 特性。

  • 数据 URL 方式:将其他渲染方式(如SVG)的结果通过 Canvas 转换为特定格式的 Data URL(Canvas的 toDataURL 方法会把图像数据编码成一个 Base64 字符串)。

优点

  • 兼容性、性能较好。
  • 支持跨域图片
  • 支持 Shadow DOM。
  • 图像优化:锐化图像、图像质量控制和压缩。

缺点

  • 浏览器兼容性问题,对老旧浏览器支持有限。
  • 文档相对较少,生态不够成熟。

真机测试(Lizhao)

iPhone 12、IOS 18.6.2: 在 DOM 中有 table 的场景下,生成的图片中,表格底部缺少一截。一样的 DOM 用 dom-to-image 生成的图片是正常的,而改用 modern-screenshot 出现了该问题。

export async function modernScreenshotToPng(element) {
  if (element) {
    const scale = await calcImageMaxScale(element)
    const config = {
      scale,
      features: {
        removeControlCharacter: false
      }
    }
    console.log('modern-screenshot')
    return domToPng(element, config)
      .catch(error => {
        console.error('导出图片: ', error)
        Toast('生成图片失败!')
        throw error
      })
  }
  return Promise.resolve('')
}

实测总结:跟 table 的 border-collapse: separate; 和 td 的 border 设置的边宽。遇到该问题,先确定 border 的边宽不要用 rem、em 等相对单位,改用 px;如果还未能解决,再尝试将 border-collapse 的值改为 collapse。

注意: 设置 border-collapse:collapse 属性,如果单元格设置 opacity: 0,单元格设置了 border: 1px solik red,边框还是会显示。

rasterizehtml

rasterizehtml 核心原理:使用 SVG foreignObject 元素来渲染 HTML。

实现原理

  • foreignObject 机制:允许在 SVG 中嵌入 HTML 内容。
  • 浏览器原生渲染:HTML 由浏览器引擎直接渲染,保证准确性。
  • SVG 作为中间格式:HTML → SVG → Canvas → PNG/JPEG。
  • 数据 URL 转换:通过 Data URL 实现跨格式转换。

重要限制

  • 跨域资源受限:外部图片、字体可能无法加载。
  • 样式作用域:需要手动处理 CSS 隔离。
  • 浏览器兼容性:依赖 foreignObject 支持。
const generateStyledHTML = (content) => `
  <!DOCTYPE html>
  <html>
  <head>
    <meta charset="UTF-8">
    <style>
      .container { color: red; }
    </style>
  </head>
  <body>
    <div class="container">
      ${content}
    </div>
  </body>
  </html>
`;

rasterizeHTML.drawHTML(generateStyledHTML("我的内容"), 800, 600);

无头浏览器

puppeteer、PhantomJS、SlimerJS 等等无头浏览器,也具备截图的能力。

puppeteer 为例:Puppeteer 是一个 Node.js 库,提供高级 API 来控制 Chrome 或 Chromium 浏览器。它可以用于生成网页截图、PDF、自动化测试等。

const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png' });
  await browser.close();
})()

优点

  • 完美渲染质量:使用真实浏览器,100%还原网页效果。
  • 完整 CSS 支持:支持所有现代 CSS 特性。
  • JavaScript执行:可以处理动态内容。
  • 跨域资源:无跨域限制。
  • 缩放控制:支持高 DPI 截图。
  • 稳定可靠:基于 Chrome 官方 API。

缺点

  • 资源消耗大:需要启动浏览器实例。
  • 速度较慢:相比客户端方案。
  • 服务器部署需要 Node.js 或者 Java 等服务器环境

常见问题

图片跨域

图片丢失(safari)

图片无法生成(canvas大小限制)

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-01-28 09:13:27

results matching ""

    No results matching ""