Featured image of post pdf.js 自定义关键词高亮及一键搜索

pdf.js 自定义关键词高亮及一键搜索

在 pdf 文件中把所有关键词进行高亮标记,并且可以点击一键搜索某个关键词

由于目前实现在公司内部的后台项目,故不展示截图,可根据代码自行实践查看效果。

背景

之前公司内部一个后台系统,需要在前端展示 pdf 文件,并且需要在半屏展示,另外半屏需要展示其他内容。

一开始是直接使用 iframe 进行打开,后来由于某些 pdf 的文字编码问题,导致 Chrome 无法正常展示文字,后来发现 Mozilla 的 pdf.js 可以展示,于是将 iframe 打开的页面替换成 pdf.js 的 html 来展示 pdf 文件。

后来为了进一步提效,有了几个需求:

  1. 后台传来数据和 pdf 文件地址,前端需要在 pdf 文件中把所有关键词进行高亮标记;
  2. 后台传来的数据每一个词都需要点击一键搜索,需要 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 等。