背景
在项目中使用pdfjs渲染页面,出现缩放和dpr异常,表现为文档内容等比缩放到页面左上部分,且上下颠倒。
问题排查
问题出现之前,使用的渲染方式是getDocument完成后,逐页面循环调用page.render,直到所有页面都加载完成。为了节省资源,引入 Intersection Observer以后,发现此问题出现。
查找资料得知,出现此问题的原因99%都是因为并行操作canvas导致的。
现在逻辑是每次收到Observer回调,就调一次渲染单页(先设置canvas宽高,再调用page.render)。Intersection Observer有时会反复针对某一页面进行回调,虽然我们已经使用了一个set进行标记加锁防止重复渲染,但是只保护了page.render的调用,漏掉了设置canvas尺寸的逻辑,这就导致了在page.render过程中依然可以设置了canvas宽高的情况,也就出现了开头这种异常情况。将canvas尺寸修改也加到保护范围后,问题解决。
解决方法
想办法确保不要同时操纵canvas,包括page.render和修改canvas height/width等。此处使用的方案是一个set,渲染中将pageNum放到set中,每次请求渲染前检查page是否在set中。注意,这只是个简单的实现,极端情况下依然可能存在竞争的情况。
伪代码如下:
const busyPageSet = new Set();
const renderPage = async (pageNum) => {
// 关键代码,判断当前页面是否被标记为正在渲染状态
if (busyPageSet.has(pageNum)) {
return;
}
busyPageSet.add(pageNum); // 标记当前页面为渲染状态
const page = await pdfDoc.getPage(pageNum);
const viewport = page.getViewport(); // 并处理scale和dpr等缩放逻辑
const canvas = getCanvasRef(pageNum); // 获取canvas
canvas.width = ...; // 设置canvas高度
await page.render({context: ..., viewport: ...}).promise; // 调用pdfjs渲染canvas
busyPageSet.delete(pageNum); // 解除标记当前页面的渲染状态
}
补充
当重复调用page.render时,pdfjs会在控制台输出告警。但是若渲染时进行了canvas的尺寸修改、直接调用draw命令,pdfjs不会发出告警。
本次问题主要原因是加锁保护时,设置的保护范围不全面,漏掉了其他canvas操作,而这又不会触发告警,导致排查的时间有点长,值得吸取经验。
参考资料
https://stackoverflow.com/questions/44885973/pdf-js-document-is-presented-upside-down-randomly
https://github.com/mozilla/pdf.js/issues/11277#issuecomment-546498526