本文首发于微保技术公众号css
因微保小程序的业务特性,常常须要用户在小程序中打开PDF用于浏览各保险条款或是产品介绍。本来的解决方案是在利用小程序的下载文档功能在新的页面下载并打开PDF文档,随着业务的发展,在H5以及iframe场景也有在线浏览PDF的需求。html
H5大都有其本身处理PDF的方案而且小程序的浏览也已经解决了,为何还须要考虑预览平台呢?前端
① 在H5页面内打开PDF,iOS系统一般会使用内置浏览器直接打开PDF,而安卓系统则大部分会自动下载PDF,而且不会提示用户。再加上小程序的处理方案,能够发现单单是打开PDF这一件事就有了三种不一样的处理方式,这样分散且不可控的处理,不是一个可持续发展的模式。git
② 在iframe上展现PDF的效果也不尽如人意,iOS系统中的iframe存在久远且影响浏览的Bug:在iOS的iframe中打开PDF会使iframe内的容器被无限撑大,最终致使PDF没法正常展现,更不要说用户的正常浏览了。github
③ 因为平台上的PDF是由各保司提供的,不能保证PDF的字体、样式都是统一的。这就可能会出现因字体过于特殊,致使系统默认方式打开的PDF一片空白。web
④ 与如今推崇的千人千面不同,PDF浏览器对用户而言是一个工具,相比起多样化的操做,统一的交互以及界面将会更容易被用户接纳。同一位用户就算在三个不一样的客户端打开咱们的页面,也能用同一套操做理念进行浏览或操做。canvas
预览平台的核心功能 -- 将PDF转换成客户端可正常浏览的格式,对小程序与H5而言,最通用且稳定的格式就是图片了。方向有了,咱们后续的实现思路不外乎这两种:离线转换、实时渲染。小程序
离线转换后端
本方案中,咱们须要开发的是一个转换服务,这个服务负责将收集到的PDF转换成图片并储存到CDN,而且须要生成一份对应的PDF特征描述文件。api
前端则须要按规则请求对应的PDF特征文件,再根据文件中的PDF特征(PDF张数、PDF储存路径等)批量加载PDF图片。看起来该方案流程清晰而且不须要考虑兼容性问题(前端加载图片基本不须要考虑兼容性)。
但这有一个很致命的缺点,就是没法应对新PDF的加载,咱们必须将全部可能使用到的PDF都手动的推送到服务中,通知其转换并存储。可是在实际操做中,想作到确保所有PDF都成功存储几乎是不太现实的,随便一个临时改动的PDF文档均可能破坏其中的平衡,而且相应的开发人员还须要时刻注意PDF的更新。
实时渲染
本方案中,咱们不须要开发后端服务,须要作的是开发一个PDF浏览“架子”,咱们只须要传入PDF的连接,就能够在前端直接渲染出PDF文档。这个架子将会接收PDF连接,直接下载PDF文件并将其解析、渲染成canvas最后转换成图片展现在页面上。
在本方案中,开发者继续不须要再次维护这个PDF项目,就算有新的PDF须要展现,也只须要在访问页面的时候把相应的url带上便可。
综合开发成本、实际使用考虑,咱们比较倾向于实时渲染方案,这时候就轮到mozilla/pdf.js出场了。
预期中浏览界面应该是全屏展现PDF文档内容,并容许用户经过滑动来浏览剩余的内容。
<body>
<script src="https://unpkg.com/pdfjs-dist@2.2.228/build/pdf.js"></script>
<script src="https://unpkg.com/pdfjs-dist@2.2.228/build/pdf.worker.js"></script>
<script src="./pdf.wesure.js"></script>
</body>
复制代码
以上就是HTML部分的代码,能够看到body里面并无任何内容,咱们看到的PDF内容都是在JS中处理完以后再统一插入到DOM树中,接下来咱们看一下生成PDF的逻辑。
function document() {
var loadingTask = pdfjsLib.getDocument({
url: path,
})
loadingTask.promise.then(function (pdf) {
var index = 1;
var div = document.createElement('DIV');
var canvas = document.createElement("CANVAS");
var className = 'container the-canvas-' + index;
div.setAttribute('class', className)
canvas.id = 'the-canvas-' + index;
div.appendChild(canvas)
document.body.appendChild(div)
pdf.getPage(index).then(function (page) {
var scale = 1;
viewport = page.getViewport({
scale: scale
});
var canvas = document.getElementById('the-canvas-' + index);
var context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
var renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext)
});
}
复制代码
首先,咱们经过pdfjs的getDocument方法将目标PDF下载下来,并得到PDF的相关配置(大小、页数等)。而后开始"组装"咱们的PDF页面,从上面的代码不难看出,一张PDF的内容的包裹关系以下:DIV > CANVAS > PDF-CONTENT。将上面的代码执行后,咱们会获得下面这种效果的PDF浏览页。
咱们会发现,PDF显示不全而且也不像是移动端的显示模式。用户想要看到完整的内容只能经过放缩,这未免体验太差了。
既然显示的结果是内容过大,那咱们可否在渲染的时候就将其缩小呢?
在考虑适配方案以前咱们先看看viewport里面获得的是什么内容。
结合打印出的内容以及API文档上的介绍,咱们能够知道这是PDF自己的属性,由于咱们传入的scale是1,因此咱们应该获得的是“一倍图”PDF的尺寸,说到“一倍图”,咱们很容易能联想到在web端处理小于12px字体的状况,实际上它们的处理方案确实很像。
当咱们须要在页面显示小于12px的字体时,咱们有一个方案就是将那部分字体大小先放大一倍(假如须要10px的字体,咱们会先获得20px的字体,而后再transform: scale(0.5, 0.5)),而后在将其缩小一倍,而后处理它的位置。
思路有了,咱们要怎么将其运用到PDF浏览里面呢?咱们尝试将全部的canvas都缩小,效果以下。
这是怎么回事呢?PDF显示的位置偏移的十分离谱,这就不得不说一下咱们的transfrom-scale,咱们在进行变形时,css会默认将其放缩的基点放在整项的中间,就等于咱们在设计时会说到的中心放缩。
因此咱们在进行放缩类的操做后,须要进行一下变形基点的设定,也就是transform-origin属性。
canvas {
transform: scale(0.5, 0.5);
transform-origin: 0 0;
}
复制代码
解决了位置偏移的问题后,又有新的问题出现了 -- 这个PDF缩放后过小了,无法铺满屏幕,那么咱们是否能够经过与页面宽度得出一个关系,让其能够铺满屏幕呢?
就拿咱们上面的测试PDF来讲,scale为1的时候,PDF宽度为750,然而视窗是iPhone8 plus的尺寸,因此将canvas缩小一半会令其没法横向铺满视窗。如今有两个方案,一个是动态的transform放缩尺寸,另外一个则是getViewport时动态计算scale数值。从css对小数数值的兼容性考虑,最终我选择了后者。
// 初始scale数值
var scale = 1;
// 获取PDF在“一倍图”时的尺寸
var viewport = page.getViewport({
scale: scale
});
// 获取body宽度
var width = document.body.clientWidth;
/** * width / viewport.width > 1 * 视窗 > PDF一倍宽度最终获得scale > 2 * 反之则会获得小于等于1的scale * 最终再*2是为了获得更清晰的渲染 */
scale = scale * width / viewport.width * 2
// 从新定义scale以后再次getViewport
viewport = page.getViewport({
scale: scale
});
复制代码
完成上述的开发后,一个可阅读的PDF页面已经完成得差很少了,可是对比原件以后惊奇的发现,印章没了。
去翻了一下项目的issue发现这是个有必定年份(2012年就已经有相关issue)的问题了,看了下源码仍是做者有意将电子签名及印章隐藏的,缘由是项目还没具有验证电子签名及印章的能力。
最终的解决方案倒比较简单,只需找到源码设置HIDDEN属性的代码,将其注释便可。
var parent = Annotation.prototype;
Util.inherit(WidgetAnnotation, Annotation, {
isViewable: function WidgetAnnotation_isViewable() {
/* if (this.data.fieldType === 'Sig') { warn('unimplemented annotation type: Widget signature'); return false; }*/
return parent.isViewable.call(this);
}
});
复制代码
在调试其余PDF文档是发现,有些页面会是一片空白,一开始觉得原件就是如此,可是对照以后发现这一页是用了特殊字体。
在搜索解决方案的时候看到getDocument有这样一个参数disableFontFace,这个参数的默认值是false,看起来将其设为true就可使用默认字体了。事实上并非的,这个参数是负责控制是否使用内置的字体渲染器来渲染。
随着搜索的深刻,看到这样一个解决方案 --
pdfjsLib.getDocument({
url: path,
cMapPacked: true,
cMapUrl: 'https://unpkg.com/pdfjs-dist@2.2.228/cmaps/'
})
复制代码
里面涉及到cMapPacked和cMapUrl两个参数,前者代表用到的cmap是二进制类型的,后者这是设定cmap的请求地址。笔者对这一块配置的理解是,若是遇到不支持的字体,将会去指定的地址获取默认字体的bcmap用于渲染替代特殊字体的默认字体。
由于以前都是用iPhone进行调试的,因此一直没感受到卡顿的状况,借用了测试机以后发现,打开页数较多的PDF会出现卡顿状况。总结了一下,缘由大概是并行了过多的渲染。一开始的写法是在getDocument以后拿到PDF页数直接for循环将全部的page同时输出。
// 伪代码
pdfjsLib.getDocument({ url: 'xxx' }).promise.then(function (pdf) {
numPages = pdf._pdfInfo.numPages
for (var i = 0;i < numPages;i++) {
pdfCreator(pdf, i + 1)
}
});
复制代码
既然同时输出会引发卡顿,可否优化成一张一张顺序渲染呢?天然是能够的,咱们能够经过递归的方式将PDF一页页输出。
var numPages = 0;
var renderFlag = 0;
// ......
pdfjsLib.getDocument({ url: 'xxx' }).promise.then(function (pdf) {
numPages = pdf._pdfInfo.numPages
pdfCreator(pdf, 1)
});
function pdfCreator(pdf, index) {
// ......
pdf.getPage(index).then(function (page) {
// ......
page.render(renderContext).promise.then(() => {
renderFlag = index
if (renderFlag < numPages) {
pdfCreator(pdf, renderFlag + 1)
}
});
})
}
复制代码
一个功能相对完整的PDF浏览页面就完成了,仍是有须要后续优化的地方,例如page.render的容错处理、PDF下载功能,甚至还能够新增懒加载功能。
参考连接 --