> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/research/10h5-ye-mian-sheng-cheng-tu-pian-fang-an-zong-jie.md).

# H5页面生成图片方案总结

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

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

### [html2canvas](https://www.npmjs.com/package/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:,`，且是**非必现\*\*的，有时又能获取正确结果。

无法确定浏览器限制的规则是什么？！

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

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

**实测总结**：

* Android 运行正常，iOS 运行，兼容性较差。
* 无法绕过浏览器设置的内容策略限制：绘制跨源的图像会污染 canvas，导致 canvas 无法读取。

#### 其他常见问题

* 模糊问题（需设置合适的 scale）。
* 滚动内容截取不完整。

### [dom-to-image](https://www.npmjs.com/package/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 导出为图片格式。

```html
<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](https://github.com/bubkoo/html-to-image/issues/52)，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](https://bugs.webkit.org/show_bug.cgi?id=39059)），但那个错误专门涉及嵌入图像。
>
> 来自——[SVG with embedded font triggers img.onload before font is available](https://bugs.webkit.org/show_bug.cgi?id=219770)

```javascript
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](https://www.npmjs.com/package/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，生成图片时报错。

```javascript
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('')
}
```

```shell
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。

```javascript
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 Worker** 和 **OffscreenCanvas**，允许在后台线程进行渲染，避免阻塞主线程。                              |
| **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 出现了该问题。

```javascript
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](https://www.npmjs.com/package/rasterizehtml)

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

#### 实现原理

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

#### 重要限制

* **跨域资源受限**：外部图片、字体可能无法加载。
* **样式作用域**：需要手动处理 CSS 隔离。
* **浏览器兼容性**：依赖 foreignObject 支持。

```javascript
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、自动化测试等。

```javascript
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大小限制）
