mcpField
D
document-generation-pdf

document-generation-pdf

github.com/curiositech文档处理257273 次安装2 天前更新
代码仓库

关于 document-generation-pdf

Document Generation & PDF Automation 面向需要在法律、人力资源和业务流程中自动生成、填写及组装 PDF 的开发者,涵盖技术选型、实现方法和生产检查。

主要功能

  1. 01PDF 库选型指导
  2. 02以程序填写表单
  3. 03HTML 转 PDF 实现方法
  4. 04合并 PDF 并组装文档包
  5. 05生产与数据处理检查清单

适用场景

  1. 01批量自动填写政府表格
  2. 02生成发票或证书
  3. 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.json
references/
document-assembly.md
pdf-lib-guide.md
puppeteer-templates.md
scripts/
document_assembler.ts
form_filler.ts

详细介绍

文档生成与 PDF 自动化

擅长以编程方式生成、填写和组装 PDF 文档,用于法律、人力资源和业务工作流。

适用场景

✅ 适用于:

  • 法律表单自动化(记录封存、移民、合同)
  • 大规模生成发票/收据
  • 创建证书(结业、参与、获奖)
  • 根据模板组装合同
  • 填写政府表单(IRS、法院文件)
  • 创建多文档材料包

❌ 不适用于:

  • 简单的 PDF 查看(使用浏览器或 pdf.js)
  • 基本文件转换(使用在线工具)
  • OCR 文本提取(使用 Tesseract.js 或 AWS Textract)
  • 手动编辑 PDF(使用 Adobe Acrobat)

技术选择

pdf-lib 与 Puppeteer 和 LaTeX 的比较
特性pdf-libPuppeteerLaTeX
表单填写✅ 原生支持❌ 复杂❌ 不支持
模板渲染❌ 不支持✅ HTML/CSS✅ 支持模板
性能(1000 个 PDF)5s60s30s
文件大小小中小
签名字段✅ 支持❌ 不支持❌ 不支持
最适合政府表单发票、报告学术论文

时间线:

  • 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 (&lt;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 叠加 | 坐标映射

评论

支持 Markdown