背景
之前开发 Arknights生涯表 的时候,有一个对结果页进行截图导出的操作,在safari上遇到个奇葩问题,这里记录下(真不愧是当代IE)
现象
需要截图的页面如下
但是在safari上会出现导出的图里,原本在网页上是图片元素的部分,被截断或者没渲染的情况,如下图
实现
提出解决方案前,简单说下这里导出操作的实现原理:
- 捕获目标dom(这里直接捕获了整个body)
- 将获取到的dom转成svg(这利用了dom-to-image)
- 创建离屏canvas绘制该svg内容
- 导出 离屏canvas 为图片保存
- 转换的目的是为了在canvas上做 超采样,让导出图清晰一些
也试过其他库成品库(例如 html2canvas),也会有这个问题,甚至还多出一些其他样式不一致问题等,所以自己手搓了
排查记录
实现的核心代码
// ...
export const savePngByCanvas = async (isDown = false) => {
const svgString = await domtoimage.toSvg(document.body!, {
bgcolor: getColorScheme() === "dark" ? "black" : "white",
});
return new Promise((res, rej) => {
// 超采样倍率
const scaleFactor = 3;
// 创建 Canvas 元素
const canvas = new OffscreenCanvas(
document.body.clientWidth * scaleFactor,
document.body.clientHeight * scaleFactor
);
const ctx = canvas.getContext("2d");
ctx?.scale(scaleFactor, scaleFactor);
// 创建图像对象
const img = new Image();
img.id = "result";
img.crossOrigin = "anonymous"; // 设置跨域
img.onload = async e => {
try {
if (e.target) {
ctx!.drawImage(img, 0, 0);
saveAs(await canvas.convertToBlob(), "Arknights.png");
res(true);
}
res(true);
} catch (error: any) {
rej(`图片保存失败${error.toString()}`);
}
};
img.onerror = err => {
rej(`图片导出失败:渲染失败,${err.toString()}`);
};
// 加载 SVG 数据到图像对象
img.src = svgString;
});
};
// ...
上述是导出的逻辑,一开始怀疑是图片 没有正确加载 或 没有加载完 就去绘制了,于是将导出操作放到了图片加载完的onload事件里,再观察网络面板以及打log,执行时图片是能正确请求且拿到内容,但safari还是有问题,且每次导出缺失的部分也不一样
接着就怀疑是 drawImage 在两个浏览器的表现行为不一致,因为两个的图形引擎确实不一样,safari是:webkit,chrome的是:Blink + skia
于是多次调试和尝试观察,得到的结论就是safari在绘制时,被绘制的内容包含图片等外部资源,drawimage并不保证画布一定加载完这些资源且绘制完,但chrome一定保证之后的操作拿到的画布一定都是绘制完整的,然后查了查一些资料,找到其他人也有反馈webkit有这个问题:canvas drawImage does not render SVG with embedded images correctly
解决
找到了问题,就可以开始处理了,既然问题是drawimage后画布不一定渲染完外部资源了,那就做一些延迟等待,再进行对canvas的导出操作即可,但是这就是另一个坑,在导出前延迟好几秒在做转换导出,发现还是会偶现图片渲染不完整的情况
最后尝试了各种异步等操作还是不行,然后想着既然是渲染不及时,那多渲染几次,一定能渲染完
于是最终版:检查用户使用Apple生态的设备时,每次渲染3次,每次渲染保证延迟5帧后在调用下一次,在最后一次操作时导出图片就可以完全解决(计算帧率的函数可以用 requestAnimationFrame)
最终版 源码