likes
comments
collection
share

类chatDoc文档定位功能开发指南

作者站长头像
站长
· 阅读数 33

类chatDoc文档定位功能开发指南

最近做一个类似chatDoc的文档定位功能,这里记录一下开发过程中如何对文件定位的。

1. 用户场景

用户在网页中上传pdf文件,保存后文件内容会被LLM(大语言模型)拆分解析,存入向量数据库中。用户在查看解析结果时,需要在页面上显示解析结果的来源,即pdf文件的具体位置。

2. 解决方案

2.1 问题分析

要定位到pdf文件到具体位置,首先就要渲染pdf,其次要能获取到解析结果来源的位置信息或者文字信息,然后操纵pdf文件,将pdf文件定位到指定位置或高亮匹配到的文字。

2.2 技术选型

没什么好聊的,pdfjs是其他开源库比如vue-pdf的基础,而我要对pdf文件进行更加细致的操作(我只是担心黑盒),所以不能选择二次封装的库,就选了pdfjs。仓库GitHub地址

2.3 开发过程

(进入正题)

2.3.1 pdfjs的引入

这里有两个要注意的点: pdfjs有两种引入方式,一种是通过下载pdfjs的包,然后当作静态资源引入,另一种是通过npm安装,而后者的话,要执行的是 npm install pdfjs-dist(pdfjs这个名字被抢注了),我在项目里采用的是后者。

在引入的时,要注意自己项目中使用的webpack或者vite的配置或版本,因为pdfjs的代码中有一些es6或更新的语法,如果webpack或者vite的配置不支持的话,会报错。保险起见可以引入2.x版本。

还有一点是,在代码中引入这个包时,注意包的导出方式,看看有没有 export default,如果有的话,就要用 import pdfjsLib from 'pdfjs-dist',如果没有的话,就要用 import * as pdfjsLib from 'pdfjs-dist'

在引入pdfjs之后,还要给pdfjs配置相同版本的worker,不然会报一个警告:Warning: Setting up fake worker

然后在vue文件中引入:

// vue 版本 2.7.13
// pdfjs 版本 2.7.570
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf' 
// 这里要将legacy下的pdf.worker.min.js放到在静态资源文件夹中,我在 vite 和 webpack 里都是这样干的,有其他方法请指点
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/js/pdf.worker.min.js'

2.3.2 pdf文件的渲染

引入pdfjs之后,接下来就要渲染pdf文件内容了。渲染的方式决定着我能否高亮匹配的文字。

  • 仅渲染图片 这种方式渲染出来的内容,是一张张图片,这样的话,无法在页面上高亮文字。

  • 仅渲染文字 这种方式渲染,能高亮文字,但是会丢失文件中的图像信息。

  • 文字和图片都渲染 这种方式渲染,能高亮文字,也能显示图片,pdfjs 的 viewer.js文件 和 vue-pdf 之类的二次封装库都是这样渲染的。

小结:采用第三种方式渲染pdf文件,这样既能高亮文字,也不会丢失信息。这种渲染模式的实现原理是,先用 canvas 渲染出图片,然后渲染文字,将文字覆盖在图片上,设置透明度,这样在网页上看起来像是高亮了图片中的文字。

<!-- pdfView.vue -->
<template>
  <div ref="pdfViewer" class="pdf-viewer"></div>
</template>
  
<script>
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf' 
pdfjsLib.GlobalWorkerOptions.workerSrc = '/static/js/pdf.worker.min.js'

export default {
  name: "PdfViewer",
  data() {
    return {
      url: "/path/pdfFile.pdf"
    }
  },
  mounted() {
    this.loadPdf();
  },
  methods: {
    // 加载pdf
    loadPdf() {
      pdfjsLib.getDocument(this.url).promise.then(pdf => {
        this.renderPdf(pdf);
      });
    },
    // 渲染pdf全部页面
    renderPdf(pdf) {
      for (let i = 1; i <= pdf.numPages; i++) {
        pdf.getPage(i).then(page => {
          this.renderPage(page);
        });
      }
    },
    async renderPage(page) {
      // 先渲染图片
      const viewport = page.getViewport({ scale: 1 });
      const canvas = document.createElement("canvas");
      const context = canvas.getContext("2d");
      canvas.height = viewport.height;
      canvas.width = viewport.width;
      const renderContext = {
        canvasContext: context,
        viewport: viewport,
      };
      await page.render(renderContext).promise;
      // 再渲染文字
      const textContent = await page.getTextContent();
      const textLayer = document.createElement("div");
      pdfjs.renderTextLayer({
        textContent: textContent,
        container: textLayer,
        viewport: viewport,
        textDivs: [],
      });
      textLayer.className = "textLayer";
      // 将文字覆盖在图片上
      const canvasWrapper = document.createElement("div");
      canvasWrapper.className = "canvasWrapper";
      canvasWrapper.appendChild(canvas);
      canvasWrapper.appendChild(textLayer);
      this.$refs.pdfViewer.appendChild(canvasWrapper);
    },
  }
}
</script>
<style scoped>
.pdf-viewer {
  width: 80vw;
  height: 80vh;
  margin: 0 auto;
  border: 1px solid #ccc;
  overflow: auto;
}
</style>
<style>
/* 不能用scoped,因为动态创建的元素不会被编译带上hash */
/* 不加上这些css修饰,元素会错位,样式来自vue-pdf-embed这个库 */
.canvasWrapper {
  position: relative;
}
.textLayer {
  text-align: initial;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  overflow: hidden;
  opacity: 0.2;
  line-height: 1;
  text-size-adjust: none;
  forced-color-adjust: none;
}
.textLayer span,
.textLayer br {
  color: transparent;
  position: absolute;
  white-space: pre;
  cursor: text;
  transform-origin: 0% 0%;
}
</style>

这样,我们得到了一个渲染pdf的组件,支持选中文字高亮操作。

2.3.3 文字高亮

这里需要注意,我们在之前的代码里获得的 textContent 内容,划分的文字不是一行一行或者一个自然段这样划分的,而是按照分隔符划分的,包括括号、逗号、句号、换行符、图标等等,也就是说,我们在匹配文字的时候,可能会被这个影响匹配结果,这是其一;其二是注意到我们是一页一页的获取文件信息,所以我们其实不能匹配跨页的内容

其实不用说也知道,当使用上面的组件渲染之后,查看元素就知道是怎么一回事了。(推荐查看)

那么,最简单的文字匹配就是使用 querySelector 方法获取元素,然后查看 textContent 中是否包含答案,如果包含的话,就将这个元素高亮。

2.3.4 文档定位

仅仅是高亮是不够的,毕竟看不到的高亮就相当于没高亮。于是在高亮的同时,我们需要将pdf文件滚动到高亮的位置。 这里的逻辑也比较简单,我们在渲染的时候可以拿到每一页的高度 viewport.height ,然后将高亮的元素所在的页数乘以每页的高度,就是高亮元素距离顶部的距离,然后将这个距离赋值给滚动条的 scrollTop 属性,就能将高亮元素滚动到顶部了。

我这里的话,LLM 会返回一个匹配结果的位置信息,包括所在的页码,自然段顶部距离页面顶部的距离,是否跨页,自然段底部距离页面顶部的距离。我的高亮方式是多渲染了一个框,把大段的结果先框起来,然后高亮其中的答案。要注意的一点是,再新渲染框时,记得销毁之前的框和高亮的文字也要取消高亮。也要注意框、文字、图片的层级关系。

2.4 其他问题

pdf的文件内容即可以来源于静态资源也可以是文件流(反正这个最后也要被转化) 静态资源的话直接填地址就好,而文件流的形式,则需要多做小小的转换。

// axios发起请求时,设置responseType为blob
axios({
  url: '...',
  method: '...',
  responseType: 'blob'
}).then(res => {
  // res.data就是文件流
})

// 将文件流转化为blob
const blob = new Blob([file], { type: 'application/pdf' })
// 将blob转化为url
const file = new FileReader()
file.readAsDataURL(blob)
file.addEventListener('load', () => {
  const url = file.result
})
// 或
// const url = URL.createObjectURL(blob)

3. 总结

这个功能的开发过程中,遇到了很多问题,比如pdfjs的引入、pdf文件的渲染、文字高亮、文档定位等等,这些问题都是在开发过程中遇到的,也是在解决问题的过程中学到的,所以这里记录一下,抛砖引玉,希望大家能不吝赐教。

转载自:https://juejin.cn/post/7271599265968488463
评论
请登录