PDF 读/写/转/OCR 一体化。触发场景:总结 PDF、读文本或表格、填 AcroForm 表单、渲染页面为图、合并/拆分/水印/旋转/加密、创建新 PDF(发票/报告)、PDF ↔ docx/md/txt 互转。典型说法:“总结这份 PDF”、“填一下这个 PDF 表单”、“把几个 PDF 合一起”、“PDF 加水印”、“PDF 转 Word”、“扫描件 OCR”、“看看 PDF 第 3 页长什么样”。用户提到 .pdf 文件、要产出 PDF、或要对 PDF 做任何改动都加载本技能。
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
读用 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。
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 再填。
JsSandbox + pdf-lib / pdfkit首选 pdf-lib(编辑已有 PDF / 合并 / 水印 / 旋转 / 填表 / 解密);
从零生成复杂布局用 pdfkit(矢量 + 文字 + 图)。
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 字体不保证覆盖。
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`)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`)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() // 若 PdfInspect 给出 checkedValue='Y',用 setValue('Y')
form.getRadioGroup('gender').select('M')
// form.flatten() // 想锁表单就 flatten
await fs.writeFile('filled.pdf', await pdf.save())
console.log('form filled')// 解密
import { PDFDocument } from 'pdf-lib'
const pdf = await PDFDocument.load(buf, { password: 'secret', ignoreEncryption: false })
await fs.writeFile('unlocked.pdf', await pdf.save())
// 旋转第 1 页 90°
pdf.getPage(0).setRotation(degrees(90))
// 抽出第 3、5 页到新 PDF
const out = await PDFDocument.create()
const [p3, p5] = await out.copyPages(pdf, [2, 4])
out.addPage(p3); out.addPage(p5)PdfInspect(summary) // textType == 'scanned'
PdfInspect(render, pageRange) // 得到 [<asset>/render-p1.png, ...]
CloudImageUnderstand(image=<上面每张 png>, question='精确识别并提取中文文字')DocConvert(from="pdf", to="docx", sourcePath="…") # PDF → Word 编辑
DocConvert(from="docx", to="pdf", sourcePath="…") # Word → PDF 分发DocConvert 比 JsSandbox + pdf-lib 快且排版忠实;互转优先它。
| 症状 | 原因 | 处理 |
|---|---|---|
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}]) 只传改动点。
itsablabla/openloaf-web
a1ab5be
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.