| name | pdflib-impl-pdf-generation |
| description | Use when building complete PDF documents from scratch with pdf-lib — invoices, reports, certificates, or multi-page layouts. Prevents content overflow by providing page-break detection and content flow patterns. Covers multi-page generation, headers/footers, page numbering, table layouts, templates. Keywords: invoice, report, template, header, footer, page number, table, multi-page, generate, create PDF, generate invoice, build report, make PDF from scratch, PDF with tables.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires pdf-lib 1.x with TypeScript/JavaScript. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
PDF Generation Workflows with pdf-lib
Quick Reference
import { PDFDocument, StandardFonts, rgb, PageSizes } from 'pdf-lib';
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
const page = pdfDoc.addPage(PageSizes.A4);
const { width, height } = page.getSize();
page.drawText('Hello World', {
x: 50,
y: height - 50,
size: 18,
font,
color: rgb(0, 0, 0),
});
const pdfBytes = await pdfDoc.save();
Generation Lifecycle
| Step | Method | Async | Notes |
|---|
| 1. Create document | PDFDocument.create() | Yes | ALWAYS await |
| 2. Embed fonts | pdfDoc.embedFont() | Yes | Embed ONCE, reuse across pages |
| 3. Embed images | pdfDoc.embedPng() / embedJpg() | Yes | Embed ONCE, draw many times |
| 4. Add pages | pdfDoc.addPage() | No | Returns PDFPage |
| 5. Draw content | page.drawText() / drawImage() / drawRectangle() | No | All sync |
| 6. Set metadata | pdfDoc.setTitle() etc. | No | Optional |
| 7. Save | pdfDoc.save() | Yes | Returns Uint8Array |
Coordinate System Rules
- Origin: Bottom-left corner (0, 0)
- Y axis: Increases UPWARD (opposite of HTML/CSS)
- Units: PDF points (1 pt = 1/72 inch)
- ALWAYS use
height - offset to position content from the top
- NEVER assume y=0 is the top of the page
(0, height) -------- (width, height) <- top of page
| |
| CONTENT AREA |
| |
(0, 0) ----------- (width, 0) <- bottom of page
Decision Trees
Which Page Size?
Need a page size?
+-- Standard paper size?
| +-- International -> PageSizes.A4 (595.28 x 841.89 pt)
| +-- North America -> PageSizes.Letter (612 x 792 pt)
| +-- Legal paper -> PageSizes.Legal (612 x 1008 pt)
+-- Custom dimensions?
| +-- pdfDoc.addPage([widthPt, heightPt])
+-- No preference?
+-- pdfDoc.addPage() (default dimensions)
Which Output Format?
Saving the PDF?
+-- Write to file (Node.js) -> pdfDoc.save() -> fs.writeFileSync('out.pdf', pdfBytes)
+-- Send as HTTP response -> pdfDoc.save() -> Buffer.from(pdfBytes)
+-- Display in browser -> pdfDoc.saveAsBase64({ dataUri: true }) -> set as iframe src
+-- Store in database -> pdfDoc.save() -> store Uint8Array as blob
+-- Email attachment -> pdfDoc.saveAsBase64() -> attach as base64
Content Overflow?
Content might not fit on page?
+-- Track current Y position manually
+-- When Y < bottomMargin -> add new page, reset Y to top
+-- NEVER rely on pdf-lib to auto-paginate (it does NOT)
Core Patterns
Pattern 1: Multi-Page Document with Content Overflow
ALWAYS track the current Y position manually. pdf-lib has NO automatic pagination.
import { PDFDocument, StandardFonts, rgb, PageSizes } from 'pdf-lib';
interface PageLayout {
topMargin: number;
bottomMargin: number;
leftMargin: number;
rightMargin: number;
lineHeight: number;
fontSize: number;
}
async function generateMultiPageDocument(
lines: string[],
layout: PageLayout,
): Promise<Uint8Array> {
const pdfDoc = await PDFDocument.create();
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
let page = pdfDoc.addPage(PageSizes.A4);
const { width, height } = page.getSize();
let currentY = height - layout.topMargin;
for (const line of lines) {
(currentY < layout.) {
page = pdfDoc.(.);
currentY = height - layout.;
}
page.(line, {
: layout.,
: currentY,
: layout.,
font,
: (, , ),
});
currentY -= layout.;
}
pdfDoc.();
}
Pattern 2: Headers and Footers on Every Page
ALWAYS add headers and footers AFTER all content pages are created, by iterating over all pages.
function addHeadersAndFooters(
pdfDoc: PDFDocument,
font: PDFFont,
headerText: string,
): void {
const pages = pdfDoc.getPages();
const totalPages = pages.length;
for (let i = 0; i < totalPages; i++) {
const page = pages[i];
const { width, height } = page.getSize();
page.drawText(headerText, {
x: 50,
y: height - 30,
size: 10,
font,
color: rgb(0.4, 0.4, 0.4),
});
page.drawLine({
start: { x: 50, y: height - 35 },
end: { x: width - 50, y: height - 35 },
thickness: 0.5,
color: rgb(0.7, 0.7, 0.7),
});
pageNumText = ;
pageNumWidth = font.(pageNumText, );
page.(pageNumText, {
: width - - pageNumWidth,
: ,
: ,
font,
: (, , ),
});
page.({
: { : , : },
: { : width - , : },
: ,
: (, , ),
});
}
}
Pattern 3: Table-Like Layout
pdf-lib has NO table primitive. ALWAYS build tables manually with rectangles and text.
interface TableColumn {
header: string;
width: number;
align?: 'left' | 'right';
}
function drawTable(
page: PDFPage,
font: PDFFont,
columns: TableColumn[],
rows: string[][],
startX: number,
startY: number,
rowHeight: number,
fontSize: number,
): number {
const headerBg = rgb(0.9, 0.9, 0.9);
const borderColor = rgb(0.5, 0.5, 0.5);
const totalWidth = columns.reduce((sum, col) => sum + col.width, 0);
let currentY = startY;
page.drawRectangle({
x: startX,
y: currentY - rowHeight,
width: totalWidth,
height: rowHeight,
color: headerBg,
borderColor,
: ,
});
cellX = startX;
( col columns) {
page.(col., {
: cellX + ,
: currentY - rowHeight + ,
: fontSize,
font,
: (, , ),
});
cellX += col.;
}
currentY -= rowHeight;
( row rows) {
cellX = startX;
( c = ; c < columns.; c++) {
page.({
: cellX,
: currentY - rowHeight,
: columns[c].,
: rowHeight,
borderColor,
: ,
});
text = row[c] || ;
textX = cellX + ;
(columns[c]. === ) {
textWidth = font.(text, fontSize);
textX = cellX + columns[c]. - textWidth - ;
}
page.(text, {
: textX,
: currentY - rowHeight + ,
: fontSize,
font,
: (, , ),
});
cellX += columns[c].;
}
currentY -= rowHeight;
}
currentY;
}
Pattern 4: Centering Text
function drawCenteredText(
page: PDFPage,
font: PDFFont,
text: string,
y: number,
fontSize: number,
color: Color = rgb(0, 0, 0),
): void {
const textWidth = font.widthOfTextAtSize(text, fontSize);
const pageWidth = page.getWidth();
page.drawText(text, {
x: (pageWidth - textWidth) / 2,
y,
size: fontSize,
font,
color,
});
}
Pattern 5: Save and Output
const pdfBytes: Uint8Array = await pdfDoc.save();
import { writeFileSync } from 'fs';
writeFileSync('output.pdf', pdfBytes);
const base64String: string = await pdfDoc.saveAsBase64();
const dataUri: string = await pdfDoc.saveAsBase64({ dataUri: true });
Critical Rules
-
ALWAYS await async methods: create(), embedFont(), embedPng(), embedJpg(), save(), saveAsBase64() are ALL async. Forgetting await on embedFont() passes a Promise instead of a PDFFont to drawText().
-
ALWAYS embed fonts and images ONCE before the page loop: Embedding is expensive. Embed at document level, then reuse the returned PDFFont/PDFImage on every page.
-
ALWAYS track Y position manually for multi-page content: pdf-lib does NOT auto-paginate. When currentY < bottomMargin, call pdfDoc.addPage() and reset currentY.
-
ALWAYS use height - offset for top-down positioning: The PDF coordinate origin is bottom-left. To place content 50pt from the top: y = height - 50.
-
NEVER assume pdf-lib wraps text across pages: maxWidth wraps text within a single drawText() call on one page. It does NOT continue onto the next page.
-
ALWAYS add headers/footers AFTER generating all content pages: Iterate pdfDoc.getPages() to stamp headers, footers, and page numbers on every page, so totalPages is accurate.
-
NEVER use rgb values 0-255: pdf-lib rgb() takes values 0.0 to 1.0. rgb(255, 0, 0) does NOT create red.
-
ALWAYS set metadata for professional documents:
pdfDoc.setTitle('Invoice #12345', { showInWindowTitleBar: true });
pdfDoc.setAuthor('Company Name');
pdfDoc.setCreator('My Application');
pdfDoc.setCreationDate(new Date());
Reference Links