基于 PDFium WebAssembly 打造的轻量级 PDF 处理库,设计目标极简 ------ 零外部依赖,既可以在 Node.js 后端运行,也能直接在浏览器中工作。它赋予你强大的 PDF 解析能力:提取文本、渲染页面、输出 PNG 图片,而且不需要安装任何原生 Canvas 包,也没有繁琐的 postinstall 脚本。
概述
ClawPDF 是 openclaw 团队开发的开源项目,旨在提供一个纯粹、安全、高效 的 PDF 处理工具。它的核心是 PDFium(Chrome 浏览器内置的 PDF 渲染引擎),通过 WebAssembly 编译后,可在任何现代 JavaScript 环境中运行。
✨ 核心亮点
- 🚀 零依赖 -- 无需安装
cairo、pango、canvas等原生库 - 🌐 跨平台 -- 完美支持 Node.js 22+ 和所有现代浏览器
- 🔒 安全 -- WASM 沙箱运行,不暴露文件系统(浏览器环境)
- ⚡ 高性能 -- 基于 PDFium 原生渲染,RGBA 位图直接输出
- 🖼️ 内置 PNG 编码器 -- 无需额外库即可生成压缩 PNG
- 🧩 清晰的生命周期管理 -- 支持
Symbol.dispose和asyncDispose,避免内存泄漏
安装
使用 npm 一键安装:
bash
npm install clawpdf
系统要求:
- Node.js 22+(ESM-only 模块,不支持 CommonJS require)
- 现代浏览器(支持 WebAssembly 和
CompressionStream)
安装完成后,你即可在代码中引入,同时也获得了命令行工具 clawpdf。
核心概念:引擎与文档
ClawPDF 的架构,核心组件的关系:
#mermaid-svg-zV1OR2v06vtLRYEm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zV1OR2v06vtLRYEm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zV1OR2v06vtLRYEm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zV1OR2v06vtLRYEm .error-icon{fill:#552222;}#mermaid-svg-zV1OR2v06vtLRYEm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zV1OR2v06vtLRYEm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zV1OR2v06vtLRYEm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zV1OR2v06vtLRYEm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zV1OR2v06vtLRYEm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zV1OR2v06vtLRYEm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zV1OR2v06vtLRYEm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zV1OR2v06vtLRYEm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zV1OR2v06vtLRYEm .marker.cross{stroke:#333333;}#mermaid-svg-zV1OR2v06vtLRYEm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zV1OR2v06vtLRYEm p{margin:0;}#mermaid-svg-zV1OR2v06vtLRYEm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zV1OR2v06vtLRYEm .cluster-label text{fill:#333;}#mermaid-svg-zV1OR2v06vtLRYEm .cluster-label span{color:#333;}#mermaid-svg-zV1OR2v06vtLRYEm .cluster-label span p{background-color:transparent;}#mermaid-svg-zV1OR2v06vtLRYEm .label text,#mermaid-svg-zV1OR2v06vtLRYEm span{fill:#333;color:#333;}#mermaid-svg-zV1OR2v06vtLRYEm .node rect,#mermaid-svg-zV1OR2v06vtLRYEm .node circle,#mermaid-svg-zV1OR2v06vtLRYEm .node ellipse,#mermaid-svg-zV1OR2v06vtLRYEm .node polygon,#mermaid-svg-zV1OR2v06vtLRYEm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zV1OR2v06vtLRYEm .rough-node .label text,#mermaid-svg-zV1OR2v06vtLRYEm .node .label text,#mermaid-svg-zV1OR2v06vtLRYEm .image-shape .label,#mermaid-svg-zV1OR2v06vtLRYEm .icon-shape .label{text-anchor:middle;}#mermaid-svg-zV1OR2v06vtLRYEm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zV1OR2v06vtLRYEm .rough-node .label,#mermaid-svg-zV1OR2v06vtLRYEm .node .label,#mermaid-svg-zV1OR2v06vtLRYEm .image-shape .label,#mermaid-svg-zV1OR2v06vtLRYEm .icon-shape .label{text-align:center;}#mermaid-svg-zV1OR2v06vtLRYEm .node.clickable{cursor:pointer;}#mermaid-svg-zV1OR2v06vtLRYEm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zV1OR2v06vtLRYEm .arrowheadPath{fill:#333333;}#mermaid-svg-zV1OR2v06vtLRYEm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zV1OR2v06vtLRYEm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zV1OR2v06vtLRYEm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zV1OR2v06vtLRYEm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zV1OR2v06vtLRYEm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zV1OR2v06vtLRYEm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zV1OR2v06vtLRYEm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zV1OR2v06vtLRYEm .cluster text{fill:#333;}#mermaid-svg-zV1OR2v06vtLRYEm .cluster span{color:#333;}#mermaid-svg-zV1OR2v06vtLRYEm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zV1OR2v06vtLRYEm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zV1OR2v06vtLRYEm rect.text{fill:none;stroke-width:0;}#mermaid-svg-zV1OR2v06vtLRYEm .icon-shape,#mermaid-svg-zV1OR2v06vtLRYEm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zV1OR2v06vtLRYEm .icon-shape p,#mermaid-svg-zV1OR2v06vtLRYEm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zV1OR2v06vtLRYEm .icon-shape .label rect,#mermaid-svg-zV1OR2v06vtLRYEm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zV1OR2v06vtLRYEm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zV1OR2v06vtLRYEm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zV1OR2v06vtLRYEm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 环境
open()
page(n)
text()
render()
png()
依赖
🔧 PDFium WebAssembly
(底层引擎)
📦 PdfEngine
WASM 实例 + 资源池
📄 PdfDocument
PDF 文件句柄
📑 PdfPage
单页操作
📝 提取文本
🎨 RGBA 位图
🖼️ PNG 图片
- PdfEngine:持有一个 WASM 实例,负责管理内存和资源。它可以打开多个文档,是服务端复用的核心。
- PdfDocument:代表一个已加载的 PDF 文件,提供页面访问、元数据读取和全文提取。
- PdfPage :代表文档中的一页(页码从 1 开始),可渲染为 RGBA 位图或直接编码为 PNG。
两种打开方式
| 方式 | 适用场景 | 生命周期管理 |
|---|---|---|
openPdf(...) |
脚本、一次性任务 | 自动创建私有引擎,文档销毁时引擎一并销毁 |
createEngine() + engine.open() |
服务器常驻进程、批量处理 | 引擎长期存活,文档需手动销毁(或通过 await using 自动释放) |
一次性使用(推荐):
ts
import { openPdf } from "clawpdf";
await using pdf = await openPdf("report.pdf");
console.log(pdf.pageCount); // 页面总数
// 当离开作用域时,pdf 自动销毁,私有引擎也随之释放
服务器复用引擎:
ts
import { createEngine } from "clawpdf";
await using engine = await createEngine(); // 引擎在应用生命周期内复用
const pdf = await engine.open(pdfBytes);
try {
console.log(pdf.text());
} finally {
pdf.destroy(); // 必须手动释放文档内存
}
// engine 会在作用域结束时自动销毁(因为使用了 await using)
💡 最佳实践 :在服务端,保持一个
engine实例存活,每次请求通过engine.open()打开新文档,处理完成后立即destroy()文档。这样既避免了反复初始化 WASM 的开销,又防止了内存泄漏。
加载 PDF
openPdf() 和 engine.open() 都接受多种输入类型:
| 输入类型 | 说明 |
|---|---|
Uint8Array / ArrayBuffer |
内存中的 PDF 字节数据 |
string(Node.js) |
本地文件路径,从磁盘读取 |
string(浏览器) |
必须是 URL (如 "https://example.com/doc.pdf") |
URL 对象 |
任何环境下的 HTTP/HTTPS URL |
Blob |
浏览器环境,通过 arrayBuffer() 读取 |
远程请求配置:
ts
await using pdf = await openPdf("https://example.com/doc.pdf", {
fetchTimeoutMs: 10_000, // 10 秒超时,0 表示不限制
signal: controller.signal, // 使用 AbortController 取消请求
});
⚠️ 在浏览器中,如果传入一个看起来像路径的字符串(如
"./local.pdf"),会抛出PdfFormatError,因为浏览器无法直接读取文件系统。请使用<input type="file">获取File对象,或通过fetch获取远程资源。
文本提取
所有页码均从 1 开始计数。ClawPDF 提取的文本顺序遵循 PDFium 的内部排序,可能与视觉阅读顺序不一致(尤其是复杂排版的 PDF),但大多数场景下足够使用。
单页提取
ts
const firstPageText = pdf.page(1).text();
多页提取
ts
const text = pdf.text({
maxPages: 5, // 最多处理 5 页(从第1页开始)
pages: [1, 3, 4], // 指定页码列表(优先级高于 maxPages)
maxChars: 200_000, // 最大字符数,超出则截断并标记 truncated.text = true
});
- 若同时指定
pages和maxPages,则pages优先,maxPages被忽略。 - 无效页码(超出文档范围)会抛出
PdfPageRangeError。 maxChars默认值为 200,000,有效防止超大 PDF 产生的文本数据撑爆内存或 AI 调用限额。
页面渲染
通过 pdf.page(n).render(options) 将页面渲染为 RGBA 位图,返回 { width, height, rgba }。
尺寸控制(四选一)
| 参数 | 说明 | 示例 |
|---|---|---|
dpi |
以 72 DPI 为基准缩放,默认 96 | { dpi: 144 } → 2 倍大小 |
scale |
直接缩放比例,1 表示 72 DPI | { scale: 1.5 } |
width |
目标像素宽度,高度自动按比例 | { width: 800 } |
height |
目标像素高度,宽度自动按比例 | { height: 600 } |
如果不提供任何尺寸参数,默认使用
{ dpi: 96 }。禁止同时指定多个 ,否则会抛出PdfError。
渲染选项
| 选项 | 默认值 | 说明 |
|---|---|---|
background |
"white" |
背景色:"white" 或 "transparent" |
forms |
false |
是否渲染 AcroForm 表单控件(如输入框、按钮) |
rotate |
0 |
额外旋转角度:0、90、180、270(在页面自带旋转基础上叠加) |
示例:
ts
const rendered = pdf.page(1).render({
dpi: 144,
forms: true,
background: "transparent",
rotate: 90, // 顺时针旋转 90 度
});
console.log(rendered.width, rendered.height);
console.log(rendered.rgba.byteLength); // 始终 = width * height * 4
🚨 渲染尺寸有硬上限
maxRenderPixels(引擎配置项,默认 10,000,000 像素)。若超出预算,会抛出PdfBudgetError。
PNG 输出
ClawPDF 内置纯 JavaScript PNG 编码器,Node.js 和浏览器通用,无需安装 sharp 或 canvas。
异步 PNG(推荐)
ts
import { writeFile } from "node:fs/promises";
const png = await pdf.page(1).png({ dpi: 144, forms: true });
await writeFile("page-1.png", png);
异步版本使用 node:zlib(Node)或 CompressionStream(浏览器)进行压缩,输出文件小。
同步 PNG
ts
const png = pdf.page(1).pngSync({ scale: 2 });
同步版本仅使用存储的 zlib 块(不重新压缩),生成的文件较大,但适用于不允许异步操作的严格场景。
独立编码 RGBA
如果你已有 { rgba, width, height } 数据,可以直接调用 encodePng:
ts
import { encodePng } from "clawpdf";
// 压缩(异步)
const compressed = await encodePng(rgba, { width, height });
// 不压缩(同步)
const stored = encodePng(rgba, { width, height, compress: false });
智能提取回退(extractPdf)
extractPdf 是专门为 AI 应用设计的高级助手函数 ,它遵循 "先文本,后图片" 的策略,确保你总能获得有意义的内容,无论是纯文本 PDF 还是扫描件。
快速上手
ts
import { extractPdf } from "clawpdf";
const result = await extractPdf("report.pdf", {
mode: "auto", // 自动模式
maxPages: 20,
minTextChars: 200, // 文本阈值
image: {
dpi: 96,
maxPixels: 4_000_000,
maxDimension: 10_000,
forms: true,
},
});
四种工作模式
| 模式 | 行为 |
|---|---|
auto |
默认 。先提取文本,若总字符数 < minTextChars,则渲染对应页为图片 |
text |
仅提取文本(不渲染图片) |
images |
仅渲染图片(不提取文本) |
both |
同时提取文本和渲染图片(所有页面) |
auto 模式流程图
#mermaid-svg-WycowgiWCGW5EoWI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WycowgiWCGW5EoWI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WycowgiWCGW5EoWI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WycowgiWCGW5EoWI .error-icon{fill:#552222;}#mermaid-svg-WycowgiWCGW5EoWI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WycowgiWCGW5EoWI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WycowgiWCGW5EoWI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WycowgiWCGW5EoWI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WycowgiWCGW5EoWI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WycowgiWCGW5EoWI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WycowgiWCGW5EoWI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WycowgiWCGW5EoWI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WycowgiWCGW5EoWI .marker.cross{stroke:#333333;}#mermaid-svg-WycowgiWCGW5EoWI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WycowgiWCGW5EoWI p{margin:0;}#mermaid-svg-WycowgiWCGW5EoWI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WycowgiWCGW5EoWI .cluster-label text{fill:#333;}#mermaid-svg-WycowgiWCGW5EoWI .cluster-label span{color:#333;}#mermaid-svg-WycowgiWCGW5EoWI .cluster-label span p{background-color:transparent;}#mermaid-svg-WycowgiWCGW5EoWI .label text,#mermaid-svg-WycowgiWCGW5EoWI span{fill:#333;color:#333;}#mermaid-svg-WycowgiWCGW5EoWI .node rect,#mermaid-svg-WycowgiWCGW5EoWI .node circle,#mermaid-svg-WycowgiWCGW5EoWI .node ellipse,#mermaid-svg-WycowgiWCGW5EoWI .node polygon,#mermaid-svg-WycowgiWCGW5EoWI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WycowgiWCGW5EoWI .rough-node .label text,#mermaid-svg-WycowgiWCGW5EoWI .node .label text,#mermaid-svg-WycowgiWCGW5EoWI .image-shape .label,#mermaid-svg-WycowgiWCGW5EoWI .icon-shape .label{text-anchor:middle;}#mermaid-svg-WycowgiWCGW5EoWI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WycowgiWCGW5EoWI .rough-node .label,#mermaid-svg-WycowgiWCGW5EoWI .node .label,#mermaid-svg-WycowgiWCGW5EoWI .image-shape .label,#mermaid-svg-WycowgiWCGW5EoWI .icon-shape .label{text-align:center;}#mermaid-svg-WycowgiWCGW5EoWI .node.clickable{cursor:pointer;}#mermaid-svg-WycowgiWCGW5EoWI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WycowgiWCGW5EoWI .arrowheadPath{fill:#333333;}#mermaid-svg-WycowgiWCGW5EoWI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WycowgiWCGW5EoWI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WycowgiWCGW5EoWI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WycowgiWCGW5EoWI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WycowgiWCGW5EoWI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WycowgiWCGW5EoWI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WycowgiWCGW5EoWI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WycowgiWCGW5EoWI .cluster text{fill:#333;}#mermaid-svg-WycowgiWCGW5EoWI .cluster span{color:#333;}#mermaid-svg-WycowgiWCGW5EoWI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WycowgiWCGW5EoWI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WycowgiWCGW5EoWI rect.text{fill:none;stroke-width:0;}#mermaid-svg-WycowgiWCGW5EoWI .icon-shape,#mermaid-svg-WycowgiWCGW5EoWI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WycowgiWCGW5EoWI .icon-shape p,#mermaid-svg-WycowgiWCGW5EoWI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WycowgiWCGW5EoWI .icon-shape .label rect,#mermaid-svg-WycowgiWCGW5EoWI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WycowgiWCGW5EoWI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WycowgiWCGW5EoWI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WycowgiWCGW5EoWI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
开始 extractPdf
提取指定页面的文本
文本长度 >= minTextChars?
返回文本结果, 不渲染图片
开始渲染页面为 PNG
是否超过图片预算?
maxPixels / maxDimension
停止渲染, 返回已生成的图片和文本
继续渲染下一页
所有页面渲染完成?
返回全部文本 + 图片
提取选项详解
| 选项 | 默认值 | 说明 |
|---|---|---|
pages |
--- | 要处理的页码列表(1-based),未指定则从第1页开始 |
maxPages |
20 |
最大处理页数(若未指定 pages) |
minTextChars |
200 |
触发图片回退的文本阈值(仅 auto 模式) |
maxTextChars |
200_000 |
文本输出上限,超出则截断 |
password |
--- | PDF 用户密码 |
engine |
--- | 可选的外部引擎实例(复用) |
image.dpi |
96 |
回退图片的 DPI |
image.maxPixels |
4_000_000 |
所有回退图片的总像素预算 |
image.maxDimension |
10_000 |
单张图片的最大宽或高(像素) |
image.forms |
true |
是否渲染表单控件 |
返回结果结构
ts
type ExtractResult = {
text: string; // 提取的文本
images: Array<{ // 渲染的图片列表
page: number;
width: number;
height: number;
bytes: Uint8Array; // PNG 字节
mimeType: "image/png";
}>;
pagesProcessed: number[]; // 实际处理的页码
truncated: { // 是否因预算限制而截断
text: boolean;
images: boolean;
};
};
适配器(方便对接 AI 模型)
ClawPDF 提供了两个内置适配器,方便你将结果直接用于多模态模型:
ts
import { toDataUrls, toMessageContent } from "clawpdf/adapters";
// 转为 Data URL 数组(可用于浏览器 img 标签)
const urls = toDataUrls(result);
// 转为 Anthropic/OpenAI 风格的消息内容块
const content = toMessageContent(result);
// 输出: [{ type: "text", text: "..." }, { type: "image", source: { data: "...", media_type: "image/png" } }]
密码保护的 PDF
ClawPDF 支持打开标准密码加密的 PDF(AES-128 或 AES-256)。
ts
// 文档 API
await using pdf = await openPdf("secret.pdf", { password: "myPassword" });
// extractPdf 同样支持
const result = await extractPdf("secret.pdf", {
password: "myPassword",
minTextChars: 200,
});
密码错误或缺失会抛出 PdfPasswordError;若 PDF 使用了不支持的加密算法(如某些专有加密),则抛出 PdfSecurityError。
🔐 安全建议 :在脚本或 CLI 中,避免将密码写在命令行中(会被 shell 历史记录)。推荐使用
--password-file选项(见下文 CLI 部分)。
命令行工具(CLI)
安装 clawpdf 后,会自动注册 clawpdf 命令,适合快速测试、脚本集成或一次性转换。
提取文本(默认行为)
bash
clawpdf report.pdf # 输出文本到 stdout
cat report.pdf | clawpdf - # 从管道读取(- 表示 stdin)
JSON 格式输出(含图片 Base64)
bash
clawpdf report.pdf --json
clawpdf extract report.pdf --mode both --pages 1,3-5 --json
JSON 结构同 ExtractResult,其中图片的 bytes 被替换为 base64 字符串。
渲染单页为 PNG
bash
clawpdf render report.pdf --page 1 > page.png
clawpdf render report.pdf --page 1 -o page.png
clawpdf render report.pdf --page 1 --inline auto # 终端内联显示(支持 Kitty/iTerm2)
常用 CLI 参数
| 参数 | 说明 |
|---|---|
| `--mode auto | text |
--pages 1,3-5 |
选择页码(1-based,支持范围) |
--max-pages N |
最大页数(默认 20) |
--max-text-chars N |
文本输出上限 |
--min-text-chars N |
触发图片回退的文本阈值 |
--dpi N / --scale N |
图片 DPI 或缩放 |
--max-pixels N / --max-dimension N |
图片预算限制 |
--forms / --no-forms |
是否渲染表单控件 |
--password <value> |
密码(不安全,避免使用) |
--password-file <path> |
从文件读取密码(推荐) |
--output-dir <dir> |
将回退图片写入 page-N.png 文件 |
| `--inline auto | kitty |
退出码
| 码 | 含义 |
|---|---|
0 |
成功 |
1 |
运行时错误(提取/渲染失败) |
2 |
无效参数或用法 |
3 |
无法读取或解析为 PDF |
4 |
密码缺失或错误 |
5 |
渲染或提取预算超限 |
在浏览器中使用
浏览器环境需使用专用入口 clawpdf/browser,它通过 import.meta.url 预置了打包好的 WASM 文件路径,无需额外配置。
ts
import { openPdf } from "clawpdf/browser";
// 假设从 <input type="file"> 获取 File 对象
const fileInput = document.getElementById("pdfInput") as HTMLInputElement;
const file = fileInput.files[0];
await using pdf = await openPdf(file);
console.log(pdf.text({ maxPages: 3 }));
自定义 WASM 路径
若需要从 CDN 或自己的服务器加载 WASM,可以传入 wasmUrl:
ts
import { createEngine } from "clawpdf/browser";
await using engine = await createEngine({
wasmUrl: "/assets/pdfium.esm.wasm",
});
const pdf = await engine.open(file);
自定义实例化
对于特殊环境(如 Service Worker 或 Deno),可提供 instantiateWasm 钩子:
ts
await using engine = await createEngine({
instantiateWasm(imports, receiveInstance) {
// 使用 WebAssembly.instantiate 或自定义加载逻辑
WebAssembly.instantiateStreaming(fetch("/pdfium.wasm"), imports)
.then(result => receiveInstance(result.instance));
},
});
⚠️ 注意 :浏览器中
openPdf的字符串输入必须是 URL,路径字符串(如"/local.pdf")会抛出错误。如需加载本地文件,请使用File或Blob。
错误类型一览
所有公共 API 错误均继承自 PdfError,便于统一捕获。
| 错误类 | 触发条件 |
|---|---|
PdfError |
所有错误的基类 |
PdfPasswordError |
密码缺失或错误 |
PdfFormatError |
输入无效、路径不存在、获取失败、PDF 格式损坏 |
PdfSecurityError |
不支持的 PDF 安全处理器(如某些专有加密) |
PdfPageRangeError |
请求的页码超出文档范围 |
PdfBudgetError |
渲染像素预算或文本字符预算超限 |
PdfDestroyedError |
在文档或引擎销毁后调用方法 |
页面属性
PdfPage 对象暴露了以下只读属性:
| 属性 | 说明 |
|---|---|
index |
页码(从 1 开始) |
width |
页面宽度(PDF 单位,1/72 英寸) |
height |
页面高度(PDF 单位) |
rotation |
页面自带的旋转角度(0, 90, 180, 270) |
服务端部署最佳实践
在 Node.js 生产环境中,建议如下模式:
ts
import { createEngine } from "clawpdf";
// 应用启动时创建引擎(全局单例)
const engine = await createEngine({
maxRenderPixels: 8_000_000, // 根据服务器内存调整
});
// 处理请求的控制器
async function handlePdfRequest(input: Buffer | Uint8Array) {
const pdf = await engine.open(input);
try {
// 尝试提取文本
const text = pdf.text({ maxPages: 10 });
if (text.length >= 200) {
return { text }; // 文本充足,无需渲染图片
}
// 文本不足,回退到图片
const png = await pdf.page(1).png({ dpi: 144 });
return { text, images: [png] };
} finally {
pdf.destroy(); // 必须释放文档内存
}
}
// 优雅关闭时销毁引擎(如果使用 await using 则自动处理)
// 若未使用 await using,需在退出前调用 engine.destroy()
关键点:
- ✅ 全局复用
engine,避免重复加载 WASM。 - ✅ 每个请求打开新文档,处理完后立即
destroy()。 - ✅ 使用
try/finally或await using确保资源释放。 - ✅ 根据可用内存调整
maxRenderPixels,防止 OOM。
API 快速参考
导出函数
ts
export {
createEngine, // 创建引擎实例
encodePng, // 独立 PNG 编码
extractPdf, // 高级提取助手
openPdf, // 快速打开(自管理引擎)
releaseExtractEngine, // 释放 extractPdf 内部缓存的引擎(用于测试)
PdfError,
PdfPasswordError,
PdfFormatError,
PdfSecurityError,
PdfPageRangeError,
PdfBudgetError,
PdfDestroyedError,
PDFIUM_RELEASE, // 当前 PDFium 版本
PDFIUM_WASM_SHA256, // WASM 文件校验和
};
核心类型
PdfInput:Uint8Array | ArrayBuffer | string | URL | BlobPdfEngine:引擎实例PdfDocument:文档实例PdfPage:页面实例RenderOptions:渲染选项ExtractOptions:提取选项ExtractResult:提取结果PdfImage:图片数据
故障排除与性能建议
常见问题
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
PdfFormatError |
输入不是有效的 PDF 文件 | 检查文件是否损坏,或路径是否正确 |
PdfPasswordError |
需要密码但未提供,或密码错误 | 提供正确密码,或确认 PDF 是否加密 |
PdfBudgetError |
渲染像素或文本字符超出限制 | 降低 DPI/缩放,或增加 maxChars / maxPixels |
| 内存占用过高 | 同时打开过多文档未释放 | 确保每个文档处理完后调用 destroy() |
| 浏览器跨域问题 | 通过 fetch 加载 PDF 时跨域 |
配置 CORS 或使用同源 URL |
性能优化小贴士
- 🧠 复用引擎 :服务端环境中,
createEngine开销较大(加载 WASM 约 1~2 秒),务必全局复用。 - 📄 及时销毁文档 :文档对象占用内存包含所有页面数据,处理完立即
destroy()。 - 🖼️ 控制图片输出 :使用
maxPixels和maxDimension防止渲染超大图片(如工程图纸)。 - 📊 文本提取优先 :对于文本型 PDF,优先使用
text()而非渲染,速度更快且内存友好。 - ⏱️ 合理设置超时:远程 PDF 读取默认 30 秒,可根据网络情况调整。