关于 document-generation-pdf
Document Generation & PDF Automation 面向需要在法律、人力资源和业务流程中自动生成、填写及组装 PDF 的开发者,涵盖技术选型、实现方法和生产检查。
主要功能
- 01PDF 库选型指导
- 02以程序填写表单
- 03HTML 转 PDF 实现方法
- 04合并 PDF 并组装文档包
- 05生产与数据处理检查清单
适用场景
- 01批量自动填写政府表格
- 02生成发票或证书
- 03组装多份文档的资料包
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill document-generation-pdf安装后在 Claude Code 中输入/document-generation-pdf确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.claude-plugin/plugin.jsonreferences/document-assembly.mdpdf-lib-guide.mdpuppeteer-templates.mdscripts/document_assembler.tsform_filler.ts详细介绍
文档生成与 PDF 自动化
擅长以编程方式生成、填写和组装 PDF 文档,用于法律、人力资源和业务工作流。
适用场景
✅ 适用于:
- 法律表单自动化(记录封存、移民、合同)
- 大规模生成发票/收据
- 创建证书(结业、参与、获奖)
- 根据模板组装合同
- 填写政府表单(IRS、法院文件)
- 创建多文档材料包
❌ 不适用于:
- 简单的 PDF 查看(使用浏览器或 pdf.js)
- 基本文件转换(使用在线工具)
- OCR 文本提取(使用 Tesseract.js 或 AWS Textract)
- 手动编辑 PDF(使用 Adobe Acrobat)
技术选择
pdf-lib 与 Puppeteer 和 LaTeX 的比较
| 特性 | pdf-lib | Puppeteer | LaTeX |
|---|---|---|---|
| 表单填写 | ✅ 原生支持 | ❌ 复杂 | ❌ 不支持 |
| 模板渲染 | ❌ 不支持 | ✅ HTML/CSS | ✅ 支持模板 |
| 性能(1000 个 PDF) | 5s | 60s | 30s |
| 文件大小 | 小 | 中 | 小 |
| 签名字段 | ✅ 支持 | ❌ 不支持 | ❌ 不支持 |
| 最适合 | 政府表单 | 发票、报告 | 学术论文 |
时间线:
- 2000 年代:使用 LaTeX 编写学术文档
- 2010:使用 PDFKit(Node.js)生成文档
- 2017:使用 Puppeteer 将 HTML 转换为 PDF
- 2019:使用 pdf-lib 进行纯 JS 表单填写
- 2024:pdf-lib 成为表单处理的黄金标准
决策树:
Need to fill existing form? → pdf-lib
Need complex layouts? → Puppeteer (HTML/CSS)
Need academic formatting? → LaTeX
Need to merge PDFs? → pdf-lib
Need digital signatures? → pdf-lib + DocuSign API
常见反模式
反模式 1:用 Puppeteer 填写简单表单
新手想法:“我会用 Puppeteer 处理所有事情,它用途广泛。”
问题:速度慢 12 倍,内存占用高 10 倍,且无法保留表单字段。
错误做法:
// ❌ Puppeteer for simple form filling (SLOW!)
import puppeteer from 'puppeteer';
async function fillForm(data: FormData): Promise<Buffer> {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Load PDF in browser
await page.goto(`file://${pdfPath}`);
// Somehow fill form fields? (hacky)
await page.evaluate((data) => {
// Can't easily access PDF form fields from DOM
// Would need to convert PDF → HTML first
}, data);
const pdf = await page.pdf();
await browser.close();
return pdf;
}
错误原因:
- 浏览器开销(每个实例占用 200MB 以上 RAM)
- 无法访问原生 PDF 表单字段
- 丢失交互式表单功能
- 速度比 pdf-lib 慢 12 倍
正确做法:
// ✅ pdf-lib for form filling (FAST!)
import { PDFDocument } from 'pdf-lib';
async function fillForm(templatePath: string, data: FormData): Promise<Uint8Array> {
// Load existing PDF form
const existingPdfBytes = await fs.readFile(templatePath);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Get form
const form = pdfDoc.getForm();
// Fill text fields
form.getTextField('applicant_name').setText(data.name);
form.getTextField('case_number').setText(data.caseNumber);
form.getTextField('date_of_birth').setText(data.dob);
// Fill checkboxes
if (data.hasPriorConvictions) {
form.getCheckBox('prior_convictions').check();
}
// Fill dropdowns
form.getDropdown('state').select(data.state);
// Flatten form (make non-editable)
form.flatten();
// Save
return await pdfDoc.save();
}
性能对比(1000 个 PDF):
- Puppeteer:60 秒,4GB RAM
- pdf-lib:5 秒,200MB RAM
背景时间线:
- 2017:Puppeteer 发布,所有人都用它处理 PDF
- 2019:pdf-lib 发布,提供完善的表单处理
- 2024:pdf-lib 成为政府表单的标准
反模式 2:不扁平化表单
问题:用户可以编辑已填写的表单,导致数据不一致。
错误做法:
// ❌ Don't flatten - form stays editable
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const form = pdfDoc.getForm();
form.getTextField('name').setText('John Doe');
const pdfBytes = await pdfDoc.save();
// User can open PDF and change "John Doe" to anything!
错误原因:
- 用户可以修改正式文档
- 数据与数据库不一致
- 违反文档完整性要求
正确做法:
// ✅ Flatten form after filling
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const form = pdfDoc.getForm();
form.getTextField('name').setText('John Doe');
// Flatten (convert fields to static text)
form.flatten();
const pdfBytes = await pdfDoc.save();
// User can't edit filled values ✅
不应扁平化的情况:
- 草稿文档(用户需要审核/编辑)
- 多步骤工作流(部分完成)
- 供用户手动填写的模板
反模式 3:从 HTML 生成 PDF 时不设置分页符
新手想法:“用 Puppeteer 把 HTML 转换为 PDF 很简单。”
问题:内容会在句子中间跨页拆分。
错误做法:
// ❌ No page break control
const html = `
<div class="contract">
<h1>Mutual Agreement</h1>
<p>Long paragraph that might split across pages...</p>
<section>
<h2>Terms and Conditions</h2>
<ol>
<li>Term 1 that could get cut off...</li>
<li>Term 2...</li>
</ol>
</section>
</div>
`;
const pdf = await page.pdf({ format: 'A4' });
// Result: Ugly page breaks in middle of sections
正确做法:
// ✅ Explicit page break control with CSS
const html = `
<style>
@media print {
.page-break { page-break-after: always; }
.avoid-break { page-break-inside: avoid; }
h1, h2, h3 {
page-break-after: avoid;
page-break-inside: avoid;
}
section {
page-break-inside: avoid;
}
}
</style>
<div class="contract">
<section class="avoid-break">
<h1>Mutual Agreement</h1>
<p>This entire section stays together...</p>
</section>
<div class="page-break"></div>
<section class="avoid-break">
<h2>Terms and Conditions</h2>
<ol>
<li>Term 1</li>
<li>Term 2</li>
</ol>
</section>
</div>
`;
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '1in',
right: '1in',
bottom: '1in',
left: '1in'
}
});
CSS 打印属性:
page-break-before: always- 在元素前强制分页page-break-after: always- 在元素后强制分页page-break-inside: avoid- 避免拆分元素
反模式 4:不处理签名字段
问题:生成的 PDF 中的签名字段无法点击。
错误做法:
// ❌ Add signature as image (not a real signature field)
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const signatureImage = await pdfDoc.embedPng(signaturePngBytes);
const page = pdfDoc.getPage(0);
page.drawImage(signatureImage, {
x: 100,
y: 100,
width: 200,
height: 50
});
await pdfDoc.save();
// Not a real signature field - can't be signed digitally
错误原因:
- 不具备法律认可效力(仅为图像)
- 无法使用 DocuSign/Adobe Sign
- 没有签名元数据(签署人、时间)
正确做法 1:创建签名字段(用于 DocuSign)
// ✅ Create signature field for electronic signing
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const form = pdfDoc.getForm();
// Create signature field
const signatureField = form.createTextField('applicant_signature');
signatureField.addToPage(pdfDoc.getPage(0), {
x: 100,
y: 100,
width: 200,
height: 50
});
// Mark as signature field (metadata)
signatureField.updateWidgets({
borderWidth: 1,
borderColor: { type: 'RGB', red: 0, green: 0, blue: 0 }
});
await pdfDoc.save();
// DocuSign can now detect and fill this field ✅
正确做法 2:集成 DocuSign API
// ✅ Send to DocuSign for e-signature
import { ApiClient, EnvelopesApi } from 'docusign-esign';
async function sendForSignature(pdfBytes: Uint8Array, signerEmail: string) {
const apiClient = new ApiClient();
apiClient.setBasePath('https://demo.docusign.net/restapi');
const envelopesApi = new EnvelopesApi(apiClient);
const envelope = {
emailSubject: 'Please sign: Expungement Petition',
documents: [{
documentBase64: Buffer.from(pdfBytes).toString('base64'),
name: 'Petition.pdf',
fileExtension: 'pdf',
documentId: '1'
}],
recipients: {
signers: [{
email: signerEmail,
name: 'John Doe',
recipientId: '1',
tabs: {
signHereTabs: [{
documentId: '1',
pageNumber: '1',
xPosition: '100',
yPosition: '100'
}]
}
}]
},
status: 'sent'
};
return await envelopesApi.createEnvelope(accountId, { envelopeDefinition: envelope });
}
反模式 5:不加密存储已填写的 PDF
问题:敏感法律文档以明文形式存储。
错误做法:
// ❌ Save PDF to disk unencrypted
const pdfBytes = await pdfDoc.save();
await fs.writeFile(`./documents/${caseId}.pdf`, pdfBytes);
// Sensitive data accessible to anyone with file system access
错误原因:
- 法律/医疗数据暴露
- 违反 HIPAA/GDPR
- 数据泄露责任风险
正确做法 1:静态加密
// ✅ Encrypt PDF with user password
const pdfBytes = await pdfDoc.save({
userPassword: generateSecurePassword(),
ownerPassword: process.env.PDF_OWNER_PASSWORD,
permissions: {
printing: 'highResolution',
modifying: false,
copying: false,
annotating: false,
fillingForms: false,
contentAccessibility: true,
documentAssembly: false
}
});
await fs.writeFile(`./documents/${caseId}.pdf`, pdfBytes);
正确做法 2:存储在加密存储中(启用 SSE 的 S3)
// ✅ Upload to S3 with server-side encryption
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3Client = new S3Client({ region: 'us-east-1' });
await s3Client.send(new PutObjectCommand({
Bucket: 'expungement-documents',
Key: `cases/${caseId}/petition.pdf`,
Body: pdfBytes,
ServerSideEncryption: 'AES256',
Metadata: {
caseId: caseId,
documentType: 'petition',
generatedAt: new Date().toISOString()
},
ACL: 'private' // Not publicly accessible
}));
// Store S3 URL in database (encrypted)
await db.documents.insert({
case_id: caseId,
s3_url: encrypt(`s3://expungement-documents/cases/${caseId}/petition.pdf`),
created_at: new Date()
});
合规要求:
- HIPAA:静态和传输中加密、访问日志记录
- GDPR:删除权、数据最小化
- 州律师协会规则:保护律师与客户之间的保密特权
生产检查清单
□ Form fields filled correctly (test with validation)
□ Forms flattened after filling (non-editable)
□ Page breaks controlled (no mid-sentence splits)
□ Signature fields created (for DocuSign/Adobe Sign)
□ PDFs encrypted at rest (S3 SSE or user password)
□ Access logged (who viewed/downloaded)
□ Auto-deletion scheduled (retention policy)
□ Fonts embedded (cross-platform compatibility)
□ File size optimized (<5MB per document)
□ Batch generation tested (1000+ PDFs)
何时使用与避免使用
| 场景 | 是否适用? |
|---|---|
| 填写 50 多份政府表格 | ✅ 是——使用 pdf-lib 自动处理 |
| 根据模板生成发票 | ✅ 是——使用 Puppeteer 从 HTML 生成 |
| 批量创建证书 | ✅ 是——使用 pdf-lib 或 LaTeX |
| 组装多文档材料包 | ✅ 是——使用 pdf-lib 合并 |
| 在浏览器中查看 PDF | ❌ 否——使用 pdf.js 或浏览器 |
| 手动编辑 PDF | ❌ 否——使用 Adobe Acrobat |
| 使用 OCR 提取文本 | ❌ 否——使用 Tesseract/Textract |
大规模叠加扁平 PDF(政府表格)
许多政府法院表格都是扁平 PDF(没有可填写字段)。你必须在精确的 X、Y 坐标处叠加文本。切勿从头生成表格——法院会拒收。
三层策略
1. FILLABLE PDF → Use form.getTextField().setText() (best)
2. FLAT PDF + COORDINATES → Draw text overlays at X,Y positions (this section)
3. NO TEMPLATE EXISTS → Emergency fallback with warning banner (worst)
反模式 6:从头生成表格
新手想法:“PDF 没有表单字段,我直接创建一个新 PDF 就行了。”
问题:法院会拒收非官方表格。自定义版式无法与官方文档匹配。
错误做法:
// ❌ NEVER DO THIS - courts reject custom forms
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([612, 792]);
page.drawText('IN THE CIRCUIT COURT OF...', { x: 200, y: 720 });
page.drawText(`Defendant: ${data.name}`, { x: 100, y: 600 });
// Result: Court clerk says "This isn't our form" and rejects filing
正确做法:在官方模板上绘制
// ✅ Load official form, draw text in blank spaces
const templateBytes = await fs.readFile('public/forms/OR-set-aside-motion.pdf');
const pdfDoc = await PDFDocument.load(templateBytes);
const page = pdfDoc.getPage(3); // Page 4 is the form
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
// Draw in blank spaces at measured coordinates
page.drawText(data.county.toUpperCase(), { x: 310, y: 700, size: 11, font });
page.drawText(data.caseNumber, { x: 432, y: 655, size: 10, font });
page.drawText(data.fullName, { x: 72, y: 568, size: 11, font });
大规模测量坐标
问题:手动测量 50 个州 × 10 多种表格的 X、Y 坐标——根本不可能。
解决方案 1:可视化坐标拾取工具
使用 pdf-coordinates——点击 PDF 即可捕获 X、Y 坐标:
- 上传 PDF,点击空白字段
- 选择坐标系:左下角(PDF 标准)
- 导出带坐标标签的注释版 PDF
- 可离线使用,无需上传数据
解决方案 2:JSON 配置(Zerodha 模式)
在 JSON 配置中定义坐标,而不是写在代码中:
// field-mappings/oregon-set-aside.json
{
"templatePath": "/forms/or/OR-criminal-set-aside.pdf",
"pageIndex": 3,
"fields": [
{
"name": "county",
"x": 310,
"y": 700,
"fontSize": 11,
"maxWidth": 120,
"transform": "uppercase"
},
{
"name": "caseNumber",
"x": 432,
"y": 655,
"fontSize": 10,
"maxWidth": 100,
"maxChars": 12
},
{
"name": "fullName",
"x": 72,
"y": 568,
"fontSize": 11,
"maxWidth": 250,
"shrinkToFit": true
}
]
}
解决方案 3:调试网格叠加
在开发期间添加临时网格:
async function drawDebugGrid(page: PDFPage, font: PDFFont) {
const { width, height } = page.getSize();
// Draw grid every 50 points
for (let x = 0; x <= width; x += 50) {
page.drawLine({
start: { x, y: 0 },
end: { x, y: height },
thickness: 0.2,
color: rgb(0.9, 0.9, 0.9),
});
if (x % 100 === 0) {
page.drawText(String(x), { x: x + 2, y: 5, size: 6, font });
}
}
// Same for horizontal lines...
}
处理文本溢出
问题:过长的姓名会溢出到相邻字段,或被截断。
三种溢出处理策略:
// 1. Truncate with ellipsis
function truncate(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
return text.slice(0, maxChars - 2) + '..';
}
// 2. Shrink font to fit (min 7pt for readability)
function shrinkToFit(text: string, maxWidthPts: number, startSize: number): number {
let fontSize = startSize;
const charWidth = (size: number) => size * 0.52; // Helvetica avg
while (fontSize > 7 && text.length * charWidth(fontSize) > maxWidthPts) {
fontSize -= 0.5;
}
return fontSize;
}
// 3. Multi-line wrap (for addresses)
page.drawText(longAddress, {
x: 100,
y: 500,
size: 9,
font,
maxWidth: 200, // pdf-lib auto-wraps at this width
lineHeight: 12,
});
字段输入限制——在数据收集阶段防止溢出:
export const FIELD_CONSTRAINTS = {
fullName: { maxLength: 35 },
county: { maxLength: 12 },
caseNumber: { maxLength: 12 },
agency: { maxLength: 28 },
charges: { maxLength: 45 },
} as const;
多州表格的生产工作流
1. DOWNLOAD official forms
└─ Store in /public/forms/{state}/ with metadata.json
2. INSPECT each form
└─ Run pdf-lib inspection to check: fillable? how many fields?
3. CATEGORIZE forms
├─ Fillable → Map field names in field-mappings/{state}.ts
└─ Flat → Measure coordinates with pdf-coordinates tool
4. CREATE JSON config for flat PDFs
└─ One JSON file per form with all X,Y coordinates
5. TEST with debug grid
└─ Generate test PDF with colored text + coordinate labels
6. QA visual verification
└─ Compare filled PDF against original blank form
7. DOCUMENT constraints
└─ Export maxLength/maxWidth limits for form inputs
法律文书提交要求
法院电子提交系统要求:
- ✅ 扁平化表格(没有交互式字段)
- ✅ 不含数字签名元数据(DocuSign 会导致电子提交失败)
- ✅ 嵌入标准字体(Helvetica、Times)
- ✅ 文件小于 10MB
- ✅ 与官方表格版式完全一致
供法院使用的扁平化处理:
// If form had fields, flatten them
const form = pdfDoc.getForm();
if (form.getFields().length > 0) {
form.flatten(); // Converts to static text
}
// For overlay text, it's already static - no flattening needed
const pdfBytes = await pdfDoc.save();
坐标系参考
PDF coordinate system (pdf-lib default):
- Origin: Bottom-left corner (0, 0)
- X increases: Left → Right
- Y increases: Bottom → Top
- Units: Points (72 points = 1 inch)
- Letter size: 612 x 792 points (8.5" x 11")
To convert from top-left origin (e.g., Adobe Acrobat display):
y_pdf = 792 - y_topLeft
Common positions (Letter size):
- Top margin: y = 720-750
- Header area: y = 700-750
- Body start: y = 650-700
- Left margin: x = 50-72
- Right edge: x = 540-560
- Bottom margin: y = 50-72
参考资料
/references/pdf-lib-guide.md- 填写表格、字段类型、扁平化、加密/references/puppeteer-templates.md- HTML 模板、分页、打印样式/references/document-assembly.md- 合并 PDF、创建材料包、添加水印- pdf-coordinates - 可视化 X、Y 坐标拾取工具
- Zerodha pdf_text_overlay - JSON 配置模式
脚本
scripts/form_filler.ts- 根据 JSON 数据填写 PDF 表格,批量处理scripts/document_assembler.ts- 合并多个 PDF、添加封面和水印scripts/generate-test-overlay-pdf.ts- 使用调试网格测试叠加坐标
此技能适用于:PDF 生成 | 表格填写 | 文档自动化 | 数字签名 | pdf-lib | Puppeteer | LaTeX | DocuSign | 扁平 PDF 叠加 | 坐标映射