| name | pdf-skill |
| description | PDF 读/写/转/OCR 一体化。触发场景:总结 PDF、读文本或表格、填 AcroForm 表单、渲染页面为图、合并/拆分/水印/旋转/加密、创建新 PDF(发票/报告)、PDF ↔ docx/md/txt 互转。典型说法:“总结这份 PDF”、“填一下这个 PDF 表单”、“把几个 PDF 合一起”、“PDF 加水印”、“PDF 转 Word”、“扫描件 OCR”、“看看 PDF 第 3 页长什么样”。用户提到 .pdf 文件、要产出 PDF、或要对 PDF 做任何改动都加载本技能。
|
PDF 技能
读用 PdfInspect;写 / 改 / 创建一律用 JsSandbox 跑 Node 脚本调用 pdf-lib / pdfkit / pdf-parse 等预装库;格式互转用 DocConvert;扫描件走 CloudImageUnderstand。
工具清单
| 工具 | 做什么 | 只读 |
|---|
PdfInspect | 读:summary / text / tables / form-fields / form-structure / images / annotations / render | 是 |
JsSandbox | 所有写操作:创建新 PDF、合并、水印、旋转、填表、加密…… 用 pdf-lib / pdfkit 代码实现 | 否 |
DocConvert | 格式互转:pdf ↔ docx / html / md / txt / xlsx | 否 |
CloudImageUnderstand | 扫描 PDF 的 OCR 入口(云端,会扣积分) | 否 |
加载(两步,缺一不可):
LoadSkill pdf-skill —— 仅把本技能文档拉进上下文。
ToolSearch(query: "select:PdfInspect,JsSandbox,DocConvert") —— 激活工具 schema。
Read 对 .pdf 只走轻量摘要;任何分析/总结/填/改/转/OCR PDF 都走 PdfInspect + JsSandbox。
1. 读:PdfInspect 是唯一入口
不知道 PDF 长啥样先 summary:
PdfInspect { action: "summary", filePath: "…" }
返回 pageCount / fileSize / characterCount / hasForm / encryption / textType,按 textType:
| textType | 下一步 |
|---|
extractable | PdfInspect(text) 直接取全文 |
scanned / cid-encoded | PdfInspect(render, pageRange) → 再调 CloudImageUnderstand 对每张 PNG OCR |
isEncrypted: true | 走下面 JsSandbox pdf-lib 解密,或 PdfInspect(password) |
大 PDF 必传 pageRange(如 "1-5"),否则触发截断。
AcroForm 表单:PdfInspect(form-fields) 拿 checkedValue / radioOptions 再填。
2. 写:JsSandbox + pdf-lib / pdfkit
首选 pdf-lib(编辑已有 PDF / 合并 / 水印 / 旋转 / 填表 / 解密);
从零生成复杂布局用 pdfkit(矢量 + 文字 + 图)。
2.1 Demo:创建一份中文发票 PDF(pdfkit)
import PDFDocument from 'pdfkit'
import fs from 'node:fs'
const doc = new PDFDocument({ size: 'A4', margin: 48 })
doc.pipe(fs.createWriteStream('invoice.pdf'))
doc.fontSize(22).text('INVOICE', { align: 'right' })
doc.moveDown()
doc.fontSize(10).text('Acme Corp', { align: 'right' })
doc.text('Invoice #2026-042 Date: 2026-04-20', { align: 'right' })
doc.moveDown()
const rows = [
['Widget', 1, 50.00],
['Gadget', 2, 30.00],
]
doc.fontSize(12).text('Description Qty Unit Total')
doc.moveTo(48, doc.y).lineTo(547, doc.y).stroke()
let total = 0
for (const [name, qty, unit] of rows) {
const line = qty * unit
total += line
doc.text(`${name.padEnd(14)} ${qty} $${unit.toFixed(2)} $${line.toFixed(2)}`)
}
doc.moveDown().fontSize(14).text(`Total: $${total.toFixed(2)}`, { align: 'right' })
doc.end()
await new Promise(r => doc.on('end', r))
console.log('invoice.pdf written')
CJK 提示:pdfkit 默认字体不含中文字形。若要出中文,先用 doc.registerFont('cn', '/path/to/NotoSansCJK.ttf') 再 doc.font('cn')。系统 fallback 字体不保证覆盖。
2.2 Demo:给已有 PDF 加 CONFIDENTIAL 水印(pdf-lib)
import { PDFDocument, rgb, degrees, StandardFonts } from 'pdf-lib'
import fs from 'node:fs/promises'
const src = await fs.readFile(process.argv[2] ?? 'input.pdf')
const pdf = await PDFDocument.load(src)
const font = await pdf.embedFont(StandardFonts.HelveticaBold)
for (const page of pdf.getPages()) {
const { width, height } = page.getSize()
page.drawText('CONFIDENTIAL', {
x: width / 2 - 140,
y: height / 2,
size: 64,
font,
color: rgb(0.9, 0.1, 0.1),
opacity: 0.25,
rotate: degrees(-30),
})
}
const out = await pdf.save()
await fs.writeFile('watermarked.pdf', out)
console.log(`watermarked ${pdf.getPageCount()} pages`)
2.3 Demo:合并 PDF(pdf-lib)
import { PDFDocument } from 'pdf-lib'
import fs from 'node:fs/promises'
const merged = await PDFDocument.create()
for (const p of ['a.pdf', 'b.pdf', 'c.pdf']) {
const src = await PDFDocument.load(await fs.readFile(p))
const pages = await merged.copyPages(src, src.getPageIndices())
pages.forEach(pg => merged.addPage(pg))
}
await fs.writeFile('merged.pdf', await merged.save())
console.log(`merged → ${merged.getPageCount()} pages`)
2.4 Demo:填 AcroForm 表单(先 PdfInspect(form-fields) 拿 checkedValue)
import { PDFDocument } from 'pdf-lib'
import fs from 'node:fs/promises'
const pdf = await PDFDocument.load(await fs.readFile('form.pdf'))
const form = pdf.getForm()
form.getTextField('full_name').setText('张三')
form.getCheckBox('agree').check()
form.getRadioGroup('gender').select('M')
await fs.writeFile('filled.pdf', await pdf.save())
console.log('form filled')
2.5 Demo:解密 / 旋转 / 抽页
import { PDFDocument } from 'pdf-lib'
const pdf = await PDFDocument.load(buf, { password: 'secret', ignoreEncryption: false })
await fs.writeFile('unlocked.pdf', await pdf.save())
pdf.getPage(0).setRotation(degrees(90))
const out = await PDFDocument.create()
const [p3, p5] = await out.copyPages(pdf, [2, 4])
out.addPage(p3); out.addPage(p5)
3. OCR(扫描件)
PdfInspect(summary) // textType == 'scanned'
PdfInspect(render, pageRange) // 得到 [<asset>/render-p1.png, ...]
CloudImageUnderstand(image=<上面每张 png>, question='精确识别并提取中文文字')
4. 格式互转
DocConvert(from="pdf", to="docx", sourcePath="…") # PDF → Word 编辑
DocConvert(from="docx", to="pdf", sourcePath="…") # Word → PDF 分发
DocConvert 比 JsSandbox + pdf-lib 快且排版忠实;互转优先它。
5. 常见错误兜底
| 症状 | 原因 | 处理 |
|---|
pdfkit 中文乱码 / 方块 | 默认字体不含 CJK 字形 | 先 registerFont('cn', '/path/to/NotoSansCJK.ttf') |
pdf-lib 加载报 EncryptedPDFError | 文件加密 | PDFDocument.load(buf, { password: '…' }) |
| 水印覆盖不均 | PDF 页面大小不同 | 循环 page.getSize() 按页算 x/y |
| 合并后字体变样 | 原 PDF 嵌字体不完整 | 用 pdf-lib 的 useObjectStreams: false 重写 |
TypeError: Cannot read properties of undefined (reading 'save') | pdfkit 没有 doc.save() / doc.restore()(那是 Canvas 2D API) | 颜色/字号状态直接用 doc.fillColor() / doc.fontSize() 切换即可,不需要保存-恢复栈。最终文档落盘用 doc.pipe(fs.createWriteStream(...)); doc.end() |
修脚本:JsSandbox(action="edit-and-run", scriptPath=<上次的>, edits=[{find,replace}]) 只传改动点。