由于目前实现在公司内部的后台项目,故不展示截图,可根据代码自行实践查看效果。
背景
之前公司内部一个后台系统,需要在前端展示 pdf 文件,并且需要在半屏展示,另外半屏需要展示其他内容。
一开始是直接使用 iframe 进行打开,后来由于某些 pdf 的文字编码问题,导致 Chrome 无法正常展示文字,后来发现 Mozilla 的 pdf.js 可以展示,于是将 iframe 打开的页面替换成 pdf.js 的 html 来展示 pdf 文件。
后来为了进一步提效,有了几个需求:
- 后台传来数据和 pdf 文件地址,前端需要在 pdf 文件中把所有关键词进行高亮标记;
- 后台传来的数据每一个词都需要点击一键搜索,需要 pdf 文件自动跳转到关键词并高亮标记。
于是就开始研究如何在使用 iframe 嵌入的 pdf.js 页面中进行实现。
实现
展示指定 pdf 文件
这个虽然花费了几个小时,但是并没有什么技术含量。在我翻了它的 viewer.js
文件后发现,其实只需要传一个查询参数即可。形如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<template>
<iframe v-if="iframeSrc" :src="iframeSrc" />
</template>
<script lang="ts" setup>
import {defineProps,computed } from 'vue';
const props = defineProps<{
pdfUrl: string;
}>();
const iframeSrc = computed(() => {
if (props.pdfUrl){
return `${window.location.origin}/pdfViewer/web/viewer.html?pdf=${props.pdfUrl}`;
}
return '';
});
</script>
|
这个地址是我项目引入 pdf.js 的文件路径,其实就是打开 pdf.js 的 html 文件,然后传入一个查询参数 pdf
,值为 pdf 文件的地址即可。
高亮关键词
首先我看了一下 pdf.js 嵌入之后的 DOM,发现他将每一页都分了两部分,一部分是 canvas,一部分是 div。
canvas 主要是为了实现画笔之类的功能,因为目前我从后台能拿到的只有文字,其他信息都没有,所以应该是用不上了。
div 中的内容就是 pdf 文件的文字内容,被各种标签包围着赋予样式,类似代码块的 DOM,这种的话我们应该可以给对应的 html 元素添加样式实现高亮。
思路有了,那就开干吧!
拿到 iframe 的 DOM
想要操作 DOM,那肯定需要先获取到 DOM 对象,那么我们就需要先拿到 iframe 的 DOM,再从里面拿到需要的 pdf 文件的根节点,后续就可以遍历根节点的所有页面元素来实现高亮了。
1
2
|
// 示例使用 ts 获取,如果在 vue/react 中,可以使用 ref,并相应使用 value 和 current
const iframe = document.querySelector('#iframeId');
|
获取到 iframe 的 DOM 之后,就可以拿到 iframe 的 window 和 document 了。
拿到 iframe 的 document 对象
在获取 iframe 的 document 之前先判断 pdf.js 的页面 js 是否已执行完成,否则 pdf 可能会出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 这里我扩展了一下 pdf.js 的 window 对象,因为我需要使用到里面的属性
type IframeWindow = {
PDFViewerApplication : {
eventBus : {
dispatch : (type : string, data : any) => void;
};
pagesCount : number;
};
} & Window;
// 检查 iframe 是否加载完成,这里我是使用 iframe 的 windows 上是否已经有了 pagesCount 属性来判断
const window = iframe.contentWindow as IframeWindow | null;
if (!window?.PDFViewerApplication?.pagesCount) return;
iframeWindow = iframe.value!.contentWindow as IframeWindow;
console.log('PDF 总页数', iframeWindow.PDFViewerApplication.pagesCount);
// 获取 iframe 的 document
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
这个 iframeDoc 就是 pdf 的 iframe 的 document 了,我们从里面拿出所有的 .page
元素,这个元素就是每一页的根节点了。
拿到 pdf 所有的 .page
元素进行高亮
在这里我获取的方式是定时轮询去获取,从 iframe 的 window 中拿到文件总页数,然后使用下面拿到的元素个数去对比,如果相等则说明已经拿到了所有的元素,这时候才开始进行操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
const pages = iframeDoc.querySelectorAll('.page');
// 这里之所以使用 setInterval,是因为 pdf.js 是动态加载页面内容的
// .page 元素里的内容会动态增删,所以如果只执行一次,后面的页面都会没有高亮效果
const highlightPDF = setInterval(() => {
if (!pages) return;
// 对后台拿到的关键词数组进行去重,如果没有关键词的就不进行遍历操作了
const keywords = [...new Set(props.keywords)];
if ((keywords.length === 1 && !props.keywords[0]) || !keywords.length)
return;
// 遍历 .page 元素进行高亮
for (let i = 0; i < pages.length; i++) {
const currentPage = pages[i];
currentPage && highlightDom(currentPage);
}
}, 1500);
|
高亮具体方法实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
const pattern = /[\(\)()\[\]\|\/-——]/g;
const highlightDom = (dom: HTMLElement) => {
// 取出 .page 元素中的 .textLayer 元素,这个元素就是 pdf 文件中的文字内容
const textLayer = dom.querySelector('.textLayer');
if (!textLayer) return;
// 这里我使用了正则去除掉所有的特殊符号,因为中英文符号可能导致不匹配
for (let node of textLayer.children) {
if (node.textContent) {
const result = props.keywords.filter(keyword =>
node.textContent!.replace(pattern, '').includes(keyword.replace(pattern, ''))
);
node.style.backgroundColor = !result.length ? 'transparent' : 'red';
}
}
};
|
OK!大功告成。这时候我们传入关键词数组就可以在 pdf 文件中看到高亮的关键词了。
这里做得稍微比较粗糙,我直接将匹配到关键词的整个元素进行高亮,会导致高亮整句话,实际上可以给该关键词使用特殊标签包裹,然后给该标签添加样式,这样可以更精确地控制高亮的范围。由于内部系统并不是特别重要,所以就没有做这个优化了。
一键搜索
另一个需求是上面后台传来的关键词需要在页面右侧展示,并且点击一键在左侧的 iframe 里的 pdf 中跳转定位。
这个我使用的就是 pdf.js 自身的搜索功能,去翻一翻它的 viewer.js 源码,发现了它的搜索调用的方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
// 这个 PDFViewerApplication 是 pdf.js 注入 window 对象的,这也是前面我扩展了 iframe 的 window 的类型的原因
PDFViewerApplication.eventBus.dispatch('find', {
source: evt.source,
type: '',
query: evt.query,
phraseSearch: evt.phraseSearch,
caseSensitive: false,
entireWord: false,
highlightAll: true,
findPrevious: false,
matchDiacritics: true,
});
|
注意上面的 window 指的是 iframe 的 window,并不是当前页面的 window,所以我们需要在 iframe 的 window 上调用这个方法。
上面这个方法调用的时候会使用 iframe 中 pdf 的搜索 input 的 value 来进行搜索,所以我们需要在 iframe 中找到这个 input,然后将我们的关键词赋值给它,然后调用上面的方法即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 获取 iframe 的 window
const iframeWindow = iframe.contentWindow as IframeWindow | null;
if (!iframeWindow) return;
// 获取 iframe 中的搜索 input
const searchInput = iframeWindow.document.querySelector('#findInput') as HTMLInputElement | null;
// 定义搜索方法
const searchKeyword = (keyword: string) => {
if (!iframeWindow || !searchInput) return;
// 将关键词赋值给 input
searchInput!.value = keyword;
// 调用 pdf.js 的搜索方法
iframeWindow.PDFViewerApplication.eventBus.dispatch('find', {
type: '',
query: keyword,
highlightAll: true,
});
};
|
由于这里我不需要配置忽略大小写,符号,整词等,所以只配置了高亮属性为 true
,如果你需要的话可以在上面的方法中传入更多配置。
OK!这样我们就可以在页面中点击关键词时候调用上面的 searchKeyword
方法,就可以在 pdf 中直接跳转并高亮对应的关键词了。
总结
这篇文章主要是讲了一下我在项目中遇到的一个需求,通过这个需求,我学习了一下如何使用 pdf.js 来实现 pdf 的预览和高亮,以及如何使用 pdf.js 的搜索功能来实现一键搜索。
发现了 iframe 使用的其他一些小技巧,比如如何获取 iframe 的 window,以及如何获取 iframe 的 document 等。