一个电子教材项目中有这样一个需求:
用户向网站上传一个PDF书籍后,网站可以对PDF书籍进行解析,并支持用户对PDF书籍的每一页做一些操作,比如:为英语课本的单词和句子添加音频热区。因为热区数量很多,所以,希望网站 “在初始化课本页面的时候,自动初始化热区,然后用户再在此基础上调整”,这样可以大大减少工作量。
我使用pdf.js来实现该功能,该库可以获取到pdf中的文本及位置、宽高,但这些位置尺寸使用起来有几处值得注意的细节(稍不注意,可能会被卡很久)。
一、pdf.js提供的文本信息
如图所示,这是一个PDF页面中获取到的文本信息。这里要用到的字段有:height(高)、width(宽)、transform(这是组matrix矩阵数据,其中最末两位分别是水平方向和垂直方向的位置信息)。
二、transform数据对应的坐标系
1)初始坐标数据
通常,我们定位一个元素时,会设置它的left和top,left的数值从左向右递增,top的数值自上而下递增;而transform中的垂直方向数值是从下往上递增的。(下图是坐标系不同导致的错误结果,这个结果是多种原因造成的,后续我们逐一修正)
2)矫正坐标数据(垂直方向翻转)
基于上一步,首先,先来矫正坐标系。我们将垂直方向的数值进行矫正:
新值 = pdf页面高度 - 旧值。
再次渲染后,会发现垂直方向的坐标系已经对了。但仍有两个问题:一个是横纵方向的定位都存在偏差,另一个是热区的宽高比实际文本所占空间大。这主要是因为“绘制pdf的canvas画布的width、height”和“canvas画布在页面布局中被定义的样式style中的width、height”不一致,二者存在比例换算。
三、数据换算
1)矫正比例换算
基于上一步,结合canvas的内外尺寸来矫正热区的宽高和定位:
left = textinfo.transform[4] / canvas.width * canvas.style.width;
top = textinfo.transform[5] / canvas.height * canvas.style.height;
width = textinfo.width / canvas.width * canvas.style.width;
height = textinfo.height / canvas.height * canvas.style.height;
再次渲染后,会发现水平方向的尺寸、定位已经对了。但垂直方向上的定位仍然存在少许偏差。这个问题很细节,我困扰了好几个小时才发觉:我们已经知道初始时的垂直坐标是自下而上的,那么在垂直翻转时,应该把文本所占的高度也减掉才对。
2)再次矫正垂直方向数值
新值 = (pdf页面高度 - 旧值 - 文本自身高度) / canvas.height * canvas.style.height。
修改后再次渲染,可以发现效果已经符合预期了。
四、相关代码片段展示
initHotspots() {
let pdfDoc = this.loadingTaskDict[this.pageActiveIndex] || this.loadingTask;
if (!pdfDoc) return;
pdfDoc.promise.then((pdf) => {
let pageIndex = this.loadingTaskDict[this.pageActiveIndex] ? 1 : this.pageActiveIndex;
pdf.getPage(pageIndex).then((page) => {
let view = page.view || [];
let pdfPageWidth = view[2] - view[0]; // pdf页面宽度(canvas.width)
let pdfPageHeight = view[3] - view[1]; // pdf页面高度(canvas.height)
page.getTextContent().then((textInfo) => {
textInfo = textInfo || {};
let textItems = textInfo.items || [];
// bookPageDom是网页中pdf页面的包裹元素(bookPageInfo.width相当于canvas.style.width)
let bookPageDom = document.querySelector('.book-page');
let bookPageInfo = bookPageDom ? bookPageDom.getBoundingClientRect() : null;
textItems.forEach((v) => {
if (/[a-zA-Z]+/i.test(v.str) && v.str.length > 7 && bookPageInfo) {
let x = (v.transform[4] / pdfPageWidth) * 100 + '%';
let y = ((pdfPageHeight - (v.transform[5] + v.height)) / pdfPageHeight) * 100 + '%';
this.addHotpot({
top: y,
'y%': y,
left: x,
'x%': x,
width: (v.width / pdfPageWidth) * bookPageInfo.width,
height: (v.height / pdfPageHeight) * bookPageInfo.height,
original: v.str,
styles: { left: x, top: y }
});
}
});
});
});
});
},
五、最后
上方的截图,因为受制于页面布局,课本页面的尺寸比较小,看不清楚。所以,下面是一张demo效果图: