
Document Generation Pdf
- 206 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Generate polished PDF documents programmatically from templates, data, or markdown—including invoices, reports, certificates, and exportable user deliverables in web apps.
About
Skill for building reliable PDF document generation in applications: layout engines, templating, data binding, pagination, fonts, and export flows suitable for invoices, reports, certificates, and customer-facing downloads.
- Template-driven PDF layout and styling
- Dynamic data merge into printable pages
- Headers, footers, pagination, and tables
- Export from HTML, markdown, or structured JSON
- Batch and on-demand generation patterns
Document Generation Pdf by the numbers
- 206 all-time installs (skills.sh)
- Ranked #241 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill document-generation-pdfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 206 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Generate polished PDF documents programmatically from templates, data, or markdown—including invoices, reports, certificates, and exportable user deliverables in web apps.
Files
Document Generation & PDF Automation
Expert in generating, filling, and assembling PDF documents programmatically for legal, HR, and business workflows.
When to Use
✅ Use for:
- Legal form automation (expungement, immigration, contracts)
- Invoice/receipt generation at scale
- Certificate creation (completion, participation, awards)
- Contract assembly from templates
- Government form filling (IRS, court filings)
- Multi-document packet creation
❌ NOT for:
- Simple PDF viewing (use browser or pdf.js)
- Basic file conversion (use online tools)
- OCR text extraction (use Tesseract.js or AWS Textract)
- PDF editing by hand (use Adobe Acrobat)
---
Technology Selection
pdf-lib vs Puppeteer vs LaTeX
| Feature | pdf-lib | Puppeteer | LaTeX |
|---|---|---|---|
| Form filling | ✅ Native | ❌ Complex | ❌ No |
| Template rendering | ❌ No | ✅ HTML/CSS | ✅ Templates |
| Performance (1000 PDFs) | 5s | 60s | 30s |
| File size | Small | Medium | Small |
| Signature fields | ✅ Yes | ❌ No | ❌ No |
| Best for | Government forms | Invoices, reports | Academic papers |
Timeline:
- 2000s: LaTeX for academic documents
- 2010: PDFKit (Node.js) for generation
- 2017: Puppeteer for HTML → PDF
- 2019: pdf-lib for pure JS form filling
- 2024: pdf-lib is gold standard for forms
Decision tree:
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---
Common Anti-Patterns
Anti-Pattern 1: Using Puppeteer for Simple Form Filling
Novice thinking: "I'll use Puppeteer for everything, it's versatile"
Problem: 12x slower, 10x more memory, can't preserve form fields.
Wrong approach:
// ❌ 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;
}Why wrong:
- Browser overhead (200MB+ RAM per instance)
- Can't access native PDF form fields
- Loses interactive form capabilities
- 12x slower than pdf-lib
Correct approach:
// ✅ 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();
}Performance comparison (1000 PDFs):
- Puppeteer: 60 seconds, 4GB RAM
- pdf-lib: 5 seconds, 200MB RAM
Timeline context:
- 2017: Puppeteer released, everyone used it for PDFs
- 2019: pdf-lib released, proper form handling
- 2024: pdf-lib is standard for government forms
---
Anti-Pattern 2: Not Flattening Forms
Problem: Users can edit filled forms, causing data inconsistencies.
Wrong approach:
// ❌ 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!Why wrong:
- User can modify official documents
- Data doesn't match database
- Violates document integrity
Correct approach:
// ✅ 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 ✅When NOT to flatten:
- Draft documents (user needs to review/edit)
- Multi-step workflows (partial completion)
- Templates for users to fill manually
---
Anti-Pattern 3: Generating PDFs from HTML Without Page Breaks
Novice thinking: "HTML → PDF is easy with Puppeteer"
Problem: Content splits mid-sentence across pages.
Wrong approach:
// ❌ 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 sectionsCorrect approach:
// ✅ 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 print properties:
page-break-before: always- Force new page before elementpage-break-after: always- Force new page after elementpage-break-inside: avoid- Keep element together
---
Anti-Pattern 4: Not Handling Signature Fields
Problem: Signature fields aren't clickable in generated PDFs.
Wrong approach:
// ❌ 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 digitallyWhy wrong:
- Not legally recognized (just an image)
- Can't use DocuSign/Adobe Sign
- No signature metadata (who, when)
Correct approach 1: Create signature field (for 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 ✅Correct approach 2: DocuSign API integration
// ✅ 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 });
}---
Anti-Pattern 5: Storing Filled PDFs Without Encryption
Problem: Sensitive legal documents stored in plain text.
Wrong approach:
// ❌ 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 accessWhy wrong:
- Legal/medical data exposed
- HIPAA/GDPR violations
- Data breach liability
Correct approach 1: Encrypt at rest
// ✅ 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);Correct approach 2: Store in encrypted storage (S3 with SSE)
// ✅ 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()
});Compliance requirements:
- HIPAA: Encryption at rest + in transit, access logging
- GDPR: Right to deletion, data minimization
- State bar rules: Attorney-client privilege protection
---
Production Checklist
□ 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)---
When to Use vs Avoid
| Scenario | Appropriate? |
|---|---|
| Fill 50+ government forms | ✅ Yes - automate with pdf-lib |
| Generate invoices from template | ✅ Yes - Puppeteer from HTML |
| Create certificates at scale | ✅ Yes - pdf-lib or LaTeX |
| Assemble multi-doc packets | ✅ Yes - pdf-lib merge |
| View PDFs in browser | ❌ No - use pdf.js or browser |
| Edit PDF by hand | ❌ No - use Adobe Acrobat |
| Extract text with OCR | ❌ No - use Tesseract/Textract |
---
Flat PDF Overlay at Scale (Government Forms)
Many 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.
The Three-Tier Strategy
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)Anti-Pattern 6: Generating Forms from Scratch
Novice thinking: "The PDF doesn't have form fields, I'll just create a new PDF"
Problem: Courts reject non-official forms. Your custom layout won't match official documents.
Wrong approach:
// ❌ 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 filingCorrect approach: Draw on top of official template
// ✅ 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 });---
Measuring Coordinates at Scale
Problem: Manually measuring X,Y for 50 states × 10+ forms = impossible.
Solution 1: Visual Coordinate Picker Tool
Use pdf-coordinates - click on PDF to capture X,Y:
- Upload PDF, click on blank fields
- Select coordinate system: Bottom-Left (PDF standard)
- Export annotated PDF with coordinate labels
- Works offline, no data upload
Solution 2: JSON Configuration (Zerodha Pattern)
Define coordinates in JSON config, not code:
// 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
}
]
}Solution 3: Debug Grid Overlay
Add temporary grid during development:
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...
}---
Text Overflow Handling
Problem: Long names overflow into adjacent fields or get cut off.
Three overflow strategies:
// 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,
});Field input constraints - Prevent overflow at data collection:
export const FIELD_CONSTRAINTS = {
fullName: { maxLength: 35 },
county: { maxLength: 12 },
caseNumber: { maxLength: 12 },
agency: { maxLength: 28 },
charges: { maxLength: 45 },
} as const;---
Production Workflow for Multi-State Forms
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---
Legal Filing Requirements
Court e-filing systems require:
- ✅ Flattened forms (no interactive fields)
- ✅ No digital signature metadata (DocuSign breaks e-filing)
- ✅ Standard fonts embedded (Helvetica, Times)
- ✅ File under 10MB
- ✅ Exact match to official form layout
Flattening for courts:
// 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();---
Coordinate System Reference
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
/references/pdf-lib-guide.md- Form filling, field types, flattening, encryption/references/puppeteer-templates.md- HTML templates, page breaks, styling for print/references/document-assembly.md- Merging PDFs, packet creation, watermarks- pdf-coordinates - Visual X,Y coordinate picker
- Zerodha pdf_text_overlay - JSON config pattern
Scripts
scripts/form_filler.ts- Fill PDF forms from JSON data, batch processingscripts/document_assembler.ts- Merge multiple PDFs, add cover pages, watermarksscripts/generate-test-overlay-pdf.ts- Test overlay coordinates with debug grid
---
This skill guides: PDF generation | Form filling | Document automation | Digital signatures | pdf-lib | Puppeteer | LaTeX | DocuSign | Flat PDF overlay | Coordinate mapping
Document Assembly
Complete guide to merging PDFs, creating document packets, adding watermarks, and advanced PDF assembly techniques.
Use Cases
- Legal packets: Petition + Evidence + Affidavits → Single filing
- HR onboarding: Offer letter + Benefits + Handbook → Welcome packet
- Medical records: Lab results + Scans + Reports → Patient file
- Financial reports: Balance sheet + Income + Cash flow → Annual report
---
Basic Merging
Merge PDFs with pdf-lib
import { PDFDocument } from 'pdf-lib';
import * as fs from 'fs';
async function mergePDFs(inputPaths: string[], outputPath: string) {
const mergedPdf = await PDFDocument.create();
for (const inputPath of inputPaths) {
// Load PDF
const pdfBytes = fs.readFileSync(inputPath);
const pdf = await PDFDocument.load(pdfBytes);
// Copy all pages
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach(page => mergedPdf.addPage(page));
}
// Save
const pdfBytes = await mergedPdf.save();
fs.writeFileSync(outputPath, pdfBytes);
}
// Usage
await mergePDFs([
'petition.pdf',
'evidence.pdf',
'affidavit.pdf'
], 'filing_packet.pdf');---
Selective Page Merging
Copy Specific Pages
async function mergeSpecificPages(
sources: Array<{ path: string; pages: number[] }>,
outputPath: string
) {
const mergedPdf = await PDFDocument.create();
for (const source of sources) {
const pdfBytes = fs.readFileSync(source.path);
const pdf = await PDFDocument.load(pdfBytes);
// Convert to 0-based indices
const pageIndices = source.pages.map(p => p - 1);
// Copy selected pages
const copiedPages = await mergedPdf.copyPages(pdf, pageIndices);
copiedPages.forEach(page => mergedPdf.addPage(page));
}
const pdfBytes = await mergedPdf.save();
fs.writeFileSync(outputPath, pdfBytes);
}
// Usage
await mergeSpecificPages([
{ path: 'report.pdf', pages: [1, 2, 3] }, // First 3 pages
{ path: 'appendix.pdf', pages: [5, 6] }, // Pages 5-6
{ path: 'summary.pdf', pages: [1] } // Just page 1
], 'compiled.pdf');---
Cover Pages
Simple Cover Page
async function addCoverPage(
inputPath: string,
outputPath: string,
coverOptions: {
title: string;
subtitle?: string;
date?: string;
}
) {
const existingPdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Create new document with cover
const newPdf = await PDFDocument.create();
// Add cover page (US Letter: 612 x 792 pts)
const coverPage = newPdf.addPage([612, 792]);
// Embed font
const helveticaBold = await newPdf.embedFont(StandardFonts.HelveticaBold);
const helvetica = await newPdf.embedFont(StandardFonts.Helvetica);
// Title (centered, 48pt)
const titleSize = 48;
const titleWidth = helveticaBold.widthOfTextAtSize(coverOptions.title, titleSize);
coverPage.drawText(coverOptions.title, {
x: (612 - titleWidth) / 2,
y: 600,
size: titleSize,
font: helveticaBold,
color: rgb(0, 0, 0)
});
// Subtitle (if provided)
if (coverOptions.subtitle) {
const subtitleSize = 18;
const subtitleWidth = helvetica.widthOfTextAtSize(coverOptions.subtitle, subtitleSize);
coverPage.drawText(coverOptions.subtitle, {
x: (612 - subtitleWidth) / 2,
y: 550,
size: subtitleSize,
font: helvetica,
color: rgb(0.3, 0.3, 0.3)
});
}
// Date (bottom)
const date = coverOptions.date || new Date().toLocaleDateString();
const dateSize = 12;
const dateWidth = helvetica.widthOfTextAtSize(date, dateSize);
coverPage.drawText(date, {
x: (612 - dateWidth) / 2,
y: 50,
size: dateSize,
font: helvetica,
color: rgb(0.5, 0.5, 0.5)
});
// Copy original pages
const copiedPages = await newPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
copiedPages.forEach(page => newPdf.addPage(page));
// Save
const pdfBytes = await newPdf.save();
fs.writeFileSync(outputPath, pdfBytes);
}---
Watermarks
Text Watermark (All Pages)
async function addWatermark(
inputPath: string,
outputPath: string,
watermarkText: string,
options: {
opacity?: number;
rotation?: number;
fontSize?: number;
} = {}
) {
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const fontSize = options.fontSize || 60;
const opacity = options.opacity !== undefined ? options.opacity : 0.3;
const rotation = options.rotation !== undefined ? options.rotation : -45;
const pages = pdfDoc.getPages();
const textWidth = font.widthOfTextAtSize(watermarkText, fontSize);
pages.forEach(page => {
const { width, height } = page.getSize();
// Center watermark
const x = (width - textWidth) / 2;
const y = height / 2;
page.drawText(watermarkText, {
x,
y,
size: fontSize,
font,
color: rgb(0.7, 0.7, 0.7),
opacity,
rotate: degrees(rotation)
});
});
const outputBytes = await pdfDoc.save();
fs.writeFileSync(outputPath, outputBytes);
}
// Usage
await addWatermark(
'confidential.pdf',
'confidential_watermarked.pdf',
'CONFIDENTIAL',
{ opacity: 0.3, rotation: -45 }
);Image Watermark (Logo)
async function addLogoWatermark(
inputPath: string,
outputPath: string,
logoPath: string,
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'bottom-right'
) {
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
// Embed logo
const logoBytes = fs.readFileSync(logoPath);
const logoImage = logoPath.endsWith('.png')
? await pdfDoc.embedPng(logoBytes)
: await pdfDoc.embedJpg(logoBytes);
const logoWidth = 100;
const logoHeight = (logoImage.height / logoImage.width) * logoWidth;
const pages = pdfDoc.getPages();
pages.forEach(page => {
const { width, height } = page.getSize();
let x: number, y: number;
switch (position) {
case 'top-left':
x = 20;
y = height - logoHeight - 20;
break;
case 'top-right':
x = width - logoWidth - 20;
y = height - logoHeight - 20;
break;
case 'bottom-left':
x = 20;
y = 20;
break;
case 'bottom-right':
x = width - logoWidth - 20;
y = 20;
break;
}
page.drawImage(logoImage, {
x,
y,
width: logoWidth,
height: logoHeight,
opacity: 0.8
});
});
const outputBytes = await pdfDoc.save();
fs.writeFileSync(outputPath, outputBytes);
}---
Page Numbering
Add Page Numbers
async function addPageNumbers(
inputPath: string,
outputPath: string,
options: {
format?: string; // "{n}", "Page {n}", "{n} of {total}"
position?: 'bottom-center' | 'bottom-right' | 'top-center';
start?: number;
fontSize?: number;
} = {}
) {
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const fontSize = options.fontSize || 10;
const position = options.position || 'bottom-center';
const start = options.start || 1;
const format = options.format || 'Page {n} of {total}';
const pages = pdfDoc.getPages();
const total = pages.length;
pages.forEach((page, index) => {
const { width, height } = page.getSize();
const pageNumber = start + index;
const text = format
.replace('{n}', pageNumber.toString())
.replace('{total}', total.toString());
const textWidth = font.widthOfTextAtSize(text, fontSize);
let x: number, y: number;
switch (position) {
case 'bottom-center':
x = (width - textWidth) / 2;
y = 30;
break;
case 'bottom-right':
x = width - textWidth - 50;
y = 30;
break;
case 'top-center':
x = (width - textWidth) / 2;
y = height - 50;
break;
}
page.drawText(text, {
x,
y,
size: fontSize,
font,
color: rgb(0, 0, 0)
});
});
const outputBytes = await pdfDoc.save();
fs.writeFileSync(outputPath, outputBytes);
}---
Table of Contents
Generate TOC from Bookmarks
async function createTableOfContents(
inputPath: string,
outputPath: string
) {
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
// Create new PDF with TOC
const newPdf = await PDFDocument.create();
// Add TOC page
const tocPage = newPdf.addPage([612, 792]);
const font = await newPdf.embedFont(StandardFonts.Helvetica);
const boldFont = await newPdf.embedFont(StandardFonts.HelveticaBold);
let yPosition = 750;
// Title
tocPage.drawText('Table of Contents', {
x: 50,
y: yPosition,
size: 18,
font: boldFont
});
yPosition -= 40;
// List sections (example - in real implementation, extract from PDF structure)
const sections = [
{ title: 'Petition', page: 1 },
{ title: 'Evidence', page: 5 },
{ title: 'Affidavit', page: 12 },
{ title: 'Exhibits', page: 15 }
];
sections.forEach(section => {
tocPage.drawText(`${section.title}`, {
x: 50,
y: yPosition,
size: 12,
font
});
const pageText = `Page ${section.page}`;
const pageTextWidth = font.widthOfTextAtSize(pageText, 12);
tocPage.drawText(pageText, {
x: 500,
y: yPosition,
size: 12,
font
});
yPosition -= 20;
});
// Copy original pages
const copiedPages = await newPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
copiedPages.forEach(page => newPdf.addPage(page));
const outputBytes = await newPdf.save();
fs.writeFileSync(outputPath, outputBytes);
}---
Metadata Preservation
Copy Metadata from Source
async function mergeWithMetadata(
inputPaths: string[],
outputPath: string
) {
const mergedPdf = await PDFDocument.create();
// Load first PDF for metadata
const firstPdfBytes = fs.readFileSync(inputPaths[0]);
const firstPdf = await PDFDocument.load(firstPdfBytes);
// Copy metadata
if (firstPdf.getTitle()) mergedPdf.setTitle(firstPdf.getTitle());
if (firstPdf.getAuthor()) mergedPdf.setAuthor(firstPdf.getAuthor());
if (firstPdf.getSubject()) mergedPdf.setSubject(firstPdf.getSubject());
if (firstPdf.getCreator()) mergedPdf.setCreator(firstPdf.getCreator());
if (firstPdf.getProducer()) mergedPdf.setProducer(firstPdf.getProducer());
// Set new metadata
mergedPdf.setCreationDate(new Date());
mergedPdf.setModificationDate(new Date());
mergedPdf.setKeywords(['merged', 'document', 'packet']);
// Copy pages from all PDFs
for (const inputPath of inputPaths) {
const pdfBytes = fs.readFileSync(inputPath);
const pdf = await PDFDocument.load(pdfBytes);
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach(page => mergedPdf.addPage(page));
}
const pdfBytes = await mergedPdf.save();
fs.writeFileSync(outputPath, pdfBytes);
}---
Advanced Assembly
Complete Document Packet
interface PacketSection {
title: string;
file: string;
pages?: number[]; // If undefined, include all pages
}
async function createDocumentPacket(
sections: PacketSection[],
outputPath: string,
options: {
addCover?: boolean;
packetTitle?: string;
addTOC?: boolean;
addPageNumbers?: boolean;
watermark?: string;
} = {}
) {
const finalPdf = await PDFDocument.create();
// 1. Add cover page
if (options.addCover) {
const coverPage = finalPdf.addPage([612, 792]);
const font = await finalPdf.embedFont(StandardFonts.HelveticaBold);
const title = options.packetTitle || 'Document Packet';
const titleSize = 36;
const titleWidth = font.widthOfTextAtSize(title, titleSize);
coverPage.drawText(title, {
x: (612 - titleWidth) / 2,
y: 600,
size: titleSize,
font,
color: rgb(0, 0, 0)
});
}
// 2. Add TOC page
if (options.addTOC) {
const tocPage = finalPdf.addPage([612, 792]);
const font = await finalPdf.embedFont(StandardFonts.Helvetica);
const boldFont = await finalPdf.embedFont(StandardFonts.HelveticaBold);
let yPosition = 750;
tocPage.drawText('Table of Contents', {
x: 50,
y: yPosition,
size: 18,
font: boldFont
});
yPosition -= 40;
let currentPage = options.addCover ? 2 : 1;
if (options.addTOC) currentPage++;
sections.forEach(section => {
tocPage.drawText(section.title, {
x: 50,
y: yPosition,
size: 12,
font
});
tocPage.drawText(`Page ${currentPage}`, {
x: 500,
y: yPosition,
size: 12,
font
});
yPosition -= 20;
// Count pages for this section
const pdfBytes = fs.readFileSync(section.file);
const pdf = PDFDocument.load(pdfBytes);
// In real implementation, await and count pages
});
}
// 3. Add sections
for (const section of sections) {
const pdfBytes = fs.readFileSync(section.file);
const pdf = await PDFDocument.load(pdfBytes);
const pageIndices = section.pages
? section.pages.map(p => p - 1)
: pdf.getPageIndices();
const copiedPages = await finalPdf.copyPages(pdf, pageIndices);
copiedPages.forEach(page => finalPdf.addPage(page));
}
// 4. Add watermark
if (options.watermark) {
const font = await finalPdf.embedFont(StandardFonts.HelveticaBold);
const pages = finalPdf.getPages();
pages.forEach(page => {
const { width, height } = page.getSize();
const textWidth = font.widthOfTextAtSize(options.watermark!, 60);
page.drawText(options.watermark!, {
x: (width - textWidth) / 2,
y: height / 2,
size: 60,
font,
color: rgb(0.7, 0.7, 0.7),
opacity: 0.3,
rotate: degrees(-45)
});
});
}
// 5. Add page numbers
if (options.addPageNumbers) {
const font = await finalPdf.embedFont(StandardFonts.Helvetica);
const pages = finalPdf.getPages();
const total = pages.length;
pages.forEach((page, index) => {
const { width } = page.getSize();
const pageNumber = index + 1;
const text = `Page ${pageNumber} of ${total}`;
const textWidth = font.widthOfTextAtSize(text, 10);
page.drawText(text, {
x: (width - textWidth) / 2,
y: 30,
size: 10,
font,
color: rgb(0, 0, 0)
});
});
}
// 6. Save
const pdfBytes = await finalPdf.save();
fs.writeFileSync(outputPath, pdfBytes);
}
// Usage
await createDocumentPacket(
[
{ title: 'Petition', file: 'petition.pdf' },
{ title: 'Evidence', file: 'evidence.pdf', pages: [1, 2, 3] },
{ title: 'Affidavit', file: 'affidavit.pdf' }
],
'filing_packet.pdf',
{
addCover: true,
packetTitle: 'Expungement Filing Packet',
addTOC: true,
addPageNumbers: true,
watermark: 'DRAFT'
}
);---
Performance Tips
1. Reuse embedded fonts across pages 2. Batch page operations instead of one-by-one 3. Load PDFs once if using same source multiple times 4. Save at the end (don't save after each operation)
Benchmark (100 merges):
- Save after each: 45 seconds
- Save at end: 3 seconds
---
Resources
pdf-lib Guide
Complete guide to pdf-lib for form filling, field manipulation, encryption, and PDF generation.
Installation
npm install pdf-libLatest version: 1.17.1 (Jan 2024)
---
Basic Form Filling
Loading and Saving
import { PDFDocument } from 'pdf-lib';
import * as fs from 'fs';
// Load existing PDF
const existingPdfBytes = fs.readFileSync('template.pdf');
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Get form
const form = pdfDoc.getForm();
// ... fill fields ...
// Save
const pdfBytes = await pdfDoc.save();
fs.writeFileSync('filled.pdf', pdfBytes);---
Form Field Types
Text Fields
// Get text field
const nameField = form.getTextField('applicant_name');
// Set text
nameField.setText('John Doe');
// Get current value
const name = nameField.getText();
// Max length
const maxLength = nameField.getMaxLength();
// Set alignment
nameField.setAlignment(Alignment.Center); // Left, Center, Right
// Set font size
nameField.setFontSize(12);
// Enable multiline
nameField.enableMultiline();
// Disable read-only
nameField.enableReadOnly(false);Common text fields:
- Name, address, phone
- Dates (as text)
- Case numbers
- Comments/notes
---
Check Boxes
// Get checkbox
const consentCheckbox = form.getCheckBox('consent_checkbox');
// Check
consentCheckbox.check();
// Uncheck
consentCheckbox.uncheck();
// Get state
const isChecked = consentCheckbox.isChecked();Use cases:
- Yes/No questions
- Consent forms
- Attestations
- Option selections
---
Radio Groups
// Get radio group
const employmentStatus = form.getRadioGroup('employment_status');
// Get options
const options = employmentStatus.getOptions();
// Returns: ['employed', 'unemployed', 'self_employed', 'retired']
// Select option
employmentStatus.select('employed');
// Get selected
const selected = employmentStatus.getSelected();
// Clear selection
employmentStatus.clear();Use cases:
- Multiple choice (one answer)
- Yes/No/Maybe
- Option groups
---
Dropdown Fields
// Get dropdown
const stateDropdown = form.getDropdown('state');
// Get options
const options = stateDropdown.getOptions();
// Returns: ['CA', 'NY', 'TX', ...]
// Select single option
stateDropdown.select('CA');
// Select multiple (if multiselect enabled)
stateDropdown.select(['CA', 'NY']);
// Get selected
const selected = stateDropdown.getSelected();
// Returns: ['CA']
// Clear
stateDropdown.clear();
// Enable multiselect
stateDropdown.enableMultiselect();Use cases:
- State/country selection
- Predefined options
- Category selection
---
Form Flattening
Critical: Flatten forms after filling to prevent user edits.
// Fill form
form.getTextField('name').setText('John Doe');
form.getCheckBox('consent').check();
// Flatten (make non-editable)
form.flatten();
// Save
const pdfBytes = await pdfDoc.save();What flattening does: 1. Converts form fields to static text/graphics 2. Removes interactive field widgets 3. Preserves visual appearance 4. Makes document non-editable
When NOT to flatten:
- Draft documents (user needs to review/edit)
- Multi-step workflows (partial completion)
- Templates for manual filling
---
Inspecting Forms
List All Fields
const fields = form.getFields();
fields.forEach(field => {
const name = field.getName();
const type = field.constructor.name; // PDFTextField, PDFCheckBox, etc.
console.log(`${name} (${type})`);
// Field-specific details
if (field instanceof PDFTextField) {
console.log(` Value: "${field.getText()}"`);
console.log(` Max length: ${field.getMaxLength()}`);
}
});Get Field by Name
try {
const field = form.getField('field_name');
// Check type
if (field instanceof PDFTextField) {
// Text field operations
} else if (field instanceof PDFCheckBox) {
// Checkbox operations
} else if (field instanceof PDFDropdown) {
// Dropdown operations
} else if (field instanceof PDFRadioGroup) {
// Radio group operations
}
} catch (err) {
console.error('Field not found:', err.message);
}---
Creating Forms
Add Text Field
const textField = form.createTextField('new_field');
textField.addToPage(page, {
x: 100,
y: 500,
width: 200,
height: 30
});
textField.setText('Default value');
textField.setFontSize(12);Add Checkbox
const checkbox = form.createCheckBox('new_checkbox');
checkbox.addToPage(page, {
x: 100,
y: 450,
width: 20,
height: 20
});
checkbox.check();Add Dropdown
const dropdown = form.createDropdown('new_dropdown');
dropdown.addToPage(page, {
x: 100,
y: 400,
width: 150,
height: 25
});
dropdown.addOptions(['Option 1', 'Option 2', 'Option 3']);
dropdown.select('Option 1');---
PDF Encryption
Password Protection
const pdfBytes = await pdfDoc.save({
userPassword: 'user123', // Password to open PDF
ownerPassword: 'owner456', // Password to change permissions
permissions: {
printing: 'highResolution', // 'lowResolution' | 'highResolution' | false
modifying: false, // Prevent modifications
copying: false, // Prevent text/image copying
annotating: false, // Prevent annotations
fillingForms: false, // Prevent form filling
contentAccessibility: true, // Allow screen readers
documentAssembly: false // Prevent page insertion/deletion
}
});Permission levels:
'highResolution'- Allow high-quality printing'lowResolution'- Allow low-quality printing onlyfalse- Disallow
Use cases:
- Legal documents: Prevent editing
- Confidential files: Require password
- HIPAA compliance: Restrict copying
- Public records: Allow reading only
---
Fonts and Styling
Embed Standard Fonts
import { StandardFonts } from 'pdf-lib';
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const timesRoman = await pdfDoc.embedFont(StandardFonts.TimesRoman);
const courier = await pdfDoc.embedFont(StandardFonts.Courier);Standard fonts (always available):
- Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
- Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
- Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
Embed Custom Fonts
const fontBytes = fs.readFileSync('CustomFont.ttf');
const customFont = await pdfDoc.embedFont(fontBytes);
// Use in text field
textField.updateWidgets({
defaultAppearance: PDFTextField.createDefaultAppearance(customFont, 12)
});---
Adding Images
// Embed PNG
const pngImageBytes = fs.readFileSync('logo.png');
const pngImage = await pdfDoc.embedPng(pngImageBytes);
// Embed JPEG
const jpgImageBytes = fs.readFileSync('signature.jpg');
const jpgImage = await pdfDoc.embedJpg(jpgImageBytes);
// Draw on page
const page = pdfDoc.getPage(0);
page.drawImage(pngImage, {
x: 100,
y: 200,
width: 150,
height: 50
});Use cases:
- Company logos
- Signature images
- Diagrams
- Photos
---
Creating PDFs from Scratch
const pdfDoc = await PDFDocument.create();
// Add blank page (US Letter: 612 x 792 points)
const page = pdfDoc.addPage([612, 792]);
// Draw text
const helvetica = await pdfDoc.embedFont(StandardFonts.Helvetica);
page.drawText('Hello, World!', {
x: 50,
y: 750,
size: 24,
font: helvetica,
color: rgb(0, 0, 0)
});
// Draw rectangle
page.drawRectangle({
x: 50,
y: 700,
width: 200,
height: 100,
borderColor: rgb(0, 0, 0),
borderWidth: 2,
color: rgb(0.95, 0.95, 0.95)
});
// Save
const pdfBytes = await pdfDoc.save();Coordinate system:
- Origin (0, 0) is bottom-left
- X increases right
- Y increases up
- Units: points (1 pt = 1/72 inch)
---
Merging PDFs
const mergedPdf = await PDFDocument.create();
// Load source PDFs
const pdf1Bytes = fs.readFileSync('doc1.pdf');
const pdf1 = await PDFDocument.load(pdf1Bytes);
const pdf2Bytes = fs.readFileSync('doc2.pdf');
const pdf2 = await PDFDocument.load(pdf2Bytes);
// Copy pages
const copiedPages1 = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
copiedPages1.forEach(page => mergedPdf.addPage(page));
const copiedPages2 = await mergedPdf.copyPages(pdf2, pdf2.getPageIndices());
copiedPages2.forEach(page => mergedPdf.addPage(page));
// Save
const pdfBytes = await mergedPdf.save();---
Metadata
// Set metadata
pdfDoc.setTitle('Petition for Expungement');
pdfDoc.setAuthor('John Doe');
pdfDoc.setSubject('Legal Document');
pdfDoc.setKeywords(['expungement', 'legal', 'petition']);
pdfDoc.setProducer('Expungement Guide');
pdfDoc.setCreator('pdf-lib');
pdfDoc.setCreationDate(new Date());
pdfDoc.setModificationDate(new Date());
// Get metadata
const title = pdfDoc.getTitle();
const author = pdfDoc.getAuthor();---
Save Options
const pdfBytes = await pdfDoc.save({
// Optimize for file size
useObjectStreams: false,
// Encryption
userPassword: 'user123',
ownerPassword: 'owner456',
permissions: { /* ... */ },
// Update existing PDF (instead of creating new)
updateFieldAppearances: false,
// Add metadata
addDefaultPage: false
});---
Common Patterns
Batch Processing
async function batchFillForms(
templatePath: string,
dataFiles: string[],
outputDir: string
) {
const templateBytes = fs.readFileSync(templatePath);
for (const dataFile of dataFiles) {
// Load template
const pdfDoc = await PDFDocument.load(templateBytes);
const form = pdfDoc.getForm();
// Load data
const data = JSON.parse(fs.readFileSync(dataFile, 'utf-8'));
// Fill fields
for (const [field, value] of Object.entries(data)) {
try {
const fieldObj = form.getField(field);
if (fieldObj instanceof PDFTextField) {
fieldObj.setText(String(value));
} else if (fieldObj instanceof PDFCheckBox) {
if (value) fieldObj.check();
}
} catch (err) {
console.warn(`Field ${field} not found`);
}
}
// Flatten and save
form.flatten();
const pdfBytes = await pdfDoc.save();
const outputPath = path.join(outputDir, path.basename(dataFile, '.json') + '.pdf');
fs.writeFileSync(outputPath, pdfBytes);
}
}Conditional Fields
const form = pdfDoc.getForm();
// Show field if condition met
const hasPriorConvictions = data.prior_convictions === 'yes';
if (hasPriorConvictions) {
form.getTextField('conviction_details').setText(data.conviction_details);
form.getTextField('conviction_date').setText(data.conviction_date);
} else {
// Hide/disable fields
const detailsField = form.getTextField('conviction_details');
detailsField.setText('N/A');
detailsField.enableReadOnly();
}Signature Placeholders
// Add signature field
const signatureField = form.createTextField('applicant_signature');
signatureField.addToPage(page, {
x: 100,
y: 100,
width: 200,
height: 50
});
// Add border for visual indication
signatureField.updateWidgets({
borderWidth: 1,
borderColor: rgb(0, 0, 0)
});
// Add placeholder text
signatureField.setText('________________________________________');
// Mark as required
signatureField.enableRequired();---
Error Handling
try {
const pdfDoc = await PDFDocument.load(pdfBytes);
const form = pdfDoc.getForm();
try {
const field = form.getField('field_name');
// ... use field
} catch (err) {
console.error('Field not found:', err.message);
// Continue with other fields
}
const outputBytes = await pdfDoc.save();
} catch (err) {
console.error('Failed to load PDF:', err.message);
throw err;
}---
Performance Tips
1. Reuse loaded templates for batch processing 2. Don't flatten until the very end 3. Use object streams for smaller file size 4. Embed fonts once per document 5. Batch save operations (don't save after each field)
Benchmark (1000 forms):
- Reuse template: 5 seconds
- Reload template each time: 45 seconds
---
Resources
Puppeteer PDF Templates
Complete guide to generating PDFs from HTML using Puppeteer with templates, page breaks, and print styling.
When to Use Puppeteer
✅ Use Puppeteer for:
- Invoices with complex layouts
- Reports with charts/graphs
- Certificates with custom designs
- Documents requiring HTML/CSS flexibility
❌ Don't use Puppeteer for:
- Simple form filling (use pdf-lib)
- Government forms (use pdf-lib)
- Batch processing 1000+ documents (too slow)
---
Installation
npm install puppeteerLatest version: 21.6.1 (Jan 2024)
Bundle size: ~300MB (includes Chromium)
---
Basic Usage
import puppeteer from 'puppeteer';
async function generatePDF(htmlContent: string, outputPath: string) {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// Load HTML
await page.setContent(htmlContent, {
waitUntil: 'networkidle0' // Wait for resources to load
});
// Generate PDF
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
margin: {
top: '1in',
right: '1in',
bottom: '1in',
left: '1in'
}
});
await browser.close();
}---
Page Formats
// Standard formats
await page.pdf({
format: 'Letter', // 8.5" x 11" (US)
// or: 'A4', 'A3', 'Legal', 'Tabloid'
});
// Custom size
await page.pdf({
width: '8.5in',
height: '11in'
});
// Landscape
await page.pdf({
format: 'Letter',
landscape: true
});Common formats:
| Format | Size (inches) | Size (mm) |
|---|---|---|
| Letter | 8.5 × 11 | 216 × 279 |
| A4 | 8.27 × 11.69 | 210 × 297 |
| Legal | 8.5 × 14 | 216 × 356 |
| A3 | 11.69 × 16.54 | 297 × 420 |
---
Print CSS
Critical @media print Rule
<style>
@media print {
/* Print-specific styles */
.no-print {
display: none !important;
}
body {
font-size: 12pt;
color: black;
}
/* Force page breaks */
.page-break {
page-break-after: always;
}
/* Avoid breaking inside elements */
.avoid-break {
page-break-inside: avoid;
}
/* Keep headings with content */
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
page-break-inside: avoid;
}
/* Keep tables together */
table, figure {
page-break-inside: avoid;
}
/* Avoid breaking after first line of paragraph */
p {
orphans: 3;
widows: 3;
}
}
</style>---
Page Break Control
Force Page Break
<style>
@media print {
.page-break {
page-break-after: always;
}
}
</style>
<section>
<h1>Section 1</h1>
<p>Content...</p>
</section>
<div class="page-break"></div>
<section>
<h1>Section 2</h1>
<p>Content...</p>
</section>Avoid Page Break
<style>
@media print {
.avoid-break {
page-break-inside: avoid;
}
}
</style>
<section class="avoid-break">
<h2>Terms and Conditions</h2>
<ol>
<li>This entire section stays together</li>
<li>No page breaks in the middle</li>
</ol>
</section>Page Break Properties
@media print {
/* Before element */
.new-page-before {
page-break-before: always;
}
/* After element */
.new-page-after {
page-break-after: always;
}
/* Inside element */
.no-break-inside {
page-break-inside: avoid;
}
/* Control orphans/widows */
p {
orphans: 3; /* Min lines at bottom of page */
widows: 3; /* Min lines at top of page */
}
}---
Invoice Template
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
font-size: 12pt;
line-height: 1.5;
color: #333;
}
.invoice {
max-width: 8.5in;
margin: 0 auto;
padding: 0.5in;
}
.header {
display: flex;
justify-content: space-between;
margin-bottom: 1in;
border-bottom: 2px solid #000;
padding-bottom: 0.25in;
}
.company-info h1 {
font-size: 24pt;
margin-bottom: 0.1in;
}
.invoice-info {
text-align: right;
}
.invoice-number {
font-size: 18pt;
font-weight: bold;
margin-bottom: 0.1in;
}
.billing-info {
display: flex;
justify-content: space-between;
margin-bottom: 0.5in;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 0.5in;
}
.items-table th {
background: #f0f0f0;
padding: 0.1in;
text-align: left;
border-bottom: 2px solid #000;
}
.items-table td {
padding: 0.1in;
border-bottom: 1px solid #ddd;
}
.items-table .amount {
text-align: right;
}
.total-section {
margin-left: auto;
width: 3in;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 0.05in 0;
}
.total-row.grand-total {
font-size: 14pt;
font-weight: bold;
border-top: 2px solid #000;
padding-top: 0.1in;
margin-top: 0.1in;
}
.footer {
margin-top: 1in;
padding-top: 0.25in;
border-top: 1px solid #ddd;
font-size: 10pt;
color: #666;
}
@media print {
body {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.items-table {
page-break-inside: avoid;
}
}
</style>
</head>
<body>
<div class="invoice">
<div class="header">
<div class="company-info">
<h1>{{company_name}}</h1>
<p>{{company_address}}</p>
<p>{{company_phone}}</p>
<p>{{company_email}}</p>
</div>
<div class="invoice-info">
<div class="invoice-number">Invoice #{{invoice_number}}</div>
<p>Date: {{invoice_date}}</p>
<p>Due: {{due_date}}</p>
</div>
</div>
<div class="billing-info">
<div class="bill-to">
<strong>Bill To:</strong><br>
{{customer_name}}<br>
{{customer_address}}<br>
{{customer_phone}}
</div>
<div class="ship-to">
<strong>Ship To:</strong><br>
{{shipping_name}}<br>
{{shipping_address}}
</div>
</div>
<table class="items-table">
<thead>
<tr>
<th>Description</th>
<th>Qty</th>
<th class="amount">Unit Price</th>
<th class="amount">Amount</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{description}}</td>
<td>{{quantity}}</td>
<td class="amount">${{unit_price}}</td>
<td class="amount">${{amount}}</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="total-section">
<div class="total-row">
<span>Subtotal:</span>
<span>${{subtotal}}</span>
</div>
<div class="total-row">
<span>Tax ({{tax_rate}}%):</span>
<span>${{tax_amount}}</span>
</div>
<div class="total-row grand-total">
<span>Total:</span>
<span>${{total}}</span>
</div>
</div>
<div class="footer">
<p>Thank you for your business!</p>
<p>Payment due within 30 days. Please make checks payable to {{company_name}}.</p>
</div>
</div>
</body>
</html>---
Certificate Template
<!DOCTYPE html>
<html>
<head>
<style>
@page {
size: 11in 8.5in landscape;
margin: 0;
}
body {
font-family: 'Georgia', serif;
margin: 0;
padding: 0;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.certificate {
width: 11in;
height: 8.5in;
padding: 1in;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
border: 20px double #333;
background: white;
}
.title {
font-size: 48pt;
font-weight: bold;
color: #2c3e50;
margin-bottom: 0.5in;
text-transform: uppercase;
letter-spacing: 0.1in;
}
.subtitle {
font-size: 20pt;
color: #7f8c8d;
margin-bottom: 0.75in;
}
.recipient {
font-size: 36pt;
font-weight: bold;
color: #34495e;
margin-bottom: 0.5in;
font-style: italic;
}
.description {
font-size: 16pt;
color: #555;
max-width: 8in;
margin-bottom: 0.75in;
line-height: 1.6;
}
.footer {
display: flex;
justify-content: space-around;
width: 100%;
max-width: 8in;
margin-top: 0.5in;
}
.signature-line {
text-align: center;
}
.line {
border-top: 2px solid #333;
width: 2.5in;
margin-bottom: 0.1in;
}
@media print {
body {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
}
</style>
</head>
<body>
<div class="certificate">
<div class="title">Certificate of Completion</div>
<div class="subtitle">This is to certify that</div>
<div class="recipient">{{recipient_name}}</div>
<div class="description">
has successfully completed the {{course_name}} program
on {{completion_date}} with a score of {{score}}%.
</div>
<div class="footer">
<div class="signature-line">
<div class="line"></div>
<div>Instructor Signature</div>
<div>{{instructor_name}}</div>
</div>
<div class="signature-line">
<div class="line"></div>
<div>Date</div>
<div>{{issue_date}}</div>
</div>
</div>
</div>
</body>
</html>---
Template Engine Integration
Handlebars
import handlebars from 'handlebars';
async function generateFromTemplate(
templatePath: string,
data: any,
outputPath: string
) {
// Load template
const templateSource = fs.readFileSync(templatePath, 'utf-8');
const template = handlebars.compile(templateSource);
// Render with data
const html = template(data);
// Generate PDF
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.pdf({
path: outputPath,
format: 'Letter',
printBackground: true
});
await browser.close();
}---
Headers and Footers
await page.pdf({
path: outputPath,
format: 'Letter',
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 10px; text-align: center; width: 100%;">
<span>Company Name - Confidential</span>
</div>
`,
footerTemplate: `
<div style="font-size: 10px; text-align: center; width: 100%; margin: 0 1in;">
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
</div>
`,
margin: {
top: '0.75in',
bottom: '0.75in',
left: '1in',
right: '1in'
}
});Template variables:
{{pageNumber}}- Current page number{{totalPages}}- Total page count{{url}}- Document URL{{title}}- Document title{{date}}- Print date
---
Performance Optimization
Reuse Browser Instance
// ❌ SLOW - Launch browser for each PDF
for (const data of dataArray) {
const browser = await puppeteer.launch();
// ...
await browser.close();
}
// ✅ FAST - Reuse browser
const browser = await puppeteer.launch();
for (const data of dataArray) {
const page = await browser.newPage();
// ...
await page.close();
}
await browser.close();Benchmark:
- Reuse browser: 100 PDFs in 15 seconds
- New browser each time: 100 PDFs in 180 seconds
Wait Strategies
// Wait for all resources
await page.setContent(html, {
waitUntil: 'networkidle0' // Wait until no network activity
});
// Wait for specific element
await page.waitForSelector('.invoice-total');
// Wait for timeout (last resort)
await page.waitForTimeout(1000);---
Resources
#!/usr/bin/env node
/**
* Document Assembler
*
* Merge multiple PDFs, add cover pages, watermarks, and page numbers.
* Create document packets for legal/HR workflows.
*
* Usage:
* npx tsx document_assembler.ts merge <output.pdf> <input1.pdf> <input2.pdf> ...
* npx tsx document_assembler.ts cover <input.pdf> <output.pdf> --title "Title" --subtitle "Subtitle"
* npx tsx document_assembler.ts watermark <input.pdf> <output.pdf> --text "CONFIDENTIAL"
* npx tsx document_assembler.ts number <input.pdf> <output.pdf> --start 1
*
* Examples:
* npx tsx document_assembler.ts merge packet.pdf petition.pdf evidence.pdf affidavit.pdf
* npx tsx document_assembler.ts cover petition.pdf petition_with_cover.pdf --title "Petition for Expungement" --subtitle "Case #12345"
* npx tsx document_assembler.ts watermark draft.pdf draft_watermarked.pdf --text "DRAFT - NOT FOR FILING"
* npx tsx document_assembler.ts number brief.pdf brief_numbered.pdf --start 1 --format "Page {n} of {total}"
*/
import * as fs from 'fs';
import * as path from 'path';
import { PDFDocument, rgb, StandardFonts, degrees } from 'pdf-lib';
interface MergeOptions {
addBookmarks?: boolean;
preserveMetadata?: boolean;
}
interface CoverPageOptions {
title: string;
subtitle?: string;
date?: string;
caseNumber?: string;
attorney?: string;
backgroundColor?: { r: number; g: number; b: number };
}
interface WatermarkOptions {
text: string;
opacity?: number;
rotation?: number;
fontSize?: number;
color?: { r: number; g: number; b: number };
diagonal?: boolean;
}
interface PageNumberOptions {
start?: number;
format?: string; // e.g., "Page {n} of {total}", "{n}", "Page {n}"
position?: 'bottom-center' | 'bottom-right' | 'top-center' | 'top-right';
fontSize?: number;
}
class DocumentAssembler {
/**
* Merge multiple PDFs into one
*/
async merge(
outputPath: string,
inputPaths: string[],
options: MergeOptions = {}
): Promise<void> {
console.log(`\n📄 Merging ${inputPaths.length} PDFs...\n`);
const mergedPdf = await PDFDocument.create();
for (const inputPath of inputPaths) {
console.log(` Adding: ${path.basename(inputPath)}`);
const pdfBytes = fs.readFileSync(inputPath);
const pdf = await PDFDocument.load(pdfBytes);
// Copy all pages
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach(page => mergedPdf.addPage(page));
console.log(` ✓ ${pdf.getPageCount()} pages copied`);
}
// Preserve metadata from first document
if (options.preserveMetadata && inputPaths.length > 0) {
const firstPdfBytes = fs.readFileSync(inputPaths[0]);
const firstPdf = await PDFDocument.load(firstPdfBytes);
if (firstPdf.getTitle()) mergedPdf.setTitle(firstPdf.getTitle());
if (firstPdf.getAuthor()) mergedPdf.setAuthor(firstPdf.getAuthor());
if (firstPdf.getSubject()) mergedPdf.setSubject(firstPdf.getSubject());
if (firstPdf.getCreator()) mergedPdf.setCreator(firstPdf.getCreator());
}
// Save
const pdfBytes = await mergedPdf.save();
fs.writeFileSync(outputPath, pdfBytes);
console.log(`\n✅ Merged PDF saved: ${outputPath}`);
console.log(` Total pages: ${mergedPdf.getPageCount()}\n`);
}
/**
* Add cover page to PDF
*/
async addCoverPage(
inputPath: string,
outputPath: string,
options: CoverPageOptions
): Promise<void> {
console.log(`\n📄 Adding cover page...\n`);
const existingPdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// Create new document with cover page
const newPdf = await PDFDocument.create();
// Add cover page
const coverPage = newPdf.addPage([612, 792]); // 8.5" x 11" (US Letter)
// Background color
if (options.backgroundColor) {
const { r, g, b } = options.backgroundColor;
coverPage.drawRectangle({
x: 0,
y: 0,
width: coverPage.getWidth(),
height: coverPage.getHeight(),
color: rgb(r, g, b)
});
}
const helveticaBold = await newPdf.embedFont(StandardFonts.HelveticaBold);
const helvetica = await newPdf.embedFont(StandardFonts.Helvetica);
let yPosition = 650;
// Title
const titleSize = 32;
const titleWidth = helveticaBold.widthOfTextAtSize(options.title, titleSize);
coverPage.drawText(options.title, {
x: (coverPage.getWidth() - titleWidth) / 2,
y: yPosition,
size: titleSize,
font: helveticaBold,
color: rgb(0, 0, 0)
});
yPosition -= 60;
// Subtitle
if (options.subtitle) {
const subtitleSize = 18;
const subtitleWidth = helvetica.widthOfTextAtSize(options.subtitle, subtitleSize);
coverPage.drawText(options.subtitle, {
x: (coverPage.getWidth() - subtitleWidth) / 2,
y: yPosition,
size: subtitleSize,
font: helvetica,
color: rgb(0.3, 0.3, 0.3)
});
yPosition -= 40;
}
// Case number
if (options.caseNumber) {
const caseSize = 14;
const caseText = `Case No. ${options.caseNumber}`;
const caseWidth = helvetica.widthOfTextAtSize(caseText, caseSize);
coverPage.drawText(caseText, {
x: (coverPage.getWidth() - caseWidth) / 2,
y: yPosition,
size: caseSize,
font: helvetica,
color: rgb(0.4, 0.4, 0.4)
});
yPosition -= 100;
}
// Attorney/Author
if (options.attorney) {
const attorneySize = 12;
const attorneyText = `Prepared by: ${options.attorney}`;
const attorneyWidth = helvetica.widthOfTextAtSize(attorneyText, attorneySize);
coverPage.drawText(attorneyText, {
x: (coverPage.getWidth() - attorneyWidth) / 2,
y: yPosition,
size: attorneySize,
font: helvetica,
color: rgb(0.5, 0.5, 0.5)
});
}
// Date (bottom)
const date = options.date || new Date().toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const dateSize = 12;
const dateWidth = helvetica.widthOfTextAtSize(date, dateSize);
coverPage.drawText(date, {
x: (coverPage.getWidth() - dateWidth) / 2,
y: 50,
size: dateSize,
font: helvetica,
color: rgb(0.5, 0.5, 0.5)
});
// Copy original pages
const copiedPages = await newPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
copiedPages.forEach(page => newPdf.addPage(page));
// Save
const pdfBytes = await newPdf.save();
fs.writeFileSync(outputPath, pdfBytes);
console.log(`✅ Cover page added: ${outputPath}`);
console.log(` Total pages: ${newPdf.getPageCount()} (1 cover + ${pdfDoc.getPageCount()} original)\n`);
}
/**
* Add watermark to all pages
*/
async addWatermark(
inputPath: string,
outputPath: string,
options: WatermarkOptions
): Promise<void> {
console.log(`\n💧 Adding watermark: "${options.text}"\n`);
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
const fontSize = options.fontSize || 60;
const opacity = options.opacity !== undefined ? options.opacity : 0.3;
const color = options.color || { r: 0.7, g: 0.7, b: 0.7 };
const rotation = options.rotation !== undefined ? options.rotation : (options.diagonal ? -45 : 0);
const pages = pdfDoc.getPages();
const textWidth = font.widthOfTextAtSize(options.text, fontSize);
pages.forEach((page, index) => {
const { width, height } = page.getSize();
// Center position
const x = (width - textWidth) / 2;
const y = height / 2;
page.drawText(options.text, {
x,
y,
size: fontSize,
font,
color: rgb(color.r, color.g, color.b),
opacity,
rotate: degrees(rotation)
});
if ((index + 1) % 10 === 0) {
console.log(` ✓ Watermarked ${index + 1} pages...`);
}
});
console.log(` ✓ Watermarked all ${pages.length} pages`);
// Save
const outputBytes = await pdfDoc.save();
fs.writeFileSync(outputPath, outputBytes);
console.log(`\n✅ Watermarked PDF saved: ${outputPath}\n`);
}
/**
* Add page numbers
*/
async addPageNumbers(
inputPath: string,
outputPath: string,
options: PageNumberOptions = {}
): Promise<void> {
console.log(`\n🔢 Adding page numbers...\n`);
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const fontSize = options.fontSize || 10;
const position = options.position || 'bottom-center';
const start = options.start || 1;
const format = options.format || 'Page {n} of {total}';
const pages = pdfDoc.getPages();
const total = pages.length;
pages.forEach((page, index) => {
const { width, height } = page.getSize();
const pageNumber = start + index;
// Format text
const text = format
.replace('{n}', pageNumber.toString())
.replace('{total}', total.toString());
const textWidth = font.widthOfTextAtSize(text, fontSize);
// Calculate position
let x: number, y: number;
switch (position) {
case 'bottom-center':
x = (width - textWidth) / 2;
y = 30;
break;
case 'bottom-right':
x = width - textWidth - 50;
y = 30;
break;
case 'top-center':
x = (width - textWidth) / 2;
y = height - 50;
break;
case 'top-right':
x = width - textWidth - 50;
y = height - 50;
break;
default:
x = (width - textWidth) / 2;
y = 30;
}
page.drawText(text, {
x,
y,
size: fontSize,
font,
color: rgb(0, 0, 0)
});
if ((index + 1) % 10 === 0) {
console.log(` ✓ Numbered ${index + 1} pages...`);
}
});
console.log(` ✓ Numbered all ${pages.length} pages`);
// Save
const outputBytes = await pdfDoc.save();
fs.writeFileSync(outputPath, outputBytes);
console.log(`\n✅ Numbered PDF saved: ${outputPath}\n`);
}
}
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
const assembler = new DocumentAssembler();
switch (command) {
case 'merge': {
if (args.length < 3) {
console.error('Usage: npx tsx document_assembler.ts merge <output.pdf> <input1.pdf> <input2.pdf> ...');
process.exit(1);
}
const outputPath = args[1];
const inputPaths = args.slice(2);
assembler.merge(outputPath, inputPaths, { preserveMetadata: true });
break;
}
case 'cover': {
if (args.length < 3) {
console.error('Usage: npx tsx document_assembler.ts cover <input.pdf> <output.pdf> --title "Title" [--subtitle "Subtitle"] [--case "12345"]');
process.exit(1);
}
const inputPath = args[1];
const outputPath = args[2];
// Parse options
const options: CoverPageOptions = { title: '' };
for (let i = 3; i < args.length; i += 2) {
const flag = args[i];
const value = args[i + 1];
switch (flag) {
case '--title':
options.title = value;
break;
case '--subtitle':
options.subtitle = value;
break;
case '--case':
options.caseNumber = value;
break;
case '--attorney':
options.attorney = value;
break;
case '--date':
options.date = value;
break;
}
}
if (!options.title) {
console.error('Error: --title is required');
process.exit(1);
}
assembler.addCoverPage(inputPath, outputPath, options);
break;
}
case 'watermark': {
if (args.length < 3) {
console.error('Usage: npx tsx document_assembler.ts watermark <input.pdf> <output.pdf> --text "TEXT" [--opacity 0.3] [--diagonal]');
process.exit(1);
}
const inputPath = args[1];
const outputPath = args[2];
// Parse options
const options: WatermarkOptions = { text: '' };
for (let i = 3; i < args.length; i++) {
const flag = args[i];
switch (flag) {
case '--text':
options.text = args[++i];
break;
case '--opacity':
options.opacity = parseFloat(args[++i]);
break;
case '--rotation':
options.rotation = parseInt(args[++i]);
break;
case '--diagonal':
options.diagonal = true;
break;
case '--size':
options.fontSize = parseInt(args[++i]);
break;
}
}
if (!options.text) {
console.error('Error: --text is required');
process.exit(1);
}
assembler.addWatermark(inputPath, outputPath, options);
break;
}
case 'number': {
if (args.length < 3) {
console.error('Usage: npx tsx document_assembler.ts number <input.pdf> <output.pdf> [--start 1] [--format "Page {n} of {total}"] [--position bottom-center]');
process.exit(1);
}
const inputPath = args[1];
const outputPath = args[2];
// Parse options
const options: PageNumberOptions = {};
for (let i = 3; i < args.length; i += 2) {
const flag = args[i];
const value = args[i + 1];
switch (flag) {
case '--start':
options.start = parseInt(value);
break;
case '--format':
options.format = value;
break;
case '--position':
options.position = value as any;
break;
case '--size':
options.fontSize = parseInt(value);
break;
}
}
assembler.addPageNumbers(inputPath, outputPath, options);
break;
}
default:
console.error('Unknown command. Available commands:');
console.error(' merge <output.pdf> <input1.pdf> <input2.pdf> ...');
console.error(' cover <input.pdf> <output.pdf> --title "Title" [options]');
console.error(' watermark <input.pdf> <output.pdf> --text "TEXT" [options]');
console.error(' number <input.pdf> <output.pdf> [options]');
process.exit(1);
}
}
export { DocumentAssembler, MergeOptions, CoverPageOptions, WatermarkOptions, PageNumberOptions };
#!/usr/bin/env node
/**
* PDF Form Filler
*
* Fill PDF forms from JSON data with validation and batch processing.
* Uses pdf-lib for native PDF form field manipulation.
*
* Usage:
* npx tsx form_filler.ts fill <template.pdf> <data.json> <output.pdf>
* npx tsx form_filler.ts batch <template.pdf> <data-dir/> <output-dir/>
* npx tsx form_filler.ts inspect <form.pdf>
*
* Examples:
* npx tsx form_filler.ts fill petition_template.pdf case_123.json petition_filled.pdf
* npx tsx form_filler.ts batch petition_template.pdf ./case_data/ ./filled_petitions/
* npx tsx form_filler.ts inspect petition_template.pdf
*/
import * as fs from 'fs';
import * as path from 'path';
import { PDFDocument, PDFTextField, PDFCheckBox, PDFDropdown, PDFRadioGroup } from 'pdf-lib';
interface FormFieldData {
name: string;
type: 'text' | 'checkbox' | 'dropdown' | 'radio';
value: string | boolean | string[];
required?: boolean;
}
interface FormData {
fields: Record<string, any>;
options?: {
flatten?: boolean;
encrypt?: boolean;
password?: string;
};
}
interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
interface FilledFormResult {
success: boolean;
outputPath?: string;
error?: string;
validation?: ValidationResult;
fieldsFilled: number;
fieldsTotal: number;
}
class FormFiller {
/**
* Fill a single PDF form
*/
async fill(
templatePath: string,
data: FormData,
outputPath: string
): Promise<FilledFormResult> {
try {
console.log(`\n📄 Filling form: ${path.basename(templatePath)}`);
// Load PDF
const existingPdfBytes = fs.readFileSync(templatePath);
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const form = pdfDoc.getForm();
// Get all fields
const fields = form.getFields();
console.log(`Found ${fields.length} form fields\n`);
// Validate data
const validation = this.validate(form, data);
if (!validation.valid) {
console.error('❌ Validation failed:');
validation.errors.forEach(err => console.error(` - ${err}`));
return {
success: false,
error: 'Validation failed',
validation,
fieldsFilled: 0,
fieldsTotal: fields.length
};
}
if (validation.warnings.length > 0) {
console.warn('⚠️ Warnings:');
validation.warnings.forEach(warn => console.warn(` - ${warn}`));
}
// Fill fields
let fieldsFilled = 0;
for (const [fieldName, fieldValue] of Object.entries(data.fields)) {
try {
const field = form.getField(fieldName);
if (field instanceof PDFTextField) {
field.setText(String(fieldValue));
console.log(`✓ Text field: ${fieldName} = "${fieldValue}"`);
fieldsFilled++;
} else if (field instanceof PDFCheckBox) {
if (fieldValue === true || fieldValue === 'true' || fieldValue === 'yes') {
field.check();
console.log(`✓ Checkbox: ${fieldName} = checked`);
} else {
field.uncheck();
console.log(`✓ Checkbox: ${fieldName} = unchecked`);
}
fieldsFilled++;
} else if (field instanceof PDFDropdown) {
field.select(String(fieldValue));
console.log(`✓ Dropdown: ${fieldName} = "${fieldValue}"`);
fieldsFilled++;
} else if (field instanceof PDFRadioGroup) {
field.select(String(fieldValue));
console.log(`✓ Radio: ${fieldName} = "${fieldValue}"`);
fieldsFilled++;
}
} catch (err) {
console.warn(`⚠️ Could not fill field "${fieldName}": ${err.message}`);
}
}
// Flatten form (make non-editable)
if (data.options?.flatten !== false) {
form.flatten();
console.log('\n🔒 Form flattened (fields are now non-editable)');
}
// Save PDF
const pdfBytes = await pdfDoc.save({
useObjectStreams: false,
...(data.options?.encrypt && {
userPassword: data.options.password || this.generatePassword(),
ownerPassword: process.env.PDF_OWNER_PASSWORD || 'owner',
permissions: {
printing: 'highResolution',
modifying: false,
copying: false,
annotating: false,
fillingForms: false,
contentAccessibility: true,
documentAssembly: false
}
})
});
fs.writeFileSync(outputPath, pdfBytes);
console.log(`\n✅ Filled form saved to: ${outputPath}`);
console.log(` Fields filled: ${fieldsFilled}/${fields.length}`);
return {
success: true,
outputPath,
validation,
fieldsFilled,
fieldsTotal: fields.length
};
} catch (error) {
console.error(`\n❌ Error filling form: ${error.message}`);
return {
success: false,
error: error.message,
fieldsFilled: 0,
fieldsTotal: 0
};
}
}
/**
* Batch fill multiple forms
*/
async batch(
templatePath: string,
dataDir: string,
outputDir: string
): Promise<void> {
console.log(`\n📦 Batch filling forms...`);
console.log(`Template: ${templatePath}`);
console.log(`Data dir: ${dataDir}`);
console.log(`Output dir: ${outputDir}\n`);
// Ensure output directory exists
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Find all JSON files
const dataFiles = fs.readdirSync(dataDir).filter(f => f.endsWith('.json'));
console.log(`Found ${dataFiles.length} data files\n`);
const results: FilledFormResult[] = [];
const startTime = Date.now();
for (const dataFile of dataFiles) {
const dataPath = path.join(dataDir, dataFile);
const outputPath = path.join(
outputDir,
dataFile.replace('.json', '.pdf')
);
const data: FormData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
const result = await this.fill(templatePath, data, outputPath);
results.push(result);
console.log('─'.repeat(60));
}
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(`\n📊 Batch Results:`);
console.log(` Total: ${results.length}`);
console.log(` ✅ Success: ${successful}`);
console.log(` ❌ Failed: ${failed}`);
console.log(` ⏱️ Time: ${elapsed}s`);
console.log(` 📈 Rate: ${(results.length / parseFloat(elapsed)).toFixed(1)} forms/sec\n`);
if (failed > 0) {
console.log('Failed forms:');
results
.filter(r => !r.success)
.forEach(r => console.log(` - ${r.error}`));
}
}
/**
* Inspect PDF form fields
*/
async inspect(pdfPath: string): Promise<void> {
console.log(`\n🔍 Inspecting form: ${path.basename(pdfPath)}\n`);
const pdfBytes = fs.readFileSync(pdfPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const form = pdfDoc.getForm();
const fields = form.getFields();
console.log(`Found ${fields.length} form fields:\n`);
fields.forEach((field, index) => {
const name = field.getName();
const type = this.getFieldType(field);
console.log(`${index + 1}. ${name}`);
console.log(` Type: ${type}`);
// Field-specific details
if (field instanceof PDFTextField) {
const value = field.getText();
const maxLength = field.getMaxLength();
console.log(` Value: "${value || '(empty)'}"`);
if (maxLength) {
console.log(` Max length: ${maxLength}`);
}
} else if (field instanceof PDFCheckBox) {
const checked = field.isChecked();
console.log(` Checked: ${checked}`);
} else if (field instanceof PDFDropdown) {
const options = field.getOptions();
const selected = field.getSelected();
console.log(` Options: [${options.join(', ')}]`);
console.log(` Selected: ${selected.join(', ') || '(none)'}`);
} else if (field instanceof PDFRadioGroup) {
const options = field.getOptions();
const selected = field.getSelected();
console.log(` Options: [${options.join(', ')}]`);
console.log(` Selected: ${selected || '(none)'}`);
}
console.log('');
});
// Generate sample JSON
console.log('📝 Sample JSON structure:\n');
const sampleData: FormData = {
fields: {},
options: {
flatten: true,
encrypt: false
}
};
fields.forEach(field => {
const name = field.getName();
if (field instanceof PDFTextField) {
sampleData.fields[name] = 'Sample text';
} else if (field instanceof PDFCheckBox) {
sampleData.fields[name] = false;
} else if (field instanceof PDFDropdown) {
const options = field.getOptions();
sampleData.fields[name] = options[0] || 'option';
} else if (field instanceof PDFRadioGroup) {
const options = field.getOptions();
sampleData.fields[name] = options[0] || 'option';
}
});
console.log(JSON.stringify(sampleData, null, 2));
console.log('\n');
}
/**
* Validate form data
*/
private validate(form: any, data: FormData): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
const formFieldNames = form.getFields().map((f: any) => f.getName());
// Check for missing required fields
for (const fieldName of formFieldNames) {
if (!(fieldName in data.fields)) {
warnings.push(`Field "${fieldName}" not provided in data`);
}
}
// Check for unknown fields
for (const fieldName of Object.keys(data.fields)) {
if (!formFieldNames.includes(fieldName)) {
warnings.push(`Field "${fieldName}" not found in PDF form`);
}
}
// Type validation
for (const [fieldName, fieldValue] of Object.entries(data.fields)) {
try {
const field = form.getField(fieldName);
if (field instanceof PDFTextField) {
if (typeof fieldValue !== 'string' && typeof fieldValue !== 'number') {
errors.push(`Field "${fieldName}" expects string/number, got ${typeof fieldValue}`);
}
} else if (field instanceof PDFCheckBox) {
if (typeof fieldValue !== 'boolean' && fieldValue !== 'true' && fieldValue !== 'false') {
warnings.push(`Field "${fieldName}" is checkbox, value "${fieldValue}" will be converted to boolean`);
}
} else if (field instanceof PDFDropdown) {
const options = field.getOptions();
if (!options.includes(String(fieldValue))) {
errors.push(`Field "${fieldName}" value "${fieldValue}" not in options: [${options.join(', ')}]`);
}
} else if (field instanceof PDFRadioGroup) {
const options = field.getOptions();
if (!options.includes(String(fieldValue))) {
errors.push(`Field "${fieldName}" value "${fieldValue}" not in options: [${options.join(', ')}]`);
}
}
} catch (err) {
// Field not found - already warned above
}
}
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Get field type as string
*/
private getFieldType(field: any): string {
if (field instanceof PDFTextField) return 'Text';
if (field instanceof PDFCheckBox) return 'Checkbox';
if (field instanceof PDFDropdown) return 'Dropdown';
if (field instanceof PDFRadioGroup) return 'Radio';
return 'Unknown';
}
/**
* Generate secure random password
*/
private generatePassword(): string {
const length = 16;
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
}
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
const filler = new FormFiller();
switch (command) {
case 'fill': {
if (args.length < 4) {
console.error('Usage: npx tsx form_filler.ts fill <template.pdf> <data.json> <output.pdf>');
process.exit(1);
}
const templatePath = args[1];
const dataPath = args[2];
const outputPath = args[3];
const data: FormData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'));
filler.fill(templatePath, data, outputPath);
break;
}
case 'batch': {
if (args.length < 4) {
console.error('Usage: npx tsx form_filler.ts batch <template.pdf> <data-dir/> <output-dir/>');
process.exit(1);
}
const templatePath = args[1];
const dataDir = args[2];
const outputDir = args[3];
filler.batch(templatePath, dataDir, outputDir);
break;
}
case 'inspect': {
if (args.length < 2) {
console.error('Usage: npx tsx form_filler.ts inspect <form.pdf>');
process.exit(1);
}
const pdfPath = args[1];
filler.inspect(pdfPath);
break;
}
default:
console.error('Unknown command. Available commands:');
console.error(' fill <template.pdf> <data.json> <output.pdf>');
console.error(' batch <template.pdf> <data-dir/> <output-dir/>');
console.error(' inspect <form.pdf>');
process.exit(1);
}
}
export { FormFiller, FormData, FormFieldData, ValidationResult, FilledFormResult };