【PDF.js】2023 最新 PDF.js 在 Vue3 中的使用

努力挣钱的小鑫 2024-09-01 08:05:02 阅读 60

因为自己写业务要定制各种 pdf 预览情况(可能),所以采用了 pdf.js ,而不是各种第三方的pdfjs的封装库,主要还是为了更好的自由度。

一、PDF.js 介绍

官方地址

中文文档

PDF.js 是一个使用 HTML5 构建的便携式文档格式查看器。

pdf.js 是社区驱动的,并由 Mozilla 支持。我们的目标是为解析和呈现 PDF 创建一个通用的、基于 Web 标准的平台。

二、 安装方法

1、下载 pdf.js

下载地址

在这里插入图片描述

我下载的版本是 <code>pdfjs-4.0.189-dist

在这里插入图片描述

2、解压包并放到项目中

解压后将完整文件夹放到 vue3 的 <code>public 文件夹内

在这里插入图片描述

3、屏蔽跨域错误,允许跨域

在 <code>web/viewer.mjs 内找到搜索 throw new Error("file origin does not match viewer's") 并注释掉,如果不注释,可能会出现跨域错误,无法正常预览文件

在这里插入图片描述

这样就算安装完成了,后面我们开始在项目中使用。

三、基础使用

1、创建 PDF 组件

我们可以创建一个 <code>PDF 组件,代码如下:

<script setup lang="ts">code>

import { -- --> onMounted, ref } from 'vue';

interface Props {

url: string; // pdf文件地址

}

const props = defineProps<Props>();

const pdfUrl = ref(''); // pdf文件地址

const fileUrl = '/pdfjs-4.0.189-dist/web/viewer.html?file='; // pdfjs文件地址

onMounted(() => {

// encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。

// 核心就是将 iframe 的 src 属性设置为 pdfjs 的地址,然后将 pdf 文件的地址作为参数传递给 pdfjs

// 例如:http://localhost:8080/pdfjs-4.0.189-dist/web/viewer.html?file=http%3A%2F%2Flocalhost%3A8080%2Fpdf%2Ftest.pdf

pdfUrl.value = fileUrl + encodeURIComponent(props.url);

});

</script>

<template>

<div class="container">

<iframe :src="pdfUrl" width="100%" height="100%"></iframe>

</div>

</template>

<style scoped lang="scss">

.container {

width: 100%;

height: 100%;

}

</style>

2、使用组件

比如我们需要预览 public 下的一个 test.pdf 文件

<div class="pdf-box">

<PDF url="/public/test.pdf" />

</div>

下面是界面默认预览效果

在这里插入图片描述

四、进阶使用

1、页面跳转

传参(初次渲染)

比如我们要跳到第 10 页,我们可以在地址里面添加参数 &page=${10}

pdfUrl.value = fileUrl + encodeURIComponent(props.url) + `&page=${ 10}`;

viewer.mjs 找到 setInitialView 函数,注意是下面这个:

在这里插入图片描述

重点:在函数末尾最下面添加下面的跳转代码(写在上面会报错,因为还没有获取到实例)

console.log(this.pdfViewer);

// 获取url参数

function getQueryVariable(variable) {

var query = window.location.search.substring(1);

var vars = query.split('&');code>

for (var i = 0; i < vars.length; i++) { -- -->

var pair = vars[i].split('=');

if (pair[0] == variable) {

return pair[1];

}

}

return false;

}

// 跳转到指定页

const page = getQueryVariable('page');

console.log(page);

if (page) {

this.pdfViewer.currentPageNumber = Number(page);

}

外部调用(渲染完成后)

const pdfFrame = document.getElementById('myIframe').contentWindow

// 方法1

pdfFrame.PDFViewerApplication.page = 10 // 传入需要让跳转的值

// 方法2

pdfFrame.PDFViewerApplication.pdfViewer.scrollPageIntoView({

pageNumber: 10,

});

2、文本标注

某些时候我们需要跳转到指定页面,然后自动标注文本,这个时候就需要自动标注了(我这个方法是传参,如果要采用调用的方式,请参考最下面的博客)

在这里插入图片描述

代码跟跳转一样,写在后面就可以了

<code> // 自动高亮文本(要解码)decodeURIComponent: 解码

const markText = decodeURIComponent(getQueryVariable('markText'));

console.log('markText===>', markText);

if (markText) { -- -->

// 对查询输入框进行赋值

document.getElementById('findInput').value = markText;

// 点击高亮按钮实现高亮显示关键词

document.getElementById('findHighlightAll').click();

}

目前我还没有找到批量标注的办法,批量标注建议还是使用下面页面+坐标,遮罩的方法

在这里插入图片描述

3、添加遮罩高亮(页码+坐标)

主要是为了解决批量标注的问题,因为 pdfjs 原生只支持单文本,不支持批量,要修改大量源码(我能力不行,太难了😥)

所以还是换了种方案,就是后端返回页码+坐标(通常是百分比坐标,因为pdf会缩放),添加遮罩层渲染的方式。

这种方法主要是找到渲染的 dom元素,因为渲染的pdf有一个叫做 <code>data-page-number="1"code> 的属性,因此我们可以通过 js 的 querySelectorAll 选择器找到对应属性的 dom 元素,然后再操作添加遮罩就可以了,代码放在下面。

在这里插入图片描述

<code> // 测试的坐标

const content_pos_1 = { -- -->

x: 0.5135954145019941,

y: 0.4662730487881233,

};

const content_pos_2 = {

x: 0.7135954145019941,

y: 0.8662730487881233,

};

// 查找属性 data-page-number='页码' 的 dom 元素code>

const pageList = document.querySelectorAll(`[data-page-number='${ page}']`);code>

console.log('查询到的dom列表===>\n', pageList[1]);

// 查询到的第一个是左侧小菜单页码div,第二个是才是展示的div

const pageView = pageList[1];

console.log('右侧展示的dom===>\n', pageView);

// 在元素上画一个div

const div = document.createElement('div');

div.style.width = (content_pos_2.x - content_pos_1.x) * 100 + '%';

div.style.height = (content_pos_2.y - content_pos_1.y) * 100 + '%';

div.style.backgroundColor = 'rgb(255, 255, 0, 0.1)';

div.style.position = 'absolute';

div.style.top = content_pos_1.y * 100 + '%';

div.style.left = content_pos_1.x * 100 + '%';

div.style.zIndex = '1'; // pdfjs 文本的层级是2 所以这里要设置为1 放着不能复制

pageView.appendChild(div);

渲染到pdf上就是下面的样子:

在这里插入图片描述

4、添加遮罩高亮(缩放动态更新)

我们会发现,在 pdf 缩放滚动等的缘故,会重新更新 pdf 的 UI 状态 ,我们添加的 div 就会消失,所以我们要在源码重新更新的时候重新添加高亮,源码内部重新添加的函数在这个位置:<code> #updateUIState

在这里插入图片描述

我们只需要将修改后重新添加的代码放在尾部就行

首先我们要修改第三部分的代码

<code> // 测试的坐标

const content_pos_1 = { -- -->

x: 0.5135954145019941,

y: 0.4662730487881233,

};

const content_pos_2 = {

x: 0.7135954145019941,

y: 0.8662730487881233,

};

// pdf 缩放会重新设置,所以放在window保存,其他地方要用

window.page = page;

window.shade = {

width: (content_pos_2.x - content_pos_1.x) * 100 + '%',

height: (content_pos_2.y - content_pos_1.y) * 100 + '%',

top: content_pos_1.y * 100 + '%',

left: content_pos_1.x * 100 + '%',

};

console.log(window.shade);

// 查找属性 data-page-number='页码' 的 dom 元素code>

const pageList = document.querySelectorAll(`[data-page-number='${ page}']`);code>

console.log('查询到的dom列表===>\n', pageList[1]);

// 查询到的第一个是左侧小菜单页码div,第二个是才是展示的div

const pageView = pageList[1];

console.log('右侧展示的dom===>\n', pageView);

// 在元素上画一个div

const div = document.createElement('div');

div.id = 'shade';

div.style.width = window.shade.width;

div.style.height = window.shade.height;

div.style.backgroundColor = 'rgb(255, 255, 0, 0.1)';

div.style.position = 'absolute';

div.style.top = window.shade.top;

div.style.left = window.shade.left;

div.style.zIndex = '1';

pageView.appendChild(div);

然后在 #updateUIState 函数的末尾添加下面的新增代码

setTimeout(() => { -- -->

if (!window.page) return;

const pageList = document.querySelectorAll(`[data-page-number='${ window.page}']`);code>

const pageView = pageList[1];

// 删除 id 为 shade 的元素(旧遮罩)

const shade = document.getElementById('shade');

if (shade) { -- -->

shade.remove();

}

const div = document.createElement('div');

div.id = 'shade';

div.style.width = window.shade.width;

div.style.height = window.shade.height;

div.style.backgroundColor = 'rgb(255, 255, 0, 0.1)';

div.style.position = 'absolute';

div.style.top = window.shade.top;

div.style.left = window.shade.left;

div.style.zIndex = '1';

pageView.appendChild(div);

}, 500);

最终效果如下:

在这里插入图片描述

ps:如果要做大量的页面+坐标渲染(后端返回的是个数组),修改下上面的代码逻辑就行,传参自己写,不难的

当然,也可以看下面的代码哈哈哈,我还是写出来吧

5、添加遮罩高亮(数组批量跨页渲染)

假设后端返回的数据格式是这样的,是一个包含 页码、坐标的标注数组,我们需要在每个页码内渲染遮罩

在这里插入图片描述

我们就需要这样传参

在这里插入图片描述

<code>setInitialView(storedHash, { rotation, sidebarView, scrollMode, spreadMode } = {}) 初始化函数中:

window.content_pos = JSON.parse(decodeURIComponent(getQueryVariable('content_pos')));

console.log(window.content_pos[0]);

window.content_pos.forEach((item, index) => { -- -->

const page = item.page_no;

const shade = {

width: (item.right_bottom.x - item.left_top.x) * 100 + '%',

height: (item.right_bottom.y - item.left_top.y) * 100 + '%',

top: item.left_top.y * 100 + '%',

left: item.left_top.x * 100 + '%',

};

console.log(shade);

const pageList = document.querySelectorAll(`[data-page-number='${ page}']`);code>

const pageView = pageList[1];

const div = document.createElement('div');

div.id = 'shade' + index;

div.style.width = shade.width;

div.style.height = shade.height;

div.style.backgroundColor = 'rgb(255, 255, 0, 0.1)';

div.style.position = 'absolute';

div.style.top = shade.top;

div.style.left = shade.left;

div.style.zIndex = '1';

pageView.appendChild(div);

});

#updateUIState(resetNumPages = false) 更新函数中:

setTimeout(() => { -- -->

if (window.content_pos) {

window.content_pos.forEach((item, index) => {

const shadeEl = document.getElementById('shade' + index);

if (shadeEl) {

shadeEl.remove();

}

const page = item.page_no;

const shade = {

width: (item.right_bottom.x - item.left_top.x) * 100 + '%',

height: (item.right_bottom.y - item.left_top.y) * 100 + '%',

top: item.left_top.y * 100 + '%',

left: item.left_top.x * 100 + '%',

};

const pageList = document.querySelectorAll(`[data-page-number='${ page}']`);code>

const pageView = pageList[1];

const div = document.createElement('div');

div.id = 'shade' + index;

div.style.width = shade.width;

div.style.height = shade.height;

div.style.backgroundColor = 'rgb(255, 255, 0, 0.1)';

div.style.position = 'absolute';

div.style.top = shade.top;

div.style.left = shade.left;

div.style.zIndex = '1';

pageView.appendChild(div);

});

}

}, 500);

效果展示,可以实现跨页,多页渲染

在这里插入图片描述

6、添加遮罩高亮(外部调用)

跟上面外部调用跳转差不多,修改源码,传参就行,初次渲染和更新渲染的源码修改还是要看上面的代码,这里只是为了做优化,在文档不变,高亮变的情况,不更新pdf直接跳转高亮。

<code>const pdfFrame = document.getElementById('myIframe').contentWindow;

pdfFrame.PDFViewerApplication.pdfViewer.scrollPageIntoView({ -- -->

pageNumber: props.dcsInfo.page_no,

content_pos: props.dcsInfo.content_pos,

});

在这里插入图片描述

注意:如果是同页不同高亮渲染,要在下面的位置写渲染,不然不会更新!!

在这里插入图片描述

7、隐藏部分工具栏

如果我们不想要默认的一些工具栏效果,我们只需要在源码的 <code>viewer.html ,找到对应的 DOM 元素,设置 style="display:none;"code> 就可以了。

在这里插入图片描述

在这里插入图片描述

8、监听滚动页码(2024-08-05更新)

在pdfjs 的更新函数中添加 Iframe 通信的代码

<code>#updateUIState(resetNumPages = false)

在这里插入图片描述

<code> window.parent.postMessage(

{ -- -->

page: pageNumber,

},

'*',

);

在父窗口监听就可以了~

function listenPage() {

// 在父窗口中

window.addEventListener(

'message',

function (event: any) {

if (event.data.page) {

// 处理消息

console.log('Received message:', event.data);

}

},

false,

);

}

在这里插入图片描述

9、修改侧边栏宽度(2024-08-13更新)

在<code>index.html中新增样式,记得添加 !important

<style>

:root { -- -->

--sidebar-width: 400px !important;

}

</style>

10、修改侧边栏预览的pdf宽度(2024-08-13更新)

搜索 THUMBNAIL_WIDTH

在这里插入图片描述

完整 PDF 组件代码

<code><script setup lang="ts">code>

import { -- --> nextTick, onMounted, ref, watch } from 'vue';

export interface RefDocsItem {

filename: string;

url: string;

page_no: number;

content_pos: {

page_no: number;

left_top: {

x: number;

y: number;

};

right_bottom: {

x: number;

y: number;

};

}[];

}

interface Props {

dcsInfo: RefDocsItem;

}

const props = defineProps<Props>();

const pdfUrl = ref(''); // pdf文件地址

let fileUrl = '/pdfjs-4.0.189-dist/web/viewer.html?file='; // pdfjs文件地址

const isRender = ref(false); // 是否渲染

// 源码渲染函数修改的位置在下面两个函数中

// 初次渲染:setInitialView(storedHash, { rotation, sidebarView, scrollMode, spreadMode } = {})

// 更新渲染:#updateUIState(resetNumPages = false)

// 跳转函数位置: scrollPageIntoView

onMounted(() => {

// encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。

// 核心就是将 iframe 的 src 属性设置为 pdfjs 的地址,然后将 pdf 文件的地址作为参数传递给 pdfjs

// 例如:http://localhost:8080/pdfjs-4.0.189-dist/web/viewer.html?file=http%3A%2F%2Flocalhost%3A8080%2Fpdf%2Ftest.pdf

console.log('dcsInfo.url===>', props.dcsInfo.url);code>

pdfUrl.value =

fileUrl +

encodeURIComponent(props.dcsInfo.url) +

`&page=${ -- -->props.dcsInfo.page_no}` +

`&content_pos=${ encodeURIComponent(

JSON.stringify(props.dcsInfo.content_pos),

)}`;

console.log('pdfUrl===>', pdfUrl.value);

nextTick(() => {

isRender.value = true;

});

});

// pdf 资源发生改变

watch(

() => props.dcsInfo,

(val, old) => {

// 判断是否需要重新渲染,因为有些只是跳页

if (isRender.value) {

// 同一个文件,跳转到指定位置

if (pdfUrl.value !== '' && val.filename === old.filename) {

// @ts-ignore

const pdfFrame = document.getElementById('myIframe').contentWindow;

// 传递参数

pdfFrame.PDFViewerApplication.pdfViewer.scrollPageIntoView({

pageNumber: props.dcsInfo.page_no,

content_pos: props.dcsInfo.content_pos,

});

} else {

pdfUrl.value =

fileUrl +

encodeURIComponent(props.dcsInfo.url) +

`&page=${ props.dcsInfo.page_no}` +

`&content_pos=${ encodeURIComponent(

JSON.stringify(props.dcsInfo.content_pos),

)}`;

}

}

},

);

</script>

<template>

<div class="container">code>

<iframe :src="pdfUrl" width="100%" height="100%" id="myIframe"></iframe>code>

</div>

</template>

<style scoped lang="scss">code>

.container { -- -->

width: 100%;

height: 100%;

}

</style>

部署报错问题

1、Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.

Nginx的 MIME TYPE问题导致的mjs文件加载出错的问题解决

后续根据开发业务持续更新😁

感谢大佬们的无私分享

详细|vue中使用PDF.js预览文件实践

vue3项目使用pdf.js插件实现:搜索高亮、修改pdf.js显示的页码、向pdf.js传值、控制搜索、处理接口文件流

pdf.js根据路径里传参数高亮显示关键字(跳转到对应页面)



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。