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 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 出现了该问题。
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 等服务器环境。