Bootstrap

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

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

一、PDF.js 介绍

官方地址
中文文档

PDF.js 是一个使用 HTML5 构建的便携式文档格式查看器。
pdf.js 是社区驱动的,并由 Mozilla 支持。我们的目标是为解析和呈现 PDF 创建一个通用的、基于 Web 标准的平台。

二、 安装方法

1、下载 pdf.js

下载地址
在这里插入图片描述
我下载的版本是 pdfjs-4.0.189-dist
在这里插入图片描述

2、解压包并放到项目中

解压后将完整文件夹放到 vue3 的 public 文件夹内
在这里插入图片描述

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

web/viewer.mjs 内找到搜索 throw new Error("file origin does not match viewer's") 并注释掉,如果不注释,可能会出现跨域错误,无法正常预览文件
在这里插入图片描述
这样就算安装完成了,后面我们开始在项目中使用。

三、基础使用

1、创建 PDF 组件

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

<script setup lang="ts">
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('&');
      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、文本标注

某些时候我们需要跳转到指定页面,然后自动标注文本,这个时候就需要自动标注了(我这个方法是传参,如果要采用调用的方式,请参考最下面的博客)
在这里插入图片描述
代码跟跳转一样,写在后面就可以了

    // 自动高亮文本(要解码)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有一个叫做 data-page-number="1" 的属性,因此我们可以通过 js 的 querySelectorAll 选择器找到对应属性的 dom 元素,然后再操作添加遮罩就可以了,代码放在下面。
在这里插入图片描述

    // 测试的坐标
    const content_pos_1 = {
      x: 0.5135954145019941,
      y: 0.4662730487881233,
    };
    const content_pos_2 = {
      x: 0.7135954145019941,
      y: 0.8662730487881233,
    };
    // 查找属性 data-page-number='页码' 的 dom 元素
    const pageList = document.querySelectorAll(`[data-page-number='${page}']`);
    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 就会消失,所以我们要在源码重新更新的时候重新添加高亮,源码内部重新添加的函数在这个位置: #updateUIState
在这里插入图片描述
我们只需要将修改后重新添加的代码放在尾部就行
首先我们要修改第三部分的代码

   // 测试的坐标
    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 元素
    const pageList = document.querySelectorAll(`[data-page-number='${page}']`);
    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}']`);
      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、添加遮罩高亮(数组批量跨页渲染)

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

我们就需要这样传参
在这里插入图片描述
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}']`);
      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}']`);
          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直接跳转高亮。

const pdfFrame = document.getElementById('myIframe').contentWindow;
 pdfFrame.PDFViewerApplication.pdfViewer.scrollPageIntoView({
    pageNumber: props.dcsInfo.page_no,
    content_pos: props.dcsInfo.content_pos,
  });

在这里插入图片描述
注意:如果是同页不同高亮渲染,要在下面的位置写渲染,不然不会更新!!
在这里插入图片描述

7、隐藏部分工具栏

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

完整 PDF 组件代码

<script setup lang="ts">
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);
  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">
    <iframe :src="pdfUrl" width="100%" height="100%" id="myIframe"></iframe>
  </div>
</template>

<style scoped lang="scss">
.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根据路径里传参数高亮显示关键字(跳转到对应页面)

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;