基于 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 一键安装:
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(底层引擎)
📦 PdfEngineWASM 实例 + 资源池
📄 PdfDocumentPDF 文件句柄
📑 PdfPage单页操作
📝 提取文本
🎨 RGBA 位图
🖼️ PNG 图片
- PdfEngine:持有一个 WASM 实例,负责管理内存和资源。它可以打开多个文档,是服务端复用的核心。
- PdfDocument:代表一个已加载的 PDF 文件,提供页面访问、元数据读取和全文提取。
- PdfPage:代表文档中的一页(页码从 1 开始),可渲染为 RGBA 位图或直接编码为 PNG。
两种打开方式
| openPdf(…) | 脚本、一次性任务 | 自动创建私有引擎,文档销毁时引擎一并销毁 |
| createEngine() + engine.open() | 服务器常驻进程、批量处理 | 引擎长期存活,文档需手动销毁(或通过 await using 自动释放) |
一次性使用(推荐):
import { openPdf } from "clawpdf";
await using pdf = await openPdf("report.pdf");
console.log(pdf.pageCount); // 页面总数
// 当离开作用域时,pdf 自动销毁,私有引擎也随之释放
服务器复用引擎:
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() 读取 |
远程请求配置:
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),但大多数场景下足够使用。
单页提取
const firstPageText = pdf.page(1).text();
多页提取
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(在页面自带旋转基础上叠加) |
示例:
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(推荐)
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
const png = pdf.page(1).pngSync({ scale: 2 });
同步版本仅使用存储的 zlib 块(不重新压缩),生成的文件较大,但适用于不允许异步操作的严格场景。
独立编码 RGBA
如果你已有 { rgba, width, height } 数据,可以直接调用 encodePng:
import { encodePng } from "clawpdf";
// 压缩(异步)
const compressed = await encodePng(rgba, { width, height });
// 不压缩(同步)
const stored = encodePng(rgba, { width, height, compress: false });
智能提取回退(extractPdf)
extractPdf 是专门为 AI 应用设计的高级助手函数,它遵循 “先文本,后图片” 的策略,确保你总能获得有意义的内容,无论是纯文本 PDF 还是扫描件。
快速上手
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 | 是否渲染表单控件 |
返回结果结构
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 提供了两个内置适配器,方便你将结果直接用于多模态模型:
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)。
// 文档 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 命令,适合快速测试、脚本集成或一次性转换。
提取文本(默认行为)
clawpdf report.pdf # 输出文本到 stdout
cat report.pdf | clawpdf – # 从管道读取(- 表示 stdin)
JSON 格式输出(含图片 Base64)
clawpdf report.pdf –json
clawpdf extract report.pdf –mode both –pages 1,3-5 –json
JSON 结构同 ExtractResult,其中图片的 bytes 被替换为 base64 字符串。
渲染单页为 PNG
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 文件路径,无需额外配置。
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:
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 钩子:
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 生产环境中,建议如下模式:
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 快速参考
导出函数
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 | Blob
- PdfEngine:引擎实例
- 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 秒,可根据网络情况调整。

![[特殊字符]DeepSeek‑Harness(DSH)小白保姆教程-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260816085112-6a817a009aabf-220x150.png)
