类chatDoc文档定位功能开发指南
类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