產(chǎn)品需求描述
后端返回 pdf 文件鏈接蚯姆,前端預(yù)覽,要求不允許用戶下載洒敏、復(fù)制、打印郭毕。
初步方案
- 瀏覽器支持 pdf 文件預(yù)覽功能,通過 window.open 的方式打開新的鏈接函荣,效果如下:
問題:瀏覽器提供的下載显押、打印控件以及復(fù)制內(nèi)容扳肛、右鍵下載等操作無法干預(yù)
- 以 iframe 的方式加載文件乘碑,并禁用 iframe 的右鍵:
<iframe ref="iframe" :src="pdfUrl" />
網(wǎng)上找到的方案大多為 document.oncontextmenu = function() { return false; }兽肤,但實測發(fā)現(xiàn)該方法僅適用于子頁面內(nèi)容沒加載之前,如果資源加載完成則右鍵操作由子頁面本身控制资铡。
思路:在 iframe 加載成功后,為子頁面注冊對應(yīng)的事件處理函數(shù)尖飞。
let iframe = this.$refs.iframe
iframe.onload = () => {
window.frames[0].contentDocument.oncontextmenu = () => false
}
問題:提示跨域
原因分析:網(wǎng)頁地址與資源鏈接的域名不一致葫松,導(dǎo)致 iframe 跨域底洗。
解決方案:由后端配置同源頭解決跨域問題咕娄,但使用 iframe 無法解決用戶復(fù)制文字的問題。
- 使用 embed 標(biāo)簽费变,禁止右鍵:
<embed :src="pdfUrl" enableContextMenu="false" />
<!-- 或者 -->
<embed :src="pdfUrl" oncontextmenu="window.event.returnValue=false" />
問題:僅在音視頻資源下生效
思考
分析
以上方案無法解決問題的原因在于利用了瀏覽器的默認(rèn)特性挚歧,且這些特性是無法干預(yù)的吁峻。因此需要轉(zhuǎn)換思路,將不可控的特性轉(zhuǎn)換為已有的可控特性矮慕。
思路
利用圖片無法選擇復(fù)制的特性啄骇,將 pdf 轉(zhuǎn)成圖片,并限制用戶無法右鍵保存痪寻。
解決方案
基于 pdf.js 將 pdf 按頁轉(zhuǎn)換為一張張圖片,通過 img 標(biāo)簽渲染蛇尚,并禁用右鍵和圖片拖拽猫态。
Step1 讀取 pdf 內(nèi)容
window.pdfjsLib.getDocument(pdfUrl)
由于資源鏈接不在本地,pdf.js 會報跨域的錯誤勇凭。
方案:參考該鏈接虾标,需要手動修改 pdf.js 的邏輯灌砖,并要求服務(wù)端配合解決跨域的問題。
修改 pdf.js 邏輯不利于后期升級和維護蘸吓,因此我們換一種思路:基于 ajax 請求撩幽。
axios.request({
url: imgUrl,
type: 'get',
responseType: 'blob'
}).then(res => {
window.pdfjsLib.getDocument(res.data)
})
成功解決跨域問題,并返回了 blob 對象宪萄,但在初始化 pdf.js 時報了如下錯誤:
查詢源碼得知拜英,pdf.js 不支持讀取 blob 對象琅催,因此需要將 blob 轉(zhuǎn)為 url:
window.pdfjsLib.getDocument(window.URL.createObjectURL(res.data))
Step2 解析文件,渲染到 canvas
調(diào)用 pdf.js 的 api 進行解析:
window.pdfjsLib.getDocument(window.URL.createObjectURL(res.data)).promise.then(pdf => {
// 解析第一頁
pdf.getPage(1).then(page => {
let scale = 1
let viewport = page.getViewport({ scale })
})
})
渲染到 canvas:
pdf.getPage(1).then(page => {
let scale = 1
let viewport = page.getViewport({ scale })
let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
canvas.width = viewport.width
canvas.height = viewport.height
let renderContext = {
canvasContext: context,
viewport: viewport
}
page.render(renderContext)
})
Step3 渲染圖片
<img :src="pdfUrl" />
page.render(renderContext)
this.pdfUrl = canvas.toDataURL('image/png')
Step4 禁止右鍵和復(fù)制
<img :src="pdfUrl" :draggable="false" oncontextmenu="return false;" />
Step5 將 pdf 的每一頁轉(zhuǎn)換為圖片
上述步驟已經(jīng)完成大體邏輯,但在 step2 中只是將 pdf 的第一頁解析成了圖片杰捂。實際需求需要解析每一頁,然后通過輪播的方式顯示圖片挨队。因此需要做以下改造:
<Carousel v-if="pdfImgsShow">
<CarouselItem v-for="(item, index) in pdfImgs" :key="index">
<img :src="item" :draggable="false" oncontextmenu="return false;" />
</CarouselItem>
</Carousel>
window.pdfjsLib.getDocument(window.URL.createObjectURL(res.data)).promise.then(async pdf => {
for(let i = 1; i <= pdf.numPages; i++) {
let page = await pdf.getPage(i)
let scale = 1
let viewport = page.getViewport({ scale })
let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
canvas.width = viewport.width
canvas.height = viewport.height
let renderContext = {
canvasContext: context,
viewport: viewport
}
page.render(renderContext)
this.pdfImgs.push(canvas.toDataURL('image/png'))
}
this.pdfImgsShow = true
})
如下圖所示盛垦,已成功生成圖片:
但當(dāng) pdf 頁數(shù)大于 1 時腾夯,控制臺會報如下錯誤:
查詢源碼得知,page.render 方法是異步函數(shù)班利,在循環(huán)體內(nèi)部調(diào)用 render 方法會導(dǎo)致同時存在多個未執(zhí)行完的 render榨呆,引發(fā)上述錯誤积蜻。
解決方法:
await page.render(renderContext).promise
this.pdfImgs.push(canvas.toDataURL('image/png'))
Step6 調(diào)整清晰度
實際測試發(fā)現(xiàn),canvas 導(dǎo)出的圖片清晰度較差宙拉。
查詢資料得知是因為 dpi 的問題丙笋,參考該文章,調(diào)整 canvas 畫布的大小:
let UNITS = 2
canvas.width = Math.floor(viewport.width * UNITS)
canvas.height = Math.floor(viewport.height * UNITS)
let renderContext = {
transform: [UNITS, 0,0, UNITS, 0, 0],
canvasContext: context,
viewport: viewport
}
完整代碼
<Carousel v-if="pdfImgsShow">
<CarouselItem v-for="(item, index) in pdfImgs" :key="index">
<img :src="item" :draggable="false" oncontextmenu="return false;" />
</CarouselItem>
</Carousel>
<canvas ref="canvas" style="display: none;" />
axios.request({
url: imgUrl,
type: 'get',
responseType: 'blob'
}).then(res => {
window.pdfjsLib.getDocument(window.URL.createObjectURL(res.data)).promise.then(async pdf => {
let UNITS = 2
for(let i = 1; i <= pdf.numPages; i++) {
let page = await pdf.getPage(i)
let scale = 1
let viewport = page.getViewport({ scale })
let canvas = this.$refs.canvas
let context = canvas.getContext('2d')
canvas.width = Math.floor(viewport.width * UNITS)
canvas.height = Math.floor(viewport.height * UNITS)
let renderContext = {
transform: [UNITS, 0,0, UNITS, 0, 0],
canvasContext: context,
viewport: viewport
}
await page.render(renderContext).promise
this.pdfImgs.push(canvas.toDataURL('image/png'))
context.clearRect(0, 0, viewport.width, viewport.height)
this.pdfImgsShow = true
}
})
})