基于 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
       }
    })
})
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容