repository

html2image

参考:高质量前端快照方案:来自页面的「自拍」

基础

背景

将网页保存为图片(以下简称为快照),是用户记录和分享页面信息的有效手段,在各种兴趣测试和营销推广等形式的活动页面中尤为常见。

快照环节通常处于页面交互流程的末端,汇总了用户最终的参与结果,直接影响到用户对于活动的完整体验。因此,生成高质量的页面快照,对于活动的传播和品牌的转化具有十分重要的意义。

本文基于云音乐往期优质活动的相关实践(例如「关于你的画」、「权力的游戏」和「你的使用说明书」等),从快照的内容完整性清晰度转换效率等多个方面,讨论将网页转换为高质量图片的实践探索。

适用场景

原理简析

依据图片是否由设备本地生成,快照可分为前端处理和后端处理两种方式。

由于后端生成的方案依赖于网络通信,不可避免地存在通信开销和等待时延,同时对于模板和数据结构变更也有一定的维护成本。

因此,出于实时性灵活性等综合考虑,我们优先选用前端处理的方式。

前端侧对于快照的处理过程,实质上是将 DOM 节点包含的视图信息转换为图片信息的过程。这个过程可以借助 canvas 的原生 API 实现,这也是方案可行性的基础。

theory

具体来说,转换过程是将目标 DOM 节点绘制到 canvas 画布,然后 canvas 画布以图片形式导出。可简单标记为绘制阶段和导出阶段两个步骤:

原生示例

具体地,对于单个 <img> 元素可按如下方式生成自身的快照:

<img id="target" src="./music-icon.png" />
// 获取目标元素
const target = document.getElementById('target');

// 新建canvas画布
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const ctx = canvas.getContext("2d");

// 导出阶段:从canvas导出新的图片
const exportNewImage = (canvas) => {
    const exportImage = document.createElement('img');
    exportImage.src = canvas.toDataURL();
    document.body.appendChild(exportImage);
}

// 绘制阶段:待图片内容加载完毕后绘制画布
target.onload = () => {
    // 将图片内容绘入画布
    ctx.drawImage(target, 0, 0, 100, 100);

    // 将画布内容导出为新的图片
    exportNewImage(canvas);
}

其中,drawImage 是 canvas 上下文对象的实例方法,提供 多种方式CanvasImageSource 源绘制到 canvas 画布上。exportNewImage 用于将 canvas 中的视图信息导出为包含图片展示的 data URI

基础方案

在上一部分中,我们可以看到基于 canvas 提供的相关基础 API,为前端侧的页面快照处理提供了可能。

然而,具体的业务应用往往更加复杂,上面的「低配版」实例显然未能覆盖多数的实际场景,例如:

因此,基于对综合业务场景的考虑,我们采用社区中认可度较高的方案:html2canvascanvas2image 作为实现快照功能的基础库。

html2canvas

提供将 DOM 绘制到 canvas 的能力

这款来自社区的神器,为开发者简化了将逐个 DOM 绘制到 canvas 的过程。简单来说,其基本原理为:

在使用方面,html2canvas 对外暴露了一个可执行函数“

  1. 它的第一个参数用于接收待绘制的目标节点 (必选)
  2. 第二个参数是可选的 配置项,用于设置涉及 canvas 导出的各个参数:
// element 为目标绘制节点,options为可选参数
html2canvas(element[,options]);  

简易调用示例如下:

import html2canvas from 'html2canvas';

const options = {};

// 输入body节点,返回包含body视图内容的canvas对象
html2canvas(document.body, options).then(function(canvas) {
    document.body.appendChild(canvas);
});

canvas2image

提供由 canvas 导出图片信息的多种方法

相比于 html2canvas 承担的复杂绘制流程,canvas2image 所要做的事情简单的多。

canvas2image 仅用于将输入的 canvas 对象按特定格式转换和存储操作,其中这两类操作均支持 PNG,JPEG,GIF,BMP 四种图片类型:

// 格式转换
Canvas2Image.convertToPNG(canvasObj, width, height);
Canvas2Image.convertToJPEG(canvasObj, width, height);
Canvas2Image.convertToGIF(canvasObj, width, height);
Canvas2Image.convertToBMP(canvasObj, width, height);

// 另存为指定格式图片
Canvas2Image.saveAsPNG(canvasObj, width, height);
Canvas2Image.saveAsJPEG(canvasObj, width, height);
Canvas2Image.saveAsGIF(canvasObj, width, height);
Canvas2Image.saveAsBMP(canvasObj, width, height);

实质上,canvas2image 只是提供了针对 canvas 基础 API 的二次封装(例如 getImageDatatoDataURL),而本身并不依赖 html2canvas

html2image

接下来,我们基于以上两个工具库,实现一个基础版的快照生成方案。同样是分为两个阶段:

具体地,我们封装一个 html2Image 的函数,用于输入目标节点以及 配置项参数,输出快照图片信息。

/**
 * @file 封装 html2image 方法
 */
import html2canvas, { Options } from 'html2canvas';
import Canvas2Image from './canvas-to-image';

/**
 * 基础版快照方案
 * @param {HTMLElement} container
 * @param {object} options html2canvas相关配置
 */
export function html2Image(container: HTMLElement, fileName: string, options?: Options) {
    return html2canvas(container, options).then((canvas) => {
        const imageEl = Canvas2Image.saveAsPNG(canvas, canvas.width, canvas.height, fileName);
        return imageEl;
    });
}

function saveFile(strData, fileName) {
    // document.location.href = strData;

    const downloadElement = document.createElement('a');
    downloadElement.href = strData;
    // 下载后文件名
    downloadElement.download = `${fileName}.png`;
    document.body.append(downloadElement);
    // 触发点击下载
    downloadElement.click();
    // 下载完成移除元素
    downloadElement.remove();
}

内容完整性

详见参考

清晰度优化

最终生成快照的清晰度,源头上取决于第一步中 DOM 转换成的 canvas 的清晰度。

以下介绍 5 种行之有效的清晰度优化方法。

使用 Px 单位

为了给到 html2canvas 明确的整数计算值,避免因小数舍入而导致的拉伸模糊,建议将布局中使用中使用 %vwvhrem 等单位的元素样式,统一改为使用 px

good:

<div style="width: 100px;"></div>

bad:

<div style="width: 30%;"></div>

优先使用 Img 标签展示图片

很多情况下,导出图片模糊是由原视图中的图片是以 css 中 background 的方式显示的。

因为 background-size 并不会反馈一个具体的宽高数值,而是通过枚举值如 contain、cover 等代表图片缩放的类型;相对于 <img> 标签, background 方式最终生成的图片会较为模糊。

将 background 改为 <img> 方式呈现,对于图片清晰度会有一定的改观。对于必须要使用 background 的场景,参见 5.25 节的解决方案。

good:

<img class="u-image" src="./music.png" alt="icon">

bad:

<div class="u-image" style="background: url(./music.png);"></div>

配置高倍的 Canvas 画布

对于高分辨率的屏幕,canvas 可通过将 css 像素与高分屏的物理像素对齐,实现一定程度的清晰度提升(这里 对两类像素有详细描述和讨论)。

在具体操作中,创建由 devicePixelRatio 放大的图像,然后使用 css 将其缩小相同的倍数,有效地提高绘制到 canvas 中的图像清晰度表现。

在使用 html2canvas 时,我们可以配置一个放缩后的 canvas 画布用于导入节点的绘制。

// convertToImage.js
import html2canvas from 'html2canvas';

// 创建用于绘制的基础canvas画布
function createBaseCanvas(scale) {
    const canvas = document.createElement("canvas");
    canvas.width = width * scale;
    canvas.height = height * scale;
    canvas.getContext("2d").scale(scale, scale);

    return canvas;
}

// 生成快照
function convertToImage(container, options = {}) {
    // 设置放大倍数
    const scale = window.devicePixelRatio;

    // 创建用于绘制的基础canvas画布
    const canvas = createBaseCanvas(scale);

    // 传入节点原始宽高
    const width = container.offsetWidth;
    const height = container.offsetHeight;   

    // html2canvas配置项
    const ops = {
        scale,
        width,
        height,
        canvas,
        useCORS: true,
        allowTaint: false,
        ...options
    };

    return html2canvas(container, ops).then(canvas => {
        const imageEl = Canvas2Image.convertToPNG(canvas, canvas.width, canvas.height);
        return imageEl;
    });
}

关闭抗锯齿

imageSmoothingEnabledCanvas 2D API 用来设置图片是否平滑的属性,true 表示图片平滑(默认值),false 表示关闭 canvas 抗锯齿。

默认情况下,canvas 的抗锯齿是开启的,可以通过关闭抗锯齿来实现一定程度上的图像锐化,提高线条边缘的清晰度。

据此,我们将以上 createBaseCanvas 方法升级为:

// 创建用于绘制的基础canvas画布
function createBaseCanvas(scale) {
    const canvas = document.createElement("canvas");
    canvas.width = width * scale;
    canvas.height = height * scale;

    const context = canvas.getContext("2d");

    // 关闭抗锯齿
    context.mozImageSmoothingEnabled = false;
    context.webkitImageSmoothingEnabled = false;
    context.msImageSmoothingEnabled = false;
    context.imageSmoothingEnabled = false;

    context.scale(scale, scale);

    return canvas;
}

锐化特定元素

受到 canvas 画布放缩的启发,我们对特定的 DOM 元素也可以采用类似的优化操作,即设置待优化元素宽高设置为 2 倍或 devicePixelRatio 倍,然后通过 css 缩放的方式控制其展示大小不变。

例如,对于必须用背景图 background 的元素,采用以下方式可明显提高快照的清晰度:

.box {
    background: url(/path/to/image) no-repeat;
    width: 100px;
    height: 100px;
    transform: scale(0.5);
    transform-origin: 0 0;
}

其中,widthheight 为实际显示宽高的 2 倍值,通过 transform: scale(0.5) 实现了元素大小的缩放,transform-origin 根据实际情况设置。