预览pdf文件(react-pdf/iframe/pdfjs+全屏)

2023-10-27 23:20

本文主要是介绍预览pdf文件(react-pdf/iframe/pdfjs+全屏),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

js预览blod流pdf文件

  • 前情提要
  • 1. 通过react-pdf插件实现
    • 1.1 基本的使用如下:
    • 1.2 下载功能
    • 1.3 打印功能
    • 1.4 其他问题
      • 1.4.1 电子签章展示问题
      • 1.4.2 同时生成多个pdf组件
  • 2. iframe实现预览pdf
  • 3. iframe预览pdf+token
    • 3.1 header添加token
    • 3.2 其他问题
  • 4 pdfjs+全屏预览(2023-07-20)
    • 4.1 工具
    • 4.2 开发
    • 4.3 详解主要方法
    • 4.4 总结
  • 5 下载
  • 最后

前情提要

首先这是一个项目需求,负责人只说了让我实现一个pdf预览打印功能,后台数据格式,页面样式都没有。好吧,那我就按照我的想法来。

1. 通过react-pdf插件实现

因为没有数据,所以我先考虑了复杂但兼容性高的实现方式:react-pdf。这其实不是一个很复杂的插件。

1.1 基本的使用如下:

import React, { useState } from 'react';
import { Document, Page} from 'react-pdf/dist/esm/entry.webpack';export default () => {const [numPages, setNumPages] = useState(null);const [pageNumber, setPageNumber] = useState(1);// pdf加载成功const onDocumentLoadSuccess = ({ numPages }) => {setNumPages(numPages);};return (<Documentfile={pdfUrl} // 文件地址className="pdf-viewer-show"onLoadSuccess={onDocumentLoadSuccess}><Page pageNumber={pageNumber} scale={1.8} /></Document>)
}

1.2 下载功能

既然有文件地址,那么下载就很简单啦。

 //下载pdfconst downloadPdf = () => {const link = document.createElement('a');link.setAttribute('download', '');link.href = pdfUrl;link.click();};

1.3 打印功能

其实打印功能是我实现起来比较麻烦的功能,要考虑的东西会更多,本质上是打印当前html的指定dom元素的内容。
首先要对打印的页面进行一些配置:

/**
* printHtml.js 打印当前html的指定dom元素的内容
* html  指定的dom元素
*/
export default function printHtml(html) {let style = getStyle();let container = getContainer(html);document.body.appendChild(style);document.body.appendChild(container);getLoadPromise(container).then(() => {window.print();document.body.removeChild(style);document.body.removeChild(container);});
}// 设置打印样式
function getStyle() {let styleContent = `#print-container {display: none;}@media print {body > :not(.print-container) {display: none;}html,body {display: block !important;}#print-container {display: block;}}`;let style = document.createElement('style');style.innerHTML = styleContent;return style;
}// 清空打印内容
function cleanPrint() {let div = document.getElementById('print-container');if (!!div) {document.querySelector('body').removeChild(div);}
}// 新建DOM,将需要打印的内容填充到DOM
function getContainer(html) {cleanPrint();let canvas = html.getElementsByTagName('canvas');let container = document.createElement('div');container.setAttribute('id', 'print-container');let imgs = '';for (let el = 0; el < canvas.length; el++) {imgs += `<img src=${canvas[el].toDataURL()} alt='' style="page-break-after:always"/>`;}container.innerHTML = imgs;return container;
}// 图片完全加载后再调用打印方法
function getLoadPromise(dom) {let imgs = dom.querySelectorAll('img');imgs = [].slice.call(imgs);if (imgs.length === 0) {return Promise.resolve();}let finishedCount = 0;return new Promise((resolve) => {function check() {finishedCount++;if (finishedCount === imgs.length) {resolve();}}imgs.forEach((img) => {img.addEventListener('load', check);img.addEventListener('error', check);});});
}

然后在pdf组件里调用(相关的下载/打印/分页按钮我就不贴出来了):

 // 打印pdfconst printPdf = () => {let $dom = document.querySelector('.pdf-viewer');// pdf预览domprintHtml($dom);};

我是分页展示pdf,但是打印时又需要所以都打印出来,所以我同时生成了两个react-pdf组件:

{/* 展示用*/}
<Documentfile={pdfBlob}className="pdf-viewer-show"onLoadSuccess={onDocumentLoadSuccess}
><Page pageNumber={pageNumber} scale={1.8} />
</Document>
{/* 下载用*/}
<Document file={pdfBlob} className="pdf-viewer">{Array.from(new Array(numPages), (el, index) => (<Pagekey={`page_${index + 1}`}pageNumber={index + 1}scale={1.8}/>))}
</Document>

1.4 其他问题

1.4.1 电子签章展示问题

react-pdf默认是不展示电子签章的,要想展示签章,那么需要修改源码:

import { Document, Page, pdfjs } from 'react-pdf/dist/esm/entry.webpack';
//更改引入pdf.worker.js路径
//将pdf.worker.js中的this.setFlags(AnnotationFlag.HIDDEN);注释掉就会显示电子签章,反之不显示。
pdfjs.GlobalWorkerOptions.workerSrc = `./pdf.worker.js`;

关于pdf.worker.js文件的获取,emmmm,我是从node_module的react-pdf文件夹里复制出来的(其实这个功能我还没有试验过,大佬们说可行)

1.4.2 同时生成多个pdf组件

由于在我同时多次调用pdf组件时出现了一个奇怪的问题:某个组件成功展示pdf那么下一个pdf组件必然无法展示pdf,再下一个组件必然可以展示pdf(像是pdf只能间隔展示)。在网上查了很多,大概猜测是由于展示的react-pdf组件未渲染完成即关闭时promise任务报错造成了任务的阻塞,我的解决办法是打开新的pdf组件时延迟渲染react-pdf组件:

const [renderStatus, setRenderStatus] = useState(false);
useEffect(() => {// 延迟渲染react-pdf组件,//解决react-pdf组件未渲染完成即关闭时promise任务报错造成的任务的阻塞// 报错存在,但是不会影响页面渲染setTimeout(() => {setRenderStatus(true);}, 200);
}, []);
return ({renderStatus&&(...react-pdf组件)}
)

2. iframe实现预览pdf

在我哼哧哼哧学习了react-pdf的使用后,我拿到了后台返回的数据:blod流pdf文件。突然就觉得我之前花费的功夫都白搭了,直接使用iframe预览pdf不香,自带下载打印功能,都不用自己开发了(没有要求样式,那还不是怎么简单怎么来,丑就丑点吧)。
由于后台的文件请求地址上可以直接带上token,我也不需要考虑token的问题了

import React from 'react';export default () => {return (<iframesrc={pdfUrl} //文件地址className="pdf-iframe"title="pdf预览"frameBorder="no"/>)
}

3. iframe预览pdf+token

3.1 header添加token

老实说,将token拼接在iframe的src里是一个极不安全的做法,token当然要放在header里啦。但是,iframe的src无法设置header,只能另辟蹊径,将pdf文件的内容通过请求获取下来,通过URL.createObjectURL()方法将内容转化为一个url赋值给iframe:

import React, { useState, useRffect } from 'react';export default () => {
// 将请求返回的pdf内容转化为blod格式时,
//有可能还未转化成功就执行URL.createObjectURL()操作了,
//所以增加一个转化是否完成的判断const [pdfLoading, setPdfLoading] = useState(false); // pdf 预览useEffect(() => {setPdfLoading(true);function handler (res) {let { status, response } = res.currentTarget;if (status === 200) {if (response && new Blob([response]).size > 4) {setPdfLoading(false);}const binaryData = []; binaryData.push(response);let data_url = window.URL.createObjectURL(new Blob(binaryData, { type: 'application/pdf' }));document.querySelector('#pdf-iframe').src = data_url;} else {console.error('no pdf :(');}}let xhr = new XMLHttpRequest();xhr.open('GET',pdfUrl);// 添加tokenxhr.setRequestHeader('Authorization',sessionStorage.getItem('Authorization'));xhr.onreadystatechange = handler;xhr.responseType = 'arraybuffer';xhr.send();},[])return (<iframeclassName="pdf-iframe"title="pdf预览"frameBorder="no"style={{display:pdfLoading?'none':'block'}}/>)
}

3.2 其他问题

  • 隐藏工具栏的方法是,在PDF文件url地址后面 拼接 #scrollbars=0&toolbar=0&statusbar=0 参数
  • 工具栏中的 title 不正确,原因是 工具栏中的title读取的是url 地址中最后一个 ‘/’ 后面的参数作为title值

4 pdfjs+全屏预览(2023-07-20)

需求总是迭代的。最新的pdf预览需求是我新负责的一个vue2项目,目前的需求是缩略图平铺展示+全屏预览翻页及放大缩小。

4.1 工具

这次事项主要使用了pdfjs插件和screenfull插件,其中pdfjs为js文件(可自行前往pdfjs官网下载),screenfull为npm包。
pdfjs放置在项目的public目录下,并在index.html中引入:
pdfjs
pdfjs引入
screenfull包通过npm instll screenfull指令下载即可
screenfull

4.2 开发

首先该项目使用了antd公共组件库,所以有部分代码中包含antd组件的调用。
样式方面可以按照自己的需求进行调整,目前我只提供自己的代码仅供参考:

template><div v-if="!pdfUrl" style="margin:100px 0;textAlign:center;width:100%"><span>请选择要预览的文件</span></div><div style="width:100%;height: 100%;" v-else><a-spin :spinning="pageRendering" style="width:100%;height: 100%;"><div style="height: 100%;"><div id="the-canvas"></div></div></a-spin></div>
</template>
<script>
import screenfull from 'screenfull' // 引入全屏插件
export default {data(){return {pdfUrl: 'pdfurl', // 要展示的pdf文件地址,currentPage: 0, // 全屏时展示的当前页totalPage: 0, // 总页数pdfScale: 1.2, // 当前缩放尺寸pageRendering: false, // pdf渲染状态pdfRef: null // pdfjs插件实例}},watch: {pdfUrl: {handler(val){// pdf文件地址变化时重新渲染if(val) this.getNumPages()},immediate: true}},methods: {// 计算PDF页码总数并显示getNumPages(){},// 全屏展示screenfullView(){},// 放大缩小scaleRender(type){},// PDF渲染pdfRender(){}}
}
</script>
<style lang="less" scoped>
#the-canvas {display: block;width: 100%;/deep/canvas {float: left;width: calc(33% - 16px);border: 1px solid #dddddd;box-sizing: border-box;margin: 0 16px 16px 0;}/deep/.the-canvas-full{.canvas-content{overflow: auto;background-color: #525659;display: flex;justify-content: center;align-items: center;width: 100%;height: calc(100% - 56px);}canvas{width: auto;margin: auto;}.tool-box{width: 100%;height: 56px;background: #323639;z-index: 9999;display: flex;justify-content: center;align-items: center;}.pages, .scale{color: #fff;font-size: 20px;background-color: #292b2c;padding: 4px 8px;margin: 0 8px;}.divider{width: 2px;height: 45%;background-color: #626262;}.prev-page, .next-page, .bigger, .smaller{width: 40px;height: 40px;color: #fff;cursor: pointer;font-size: 32px;user-select: none;display: flex;align-items: center;justify-content: center;}.prev-page.disabled,.next-page.disabled, .smaller.disabled, .bigger.disabled{color: rgba(255,255,255,.45);cursor: not-allowed;}}
}
</style>

4.3 详解主要方法

  • getNumPages -计算PDF页码总数并显示
  getNumPages() {if (!this.pdfUrl) return // 不存在pdf地址直接返回,也可以在这里做错误提示this.pageRendering = trueconst pdfjsLib = window['pdfjs-dist/build/pdf']pdfjsLib.GlobalWorkerOptions.workerSrc = `/js/pdf.worker.js` // 引入pdfjs文件,路径请按照实际位置进行调整const loadingTask = pdfjsLib.getDocument({url: this.pdfUrl,// 引入pdf.js使用字体,如需要用到的字体不存在(展示文档缺少内容)可在/js/cmaps文件夹下增加对应的字体文件// 步骤:将地址更换为https://cdn.jsdelivr.net/npm/pdfjs-dist@2.9.359/cmaps/,通过控制// 台查看文档所需字体,再去该地址将对应字体文件下载至本地后加到/js/cmaps文件夹中cMapUrl: `/js/cmaps/`,cMapPacked: true})const that = thisloadingTask.promise.then(function (pdf) {that.pdfRef = pdf // 保存当前pdf实例that.pageRendering = falseconst canvasElms = document.getElementById('the-canvas').querySelectorAll('canvas')if (canvasElms.length) {for (let i = 0; i < canvasElms.length; i++) {canvasElms[i].remove()}}that.totalPage = pdf.numPages // 总页数for (let i = 1; i <= pdf.numPages; i++) { // 循环渲染每一页const canvasObj = document.createElement('canvas')canvasObj.setAttribute('id', `the-canvas-${i}`)canvasObj.addEventListener('click', () => { // 为每一页绑定全屏事件that.currentPage = ithat.screenfullView()})document.getElementById('the-canvas').append(canvasObj)pdf.getPage(i).then(function (page) { // 渲染PDFconst scale = that.pdfScaleconst viewport = page.getViewport({ scale: scale })const canvas = document.getElementById(`the-canvas-${i}`)const context = canvas.getContext('2d')canvas.height = viewport.heightcanvas.width = viewport.widthconst renderContext = {canvasContext: context,viewport: viewport}page.render(renderContext)})}},reason => {// 这里为渲染失败的回调,可做错误提示})}
  • screenfullView-全屏
  screenfullView() {if (!screenfull.isEnabled) return // 判断系统是否允许全屏,不允许直接返回const parent = document.getElementById('the-canvas')// 创建一个canvas副本const canvas = document.createElement('canvas')canvas.setAttribute('id', `the-canvas-full-${this.currentPage}`)// 判断是否处在全屏状态// 如果不是则创建一个dom展示要全屏展示的内容,如果存在则不创建,直接替换现有dom的内容let fullDom = document.querySelector(`.the-canvas-full`)if (!fullDom) {fullDom = document.createElement('div')fullDom.setAttribute('class', `the-canvas-full`)parent.append(fullDom)} else {fullDom.innerHTML = ''}// 定义工具栏const toolBox = document.createElement('div')toolBox.setAttribute('class', 'tool-box')// 翻页let prePage = nulllet nextPage = nullif (this.totalPage > 1) { // 总页数大于1时展示翻页let prevClass = 'prev-page'let nextClass = 'next-page'prePage = document.createElement('div')nextPage = document.createElement('div')if (this.currentPage > 1) { // 当前页码大于1,为当前页的向前翻页dom绑定翻页事件prePage.addEventListener('click', () => {this.currentPage = this.currentPage - 1this.screenfullView()})}if (this.currentPage < this.totalPage) {// 当前页码小于总页码,为当前页的向后翻页dom绑定翻页事件nextPage.addEventListener('click', () => {this.currentPage = this.currentPage + 1this.screenfullView()})}// 页面等于1或等于总页码,对应翻页dom绑定禁用classif (this.currentPage === 1) prevClass = 'prev-page disabled'if (this.currentPage === this.totalPage) nextClass = 'next-page disabled'prePage.setAttribute('class', prevClass)nextPage.setAttribute('class', nextClass)prePage.textContent = '<'nextPage.textContent = '>'// 页码展示器const pages = document.createElement('div')pages.setAttribute('class', 'pages')pages.textContent = `${this.currentPage}/${this.totalPage}`// 将页码dom添加进工具栏toolBox.append(prePage)toolBox.append(pages)toolBox.append(nextPage)// 分割线const divider = document.createElement('div')divider.setAttribute('class', 'divider')toolBox.append(divider)}// 放大缩小const bigger = document.createElement('div')const smaller = document.createElement('div')bigger.setAttribute('class', 'bigger')smaller.setAttribute('class', 'smaller')bigger.textContent = '+'smaller.textContent = '-'// 对应dom绑定放大缩小事件smaller.addEventListener('click', () => this.scaleRender('smaller'))bigger.addEventListener('click', () => this.scaleRender('bigger'))// 缩放比例展示器const scale = document.createElement('div')scale.setAttribute('class', 'scale')scale.textContent = `${parseInt(this.pdfScale * 100)}%`// 将缩放dom添加进工具栏toolBox.append(smaller)toolBox.append(scale)toolBox.append(bigger)// 根据新的参数重新渲染PDFconst canvasContent = document.createElement('div')canvasContent.setAttribute('class', 'canvas-content')canvasContent.append(canvas)fullDom.append(canvasContent)this.pdfRender()// 将工具栏添加进全屏domfullDom.append(toolBox)if (!screenfull.isFullscreen) {screenfull.request(fullDom)}// 关闭全屏状态监听回调事件const reBack = () => {this.$nextTick(() => { // 确保回调时获取的dom元素为最新的const currentFull = document.querySelector(`.the-canvas-full`)if (!currentFull || screenfull.isFullscreen) returnconst parent = document.getElementById('the-canvas')screenfull.off('change', reBack) // 取消监听parent.removeChild(currentFull) this.pdfScale = 1.2})}screenfull.on('change', reBack) // 监听}
  • scaleRender-放大缩小
    该方法存在一个入参,标识当前为放大还是缩小事件,并对js小数相加减的精准度问题进行了兼容。另外,里面存在较多常量,可根据实际需求进行调整。
  scaleRender(type) {let pdfScale = this.pdfScaleif ((pdfScale === 5 && type === 'bigger') || (pdfScale === 0.25 && type === 'smaller')) return // 最大比例为5,最小比例为0.25,可自行调整if (type === 'bigger') { // 放大时,根据当前比例动态变化放大范围if (pdfScale >= 2) {pdfScale = pdfScale + 1} else if (pdfScale >= 0.5) {const m = Math.pow(10, 2)const res = (pdfScale * m + 0.1 * m) / mpdfScale = Math.round(res * m) / m} else if (pdfScale >= 0.25) {const m = Math.pow(10, 2)const res = (pdfScale * m + 0.05 * m) / mpdfScale = Math.round(res * m) / m}} else {// 缩小时,根据当前比例动态变化缩小范围if (pdfScale <= 0.5) {const m = Math.pow(10, 2)const res = (pdfScale * m - 0.05 * m) / mpdfScale = Math.round(res * m) / m} else if (pdfScale <= 2) {const m = Math.pow(10, 2)const res = (pdfScale * m - 0.1 * m) / mpdfScale = Math.round(res * m) / m} else if (pdfScale <= 5) {pdfScale = pdfScale - 1}}this.pdfScale = pdfScale// 重新渲染PDF并更新缩放比例展示器this.pdfRender() const scale = document.querySelector('.scale')scale.textContent = `${parseInt(this.pdfScale * 100)}%`const bigger = document.querySelector('.bigger')const smaller = document.querySelector('.smaller')// 到达所设定的缩放上下限时,禁用缩放dombigger.setAttribute('class', this.pdfScale === 5 ? 'bigger disabled' : 'bigger')smaller.setAttribute('class', this.pdfScale === 0.25 ? 'smaller disabled' : 'smaller')}
  • pdfRender-pdf渲染
  pdfRender() {const that = thisthis.pdfRef.getPage(this.currentPage).then(function (page) {const scale = that.pdfScaleconst viewport = page.getViewport({ scale: scale })const canvas = document.getElementById(`the-canvas-full-${that.currentPage}`)const context = canvas.getContext('2d')canvas.height = viewport.heightcanvas.width = viewport.widthconst renderContext = {canvasContext: context,viewport: viewport}page.render(renderContext)})}

4.4 总结

其实pdfjs提供了相关的翻页放大缩小事件,但是为了兼容全屏并且实现较为灵活的配置,就自己实现了一下,总的来说,不考虑性能啥的,基本满足了我的需求,以下是效果图:
请添加图片描述

请添加图片描述
请添加图片描述

5 下载

附带一个我常用的文件下载方法,兼容IE

const download = () => {// 创建Blob对象 传入一个合适的MIME类型// file 为blob文件流,name为文件名称const blob = new Blob([file], { type: 'application/vnd.ms-excel,charset=UTF-8' });// 使用 Blob 创建一个指向类型化数组的URLconst csvUrl = URL.createObjectURL(blob);// IE下载if (window.navigator.msSaveBlob) {try {window.navigator.msSaveBlob(blob, name);} catch (e) {console.log(e);}} else {let link = document.createElement('a');link.download = name; //文件名字link.href = csvUrl;link.click(); // 触发下载}
}

最后

大概是这么多了,想到哪写到哪吧。感谢大佬们的帮助,有错误欢迎指出,我们一起进步呀。

这篇关于预览pdf文件(react-pdf/iframe/pdfjs+全屏)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/289099

相关文章

vue基于ElementUI动态设置表格高度的3种方法

《vue基于ElementUI动态设置表格高度的3种方法》ElementUI+vue动态设置表格高度的几种方法,抛砖引玉,还有其它方法动态设置表格高度,大家可以开动脑筋... 方法一、css + js的形式这个方法需要在表格外层设置一个div,原理是将表格的高度设置成外层div的高度,所以外层的div需要

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一

Vue项目中Element UI组件未注册的问题原因及解决方法

《Vue项目中ElementUI组件未注册的问题原因及解决方法》在Vue项目中使用ElementUI组件库时,开发者可能会遇到一些常见问题,例如组件未正确注册导致的警告或错误,本文将详细探讨这些问题... 目录引言一、问题背景1.1 错误信息分析1.2 问题原因二、解决方法2.1 全局引入 Element

详解如何在React中执行条件渲染

《详解如何在React中执行条件渲染》在现代Web开发中,React作为一种流行的JavaScript库,为开发者提供了一种高效构建用户界面的方式,条件渲染是React中的一个关键概念,本文将深入探讨... 目录引言什么是条件渲染?基础示例使用逻辑与运算符(&&)使用条件语句列表中的条件渲染总结引言在现代

详解Vue如何使用xlsx库导出Excel文件

《详解Vue如何使用xlsx库导出Excel文件》第三方库xlsx提供了强大的功能来处理Excel文件,它可以简化导出Excel文件这个过程,本文将为大家详细介绍一下它的具体使用,需要的小伙伴可以了解... 目录1. 安装依赖2. 创建vue组件3. 解释代码在Vue.js项目中导出Excel文件,使用第三

Java实现Excel与HTML互转

《Java实现Excel与HTML互转》Excel是一种电子表格格式,而HTM则是一种用于创建网页的标记语言,虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,下面我们就来看看... Excel是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言。虽然两

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一

基于C#实现PDF文件合并工具

《基于C#实现PDF文件合并工具》这篇文章主要为大家详细介绍了如何基于C#实现一个简单的PDF文件合并工具,文中的示例代码简洁易懂,有需要的小伙伴可以跟随小编一起学习一下... 界面主要用于发票PDF文件的合并。经常出差要报销的很有用。代码using System;using System.Col

Java操作PDF文件实现签订电子合同详细教程

《Java操作PDF文件实现签订电子合同详细教程》:本文主要介绍如何在PDF中加入电子签章与电子签名的过程,包括编写Word文件、生成PDF、为PDF格式做表单、为表单赋值、生成文档以及上传到OB... 目录前言:先看效果:1.编写word文件1.2然后生成PDF格式进行保存1.3我这里是将文件保存到本地后

通过C#获取PDF中指定文本或所有文本的字体信息

《通过C#获取PDF中指定文本或所有文本的字体信息》在设计和出版行业中,字体的选择和使用对最终作品的质量有着重要影响,然而,有时我们可能会遇到包含未知字体的PDF文件,这使得我们无法准确地复制或修改文... 目录引言C# 获取PDF中指定文本的字体信息C# 获取PDF文档中用到的所有字体信息引言在设计和出