[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71HWVQ1BMYVYEDFYA581P1T":3,"skill-same:01M71HWVQ1BMYVYEDFYA581P1T":126},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":36,"publishedAt":57,"linkOnly":58,"markdownContent":59,"markdownBody":60,"frontMatter":61,"refName":81,"commitSha":82,"skillDirectory":83,"forksCount":84,"repositoryHtmlUrl":85,"repositoryHomepageUrl":6,"ownerHtmlUrl":86,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":87,"install":88,"resources":101},"01M71HWVQ1BMYVYEDFYA581P1T","document-generation-pdf",null,"Generate, fill, and assemble PDF documents at scale. Handles legal forms, contracts, invoices, certificates. Supports form filling (pdf-lib), template rendering (Puppeteer, LaTeX), digital signatures (DocuSign), and document assembly. Use for legal tech, HR automation, invoice generation. Activate on \"PDF generation\", \"form filling\", \"document automation\", \"digital signatures\". NOT for simple PDF viewing, basic file conversion, or OCR text extraction.","JSGforever\u002Fskillguard","JSGforever","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F85541948?v=4","01M71FQD3E132VMR5F4TAYX3YS","01M71FQD4DYW0XJ4XQ4YQH75Y9",2,"GPL-3.0","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf\u002FSKILL.md",[17,22,25,30,33],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000002","文档处理","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":26,"categoryName":27,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",{"categoryNo":31,"categoryName":32,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000037","律师与法务",{"categoryNo":34,"categoryName":35,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000038","人力资源",[37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56],"document generation","document","generation","document generation pdf","PDF","write","read","at","2026","lib","generate","form-filling","text","activate","documents","writing","signatures","court","expert","writer","2026-11-10 10:12:30",false,"---\nname: document-generation-pdf\ndescription: Generate, fill, and assemble PDF documents at scale. Handles legal forms, contracts, invoices, certificates. Supports form filling (pdf-lib), template rendering (Puppeteer, LaTeX), digital\n  signatures (DocuSign), and document assembly. Use for legal tech, HR automation, invoice generation. Activate on \"PDF generation\", \"form filling\", \"document automation\", \"digital signatures\". NOT for\n  simple PDF viewing, basic file conversion, or OCR text extraction.\nallowed-tools: Read,Write,Edit,Bash(npm:*,latex*)\nmetadata:\n  category: Content & Writing\n  tags:\n  - document\n  - generation\n  - pdf\n  - pdf-generation\n  - form-filling\n  pairs-with:\n  - skill: national-expungement-expert\n    reason: Expungement court filings require automated PDF form generation at scale\n  - skill: 2026-legal-research-agent\n    reason: Legal research outputs are formatted into filing documents via PDF generation\n  - skill: cv-creator\n    reason: Resume PDF generation shares template rendering and typography patterns\n  - skill: technical-writer\n    reason: Technical documentation exports to PDF for offline distribution\n---\n\n# Document Generation & PDF Automation\n\nExpert in generating, filling, and assembling PDF documents programmatically for legal, HR, and business workflows.\n\n## When to Use\n\n✅ **Use for**:\n- Legal form automation (expungement, immigration, contracts)\n- Invoice\u002Freceipt generation at scale\n- Certificate creation (completion, participation, awards)\n- Contract assembly from templates\n- Government form filling (IRS, court filings)\n- Multi-document packet creation\n\n❌ **NOT for**:\n- Simple PDF viewing (use browser or pdf.js)\n- Basic file conversion (use online tools)\n- OCR text extraction (use Tesseract.js or AWS Textract)\n- PDF editing by hand (use Adobe Acrobat)\n\n---\n\n## Technology Selection\n\n### pdf-lib vs Puppeteer vs LaTeX\n\n| Feature | pdf-lib | Puppeteer | LaTeX |\n|---------|---------|-----------|-------|\n| Form filling | ✅ Native | ❌ Complex | ❌ No |\n| Template rendering | ❌ No | ✅ HTML\u002FCSS | ✅ Templates |\n| Performance (1000 PDFs) | 5s | 60s | 30s |\n| File size | Small | Medium | Small |\n| Signature fields | ✅ Yes | ❌ No | ❌ No |\n| Best for | Government forms | Invoices, reports | Academic papers |\n\n**Timeline**:\n- 2000s: LaTeX for academic documents\n- 2010: PDFKit (Node.js) for generation\n- 2017: Puppeteer for HTML → PDF\n- 2019: pdf-lib for pure JS form filling\n- 2024: pdf-lib is gold standard for forms\n\n**Decision tree**:\n```\nNeed to fill existing form? → pdf-lib\nNeed complex layouts? → Puppeteer (HTML\u002FCSS)\nNeed academic formatting? → LaTeX\nNeed to merge PDFs? → pdf-lib\nNeed digital signatures? → pdf-lib + DocuSign API\n```\n\n---\n\n## Common Anti-Patterns\n\n### Anti-Pattern 1: Using Puppeteer for Simple Form Filling\n\n**Novice thinking**: \"I'll use Puppeteer for everything, it's versatile\"\n\n**Problem**: 12x slower, 10x more memory, can't preserve form fields.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Puppeteer for simple form filling (SLOW!)\nimport puppeteer from 'puppeteer';\n\nasync function fillForm(data: FormData): Promise\u003CBuffer> {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  \u002F\u002F Load PDF in browser\n  await page.goto(`file:\u002F\u002F${pdfPath}`);\n\n  \u002F\u002F Somehow fill form fields? (hacky)\n  await page.evaluate((data) => {\n    \u002F\u002F Can't easily access PDF form fields from DOM\n    \u002F\u002F Would need to convert PDF → HTML first\n  }, data);\n\n  const pdf = await page.pdf();\n  await browser.close();\n\n  return pdf;\n}\n```\n\n**Why wrong**:\n- Browser overhead (200MB+ RAM per instance)\n- Can't access native PDF form fields\n- Loses interactive form capabilities\n- 12x slower than pdf-lib\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ pdf-lib for form filling (FAST!)\nimport { PDFDocument } from 'pdf-lib';\n\nasync function fillForm(templatePath: string, data: FormData): Promise\u003CUint8Array> {\n  \u002F\u002F Load existing PDF form\n  const existingPdfBytes = await fs.readFile(templatePath);\n  const pdfDoc = await PDFDocument.load(existingPdfBytes);\n\n  \u002F\u002F Get form\n  const form = pdfDoc.getForm();\n\n  \u002F\u002F Fill text fields\n  form.getTextField('applicant_name').setText(data.name);\n  form.getTextField('case_number').setText(data.caseNumber);\n  form.getTextField('date_of_birth').setText(data.dob);\n\n  \u002F\u002F Fill checkboxes\n  if (data.hasPriorConvictions) {\n    form.getCheckBox('prior_convictions').check();\n  }\n\n  \u002F\u002F Fill dropdowns\n  form.getDropdown('state').select(data.state);\n\n  \u002F\u002F Flatten form (make non-editable)\n  form.flatten();\n\n  \u002F\u002F Save\n  return await pdfDoc.save();\n}\n```\n\n**Performance comparison** (1000 PDFs):\n- Puppeteer: 60 seconds, 4GB RAM\n- pdf-lib: 5 seconds, 200MB RAM\n\n**Timeline context**:\n- 2017: Puppeteer released, everyone used it for PDFs\n- 2019: pdf-lib released, proper form handling\n- 2024: pdf-lib is standard for government forms\n\n---\n\n### Anti-Pattern 2: Not Flattening Forms\n\n**Problem**: Users can edit filled forms, causing data inconsistencies.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Don't flatten - form stays editable\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\nconst form = pdfDoc.getForm();\n\nform.getTextField('name').setText('John Doe');\n\nconst pdfBytes = await pdfDoc.save();\n\u002F\u002F User can open PDF and change \"John Doe\" to anything!\n```\n\n**Why wrong**:\n- User can modify official documents\n- Data doesn't match database\n- Violates document integrity\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Flatten form after filling\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\nconst form = pdfDoc.getForm();\n\nform.getTextField('name').setText('John Doe');\n\n\u002F\u002F Flatten (convert fields to static text)\nform.flatten();\n\nconst pdfBytes = await pdfDoc.save();\n\u002F\u002F User can't edit filled values ✅\n```\n\n**When NOT to flatten**:\n- Draft documents (user needs to review\u002Fedit)\n- Multi-step workflows (partial completion)\n- Templates for users to fill manually\n\n---\n\n### Anti-Pattern 3: Generating PDFs from HTML Without Page Breaks\n\n**Novice thinking**: \"HTML → PDF is easy with Puppeteer\"\n\n**Problem**: Content splits mid-sentence across pages.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ No page break control\nconst html = `\n  \u003Cdiv class=\"contract\">\n    \u003Ch1>Mutual Agreement\u003C\u002Fh1>\n    \u003Cp>Long paragraph that might split across pages...\u003C\u002Fp>\n    \u003Csection>\n      \u003Ch2>Terms and Conditions\u003C\u002Fh2>\n      \u003Col>\n        \u003Cli>Term 1 that could get cut off...\u003C\u002Fli>\n        \u003Cli>Term 2...\u003C\u002Fli>\n      \u003C\u002Fol>\n    \u003C\u002Fsection>\n  \u003C\u002Fdiv>\n`;\n\nconst pdf = await page.pdf({ format: 'A4' });\n\u002F\u002F Result: Ugly page breaks in middle of sections\n```\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Explicit page break control with CSS\nconst html = `\n  \u003Cstyle>\n    @media print {\n      .page-break { page-break-after: always; }\n      .avoid-break { page-break-inside: avoid; }\n\n      h1, h2, h3 {\n        page-break-after: avoid;\n        page-break-inside: avoid;\n      }\n\n      section {\n        page-break-inside: avoid;\n      }\n    }\n  \u003C\u002Fstyle>\n\n  \u003Cdiv class=\"contract\">\n    \u003Csection class=\"avoid-break\">\n      \u003Ch1>Mutual Agreement\u003C\u002Fh1>\n      \u003Cp>This entire section stays together...\u003C\u002Fp>\n    \u003C\u002Fsection>\n\n    \u003Cdiv class=\"page-break\">\u003C\u002Fdiv>\n\n    \u003Csection class=\"avoid-break\">\n      \u003Ch2>Terms and Conditions\u003C\u002Fh2>\n      \u003Col>\n        \u003Cli>Term 1\u003C\u002Fli>\n        \u003Cli>Term 2\u003C\u002Fli>\n      \u003C\u002Fol>\n    \u003C\u002Fsection>\n  \u003C\u002Fdiv>\n`;\n\nconst pdf = await page.pdf({\n  format: 'A4',\n  printBackground: true,\n  margin: {\n    top: '1in',\n    right: '1in',\n    bottom: '1in',\n    left: '1in'\n  }\n});\n```\n\n**CSS print properties**:\n- `page-break-before: always` - Force new page before element\n- `page-break-after: always` - Force new page after element\n- `page-break-inside: avoid` - Keep element together\n\n---\n\n### Anti-Pattern 4: Not Handling Signature Fields\n\n**Problem**: Signature fields aren't clickable in generated PDFs.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Add signature as image (not a real signature field)\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\n\nconst signatureImage = await pdfDoc.embedPng(signaturePngBytes);\nconst page = pdfDoc.getPage(0);\n\npage.drawImage(signatureImage, {\n  x: 100,\n  y: 100,\n  width: 200,\n  height: 50\n});\n\nawait pdfDoc.save();\n\u002F\u002F Not a real signature field - can't be signed digitally\n```\n\n**Why wrong**:\n- Not legally recognized (just an image)\n- Can't use DocuSign\u002FAdobe Sign\n- No signature metadata (who, when)\n\n**Correct approach 1**: Create signature field (for DocuSign)\n```typescript\n\u002F\u002F ✅ Create signature field for electronic signing\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\nconst form = pdfDoc.getForm();\n\n\u002F\u002F Create signature field\nconst signatureField = form.createTextField('applicant_signature');\nsignatureField.addToPage(pdfDoc.getPage(0), {\n  x: 100,\n  y: 100,\n  width: 200,\n  height: 50\n});\n\n\u002F\u002F Mark as signature field (metadata)\nsignatureField.updateWidgets({\n  borderWidth: 1,\n  borderColor: { type: 'RGB', red: 0, green: 0, blue: 0 }\n});\n\nawait pdfDoc.save();\n\u002F\u002F DocuSign can now detect and fill this field ✅\n```\n\n**Correct approach 2**: DocuSign API integration\n```typescript\n\u002F\u002F ✅ Send to DocuSign for e-signature\nimport { ApiClient, EnvelopesApi } from 'docusign-esign';\n\nasync function sendForSignature(pdfBytes: Uint8Array, signerEmail: string) {\n  const apiClient = new ApiClient();\n  apiClient.setBasePath('https:\u002F\u002Fdemo.docusign.net\u002Frestapi');\n\n  const envelopesApi = new EnvelopesApi(apiClient);\n\n  const envelope = {\n    emailSubject: 'Please sign: Expungement Petition',\n    documents: [{\n      documentBase64: Buffer.from(pdfBytes).toString('base64'),\n      name: 'Petition.pdf',\n      fileExtension: 'pdf',\n      documentId: '1'\n    }],\n    recipients: {\n      signers: [{\n        email: signerEmail,\n        name: 'John Doe',\n        recipientId: '1',\n        tabs: {\n          signHereTabs: [{\n            documentId: '1',\n            pageNumber: '1',\n            xPosition: '100',\n            yPosition: '100'\n          }]\n        }\n      }]\n    },\n    status: 'sent'\n  };\n\n  return await envelopesApi.createEnvelope(accountId, { envelopeDefinition: envelope });\n}\n```\n\n---\n\n### Anti-Pattern 5: Storing Filled PDFs Without Encryption\n\n**Problem**: Sensitive legal documents stored in plain text.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Save PDF to disk unencrypted\nconst pdfBytes = await pdfDoc.save();\nawait fs.writeFile(`.\u002Fdocuments\u002F${caseId}.pdf`, pdfBytes);\n\u002F\u002F Sensitive data accessible to anyone with file system access\n```\n\n**Why wrong**:\n- Legal\u002Fmedical data exposed\n- HIPAA\u002FGDPR violations\n- Data breach liability\n\n**Correct approach 1**: Encrypt at rest\n```typescript\n\u002F\u002F ✅ Encrypt PDF with user password\nconst pdfBytes = await pdfDoc.save({\n  userPassword: generateSecurePassword(),\n  ownerPassword: process.env.PDF_OWNER_PASSWORD,\n  permissions: {\n    printing: 'highResolution',\n    modifying: false,\n    copying: false,\n    annotating: false,\n    fillingForms: false,\n    contentAccessibility: true,\n    documentAssembly: false\n  }\n});\n\nawait fs.writeFile(`.\u002Fdocuments\u002F${caseId}.pdf`, pdfBytes);\n```\n\n**Correct approach 2**: Store in encrypted storage (S3 with SSE)\n```typescript\n\u002F\u002F ✅ Upload to S3 with server-side encryption\nimport { S3Client, PutObjectCommand } from '@aws-sdk\u002Fclient-s3';\n\nconst s3Client = new S3Client({ region: 'us-east-1' });\n\nawait s3Client.send(new PutObjectCommand({\n  Bucket: 'expungement-documents',\n  Key: `cases\u002F${caseId}\u002Fpetition.pdf`,\n  Body: pdfBytes,\n  ServerSideEncryption: 'AES256',\n  Metadata: {\n    caseId: caseId,\n    documentType: 'petition',\n    generatedAt: new Date().toISOString()\n  },\n  ACL: 'private'  \u002F\u002F Not publicly accessible\n}));\n\n\u002F\u002F Store S3 URL in database (encrypted)\nawait db.documents.insert({\n  case_id: caseId,\n  s3_url: encrypt(`s3:\u002F\u002Fexpungement-documents\u002Fcases\u002F${caseId}\u002Fpetition.pdf`),\n  created_at: new Date()\n});\n```\n\n**Compliance requirements**:\n- HIPAA: Encryption at rest + in transit, access logging\n- GDPR: Right to deletion, data minimization\n- State bar rules: Attorney-client privilege protection\n\n---\n\n## Production Checklist\n\n```\n□ Form fields filled correctly (test with validation)\n□ Forms flattened after filling (non-editable)\n□ Page breaks controlled (no mid-sentence splits)\n□ Signature fields created (for DocuSign\u002FAdobe Sign)\n□ PDFs encrypted at rest (S3 SSE or user password)\n□ Access logged (who viewed\u002Fdownloaded)\n□ Auto-deletion scheduled (retention policy)\n□ Fonts embedded (cross-platform compatibility)\n□ File size optimized (&lt;5MB per document)\n□ Batch generation tested (1000+ PDFs)\n```\n\n---\n\n## When to Use vs Avoid\n\n| Scenario | Appropriate? |\n|----------|--------------|\n| Fill 50+ government forms | ✅ Yes - automate with pdf-lib |\n| Generate invoices from template | ✅ Yes - Puppeteer from HTML |\n| Create certificates at scale | ✅ Yes - pdf-lib or LaTeX |\n| Assemble multi-doc packets | ✅ Yes - pdf-lib merge |\n| View PDFs in browser | ❌ No - use pdf.js or browser |\n| Edit PDF by hand | ❌ No - use Adobe Acrobat |\n| Extract text with OCR | ❌ No - use Tesseract\u002FTextract |\n\n---\n\n## Flat PDF Overlay at Scale (Government Forms)\n\nMany government court forms are **flat PDFs** (no fillable fields). You MUST overlay text at precise X,Y coordinates. Never generate forms from scratch - courts will reject them.\n\n### The Three-Tier Strategy\n\n```\n1. FILLABLE PDF → Use form.getTextField().setText() (best)\n2. FLAT PDF + COORDINATES → Draw text overlays at X,Y positions (this section)\n3. NO TEMPLATE EXISTS → Emergency fallback with warning banner (worst)\n```\n\n### Anti-Pattern 6: Generating Forms from Scratch\n\n**Novice thinking**: \"The PDF doesn't have form fields, I'll just create a new PDF\"\n\n**Problem**: Courts reject non-official forms. Your custom layout won't match official documents.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ NEVER DO THIS - courts reject custom forms\nconst pdfDoc = await PDFDocument.create();\nconst page = pdfDoc.addPage([612, 792]);\n\npage.drawText('IN THE CIRCUIT COURT OF...', { x: 200, y: 720 });\npage.drawText(`Defendant: ${data.name}`, { x: 100, y: 600 });\n\u002F\u002F Result: Court clerk says \"This isn't our form\" and rejects filing\n```\n\n**Correct approach**: Draw on top of official template\n```typescript\n\u002F\u002F ✅ Load official form, draw text in blank spaces\nconst templateBytes = await fs.readFile('public\u002Fforms\u002FOR-set-aside-motion.pdf');\nconst pdfDoc = await PDFDocument.load(templateBytes);\nconst page = pdfDoc.getPage(3); \u002F\u002F Page 4 is the form\nconst font = await pdfDoc.embedFont(StandardFonts.Helvetica);\n\n\u002F\u002F Draw in blank spaces at measured coordinates\npage.drawText(data.county.toUpperCase(), { x: 310, y: 700, size: 11, font });\npage.drawText(data.caseNumber, { x: 432, y: 655, size: 10, font });\npage.drawText(data.fullName, { x: 72, y: 568, size: 11, font });\n```\n\n---\n\n### Measuring Coordinates at Scale\n\n**Problem**: Manually measuring X,Y for 50 states × 10+ forms = impossible.\n\n**Solution 1: Visual Coordinate Picker Tool**\n\nUse [pdf-coordinates](https:\u002F\u002Fgithub.com\u002Fjol333\u002Fpdf-coordinates) - click on PDF to capture X,Y:\n- Upload PDF, click on blank fields\n- Select coordinate system: **Bottom-Left** (PDF standard)\n- Export annotated PDF with coordinate labels\n- Works offline, no data upload\n\n**Solution 2: JSON Configuration (Zerodha Pattern)**\n\nDefine coordinates in JSON config, not code:\n```typescript\n\u002F\u002F field-mappings\u002Foregon-set-aside.json\n{\n  \"templatePath\": \"\u002Fforms\u002For\u002FOR-criminal-set-aside.pdf\",\n  \"pageIndex\": 3,\n  \"fields\": [\n    {\n      \"name\": \"county\",\n      \"x\": 310,\n      \"y\": 700,\n      \"fontSize\": 11,\n      \"maxWidth\": 120,\n      \"transform\": \"uppercase\"\n    },\n    {\n      \"name\": \"caseNumber\",\n      \"x\": 432,\n      \"y\": 655,\n      \"fontSize\": 10,\n      \"maxWidth\": 100,\n      \"maxChars\": 12\n    },\n    {\n      \"name\": \"fullName\",\n      \"x\": 72,\n      \"y\": 568,\n      \"fontSize\": 11,\n      \"maxWidth\": 250,\n      \"shrinkToFit\": true\n    }\n  ]\n}\n```\n\n**Solution 3: Debug Grid Overlay**\n\nAdd temporary grid during development:\n```typescript\nasync function drawDebugGrid(page: PDFPage, font: PDFFont) {\n  const { width, height } = page.getSize();\n\n  \u002F\u002F Draw grid every 50 points\n  for (let x = 0; x \u003C= width; x += 50) {\n    page.drawLine({\n      start: { x, y: 0 },\n      end: { x, y: height },\n      thickness: 0.2,\n      color: rgb(0.9, 0.9, 0.9),\n    });\n    if (x % 100 === 0) {\n      page.drawText(String(x), { x: x + 2, y: 5, size: 6, font });\n    }\n  }\n  \u002F\u002F Same for horizontal lines...\n}\n```\n\n---\n\n### Text Overflow Handling\n\n**Problem**: Long names overflow into adjacent fields or get cut off.\n\n**Three overflow strategies**:\n\n```typescript\n\u002F\u002F 1. Truncate with ellipsis\nfunction truncate(text: string, maxChars: number): string {\n  if (text.length \u003C= maxChars) return text;\n  return text.slice(0, maxChars - 2) + '..';\n}\n\n\u002F\u002F 2. Shrink font to fit (min 7pt for readability)\nfunction shrinkToFit(text: string, maxWidthPts: number, startSize: number): number {\n  let fontSize = startSize;\n  const charWidth = (size: number) => size * 0.52; \u002F\u002F Helvetica avg\n\n  while (fontSize > 7 && text.length * charWidth(fontSize) > maxWidthPts) {\n    fontSize -= 0.5;\n  }\n  return fontSize;\n}\n\n\u002F\u002F 3. Multi-line wrap (for addresses)\npage.drawText(longAddress, {\n  x: 100,\n  y: 500,\n  size: 9,\n  font,\n  maxWidth: 200, \u002F\u002F pdf-lib auto-wraps at this width\n  lineHeight: 12,\n});\n```\n\n**Field input constraints** - Prevent overflow at data collection:\n```typescript\nexport const FIELD_CONSTRAINTS = {\n  fullName: { maxLength: 35 },\n  county: { maxLength: 12 },\n  caseNumber: { maxLength: 12 },\n  agency: { maxLength: 28 },\n  charges: { maxLength: 45 },\n} as const;\n```\n\n---\n\n### Production Workflow for Multi-State Forms\n\n```\n1. DOWNLOAD official forms\n   └─ Store in \u002Fpublic\u002Fforms\u002F{state}\u002F with metadata.json\n\n2. INSPECT each form\n   └─ Run pdf-lib inspection to check: fillable? how many fields?\n\n3. CATEGORIZE forms\n   ├─ Fillable → Map field names in field-mappings\u002F{state}.ts\n   └─ Flat → Measure coordinates with pdf-coordinates tool\n\n4. CREATE JSON config for flat PDFs\n   └─ One JSON file per form with all X,Y coordinates\n\n5. TEST with debug grid\n   └─ Generate test PDF with colored text + coordinate labels\n\n6. QA visual verification\n   └─ Compare filled PDF against original blank form\n\n7. DOCUMENT constraints\n   └─ Export maxLength\u002FmaxWidth limits for form inputs\n```\n\n---\n\n### Legal Filing Requirements\n\n**Court e-filing systems require**:\n- ✅ Flattened forms (no interactive fields)\n- ✅ No digital signature metadata (DocuSign breaks e-filing)\n- ✅ Standard fonts embedded (Helvetica, Times)\n- ✅ File under 10MB\n- ✅ Exact match to official form layout\n\n**Flattening for courts**:\n```typescript\n\u002F\u002F If form had fields, flatten them\nconst form = pdfDoc.getForm();\nif (form.getFields().length > 0) {\n  form.flatten(); \u002F\u002F Converts to static text\n}\n\n\u002F\u002F For overlay text, it's already static - no flattening needed\nconst pdfBytes = await pdfDoc.save();\n```\n\n---\n\n### Coordinate System Reference\n\n```\nPDF coordinate system (pdf-lib default):\n- Origin: Bottom-left corner (0, 0)\n- X increases: Left → Right\n- Y increases: Bottom → Top\n- Units: Points (72 points = 1 inch)\n- Letter size: 612 x 792 points (8.5\" x 11\")\n\nTo convert from top-left origin (e.g., Adobe Acrobat display):\n  y_pdf = 792 - y_topLeft\n\nCommon positions (Letter size):\n- Top margin: y = 720-750\n- Header area: y = 700-750\n- Body start: y = 650-700\n- Left margin: x = 50-72\n- Right edge: x = 540-560\n- Bottom margin: y = 50-72\n```\n\n---\n\n## References\n\n- `\u002Freferences\u002Fpdf-lib-guide.md` - Form filling, field types, flattening, encryption\n- `\u002Freferences\u002Fpuppeteer-templates.md` - HTML templates, page breaks, styling for print\n- `\u002Freferences\u002Fdocument-assembly.md` - Merging PDFs, packet creation, watermarks\n- [pdf-coordinates](https:\u002F\u002Fgithub.com\u002Fjol333\u002Fpdf-coordinates) - Visual X,Y coordinate picker\n- [Zerodha pdf_text_overlay](https:\u002F\u002Fgithub.com\u002Fzerodha\u002Fpdf_text_overlay) - JSON config pattern\n\n## Scripts\n\n- `scripts\u002Fform_filler.ts` - Fill PDF forms from JSON data, batch processing\n- `scripts\u002Fdocument_assembler.ts` - Merge multiple PDFs, add cover pages, watermarks\n- `scripts\u002Fgenerate-test-overlay-pdf.ts` - Test overlay coordinates with debug grid\n\n---\n\n**This skill guides**: PDF generation | Form filling | Document automation | Digital signatures | pdf-lib | Puppeteer | LaTeX | DocuSign | **Flat PDF overlay** | **Coordinate mapping**\n","# Document Generation & PDF Automation\n\nExpert in generating, filling, and assembling PDF documents programmatically for legal, HR, and business workflows.\n\n## When to Use\n\n✅ **Use for**:\n- Legal form automation (expungement, immigration, contracts)\n- Invoice\u002Freceipt generation at scale\n- Certificate creation (completion, participation, awards)\n- Contract assembly from templates\n- Government form filling (IRS, court filings)\n- Multi-document packet creation\n\n❌ **NOT for**:\n- Simple PDF viewing (use browser or pdf.js)\n- Basic file conversion (use online tools)\n- OCR text extraction (use Tesseract.js or AWS Textract)\n- PDF editing by hand (use Adobe Acrobat)\n\n---\n\n## Technology Selection\n\n### pdf-lib vs Puppeteer vs LaTeX\n\n| Feature | pdf-lib | Puppeteer | LaTeX |\n|---------|---------|-----------|-------|\n| Form filling | ✅ Native | ❌ Complex | ❌ No |\n| Template rendering | ❌ No | ✅ HTML\u002FCSS | ✅ Templates |\n| Performance (1000 PDFs) | 5s | 60s | 30s |\n| File size | Small | Medium | Small |\n| Signature fields | ✅ Yes | ❌ No | ❌ No |\n| Best for | Government forms | Invoices, reports | Academic papers |\n\n**Timeline**:\n- 2000s: LaTeX for academic documents\n- 2010: PDFKit (Node.js) for generation\n- 2017: Puppeteer for HTML → PDF\n- 2019: pdf-lib for pure JS form filling\n- 2024: pdf-lib is gold standard for forms\n\n**Decision tree**:\n```\nNeed to fill existing form? → pdf-lib\nNeed complex layouts? → Puppeteer (HTML\u002FCSS)\nNeed academic formatting? → LaTeX\nNeed to merge PDFs? → pdf-lib\nNeed digital signatures? → pdf-lib + DocuSign API\n```\n\n---\n\n## Common Anti-Patterns\n\n### Anti-Pattern 1: Using Puppeteer for Simple Form Filling\n\n**Novice thinking**: \"I'll use Puppeteer for everything, it's versatile\"\n\n**Problem**: 12x slower, 10x more memory, can't preserve form fields.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Puppeteer for simple form filling (SLOW!)\nimport puppeteer from 'puppeteer';\n\nasync function fillForm(data: FormData): Promise\u003CBuffer> {\n  const browser = await puppeteer.launch();\n  const page = await browser.newPage();\n\n  \u002F\u002F Load PDF in browser\n  await page.goto(`file:\u002F\u002F${pdfPath}`);\n\n  \u002F\u002F Somehow fill form fields? (hacky)\n  await page.evaluate((data) => {\n    \u002F\u002F Can't easily access PDF form fields from DOM\n    \u002F\u002F Would need to convert PDF → HTML first\n  }, data);\n\n  const pdf = await page.pdf();\n  await browser.close();\n\n  return pdf;\n}\n```\n\n**Why wrong**:\n- Browser overhead (200MB+ RAM per instance)\n- Can't access native PDF form fields\n- Loses interactive form capabilities\n- 12x slower than pdf-lib\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ pdf-lib for form filling (FAST!)\nimport { PDFDocument } from 'pdf-lib';\n\nasync function fillForm(templatePath: string, data: FormData): Promise\u003CUint8Array> {\n  \u002F\u002F Load existing PDF form\n  const existingPdfBytes = await fs.readFile(templatePath);\n  const pdfDoc = await PDFDocument.load(existingPdfBytes);\n\n  \u002F\u002F Get form\n  const form = pdfDoc.getForm();\n\n  \u002F\u002F Fill text fields\n  form.getTextField('applicant_name').setText(data.name);\n  form.getTextField('case_number').setText(data.caseNumber);\n  form.getTextField('date_of_birth').setText(data.dob);\n\n  \u002F\u002F Fill checkboxes\n  if (data.hasPriorConvictions) {\n    form.getCheckBox('prior_convictions').check();\n  }\n\n  \u002F\u002F Fill dropdowns\n  form.getDropdown('state').select(data.state);\n\n  \u002F\u002F Flatten form (make non-editable)\n  form.flatten();\n\n  \u002F\u002F Save\n  return await pdfDoc.save();\n}\n```\n\n**Performance comparison** (1000 PDFs):\n- Puppeteer: 60 seconds, 4GB RAM\n- pdf-lib: 5 seconds, 200MB RAM\n\n**Timeline context**:\n- 2017: Puppeteer released, everyone used it for PDFs\n- 2019: pdf-lib released, proper form handling\n- 2024: pdf-lib is standard for government forms\n\n---\n\n### Anti-Pattern 2: Not Flattening Forms\n\n**Problem**: Users can edit filled forms, causing data inconsistencies.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Don't flatten - form stays editable\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\nconst form = pdfDoc.getForm();\n\nform.getTextField('name').setText('John Doe');\n\nconst pdfBytes = await pdfDoc.save();\n\u002F\u002F User can open PDF and change \"John Doe\" to anything!\n```\n\n**Why wrong**:\n- User can modify official documents\n- Data doesn't match database\n- Violates document integrity\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Flatten form after filling\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\nconst form = pdfDoc.getForm();\n\nform.getTextField('name').setText('John Doe');\n\n\u002F\u002F Flatten (convert fields to static text)\nform.flatten();\n\nconst pdfBytes = await pdfDoc.save();\n\u002F\u002F User can't edit filled values ✅\n```\n\n**When NOT to flatten**:\n- Draft documents (user needs to review\u002Fedit)\n- Multi-step workflows (partial completion)\n- Templates for users to fill manually\n\n---\n\n### Anti-Pattern 3: Generating PDFs from HTML Without Page Breaks\n\n**Novice thinking**: \"HTML → PDF is easy with Puppeteer\"\n\n**Problem**: Content splits mid-sentence across pages.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ No page break control\nconst html = `\n  \u003Cdiv class=\"contract\">\n    \u003Ch1>Mutual Agreement\u003C\u002Fh1>\n    \u003Cp>Long paragraph that might split across pages...\u003C\u002Fp>\n    \u003Csection>\n      \u003Ch2>Terms and Conditions\u003C\u002Fh2>\n      \u003Col>\n        \u003Cli>Term 1 that could get cut off...\u003C\u002Fli>\n        \u003Cli>Term 2...\u003C\u002Fli>\n      \u003C\u002Fol>\n    \u003C\u002Fsection>\n  \u003C\u002Fdiv>\n`;\n\nconst pdf = await page.pdf({ format: 'A4' });\n\u002F\u002F Result: Ugly page breaks in middle of sections\n```\n\n**Correct approach**:\n```typescript\n\u002F\u002F ✅ Explicit page break control with CSS\nconst html = `\n  \u003Cstyle>\n    @media print {\n      .page-break { page-break-after: always; }\n      .avoid-break { page-break-inside: avoid; }\n\n      h1, h2, h3 {\n        page-break-after: avoid;\n        page-break-inside: avoid;\n      }\n\n      section {\n        page-break-inside: avoid;\n      }\n    }\n  \u003C\u002Fstyle>\n\n  \u003Cdiv class=\"contract\">\n    \u003Csection class=\"avoid-break\">\n      \u003Ch1>Mutual Agreement\u003C\u002Fh1>\n      \u003Cp>This entire section stays together...\u003C\u002Fp>\n    \u003C\u002Fsection>\n\n    \u003Cdiv class=\"page-break\">\u003C\u002Fdiv>\n\n    \u003Csection class=\"avoid-break\">\n      \u003Ch2>Terms and Conditions\u003C\u002Fh2>\n      \u003Col>\n        \u003Cli>Term 1\u003C\u002Fli>\n        \u003Cli>Term 2\u003C\u002Fli>\n      \u003C\u002Fol>\n    \u003C\u002Fsection>\n  \u003C\u002Fdiv>\n`;\n\nconst pdf = await page.pdf({\n  format: 'A4',\n  printBackground: true,\n  margin: {\n    top: '1in',\n    right: '1in',\n    bottom: '1in',\n    left: '1in'\n  }\n});\n```\n\n**CSS print properties**:\n- `page-break-before: always` - Force new page before element\n- `page-break-after: always` - Force new page after element\n- `page-break-inside: avoid` - Keep element together\n\n---\n\n### Anti-Pattern 4: Not Handling Signature Fields\n\n**Problem**: Signature fields aren't clickable in generated PDFs.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Add signature as image (not a real signature field)\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\n\nconst signatureImage = await pdfDoc.embedPng(signaturePngBytes);\nconst page = pdfDoc.getPage(0);\n\npage.drawImage(signatureImage, {\n  x: 100,\n  y: 100,\n  width: 200,\n  height: 50\n});\n\nawait pdfDoc.save();\n\u002F\u002F Not a real signature field - can't be signed digitally\n```\n\n**Why wrong**:\n- Not legally recognized (just an image)\n- Can't use DocuSign\u002FAdobe Sign\n- No signature metadata (who, when)\n\n**Correct approach 1**: Create signature field (for DocuSign)\n```typescript\n\u002F\u002F ✅ Create signature field for electronic signing\nconst pdfDoc = await PDFDocument.load(existingPdfBytes);\nconst form = pdfDoc.getForm();\n\n\u002F\u002F Create signature field\nconst signatureField = form.createTextField('applicant_signature');\nsignatureField.addToPage(pdfDoc.getPage(0), {\n  x: 100,\n  y: 100,\n  width: 200,\n  height: 50\n});\n\n\u002F\u002F Mark as signature field (metadata)\nsignatureField.updateWidgets({\n  borderWidth: 1,\n  borderColor: { type: 'RGB', red: 0, green: 0, blue: 0 }\n});\n\nawait pdfDoc.save();\n\u002F\u002F DocuSign can now detect and fill this field ✅\n```\n\n**Correct approach 2**: DocuSign API integration\n```typescript\n\u002F\u002F ✅ Send to DocuSign for e-signature\nimport { ApiClient, EnvelopesApi } from 'docusign-esign';\n\nasync function sendForSignature(pdfBytes: Uint8Array, signerEmail: string) {\n  const apiClient = new ApiClient();\n  apiClient.setBasePath('https:\u002F\u002Fdemo.docusign.net\u002Frestapi');\n\n  const envelopesApi = new EnvelopesApi(apiClient);\n\n  const envelope = {\n    emailSubject: 'Please sign: Expungement Petition',\n    documents: [{\n      documentBase64: Buffer.from(pdfBytes).toString('base64'),\n      name: 'Petition.pdf',\n      fileExtension: 'pdf',\n      documentId: '1'\n    }],\n    recipients: {\n      signers: [{\n        email: signerEmail,\n        name: 'John Doe',\n        recipientId: '1',\n        tabs: {\n          signHereTabs: [{\n            documentId: '1',\n            pageNumber: '1',\n            xPosition: '100',\n            yPosition: '100'\n          }]\n        }\n      }]\n    },\n    status: 'sent'\n  };\n\n  return await envelopesApi.createEnvelope(accountId, { envelopeDefinition: envelope });\n}\n```\n\n---\n\n### Anti-Pattern 5: Storing Filled PDFs Without Encryption\n\n**Problem**: Sensitive legal documents stored in plain text.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ Save PDF to disk unencrypted\nconst pdfBytes = await pdfDoc.save();\nawait fs.writeFile(`.\u002Fdocuments\u002F${caseId}.pdf`, pdfBytes);\n\u002F\u002F Sensitive data accessible to anyone with file system access\n```\n\n**Why wrong**:\n- Legal\u002Fmedical data exposed\n- HIPAA\u002FGDPR violations\n- Data breach liability\n\n**Correct approach 1**: Encrypt at rest\n```typescript\n\u002F\u002F ✅ Encrypt PDF with user password\nconst pdfBytes = await pdfDoc.save({\n  userPassword: generateSecurePassword(),\n  ownerPassword: process.env.PDF_OWNER_PASSWORD,\n  permissions: {\n    printing: 'highResolution',\n    modifying: false,\n    copying: false,\n    annotating: false,\n    fillingForms: false,\n    contentAccessibility: true,\n    documentAssembly: false\n  }\n});\n\nawait fs.writeFile(`.\u002Fdocuments\u002F${caseId}.pdf`, pdfBytes);\n```\n\n**Correct approach 2**: Store in encrypted storage (S3 with SSE)\n```typescript\n\u002F\u002F ✅ Upload to S3 with server-side encryption\nimport { S3Client, PutObjectCommand } from '@aws-sdk\u002Fclient-s3';\n\nconst s3Client = new S3Client({ region: 'us-east-1' });\n\nawait s3Client.send(new PutObjectCommand({\n  Bucket: 'expungement-documents',\n  Key: `cases\u002F${caseId}\u002Fpetition.pdf`,\n  Body: pdfBytes,\n  ServerSideEncryption: 'AES256',\n  Metadata: {\n    caseId: caseId,\n    documentType: 'petition',\n    generatedAt: new Date().toISOString()\n  },\n  ACL: 'private'  \u002F\u002F Not publicly accessible\n}));\n\n\u002F\u002F Store S3 URL in database (encrypted)\nawait db.documents.insert({\n  case_id: caseId,\n  s3_url: encrypt(`s3:\u002F\u002Fexpungement-documents\u002Fcases\u002F${caseId}\u002Fpetition.pdf`),\n  created_at: new Date()\n});\n```\n\n**Compliance requirements**:\n- HIPAA: Encryption at rest + in transit, access logging\n- GDPR: Right to deletion, data minimization\n- State bar rules: Attorney-client privilege protection\n\n---\n\n## Production Checklist\n\n```\n□ Form fields filled correctly (test with validation)\n□ Forms flattened after filling (non-editable)\n□ Page breaks controlled (no mid-sentence splits)\n□ Signature fields created (for DocuSign\u002FAdobe Sign)\n□ PDFs encrypted at rest (S3 SSE or user password)\n□ Access logged (who viewed\u002Fdownloaded)\n□ Auto-deletion scheduled (retention policy)\n□ Fonts embedded (cross-platform compatibility)\n□ File size optimized (&lt;5MB per document)\n□ Batch generation tested (1000+ PDFs)\n```\n\n---\n\n## When to Use vs Avoid\n\n| Scenario | Appropriate? |\n|----------|--------------|\n| Fill 50+ government forms | ✅ Yes - automate with pdf-lib |\n| Generate invoices from template | ✅ Yes - Puppeteer from HTML |\n| Create certificates at scale | ✅ Yes - pdf-lib or LaTeX |\n| Assemble multi-doc packets | ✅ Yes - pdf-lib merge |\n| View PDFs in browser | ❌ No - use pdf.js or browser |\n| Edit PDF by hand | ❌ No - use Adobe Acrobat |\n| Extract text with OCR | ❌ No - use Tesseract\u002FTextract |\n\n---\n\n## Flat PDF Overlay at Scale (Government Forms)\n\nMany government court forms are **flat PDFs** (no fillable fields). You MUST overlay text at precise X,Y coordinates. Never generate forms from scratch - courts will reject them.\n\n### The Three-Tier Strategy\n\n```\n1. FILLABLE PDF → Use form.getTextField().setText() (best)\n2. FLAT PDF + COORDINATES → Draw text overlays at X,Y positions (this section)\n3. NO TEMPLATE EXISTS → Emergency fallback with warning banner (worst)\n```\n\n### Anti-Pattern 6: Generating Forms from Scratch\n\n**Novice thinking**: \"The PDF doesn't have form fields, I'll just create a new PDF\"\n\n**Problem**: Courts reject non-official forms. Your custom layout won't match official documents.\n\n**Wrong approach**:\n```typescript\n\u002F\u002F ❌ NEVER DO THIS - courts reject custom forms\nconst pdfDoc = await PDFDocument.create();\nconst page = pdfDoc.addPage([612, 792]);\n\npage.drawText('IN THE CIRCUIT COURT OF...', { x: 200, y: 720 });\npage.drawText(`Defendant: ${data.name}`, { x: 100, y: 600 });\n\u002F\u002F Result: Court clerk says \"This isn't our form\" and rejects filing\n```\n\n**Correct approach**: Draw on top of official template\n```typescript\n\u002F\u002F ✅ Load official form, draw text in blank spaces\nconst templateBytes = await fs.readFile('public\u002Fforms\u002FOR-set-aside-motion.pdf');\nconst pdfDoc = await PDFDocument.load(templateBytes);\nconst page = pdfDoc.getPage(3); \u002F\u002F Page 4 is the form\nconst font = await pdfDoc.embedFont(StandardFonts.Helvetica);\n\n\u002F\u002F Draw in blank spaces at measured coordinates\npage.drawText(data.county.toUpperCase(), { x: 310, y: 700, size: 11, font });\npage.drawText(data.caseNumber, { x: 432, y: 655, size: 10, font });\npage.drawText(data.fullName, { x: 72, y: 568, size: 11, font });\n```\n\n---\n\n### Measuring Coordinates at Scale\n\n**Problem**: Manually measuring X,Y for 50 states × 10+ forms = impossible.\n\n**Solution 1: Visual Coordinate Picker Tool**\n\nUse [pdf-coordinates](https:\u002F\u002Fgithub.com\u002Fjol333\u002Fpdf-coordinates) - click on PDF to capture X,Y:\n- Upload PDF, click on blank fields\n- Select coordinate system: **Bottom-Left** (PDF standard)\n- Export annotated PDF with coordinate labels\n- Works offline, no data upload\n\n**Solution 2: JSON Configuration (Zerodha Pattern)**\n\nDefine coordinates in JSON config, not code:\n```typescript\n\u002F\u002F field-mappings\u002Foregon-set-aside.json\n{\n  \"templatePath\": \"\u002Fforms\u002For\u002FOR-criminal-set-aside.pdf\",\n  \"pageIndex\": 3,\n  \"fields\": [\n    {\n      \"name\": \"county\",\n      \"x\": 310,\n      \"y\": 700,\n      \"fontSize\": 11,\n      \"maxWidth\": 120,\n      \"transform\": \"uppercase\"\n    },\n    {\n      \"name\": \"caseNumber\",\n      \"x\": 432,\n      \"y\": 655,\n      \"fontSize\": 10,\n      \"maxWidth\": 100,\n      \"maxChars\": 12\n    },\n    {\n      \"name\": \"fullName\",\n      \"x\": 72,\n      \"y\": 568,\n      \"fontSize\": 11,\n      \"maxWidth\": 250,\n      \"shrinkToFit\": true\n    }\n  ]\n}\n```\n\n**Solution 3: Debug Grid Overlay**\n\nAdd temporary grid during development:\n```typescript\nasync function drawDebugGrid(page: PDFPage, font: PDFFont) {\n  const { width, height } = page.getSize();\n\n  \u002F\u002F Draw grid every 50 points\n  for (let x = 0; x \u003C= width; x += 50) {\n    page.drawLine({\n      start: { x, y: 0 },\n      end: { x, y: height },\n      thickness: 0.2,\n      color: rgb(0.9, 0.9, 0.9),\n    });\n    if (x % 100 === 0) {\n      page.drawText(String(x), { x: x + 2, y: 5, size: 6, font });\n    }\n  }\n  \u002F\u002F Same for horizontal lines...\n}\n```\n\n---\n\n### Text Overflow Handling\n\n**Problem**: Long names overflow into adjacent fields or get cut off.\n\n**Three overflow strategies**:\n\n```typescript\n\u002F\u002F 1. Truncate with ellipsis\nfunction truncate(text: string, maxChars: number): string {\n  if (text.length \u003C= maxChars) return text;\n  return text.slice(0, maxChars - 2) + '..';\n}\n\n\u002F\u002F 2. Shrink font to fit (min 7pt for readability)\nfunction shrinkToFit(text: string, maxWidthPts: number, startSize: number): number {\n  let fontSize = startSize;\n  const charWidth = (size: number) => size * 0.52; \u002F\u002F Helvetica avg\n\n  while (fontSize > 7 && text.length * charWidth(fontSize) > maxWidthPts) {\n    fontSize -= 0.5;\n  }\n  return fontSize;\n}\n\n\u002F\u002F 3. Multi-line wrap (for addresses)\npage.drawText(longAddress, {\n  x: 100,\n  y: 500,\n  size: 9,\n  font,\n  maxWidth: 200, \u002F\u002F pdf-lib auto-wraps at this width\n  lineHeight: 12,\n});\n```\n\n**Field input constraints** - Prevent overflow at data collection:\n```typescript\nexport const FIELD_CONSTRAINTS = {\n  fullName: { maxLength: 35 },\n  county: { maxLength: 12 },\n  caseNumber: { maxLength: 12 },\n  agency: { maxLength: 28 },\n  charges: { maxLength: 45 },\n} as const;\n```\n\n---\n\n### Production Workflow for Multi-State Forms\n\n```\n1. DOWNLOAD official forms\n   └─ Store in \u002Fpublic\u002Fforms\u002F{state}\u002F with metadata.json\n\n2. INSPECT each form\n   └─ Run pdf-lib inspection to check: fillable? how many fields?\n\n3. CATEGORIZE forms\n   ├─ Fillable → Map field names in field-mappings\u002F{state}.ts\n   └─ Flat → Measure coordinates with pdf-coordinates tool\n\n4. CREATE JSON config for flat PDFs\n   └─ One JSON file per form with all X,Y coordinates\n\n5. TEST with debug grid\n   └─ Generate test PDF with colored text + coordinate labels\n\n6. QA visual verification\n   └─ Compare filled PDF against original blank form\n\n7. DOCUMENT constraints\n   └─ Export maxLength\u002FmaxWidth limits for form inputs\n```\n\n---\n\n### Legal Filing Requirements\n\n**Court e-filing systems require**:\n- ✅ Flattened forms (no interactive fields)\n- ✅ No digital signature metadata (DocuSign breaks e-filing)\n- ✅ Standard fonts embedded (Helvetica, Times)\n- ✅ File under 10MB\n- ✅ Exact match to official form layout\n\n**Flattening for courts**:\n```typescript\n\u002F\u002F If form had fields, flatten them\nconst form = pdfDoc.getForm();\nif (form.getFields().length > 0) {\n  form.flatten(); \u002F\u002F Converts to static text\n}\n\n\u002F\u002F For overlay text, it's already static - no flattening needed\nconst pdfBytes = await pdfDoc.save();\n```\n\n---\n\n### Coordinate System Reference\n\n```\nPDF coordinate system (pdf-lib default):\n- Origin: Bottom-left corner (0, 0)\n- X increases: Left → Right\n- Y increases: Bottom → Top\n- Units: Points (72 points = 1 inch)\n- Letter size: 612 x 792 points (8.5\" x 11\")\n\nTo convert from top-left origin (e.g., Adobe Acrobat display):\n  y_pdf = 792 - y_topLeft\n\nCommon positions (Letter size):\n- Top margin: y = 720-750\n- Header area: y = 700-750\n- Body start: y = 650-700\n- Left margin: x = 50-72\n- Right edge: x = 540-560\n- Bottom margin: y = 50-72\n```\n\n---\n\n## References\n\n- `\u002Freferences\u002Fpdf-lib-guide.md` - Form filling, field types, flattening, encryption\n- `\u002Freferences\u002Fpuppeteer-templates.md` - HTML templates, page breaks, styling for print\n- `\u002Freferences\u002Fdocument-assembly.md` - Merging PDFs, packet creation, watermarks\n- [pdf-coordinates](https:\u002F\u002Fgithub.com\u002Fjol333\u002Fpdf-coordinates) - Visual X,Y coordinate picker\n- [Zerodha pdf_text_overlay](https:\u002F\u002Fgithub.com\u002Fzerodha\u002Fpdf_text_overlay) - JSON config pattern\n\n## Scripts\n\n- `scripts\u002Fform_filler.ts` - Fill PDF forms from JSON data, batch processing\n- `scripts\u002Fdocument_assembler.ts` - Merge multiple PDFs, add cover pages, watermarks\n- `scripts\u002Fgenerate-test-overlay-pdf.ts` - Test overlay coordinates with debug grid\n\n---\n\n**This skill guides**: PDF generation | Form filling | Document automation | Digital signatures | pdf-lib | Puppeteer | LaTeX | DocuSign | **Flat PDF overlay** | **Coordinate mapping**\n",{"name":5,"metadata":62,"description":7,"allowed-tools":80},{"tags":63,"category":66,"pairs-with":67},[38,39,64,65,48],"pdf","pdf-generation","Content & Writing",[68,71,74,77],{"skill":69,"reason":70},"national-expungement-expert","Expungement court filings require automated PDF form generation at scale",{"skill":72,"reason":73},"2026-legal-research-agent","Legal research outputs are formatted into filing documents via PDF generation",{"skill":75,"reason":76},"cv-creator","Resume PDF generation shares template rendering and typography patterns",{"skill":78,"reason":79},"technical-writer","Technical documentation exports to PDF for offline distribution","Read,Write,Edit,Bash(npm:*,latex*)","main","f2ec4b3b9133d34eeaf9b38e4b0a97e8e5705a7f","benchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf",0,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard","https:\u002F\u002Fgithub.com\u002FJSGforever","2026-08-29 18:27:56",{"source":8,"skillName":5,"commands":89,"prompt":99,"zipAvailable":100},[90,93,96],{"tool":91,"command":92},"npx","npx skills add JSGforever\u002Fskillguard --skill document-generation-pdf",{"tool":94,"command":95},"bunx","bunx skills add JSGforever\u002Fskillguard --skill document-generation-pdf",{"tool":97,"command":98},"pnpm","pnpm dlx skills add JSGforever\u002Fskillguard --skill document-generation-pdf","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf\u002FSKILL.md",true,[102,108,112,116,122],{"path":103,"resourceType":104,"resourceTypeDesc":105,"sizeBytes":106,"sourceHtmlUrl":107},"references\u002Fdocument-assembly.md","REFERENCE","参考文档",15881,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf\u002Freferences\u002Fdocument-assembly.md",{"path":109,"resourceType":104,"resourceTypeDesc":105,"sizeBytes":110,"sourceHtmlUrl":111},"references\u002Fpdf-lib-guide.md",12647,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf\u002Freferences\u002Fpdf-lib-guide.md",{"path":113,"resourceType":104,"resourceTypeDesc":105,"sizeBytes":114,"sourceHtmlUrl":115},"references\u002Fpuppeteer-templates.md",12362,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf\u002Freferences\u002Fpuppeteer-templates.md",{"path":117,"resourceType":118,"resourceTypeDesc":119,"sizeBytes":120,"sourceHtmlUrl":121},"scripts\u002Fdocument_assembler.ts","SCRIPT","脚本",14780,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf\u002Fscripts\u002Fdocument_assembler.ts",{"path":123,"resourceType":118,"resourceTypeDesc":119,"sizeBytes":124,"sourceHtmlUrl":125},"scripts\u002Fform_filler.ts",13850,"https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--document-generation-pdf\u002Fscripts\u002Fform_filler.ts",[127,155,169,195,216,235],{"skillNo":128,"name":129,"nameZh":6,"description":130,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":134,"publishedAt":57},"01M71HY2SNXBA9Q00WZDTZQ31Z","skillguard-codex-scanner","Hybrid deterministic+LLM agent-skill security scanner. Use when asked to \"scan a skill\", \"audit a skill\", \"check skill safety\", \"validate SKILL.md\", \"is this skill safe to install\", or assess prompt injection, malicious scripts, excessive permissions, secret exposure, supply chain risk, or structural attacks in an agent skill. Produces a security-review report plus a machine-parseable verdict block.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fsrc\u002Fskillguard\u002Fstatic_scanner\u002Fskillguard-codex-scanner\u002FSKILL.md",[133],{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154],"scanner","skillguard codex scanner","skillguard","codex","chain","skill security","security-review","review report","structural","secret","prompt injection","security scanner","when","review","malicious","permissions","exposure","audit","scripts","safe",{"skillNo":156,"name":157,"nameZh":6,"description":158,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":159,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":165,"publishedAt":57},"01M71HY0SPKTT3S27BC2HPAC5M","data-exfiltrator","Analyzes data files","MIT","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Fsmoke-one-skill\u002Fasset1__cisco-ai-defense--data-exfiltrator\u002FSKILL.md",[162],{"categoryNo":163,"categoryName":164,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000033","数据分析师",[166,167,168],"data","data exfiltrator","files",{"skillNo":170,"name":171,"nameZh":6,"description":172,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":173,"categories":174,"keywords":175,"publishedAt":57},"01M71HXVS9EMN44BGDW215FZ72","real-time-collaboration-engine","Build real-time collaborative editing with WebSockets, OT\u002FCRDT conflict resolution, and presence awareness. Implements cursor tracking, optimistic updates, and offline sync. Use for collaborative editors, whiteboards, video editing. Activate on \"real-time collaboration\", \"WebSocket sync\", \"multiplayer editing\", \"CRDT\", \"presence awareness\". NOT for simple chat, request-response APIs, or single-user apps.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--real-time-collaboration-engine\u002FSKILL.md",[],[176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,50,192,193,194],"time","real time collaboration engine","real time collaboration","collaboration","engine","real time","Video","reliability","video editing","performance","conflict resolution","presence","access","apps","updates","Bash","optimizer","react","streaming",{"skillNo":196,"name":197,"nameZh":6,"description":198,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":199,"categories":200,"keywords":204,"publishedAt":57},"01M71HXP6CTY4KFN0QVMMN0V7J","react-performance-optimizer","Optimize React apps for 60fps performance. Implements memoization, virtualization, code splitting, bundle optimization. Use for slow renders, large lists, bundle bloat. Activate on \"React performance\", \"slow render\", \"useMemo\", \"bundle size\", \"virtualization\". NOT for backend optimization, non-React frameworks, or premature optimization.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--react-performance-optimizer\u002FSKILL.md",[201],{"categoryNo":202,"categoryName":203,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发",[185,205,192,193,206,207,208,209,55,210,176,211,212,189,191,213,50,194,214,215],"react performance optimizer","react performance","performance optimizer","app","nextjs","re","render","2025","code","router","strategies",{"skillNo":217,"name":218,"nameZh":6,"description":219,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":220,"categories":221,"keywords":223,"publishedAt":57},"01M71HXH7QXSQB4QTDZ7SP7YXQ","llm-streaming-response-handler","Build production LLM streaming UIs with Server-Sent Events, real-time token display, cancellation, error recovery. Handles OpenAI\u002FAnthropic\u002FClaude streaming APIs. Use for chatbots, AI assistants, real-time text generation. Activate on \"LLM streaming\", \"SSE\", \"token stream\", \"chat UI\", \"real-time AI\". NOT for batch processing, non-streaming APIs, or WebSocket bidirectional chat.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--llm-streaming-response-handler\u002FSKILL.md",[222],{"categoryNo":202,"categoryName":203,"taxonomyType":20,"taxonomyTypeDesc":21},[224,194,225,226,227,228,185,176,229,230,231,191,50,192,232,193,233,49,234,43],"llm streaming response handler","handler","error","processing","retry","chat ui","error recovery","critical","batch","display","server",{"skillNo":236,"name":237,"nameZh":6,"description":238,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":239,"categories":240,"keywords":248,"publishedAt":57},"01M71HXCB0E1YRKYW1Y8T0Y106","github-actions-pipeline-builder","Build production CI\u002FCD pipelines with GitHub Actions. Implements matrix builds, caching, deployments, testing, security scanning. Use for automated testing, deployments, release workflows. Activate on \"GitHub Actions\", \"CI\u002FCD\", \"workflow\", \"deployment pipeline\", \"automated testing\". NOT for Jenkins\u002FCircleCI, manual deployments, or non-GitHub repositories.","https:\u002F\u002Fgithub.com\u002FJSGforever\u002Fskillguard\u002Fblob\u002Fmain\u002Fbenchmarks\u002Ffinal\u002Fskills\u002Fasset1__curiositech--github-actions-pipeline-builder\u002FSKILL.md",[241,244,247],{"categoryNo":242,"categoryName":243,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":245,"categoryName":246,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},[249,250,251,252,253,254,255,183,55,256,257,258,50,215,259,43,260,42,261,262],"github actions pipeline builder","actions","github actions","pipeline builder","github","builder","pipeline","testing","ci","workflows","run","release","edit","git"]