【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;" 就可以了。
在这里插入图片描述
在这里插入图片描述

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

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

#updateUIState(resetNumPages = false)

在这里插入图片描述

    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更新)

index.html中新增样式,记得添加 !important

    <style>
      :root {
        --sidebar-width: 400px !important;
      }
    </style>

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

搜索 THUMBNAIL_WIDTH

在这里插入图片描述

完整 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根据路径里传参数高亮显示关键字(跳转到对应页面)

### 回答1: 索尼47EC笔记本驱动是指适用于索尼47EC笔记本电脑的设备驱动程序。设备驱动程序是一种软件,用于与硬件设备进行通信和控制,以使设备能够正常工作。 一般来说,新的笔记本电脑在购买时会附带一个驱动光盘或USB驱动程序,其包含了与笔记本电脑硬件相匹配的各个设备的驱动程序。用户可以通过将光盘插入光驱或将USB插入电脑来安装驱动程序。 另外,用户也可以通过访问索尼官方网站来下载和安装最新的笔记本电脑驱动程序。在官方网站上,用户可以通过输入具体的机型和操作系统信息来找到适用于自己设备的驱动程序。下载后,用户只需双击安装文件并按照提示完成安装过程。 笔记本电脑的驱动程序通常包括声卡驱动、显卡驱动、无线网卡驱动、蓝牙驱动、触摸板驱动等等。安装适当的驱动程序可以确保这些设备正常运行并获得最佳性能。 总之,索尼47EC笔记本驱动是为了使笔记本电脑内部各个硬件设备正常工作而需要安装的软件。用户可以通过光盘、USB驱动程序或者官方网站来获取并安装适用于自己设备的最新驱动程序,以确保电脑的稳定性和性能。 ### 回答2: 索尼47ec笔记本驱动是指用于支持和运行该型号笔记本电脑正常工作的软件程序。笔记本驱动程序是使用操作系统和硬件之间的桥梁,使它们能够相互协作,并正确地执行各种功能。 想要获得索尼47ec笔记本驱动程序,您可以通过以下几种方式进行获取和更新: 1. 官方网站下载:您可以访问索尼(Sony)的官方网站,找到他们的支持页面并在那里寻找到47ec型号笔记本的驱动。在网站上,您可以选择您的操作系统(如Windows 10)以及所需驱动的类型(例如声卡或显卡驱动),然后下载并安装所需的驱动程序。 2. 驱动更新软件:您可以使用一些第三方的驱动更新软件来检测和更新索尼47ec笔记本的驱动程序。这些软件通常会自动扫描您的电脑,检测到需要更新的驱动,并提供下载和安装的选项。 3. Windows更新:在一些情况下,Windows操作系统也会提供更新的驱动程序。您可以打开Windows更新设置,让系统自动搜索并下载适用于您的索尼47ec笔记本的驱动程序。 无论选择哪种方式,确保您下载和安装的驱动程序是来自可信的来源,并且与您的操作系统和硬件兼容。此外,建议定期检查和更新驱动程序,以确保您的笔记本电脑以最佳状态运行。 ### 回答3: 索尼47ec笔记本是一款优秀的设备,它需要正确的驱动程序来确保其正常运行。在寻找索尼47ec笔记本驱动时,我们可以采取以下步骤: 1. 确定操作系统:首先,我们需要确定笔记本电脑使用的操作系统是Windows还是macOS。这将有助于我们找到适用于该操作系统的驱动程序。 2. 访问索尼官方网站:我们可以登录索尼官方网站,然后在支持或驱动程序下载部分找到相关的驱动程序。输入正确的型号和操作系统信息,以便筛选驱动程序的列表。 3. 下载并安装驱动程序:找到适用于索尼47ec笔记本的驱动程序后,我们可以点击下载按钮将其保存到我们的电脑。一旦下载完成,我们可以双击该文件并按照安装向导的指示进行安装。 4. 更新驱动程序:驱动程序可能会定期发布更新版本,这些更新通常包括修复漏洞和改进性能。所以,一旦我们安装了适用于索尼47ec笔记本的驱动程序,我们可以定期检查索尼官方网站以获取最新版本的驱动程序,并按照上述步骤进行更新。 5. 驱动程序备份:为了避免不良情况和数据丢失,我们建议在安装新驱动程序之前备份当前的驱动程序。这样,如果新驱动程序出现问题,我们可以轻松地恢复到之前的版本。 总之,为了找到适用于索尼47ec笔记本的驱动程序,我们应前往索尼官方网站,确定操作系统,下载并安装驱动程序。另外,我们还可以定期检查驱动程序的更新版本,并备份当前驱动程序,以确保电脑的最佳性能和稳定性。
评论 68
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值