zzxworld

使用 SVG + Canvas 把 HTML 转换为图片

最近准备做一个新的项目,其中有一个分享功能希望能够把 HTML 页面以图片的方式发布到社交平台。所以研究了一下 HTML 转图片的功能,实现这个功能需要用到 SVG 和 Canvas 技术。

先来说说大致流程,就跟「把大象关进冰箱」一样,需要三步:

  1. 首先,确定需要渲染的 HTML 元素和 CSS 样式,CSS 样式最好能独立出来。
  2. 使用 SVG 元素包裹需要渲染的 HTML 代码。
  3. 使用 Canvas 把 SVG 代码转换为 PNG 格式图片。

我将从一段简单的 HTML 代码开始,来详细介绍一下这三步的流程。

首先来看一段简单的 HTML 代码:

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>HTML to Image</title>
<style>
.card {
    background-color: white;
    margin: 2em;
    display: flex;
    box-shadow: 0 0 1em rgba(0, 0, 0, 0.2);
    border-radius: 0.5em;
    width: 400px;
}

.cover {
    width: 180px;
}

.cover img {
    display: block;
    max-width: 100%;
    border-radius: 0.5em 0 0 0.5em;
}

.text {
    flex: 1;
    text-align: center;
    padding-top: 2em;
}

.text h1 {
    font-size: 1em;
}
</style>
</head>
<body>
    <h2>HTML</h2>
    <div class="card">
        <div class="cover">
            <img src="/libai.jpg" />
        </div>
        <div class="text">
            <h1>静夜思</h1>
            <p>床前看月光</p>
            <p>疑是地上霜</p>
            <p>抬头望山月</p>
            <p>低头思故乡</p>
        </div>
    </div>
</body>
</html>

引用了李白的一首诗,这段 HTML 和 CSS 代码的显示效果如下:

Screenshot HTML

假设这段代码中 class 属性为 card 的标签就是要转换问图片的内容。先看看如何把它包裹到 SVG 中并显示。把 body 标签中的内容替换为如下代码:

<h2>SVG</h2>
<svg width="470" height="320" viewBox="0 0 470 320">
  <foreignObject width="100%" height="100%">
    <div xmlns="http://www.w3.org/1999/xhtml">
        <div class="card">
            <div class="cover">
                <img src="/libai.jpg" />
            </div>
            <div class="text">
                <h1>静夜思</h1>
                <p>床前看月光</p>
                <p>疑是地上霜</p>
                <p>抬头望山月</p>
                <p>低头思故乡</p>
            </div>
        </div>
    </div>
  </foreignObject>
</svg>

这段代码主要就是 foreignObject 标签和它的 xmlns 命名空间属性。定义为 HTML 的命名空间后,就可以使 SVG 标签以 HTML 的方式渲染其中的内容。来看看效果:

Screenshot SVG

是不是和 HTML 效果一模一样?

SVG 是支持直接在 img 标签中显示的图片格式。有了这个 SVG 的渲染内容,接下来就只需要通过 Canvas 图形绘制技术来渲染 SVG 图片并转换格式就行了。

svg 标签后添加以下代码:

<button>生成图片</button>

<script>
    // 把图片资源转换为 Data URI 格式
    const buildImageData = (resource) => {
        const canvas = document.createElement('canvas')
        const context = canvas.getContext('2d')

        canvas.width = resource.width
        canvas.height = resource.height
        context.drawImage(resource, 0, 0)

        return canvas.toDataURL()
    }

    // 点击按钮时生成图片并下载
    document.querySelector('button').addEventListener('click', () => {
        const img = new Image()
        const svg = document.querySelector('svg').cloneNode(true)

        // 把 style 标签中的 CSS 代码引入 SVG 图片中
        svg.querySelector('foreignObject')
          .before(document.querySelector('style')
          .cloneNode(true))

        // 转换 HTML 代码中的图片地址
        svgImg = svg.querySelector('img')
        svgImg.src = buildImageData(svgImg)

        // 设置 HTML 转 SVG 后的图片内容格式
        img.src = 'data:image/svg+xml;charset=utf-8,'+
            new XMLSerializer().serializeToString(svg)

        // 延时 100 毫秒,等待 img 对象完成图片的渲染加载后执行下载
        setTimeout(() => {
            const a = document.createElement('a')
            a.download = 'image.png'
            a.href = buildImageData(img)
            a.click()
        }, 100)
    })
</script>

代码中关键位置添加了有注释,这里就不再重复解释。主要有两个关键点:

  1. 必须把和导出 HTML 相关的 CSS 样式都塞到 svg 标签中去,否则生成的图片会没有样式。
  2. 如果要导出的 HTML 中包含 URL 地址的 img 标签,需要转换为使用 Data URI 格式,否则会出错。

点击「生成图片」按钮,就会出现一个下载提示。下载内容是 HTML 转换成的 PNG 图片。这是上面 HTML 内容转换成图片的结果:

Screenshot 1