基于 pdf.js 的前端 PDF 预览方案

产品需求描述

后端返回 pdf 文件链接,前端预览,要求不允许用户下载、复制、打印。

初步方案

  1. 浏览器支持 pdf 文件预览功能,通过 window.open 的方式打开新的链接,效果如下:

问题:浏览器提供的下载、打印控件以及复制内容、右键下载等操作无法干预

  1. 以 iframe 的方式加载文件,并禁用 iframe 的右键:
<iframe ref="iframe" :src="pdfUrl" />

网上找到的方案大多为 document.oncontextmenu = function() { return false; },但实测发现该方法仅适用于子页面内容没加载之前,如果资源加载完成则右键操作由子页面本身控制。

思路:在 iframe 加载成功后,为子页面注册对应的事件处理函数。

let iframe = this.$refs.iframe
iframe.onload = () => {
    window.frames[0].contentDocument.oncontextmenu = () => false
}

问题:提示跨域

原因分析:网页地址与资源链接的域名不一致,导致 iframe 跨域。

解决方案:由后端配置同源头解决跨域问题,但使用 iframe 无法解决用户复制文字的问题。

  1. 使用 embed 标签,禁止右键:
<embed :src="pdfUrl" enableContextMenu="false" />
<!-- 或者 -->
<embed :src="pdfUrl" oncontextmenu="window.event.returnValue=false" />

问题:仅在音视频资源下生效

思考

分析

以上方案无法解决问题的原因在于利用了浏览器的默认特性,且这些特性是无法干预的。因此需要转换思路,将不可控的特性转换为已有的可控特性。

思路

利用图片无法选择复制的特性,将 pdf 转成图片,并限制用户无法右键保存。

解决方案

基于 pdf.js 将 pdf 按页转换为一张张图片,通过 img 标签渲染,并禁用右键和图片拖拽。

Step1 读取 pdf 内容

window.pdfjsLib.getDocument(pdfUrl)

由于资源链接不在本地,pdf.js 会报跨域的错误。

方案:参考该链接,需要手动修改 pdf.js 的逻辑,并要求服务端配合解决跨域的问题。

修改 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 转为 url:

window.pdfjsLib.getDocument(window.URL.createObjectURL(res.data))

Step2 解析文件,渲染到 canvas

调用 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 禁止右键和复制

<img :src="pdfUrl" :draggable="false" oncontextmenu="return false;" />

Step5 将 pdf 的每一页转换为图片

上述步骤已经完成大体逻辑,但在 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
})

如下图所示,已成功生成图片:

但当 pdf 页数大于 1 时,控制台会报如下错误:

查询源码得知,page.render 方法是异步函数,在循环体内部调用 render 方法会导致同时存在多个未执行完的 render,引发上述错误。

解决方法:

await page.render(renderContext).promise
this.pdfImgs.push(canvas.toDataURL('image/png'))

Step6 调整清晰度

实际测试发现,canvas 导出的图片清晰度较差。
查询资料得知是因为 dpi 的问题,参考该文章,调整 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
       }
    })
})
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容