Rust 赋能前端:PDF 分页/关键词标注/转图片/抽取文本/抽取图片/翻转...
Rust 赋能前端:PDF 分页/关键词标注/转图片/抽取文本/抽取图片/翻转...
❝我从不幻想成功。我只会为了成功努力实践
大家好,我是柒八九。一个专注于前端开发技术/Rust
及AI
应用知识分享的Coder
❝此篇文章所涉及到的技术有
WebAssembly
Mupdf
Pdf
操作(分页展示/文本抽离/文本标注/获取超链接/Pdf转图片/翻转/截取
)
因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请大家酌情观看。
前言
从上次发文SSE打扮你的AI应用,让它美美哒已经差不多过了快2个月了。
这期间呢,发生了很多事情。
足足有8周的时间。
❝一周就是一年的
2%
啊
那就是这一年的16%
的时间,确实抽不出时间来写文章。这里向广大的读者说一句抱歉。
不过呢,这段时间内,也没闲着。虽然,没时间写文章,但是做了很多有趣的功能。
- 操作
PDF
(就是这篇文章的主要内容) EPub
分页展示- 视频抽帧(
Rust
+WebAssembly
) - 图片OCR识别(
AI模型
+Rust
+WebAssembly
) - 图片基于关键词进行标注(
Rust
+WebAssembly
) - 音频文件识别(AI模型+
Python
) SRT
文件标注- 文本翻译(AI模型+
Python
) - leafletjs[1](GIS相关)
大家不要着急,这些内容都准备写成文章。
老粉都知道,之前我们在Rust 赋能前端 -- 写一个 File 转 Img 的功能/AI 赋能前端 -- 文本内容概要生成介绍过"文档操作"的功能。
❝而就在之后,我们其中一个需求中,又新增了一个对PDF分页展示和关键词标注的功能点。
也就是说,我们无法直接使用iframe
亦或者pdfjs-dist[2]等PDF
常规解决方案来实现上述操作。
❝这已经超出了正常前端的技能范畴了,那么我们就需要把视角移到其他语言环境(
C/C++/Rust
)是否有成熟的解决方案。然后配套WebAssembly
来实现我们的目的。
而今天,我们就来讲讲。在前端如何使用WebAssembly
来拓展前端应用的功能,实现之前不能或者不好实现的功能。
之前呢,我们在Rust 赋能前端 -- 写一个 File 转 Img 的功能就介绍过mupdf
。
上次呢,我们使用mupdf-js[]对Pdf
进行了一些操作。例如,将PDF
转成text/png/svg/html
。但是呢,在使用mupdf-js
有一个弊端就是,有些高级功能,例如(翻转/文本标注/获取pdf中的图片
等)无法实现。
所以,今天我们绕过mupdf-js
而是直接使用mupdf[4]的编译好的wasm
文档来执行相关操作。
首先,我们会使用mupdf
为大家讲解下面的各种操作。
例如:
- '获取元数据'
- '页数'
- '结构化文本'
- '抽取图片'
- '获取标注信息'
- '文本查询'
- '获取文档中超链接'
- '获取文档大小'
- 'pdf转图片'
- '添加文本'
- '翻转'
- '截取'
- '文档分割'
其次,我们会基于上面的各种能力,来实现一个Pdf分页和关键词标注的操作。
好了,天不早了,干点正事哇。
我们能所学到的知识点
❝
- 项目初始化
- 使用Mupdf 蹂躏 PDF
- PDF 分页展示和文本标注
1. 项目初始化
从上面的演示效果,是不是有种似曾相识的感觉,对呢。我们还是基于f_cli_f[5]来构建的前端Vite+React+TS
项目。
❝
f_cli_f
,我们后期打算升级一版,敬请期待。
当我们通过yarn/npm
安装好对应的包时。我们就可以在pages
新建一个Pdf2Img
的目录。
然后构建如下的目录结构
代码语言:javascript代码运行次数:0运行复制├──
├──
├──
└──
这里呢,我们没有使用Web Worker
或者Comlink[6]。是因为,在之前Rust 赋能前端 -- 写一个 File 转 Img 的功能/AI 赋能前端 -- 文本内容概要生成就有过相关的解释。所有,这里为了行文方便,就选择了最简单的方式 - Promise
来处理针对PDF
的相关操作。
我们在前端项目中,新建一个wasm
来存放在前端项目中要用到的各种wasm
。
然后,我们创建一个mupdf
的文件夹,这里面是存放mupdf
编译后的文件内容。
├── mupdf-wasm.js
├── mupdf-wasm.wasm
├── mupdf.
└── mupdf.js
具体针对mupdf
的wasm
文件从哪里来,我们可以通过mupdf_github
亦或者npm
包来获取。当然,之前也介绍过,如果你还想使用更高级的功能,我们也可以自己通过命令来编译。
下面,我们就以功能点来各自介绍它们的作用。
2. 使用 Mupdf 蹂躏 PDF
这个标题确实吓人,但是看了下面的操作后,发现这个词真是很贴切。
下面,我们就来讲讲在前言出现过的mupdf
的各种能力。
在这节中,我们就直接使用代码来演示mupdf
赋予我们的能力。如果想了解更多关于Mupdf
在前端环境的使用方式,可以翻看mupdf_core API[7]。
我们将初始化mupdf
放置在文件中。然后,我们在
存储一个全局变量(
doc
)来表示mupdf
的实例。
let doc: mupdf.Document;
export function getInstance(buffer: ArrayBuffer) {
doc = mupdf.(buffer, 'application/pdf');
}
这样,在页面中,我们可以通过input
的onChange
或者通过fetch
来获取pdf
的ArrayBuffer
资源。
而我们这里是通过input
来处理pdf
。在中,我们有一个
input
的onChange
的回调。
ct handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
ct files = files;
if (files && files.length > 0) {
ct file = files[0];
ct reader = new FileReader();
= (event) => {
ct arrayBuffer = ?.result as ArrayBuffer;
getInstance(arrayBuffer);
};
reader.readAsArrayBuffer(file);
}
};
这样,我们在每次接收到arrayBuffer
后,就会实例化对于的mupdf
。
我们可以通过如下代码来获取一个PDF
的元数据信息。
export function getMetadata(): Promise<string> {
return new Promise((res) => {
ct format = doc.getMetaData('format');
ct modificationDate = doc.getMetaData('info:ModDate');
ct encryption = doc.getMetaData('encryption');
ct author = doc.getMetaData('info:Author');
ct title = doc.getMetaData('info:Title');
res(`format-${format}
encryption-${encryption}
modificationDate-${modificationDate}
title-${title}
author-${author}
`);
});
}
当然,我们还可以获取其他的元数据信息。
我们还可以通过setMetaData(key: string, value: string)
来对某个元数据设定值。
效果展示
我们可以通过()
来获取pdf
的总页数。
那既然,PDF
的总页面信息有了,是不是我们可以将其做分页处理。这个我们在下一节中会讲到。
❝有些功能,我们为了行文的方便,在介绍一些功能时,只对其某个
page
页面进行处理。其实,如果你想通过下面的功能获取全部()
来对全文档进行操作处理。
效果展示
我们可以通过toStructuredText
来抽离指定页面的文本内容。
export function getStructuredText(page: number): Promise<string> {
return new Promise((res) => {
ct pageContent = doc.loadPage(page);
ct json = ('preserve-whitespace').asJSO();
res(json);
});
}
通过toStructuredText
返回了一个StructuredText
类型的数据。
export declare class StructuredText extends Userdata<'fz_stext_page'> {
static readonly _drop: (p: Pointer<'fz_stext_page'>) => void;
static readonly SELECT_CHARS = 0;
static readonly SELECT_WORDS = 1;
static readonly SELECT_LIES = 2;
walk(walker: StructuredTextWalker): void;
asJSO(scale?: number): string;
copy(p: Point, q: Point): string;
highlight(p: Point, q: Point, max_hits?: number): Quad[];
search(needle: string, max_hits?: number): Quad[][];
}
从上面定义我们可以看到,我们可以基于StructuredText
做更精细化的操作。(walk
/search
等)
效果展示
在中定义如下代码
type imagesType = {
bbox: [number, number, number, number];
matrix: [number, number, number, number, number, number];
image: mupdf.Image;
};
export function getImages(page: number): Promise<imagesType[]> {
return new Promise((res) => {
ct result: imagesType[] = [];
ct pageContent = doc.loadPage(page);
('preserve-images').walk({
onImageBlock(bbox, matrix, image) {
result.push({ bbox, matrix, image });
},
});
res(result);
});
}
然后在页面中的指定函数中,处理getImages
返回的数据信息
if (ability === '抽取图片') {
res = await getImages(page);
ct arr = [];
(async (item) => {
ct image = item.image;
ct pngUint8Array = ().asPG();
ct blob = new Blob([pngUint8Array], { type: 'image/svg' });
ct url = await BlobToObjectURL(blob);
arr.push(url);
});
setImgUrlArr(arr);
}
上面的代码就是用于抽取某页pdf
中图片信息。
效果展示
在中定义如下代码
type Quad = [number, number, number, number, number, number, number, number];
export function search(page: number, keywords: string): Promise<Quad[][]> {
return new Promise((res) => {
ct pageContent = doc.loadPage(page);
ct result = pageContent.search(keywords);
res(result);
});
}
效果展示
我们通过search
可以获得对应keywords
在原文档中的位置。那么,就给我提供了一个机会,用于该文本的标注处理。这个我们在下一节中介绍。
在中定义如下代码
export function getLinks(page: number): Promise<mupdf.Link[]> {
return new Promise((res) => {
ct pageContent = doc.loadPage(page);
ct links = pageContent.getLinks();
res(links);
});
}
效果展示
我们可以看到,它能准确识别出pdf
文档中的超链接。
type Rect = [number, number, number, number];
export function getBounds(page: number): Promise<Rect> {
return new Promise((res) => {
ct pageContent = doc.loadPage(page);
ct bounds = pageContent.getBounds();
res(bounds);
});
}
效果展示
export function getContentByPage(page: number): Promise<string> {
return new Promise((res) => {
ct pageContent = doc.loadPage(page);
ct pixmap = (
mupdf.Matrix.identity,
mupdf.ColorSpace.DeviceRGB,
false,
true
);
ct pngUint8Array = pixmap.asPG();
ct blob = new Blob([pngUint8Array], { type: 'image/svg' });
blobToBase64Uri(blob).then((base64) => {
res(base64);
});
});
}
效果展示
export type textType = {
text: string;
x: number;
y: number;
fontFamily: string;
fontSize: number;
};
export function addText(pageumber: number, textInfo: textType): Promise<Uint8Array> {
return new Promise((res) => {
ct { text, x, y, fontFamily, fontSize } = textInfo;
ct page = doc.loadPage(pageumber) as mupdf.PDFPage;
ct pageObj = page.getObject();
ct pdfDocument = doc as mupdf.PDFDocument;
ct font = pdfDocument.addSimpleFont(new mupdf.Font(fontFamily));
let resources = pageObj.get('Resources');
if (!resources.isDictionary())
pageObj.put('Resources', (resources = ()));
let resFonts = resources.get('Font');
if (!resFonts.isDictionary()) resources.put('Font', (resFonts = ()));
resFonts.put('F1', font);
// create drawing operati
ct extra_contents = pdfDocument.addStream(
'BT /F1 ' + fontSize + ' Tf 1 0 0 1 ' + x + ' ' + y + ' Tm (' + text + ') Tj ET',
{}
);
// add drawing operati to page contents
ct page_contents = pageObj.get('Contents');
if (page_contents.isArray()) {
// Contents is already an array, so append our new buffer object.
page_contents.push(extra_contents);
} else {
// Contents is not an array, so change it into an array
// and then append our new buffer object.
ct new_page_contents = ();
new_page_contents.push(page_contents);
new_page_contents.push(extra_contents);
pageObj.put('Contents', new_page_contents);
}
ct outputBuffer = pdfDocument.saveToBuffer('incremental');
res(outputBuffer.asUint8Array());
});
}
效果展示
export function rotate(pageumber: number, degrees: number): Promise<Uint8Array> {
return new Promise((res) => {
ct page = doc.loadPage(pageumber) as mupdf.PDFPage;
ct pageObj = page.getObject();
ct rotate = pageObj.getInheritable('Rotate');
pageObj.put('Rotate', (rotate as unknown as number) + degrees);
ct outputBuffer = (doc as mupdf.PDFDocument).saveToBuffer('incremental');
res(outputBuffer.asUint8Array());
});
}
效果展示
type CropInfo = {
x: number;
y: number;
width: number;
height: number;
};
export function crop(pageumber: number, cropInfo: CropInfo): Promise<Uint8Array> {
return new Promise((res) => {
ct { x, y, width, height } = cropInfo;
ct page = doc.loadPage(pageumber) as mupdf.PDFPage;
page.setPageBox('CropBox', [x, y, x + width, y + height]);
ct outputBuffer = (doc as mupdf.PDFDocument).saveToBuffer('incremental');
res(outputBuffer.asUint8Array());
});
}
效果展示
export function split(): Promise<Uint8Array[]> {
return new Promise((res) => {
ct splitDocuments: Uint8Array[] = [];
ct pdfDocument = doc as mupdf.PDFDocument;
for (let i = 0; i < (); i++) {
ct newDoc = new mupdf.PDFDocument();
newDoc.graftPage(0, pdfDocument, i);
ct buffer = newDoc.saveToBuffer('compress');
splitDocuments.push(buffer.asUint8Array());
res(splitDocuments);
}
});
}
效果展示
. PDF 分页展示和文本标注
我们在第二节,展示了如何使用mupdf
处理pdf
。而现在呢,我们就糅合上面的几种能力来实现一个,PDF分页和文本标注
。
我们把用到的一些能力放到下面
- 获取PDF总页数
- PDF转图片
- 文本查询
我们的主要逻辑都集中在和
。
主要代码如下:
代码语言:javascript代码运行次数:0运行复制import { Pagination, Spin } from 'antd';
import { useEffect, useState } from 'react';
import { getPageCount, getContentByPage, searchKeyWords, SearchResult } from './pdf';
import KeyWordsHight from './KeyWordsHight';
export interface MuFileProps {
buffer: ArrayBuffer;
searchQuery: string;
}
ct PdfShow: React.FC<MuFileProps> = (props) => {
ct { buffer, searchQuery } = props;
ct [count, setCount] = useState(0);
ct [loading, setLoading] = useState(false);
ct [page, setPage] = useState(0);
ct [fileContent, setFileContent] = useState('');
ct [keywordsResults, setKeywordsResults] = useState({} as SearchResult);
useEffect(() => {
ct getFileInfo = async () => {
setLoading(true);
ct pageCount = await getPageCount(buffer);
setCount(pageCount);
setPage(1);
ct pageContent = await getContentByPage(0);
setFileContent(pageContent);
setLoading(false);
};
buffer && getFileInfo();
}, [buffer]);
useEffect(() => {
ct searchKey = async () => {
ct results = await searchKeyWords(searchQuery, 1);
setKeywordsResults(results);
};
if (searchQuery) {
searchKey();
}
}, [searchQuery]);
ct onPaginationChange = async (page: number) => {
setKeywordsResults({} as SearchResult);
setLoading(true);
setPage(page);
ct pageContent = await getContentByPage(page - 1);
setFileContent(pageContent);
setLoading(false);
};
return (
<Spin spinning={loading}>
<section style={{ height: '100%', width: '100%' }}>
<div
style={{
position: 'sticky',
top: 0,
zIndex: 1,
backgroundColor: 'white',
padding: '10px 0px',
}}
>
{count > 0 && (
<Pagination
hideOnSinglePage
defaultCurrent={1}
defaultPageSize={1}
total={count}
showQuickJumper
showSizeChanger={false}
onChange={onPaginationChange}
/>
)}
</div>
<KeyWordsHight
fileContent={fileContent}
pageumber={page}
searchResults={keywordsResults}
/>
</section>
</Spin>
);
};
export default PdfShow;
从代码中,我们可以看出。
PdfShow
主要是接收buffer
数据,然后通过中的各种方法来初始化页面总数(
count
),进而构建和分页(Pagination
)相关的逻辑,在处理page
的过程中,通过fileContent
来保存mupdf
处理后的信息。
针对searchQuery
,我们是通过中的
searchKeyWords
来到对应的关键词的位置信息,并存储到keywordsResults
中。
searchKeyWords的相关逻辑
代码语言:javascript代码运行次数:0运行复制export type Box = {
x: number;
y: number;
w: number;
h: number;
};
export type SearchResult = {
page?: number;
results: Box[];
pageWidth?: number;
pageHeight?: number;
};
export function searchKeyWords(keywords: string, page: number): Promise<SearchResult> {
return new Promise((res) => {
ct pageContent = doc.loadPage(page);
ct hits = pageContent.search(keywords);
ct result = [];
for (ct hit of hits) {
for (ct quad of hit) {
ct [ulx, uly, urx, ury, llx, lly, lrx, lry] = quad;
result.push({
x: ulx,
y: uly,
w: urx - ulx,
h: lly - uly,
});
}
}
res({ results: result });
});
}
随后,我们将fileContent
和keywordsResults
都传人到KeyWordsHight
组件中。
import { useEffect, useRef, useState } from 'react';
import { Box, SearchResult } from './pdf';
type PngPageProps = {
fileContent: string;
pageumber: number;
searchResults?: SearchResult;
};
ct KeyWordsHight = ({ fileContent, pageumber, searchResults }: PngPageProps) => {
ct imgRef = useRef<HTMLImageElement>(null);
ct [boxes, setBoxes] = useState([] as Box[]);
useEffect(() => {
if ( && searchResults?.results?.length) {
ct { results } = searchResults;
if (results.length) {
setBoxes(
results?.map(
(res) =>
({
x: res.x,
y: res.y,
w: res.w,
h: res.h,
}) as Box
)
);
}
}
}, [searchResults]);
if (boxes.length) {
return (
<div style={{ position: 'relative', margin: '10px' }}>
<img ref={imgRef} src={fileContent} />
<div style={{ margin: '10px' }}>
{(({ x, y, w, h }, key) => (
<div
key={key}
style={{
left: `${x}px`,
top: `${y}px`,
width: `${w}px`,
height: `${h}px`,
position: 'absolute',
backgroundColor: 'yellow',
opacity: 0.5,
}}
/>
))}
</div>
</div>
);
}
return (
<div key={pageumber} style={{ position: 'relative' }}>
<img ref={imgRef} src={fileContent} />
</div>
);
};
export default KeyWordsHight;
由于我们将pdf
转换成了图片资源(fileContent
),然后它可以直接给<img/>
。
然后,我们基于searchResults
是否含有boxes
信息,来判定是否有标注信息。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
Reference
[1]
leafletjs: /
[2]
pdfjs-dist:
[]
mupdf-js:
[4]
mupdf: /
[5]
f_cli_f:
[6]
Comlink:
[7]
mupdf_core API: .html#core-api
本文参与 腾讯云自媒体同步曝光计划,分享自。原始发表:2024-09-04,如有侵权请联系 cloudcommunity@tencent 删除分页前端数据rustpdf#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格
上一篇:Rust 赋能前端: 视频抽帧
下一篇:你好!2025!
推荐阅读
留言与评论(共有 8 条评论) |
本站网友 酒渣鼻 | 28分钟前 发表 |
如果觉得不错 | |
本站网友 都昌二手房 | 21分钟前 发表 |
而我们这里是通过input来处理pdf | |
本站网友 喷奶水 | 22分钟前 发表 |
就会实例化对于的mupdf | |
本站网友 4000000000 | 27分钟前 发表 |
lly | |
本站网友 中国核武 | 22分钟前 发表 |
就会实例化对于的mupdf | |
本站网友 世纪城地图 | 27分钟前 发表 |
没时间写文章 | |
本站网友 不老歌 | 19分钟前 发表 |
0 |