本文首發於微保技術公衆號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下載功能,甚至還能夠新增懶加載功能。
參考連接 --