All-in-one PDF read / write / convert / OCR. Trigger scenarios: summarize a PDF, extract text or tables, fill an AcroForm, render pages as images, merge, split, add a watermark, rotate, encrypt/decrypt, create a new PDF (invoice / report / receipt), convert between PDF and docx/md/txt. Typical phrasings: "summarize this PDF", "fill out this PDF form", "merge these PDFs", "add a watermark to the PDF", "convert PDF to Word", "OCR this scan", "show me what page 3 of the PDF looks like". Load this skill whenever the user mentions a .pdf file, needs to produce a PDF, or wants to make any change to a PDF.
74
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Read with PdfInspect; all write / modify / create operations use JsSandbox to run Node scripts via pdf-lib / pdfkit / pdf-parse and similar pre-installed libraries; format conversion uses DocConvert; scanned PDFs go through CloudImageUnderstand.
| Tool | Responsibility | Read-only |
|---|---|---|
PdfInspect | Read: summary / text / tables / form-fields / form-structure / images / annotations / render | Yes |
JsSandbox | All write operations: create new PDF, merge, watermark, rotate, fill forms, decrypt… implemented with pdf-lib / pdfkit code | No |
DocConvert | Format conversion: pdf ↔ docx / html / md / txt / xlsx | No |
CloudImageUnderstand | OCR entry point for scanned PDFs (cloud, consumes credits) | No |
Loading (two steps, both required):
LoadSkill pdf-skill— only pulls this skill doc into context.ToolSearch(query: "select:PdfInspect,JsSandbox,DocConvert")— activates the tool schemas.
Readon a .pdf gives only a lightweight summary; any analysis / summarization / fill / edit / convert / OCR of a PDF must go throughPdfInspect+JsSandbox.
PdfInspect is the only entry pointWhen you don't know what the PDF looks like, run summary first:
PdfInspect { action: "summary", filePath: "…" }Returns pageCount / fileSize / characterCount / hasForm / encryption / textType. Branch on textType:
| textType | Next step |
|---|---|
extractable | PdfInspect(text) — direct full-text extraction |
scanned / cid-encoded | PdfInspect(render, pageRange) → CloudImageUnderstand on each PNG for OCR |
isEncrypted: true | Decrypt with JsSandbox + pdf-lib, or call PdfInspect(password) |
Large PDFs must pass pageRange (e.g. "1-5"), otherwise output will be truncated.
AcroForm forms: call PdfInspect(form-fields) to get checkedValue / radioOptions before filling.
JsSandbox + pdf-lib / pdfkitPrefer pdf-lib (edit existing PDFs / merge / watermark / rotate / fill forms / decrypt);
use pdfkit from scratch when you need complex layouts (vector + text + images).
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()
// Simple table
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 note: pdfkit's default fonts have no CJK glyphs. For Chinese output, call
doc.registerFont('cn', '/path/to/NotoSansCJK.ttf')first, thendoc.font('cn'). System fallback fonts are not guaranteed to cover CJK.
CONFIDENTIAL watermark to an existing PDF (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) first to get 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('John Smith')
form.getCheckBox('agree').check() // use setValue('Y') if PdfInspect gives checkedValue='Y'
form.getRadioGroup('gender').select('M')
// form.flatten() // call this to lock the form
await fs.writeFile('filled.pdf', await pdf.save())
console.log('form filled')// Decrypt
import { PDFDocument } from 'pdf-lib'
const pdf = await PDFDocument.load(buf, { password: 'secret', ignoreEncryption: false })
await fs.writeFile('unlocked.pdf', await pdf.save())
// Rotate page 1 by 90°
pdf.getPage(0).setRotation(degrees(90))
// Extract pages 3 and 5 into a new 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) // yields [<asset>/render-p1.png, ...]
CloudImageUnderstand(image=<each png>, question='Extract all text accurately')DocConvert(from="pdf", to="docx", sourcePath="…") # PDF → Word for editing
DocConvert(from="docx", to="pdf", sourcePath="…") # Word → PDF for distributionDocConvert is faster and better at preserving layout than JsSandbox + pdf-lib; prefer it for format conversion.
| Symptom | Cause | Fix |
|---|---|---|
Garbled / square characters with pdfkit | Default font has no CJK glyphs | Call registerFont('cn', '/path/to/NotoSansCJK.ttf') first |
pdf-lib throws EncryptedPDFError on load | File is encrypted | PDFDocument.load(buf, { password: '…' }) |
| Watermark uneven across pages | Pages have different sizes | Loop page.getSize() and compute x/y per page |
| Merged PDF has font rendering differences | Source PDFs have incomplete embedded fonts | Rewrite with pdf-lib's useObjectStreams: false |
TypeError: Cannot read properties of undefined (reading 'save') | pdfkit has no doc.save() / doc.restore() (those are Canvas 2D APIs) | Switch colors / sizes directly via doc.fillColor() / doc.fontSize() — no save/restore stack needed. Finish the document with doc.pipe(fs.createWriteStream(...)); doc.end() |
To patch a script: JsSandbox(action="edit-and-run", scriptPath=<previous path>, edits=[{find,replace}]) — only pass the diff.
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.