Examples
These are complete TypeScript examples that complement the docs playground.
Each one is a complete, self-contained program: it imports from
@crstnmac/sempdf, builds a document, and export defaults it. Paste any of
them into the playground (or a local script) and run it as-is.
The snippets below are embedded directly from the repository's sample files.
Run these
Open the playground to experiment with Document JSON, or copy a
TypeScript snippet into your own project. For non-browser usage,
swap export default doc; for await writeFile("out.pdf", doc.toUint8Array());.
Rich text & layout
Document-wide text defaults, registered font families, mm() units, inline
rich text with underline/strike, justified alignment, vertical alignment,
shape helpers (rect, circle), named colors, and richParagraph template
blocks. The flowing portion is rendered with renderTemplate().
import { createDocument, rgb, mm } from "@crstnmac/sempdf";
const TABLE_BORDER = rgb(0.72, 0.76, 0.82);
const TABLE_HEADER = rgb(0.92, 0.94, 0.98);
// Document-wide text defaults are inherited by every text call.
const doc = createDocument({
title: "Rich Text & Layout",
language: "en-US",
defaults: { font: "Helvetica", fontSize: 12, color: rgb(0.15, 0.15, 0.2) }
});
// Register a font family so bold/italic resolve by flag. Faces may be built-in
// names (shown here), embedded font handles, or raw font bytes.
const sans = doc.registerFontFamily("Sans", {
regular: "Helvetica",
bold: "Helvetica-Bold",
italic: "Helvetica-Oblique",
boldItalic: "Helvetica-BoldOblique"
});
const page = doc.addPage({ size: "A4" });
// mm() units, chained calls, bold/italic flags, and underline/strike decoration.
page
.text("Rich text & layout", { x: mm(20), y: mm(272), font: sans, bold: true, fontSize: 22 })
.text("Authored with mm() units and chained calls.", { x: mm(20), y: mm(264), italic: true, color: "navy" })
.text("Underlined and struck-through runs.", { x: mm(20), y: mm(257), underline: true, strike: true, fontSize: 11 });
// Inline rich text - mixed styles and an inline link wrapping together.
page.richText(
[
{ text: "Inline rich text mixes " },
{ text: "bold", bold: true },
{ text: ", " },
{ text: "italic", italic: true },
{ text: ", " },
{ text: "color", color: rgb(0.80, 0.10, 0.10) },
{ text: ", " },
{ text: "underline", underline: true },
{ text: ", and a " },
{ text: "clickable link", color: rgb(0.10, 0.30, 0.80), link: "https://sempdf.criston.dev" },
{ text: " on the same wrapping lines." }
],
{ x: mm(20), y: mm(247), width: mm(170), fontSize: 13, lineHeight: 18 }
);
// Justified alignment for rich text.
page.richText(
[{ text: "Justify spreads words across the full measure so both edges of the column line up cleanly along its width." }],
{ x: mm(20), y: mm(227), width: mm(170), fontSize: 12, align: "justify" }
);
// Vertical alignment inside a fixed-height box (framed with a 1.3 shape helper).
page.rect(mm(20), mm(165), mm(80), mm(40), { stroke: rgb(0.7, 0.7, 0.7), lineWidth: 0.5 });
page.textBlock("Centered in a 40mm box (verticalAlign: middle).", {
x: mm(22), y: mm(203), width: mm(76), height: mm(36),
verticalAlign: "middle", align: "center", fontSize: 12, lineHeight: 16
});
// Shape helpers: a labelled circle alongside the box.
page.circle(mm(140), mm(185), mm(18), { fill: "teal" });
page.text("circle()", { x: mm(130), y: mm(150), fontSize: 11, color: rgb(0.45, 0.45, 0.45) });
// Per-cell vertical alignment in a table row.
page.table(
[[
{ text: "A long cell that wraps onto a few lines inside its column to make the row tall." },
{ text: "top", verticalAlign: "top", header: true },
{ text: "middle", verticalAlign: "middle", header: true },
{ text: "bottom", verticalAlign: "bottom", header: true }
]],
{
x: mm(20),
y: mm(120),
width: mm(170),
columnWidths: [mm(85), mm(28), mm(28), mm(29)],
fontSize: 11,
cellPadding: 4,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER
}
);
// richParagraph template block (rendered on a second page).
doc.renderTemplate({
page: { size: "A4", margin: 56 },
styles: { heading1: { fontSize: 22, color: rgb(0.11, 0.22, 0.45) } },
blocks: [
{ type: "heading", text: "richParagraph template block", options: { level: 1 } },
{
type: "richParagraph",
runs: [
{ text: "Status: " },
{ text: "PASSED", bold: true, color: rgb(0, 0.5, 0) },
{ text: " - see the " },
{ text: "report", link: "https://sempdf.criston.dev", color: rgb(0.1, 0.3, 0.8) },
{ text: " for details." }
]
}
]
});
export default doc;
Text formatting
Superscript/subscript and baseline rise, render modes (stroke outline,
fillStroke, invisible), horizontalScaling to condense/expand glyphs, and
inline highlight — shown on template paragraphs and rich-text runs. See
Text and Fonts.
import { createDocument, rgb } from "@crstnmac/sempdf";
// Superscript/subscript, baseline rise, render modes (outline/invisible),
// horizontal scaling, and inline highlight — on both template paragraphs and
// per-run richParagraph content.
const doc = createDocument({
title: "Text Formatting",
language: "en-US",
defaults: { font: "Helvetica", fontSize: 13, color: rgb(0.15, 0.15, 0.2) }
});
doc.renderTemplate({
page: { size: "A4", margin: 56 },
styles: {
heading2: { fontSize: 11, color: "navy", marginTop: 12, marginBottom: 8 }
},
blocks: [
{ type: "heading", text: "Superscript & subscript", options: { level: 1 } },
{
type: "richParagraph",
runs: [
{ text: "Einstein: E = mc" },
{ text: "2", superscript: true },
{ text: " Water: H" },
{ text: "2", subscript: true },
{ text: "O" },
{ text: " Footnote" },
{ text: "12", superscript: true }
],
options: { fontSize: 15, marginBottom: 14 }
},
{ type: "heading", text: "Render modes (fill / stroke / fillStroke)", options: { level: 2 } },
{
type: "richParagraph",
runs: [
{ text: "Filled ", fontSize: 26, bold: true },
{
text: "Outline ",
fontSize: 26,
bold: true,
renderMode: "stroke",
strokeWidth: 0.6,
strokeColor: rgb(0.8, 0.1, 0.1)
},
{
text: "Both",
fontSize: 26,
bold: true,
renderMode: "fillStroke",
strokeWidth: 0.5,
strokeColor: "navy",
color: rgb(0.85, 0.9, 1)
}
],
options: { marginBottom: 14 }
},
{ type: "heading", text: "Horizontal scaling", options: { level: 2 } },
{ type: "paragraph", text: "CONDENSED 70%", options: { fontSize: 16, horizontalScaling: 70, marginBottom: 6 } },
{ type: "paragraph", text: "NORMAL 100%", options: { fontSize: 16, marginBottom: 6 } },
{ type: "paragraph", text: "EXPANDED 140%", options: { fontSize: 16, horizontalScaling: 140, marginBottom: 14 } },
{ type: "heading", text: "Highlight", options: { level: 2 } },
{
type: "paragraph",
text: "Whole line highlighted like a marker.",
options: { highlight: "yellow", marginBottom: 10 }
},
{
type: "richParagraph",
runs: [
{ text: "Review status: " },
{ text: "APPROVED", bold: true, highlight: rgb(0.8, 1, 0.8) },
{ text: " then " },
{ text: "REJECTED", bold: true, color: "white", highlight: rgb(0.85, 0.2, 0.2) }
]
}
]
});
export default doc;
Google Fonts
Fetch several Google Fonts (Inter, Geist, Instrument Serif with an italic face, and Bitcount Grid Single) and embed them as font families. In Node, use loadGoogleFont("Inter", { weight: 700 }); this browser sample fetches ready-made .ttf files from the CORS-enabled @expo-google-fonts packages on jsDelivr instead, because the Google Fonts API serves woff2 to browsers, which the engine cannot decode. See Text and Fonts → Google Fonts.
import { createDocument, rgb } from "@crstnmac/sempdf";
// In Node, call loadGoogleFont("Geist", { weight: 700 }) and embed the bytes.
// Browsers receive woff2 from the Google Fonts API (which the engine cannot
// decode), so this sample fetches ready-made .ttf files from the CORS-enabled
// @expo-google-fonts packages on jsDelivr instead.
const cdn = "https://cdn.jsdelivr.net/npm/@expo-google-fonts";
const FONTS = {
inter400: `${cdn}/inter/400Regular/Inter_400Regular.ttf`,
inter700: `${cdn}/inter/700Bold/Inter_700Bold.ttf`,
geist400: `${cdn}/geist/400Regular/Geist_400Regular.ttf`,
geist700: `${cdn}/geist/700Bold/Geist_700Bold.ttf`,
instrumentRegular: `${cdn}/instrument-serif/400Regular/InstrumentSerif_400Regular.ttf`,
instrumentItalic: `${cdn}/instrument-serif/400Regular_Italic/InstrumentSerif_400Regular_Italic.ttf`,
bitcount: `${cdn}/bitcount-grid-single/400Regular/BitcountGridSingle_400Regular.ttf`
};
async function ttf(url: string): Promise<Uint8Array> {
return new Uint8Array(await (await fetch(url)).arrayBuffer());
}
const doc = createDocument({ title: "Google Fonts", language: "en-US" });
// Fetch every face in parallel. Multiple faces under one template font family
// let bold/italic resolve by flag.
const [inter400, inter700, geist400, geist700, instReg, instItal, bitcount] = await Promise.all([
ttf(FONTS.inter400),
ttf(FONTS.inter700),
ttf(FONTS.geist400),
ttf(FONTS.geist700),
ttf(FONTS.instrumentRegular),
ttf(FONTS.instrumentItalic),
ttf(FONTS.bitcount)
]);
const specimen = "The quick brown fox jumps 0123456789";
doc.renderTemplate({
fonts: {
Inter: { regular: inter400, bold: inter700 },
Geist: { regular: geist400, bold: geist700 },
"Instrument Serif": { regular: instReg, italic: instItal },
"Bitcount Grid Single": { regular: bitcount }
},
page: { size: "A4", margin: 56 },
blocks: [
{
type: "heading",
text: "Google Fonts, embedded",
options: { level: 1, font: "Geist", bold: true, fontSize: 24 }
},
{
type: "paragraph",
text: "Fetched as .ttf and registered through renderTemplate().",
options: { font: "Geist", fontSize: 11, color: rgb(0.4, 0.4, 0.45), marginBottom: 24 }
},
{ type: "paragraph", text: "Inter", options: { font: "Inter", fontSize: 9, color: rgb(0.5, 0.5, 0.55), marginBottom: 12 } },
{ type: "paragraph", text: specimen, options: { font: "Inter", fontSize: 20, marginBottom: 18 } },
{ type: "paragraph", text: "Inter Bold", options: { font: "Inter", fontSize: 9, color: rgb(0.5, 0.5, 0.55), marginBottom: 12 } },
{ type: "paragraph", text: specimen, options: { font: "Inter", bold: true, fontSize: 20, marginBottom: 18 } },
{ type: "paragraph", text: "Geist", options: { font: "Geist", fontSize: 9, color: rgb(0.5, 0.5, 0.55), marginBottom: 12 } },
{ type: "paragraph", text: specimen, options: { font: "Geist", fontSize: 20, marginBottom: 18 } },
{ type: "paragraph", text: "Instrument Serif", options: { font: "Instrument Serif", fontSize: 9, color: rgb(0.5, 0.5, 0.55), marginBottom: 12 } },
{ type: "paragraph", text: specimen, options: { font: "Instrument Serif", fontSize: 20, marginBottom: 18 } },
{ type: "paragraph", text: "Instrument Serif Italic", options: { font: "Instrument Serif", fontSize: 9, color: rgb(0.5, 0.5, 0.55), marginBottom: 12 } },
{ type: "paragraph", text: specimen, options: { font: "Instrument Serif", italic: true, fontSize: 20, marginBottom: 18 } },
{ type: "paragraph", text: "Bitcount Grid Single", options: { font: "Bitcount Grid Single", fontSize: 9, color: rgb(0.5, 0.5, 0.55), marginBottom: 12 } },
{ type: "paragraph", text: specimen, options: { font: "Bitcount Grid Single", fontSize: 20 } }
]
});
export default doc;
Stylesheets & tables
Define reusable styles once and apply them by block type or class, then render a
header-row table — all through renderTemplate().
import { createDocument, rgb } from "@crstnmac/sempdf";
// Build a PDF and `export default` the document.
const doc = createDocument({
title: "Stylesheets and Tables Demo",
language: "en-US",
xmpMetadata: {
creator: "sempdf playground"
}
});
doc.renderTemplate({
page: { size: "A4", margin: 56 },
styles: {
heading1: { fontSize: 28, color: rgb(0.11, 0.22, 0.45) },
heading2: { fontSize: 16, color: rgb(0.11, 0.22, 0.45) },
paragraph: { fontSize: 12, lineHeight: 18 },
muted: { color: rgb(0.45, 0.45, 0.45) }
},
blocks: [
{ type: "heading", text: "Hello from sempdf", options: { level: 1 } },
{ type: "paragraph", text: "Generated in the browser - no server, no dependencies.", class: "muted" },
{ type: "heading", text: "Stylesheets", options: { level: 2 } },
{ type: "paragraph", text: "Define reusable styles once and apply them by block type or via a class." },
{ type: "paragraph", text: "Edit this code on the left and hit Run.", class: "muted" },
{ type: "heading", text: "Tables", options: { level: 2 } },
{ type: "paragraph", text: "Pass `borders` to control which lines draw: \"all\", \"horizontal\", \"outer\", or \"none\". Set `borderColor`/`borderWidth` to style them.", class: "muted" },
{ type: "paragraph", text: "borders: \"all\" - full grid" },
{
type: "table",
rows: [
[{ text: "Item", header: true }, { text: "Qty", header: true }, { text: "Price", header: true }],
["Notebook", "2", "$11.00"],
["Pen", "5", "$7.50"]
],
options: { headerRows: 1, borders: "all", borderColor: rgb(0.6, 0.6, 0.6), headerBackground: rgb(0.92, 0.94, 0.98) }
},
{ type: "paragraph", text: "borders: \"horizontal\" - row rules only" },
{
type: "table",
rows: [
[{ text: "Item", header: true }, { text: "Qty", header: true }, { text: "Price", header: true }],
["Notebook", "2", "$11.00"],
["Pen", "5", "$7.50"]
],
options: { headerRows: 1, borders: "horizontal", borderColor: rgb(0.2, 0.3, 0.5), borderWidth: 1, headerBackground: rgb(0.92, 0.94, 0.98) }
},
{ type: "paragraph", text: "borders: \"outer\" - frame only, with zebra stripes" },
{
type: "table",
rows: [
[{ text: "Item", header: true }, { text: "Qty", header: true }, { text: "Price", header: true }],
["Notebook", "2", "$11.00"],
["Pen", "5", "$7.50"]
],
options: { headerRows: 1, borders: "outer", borderColor: rgb(0.4, 0.4, 0.4), headerBackground: rgb(0.92, 0.94, 0.98), zebra: true }
}
]
});
export default doc;
Import image (browser)
Load an image without node:fs by fetching it as an ArrayBuffer, then let
page.image() detect its format and embed it in the PDF.
import { createDocument, rgb } from "@crstnmac/sempdf";
// The playground runs in the browser, so load the image with fetch instead of
// node:fs. page.image() detects the image format from its bytes.
const response = await fetch("/examples/assets/company-logo.png");
if (!response.ok) {
throw new Error(`Could not load image: ${response.status} ${response.statusText}`);
}
const imageBytes = await response.arrayBuffer();
const doc = createDocument({
title: "Imported Image",
language: "en-US"
});
const page = doc.addPage({ size: "A4" });
page.text("Import an image in the browser", {
x: 56,
y: 790,
fontSize: 24,
color: rgb(0.11, 0.22, 0.45)
});
page.text("Fetch the image as an ArrayBuffer, then pass its bytes to page.image().", {
x: 56,
y: 760,
fontSize: 11
});
page.image(imageBytes, {
x: 56,
y: 640,
width: 360,
height: 81,
altText: "Brightforge Systems logo"
});
export default doc;
Single page template
A minimal single-page document rendered from declarative template blocks.
import { createDocument, rgb } from "@crstnmac/sempdf";
const doc = createDocument({
title: "Single Page Demo",
language: "en-US",
xmpMetadata: {
creator: "sempdf playground"
}
});
doc.renderTemplate({
page: { size: "A4", margin: 56 },
blocks: [
{
type: "heading",
text: "Hello from sempdf",
options: { level: 1, fontSize: 28, color: rgb(0.11, 0.22, 0.45) }
},
{
type: "paragraph",
text: "Rendered from a declarative template on a single page.",
options: { fontSize: 12 }
}
]
});
export default doc;
Hello world (template)
The renderTemplate() starting point, with document info and XMP metadata.
import { createDocument, hex } from "@crstnmac/sempdf";
const doc = createDocument({
language: "en-US",
info: {
title: "Hello World",
author: "sempdf",
creationDate: "D:20260413213000Z",
modificationDate: "D:20260413213000Z"
}
});
doc.setXmpMetadata({
title: "Hello World",
creator: "sempdf",
creatorTool: "sempdf",
createDate: "2026-04-13T21:30:00Z",
modifyDate: "2026-04-13T21:30:00Z",
metadataDate: "2026-04-13T21:30:00Z"
});
doc.renderTemplate({
info: { title: "Hello World", author: "sempdf" },
page: { size: "A4" },
blocks: [
{
type: "heading",
text: "Hello from sempdf",
options: { level: 1, color: hex("1c2f6b") }
},
{
type: "paragraph",
text: "This PDF was generated using the renderTemplate API, which provides a production-grade template system with automatic page breaks, headings, tables, and images.",
options: { fontSize: 12 }
}
]
});
export default doc;
Invoice
A compact invoice built from a styled heading, a muted subtitle, and a totals table.
import { createDocument, rgb } from "@crstnmac/sempdf";
const TABLE_BORDER = rgb(0.72, 0.76, 0.82);
const TABLE_HEADER = rgb(0.92, 0.94, 0.98);
const doc = createDocument({
title: "Invoice 1042",
language: "en-US",
xmpMetadata: {
creator: "sempdf playground"
}
});
doc.renderTemplate({
page: { size: "A4", margin: 56 },
styles: {
heading1: { fontSize: 26, color: rgb(0.1, 0.1, 0.12) },
muted: { color: rgb(0.5, 0.5, 0.5), fontSize: 10 },
paragraph: { fontSize: 11, lineHeight: 16 }
},
blocks: [
{ type: "heading", text: "Invoice #1042", options: { level: 1 } },
{ type: "paragraph", text: "Acme Corp - Issued 2026-05-29", class: "muted" },
{
type: "table",
rows: [
[{ text: "Description", header: true }, { text: "Qty", header: true }, { text: "Amount", header: true }],
["Design work", "10", "$1,000.00"],
["Development", "20", "$3,000.00"],
[{ text: "Total", header: true }, "-", "$4,000.00"]
],
options: {
headerRows: 1,
borders: "all",
borderColor: TABLE_BORDER,
headerBackground: TABLE_HEADER
}
}
]
});
export default doc;
JSON to PDF
An entire document — metadata, a footer with page tokens, a table, and
absolute-positioned blocks — described as one PdfDocumentJson
value and rendered with renderDocumentJson().
/**
* Declarative JSON -> PDF, the full surface in one value.
*
* A tagged (accessible) two-page report described entirely as `PdfDocumentJson`:
* document metadata and language, a tagged structure tree, an auto-generated
* bookmark outline, artifact header/footer with page tokens, headings, rich
* inline text, ordered/unordered lists, a header-row table, a sectioned figure
* with alternate text, a note annotation, a hyperlink, and absolute-positioned
* blocks (an approval stamp and a signature field) placed at exact coordinates.
*
* npm run build && node dist/examples/json-to-pdf.js
*/
import { writeFile } from "node:fs/promises";
import { renderDocumentJson, type PdfDocumentJson } from "../src/index.js";
/** A 1x1 solid-color 24-bit BMP, base64-encoded — embedded inline as a figure. */
function solidBmpBase64(r: number, g: number, b: number): string {
const bmp = Buffer.alloc(58);
bmp.write("BM", 0, "ascii");
bmp.writeUInt32LE(58, 2); // file size
bmp.writeUInt32LE(54, 10); // pixel-data offset
bmp.writeUInt32LE(40, 14); // DIB header size
bmp.writeInt32LE(1, 18); // width
bmp.writeInt32LE(1, 22); // height
bmp.writeUInt16LE(1, 26); // planes
bmp.writeUInt16LE(24, 28); // bits per pixel
bmp.writeUInt32LE(4, 34); // image size
bmp[54] = b; // BMP stores BGR
bmp[55] = g;
bmp[56] = r;
return bmp.toString("base64");
}
const report: PdfDocumentJson = {
// ── Document options ──────────────────────────────────────────────────────
title: "Accessibility Conformance Report",
info: { author: "sempdf", subject: "WCAG 2.2 audit", keywords: "accessibility, pdf, tagged" },
language: "en-US",
tagged: true,
compress: true,
autoOutline: { maxLevel: 3 },
// ── Page, header, footer ──────────────────────────────────────────────────
page: { size: "A4", margin: 56, headerHeight: 40, footerHeight: 40 },
header: [
{ type: "paragraph", text: "{{title}}", options: { fontSize: 8, color: "gray", tag: "Artifact" } },
],
footer: [
{
type: "paragraph",
text: "Page {{pageNumber}} of {{pageCount}}",
options: { fontSize: 8, color: "gray", align: "center", tag: "Artifact" },
},
],
// ── Body ──────────────────────────────────────────────────────────────────
blocks: [
{ type: "heading", text: "Accessibility Conformance Report", options: { level: 1 } },
{
type: "paragraph",
text: "This report records the results of a tagged-PDF audit and is itself produced as a tagged, PDF/UA-style document.",
},
{
type: "richParagraph",
runs: [
{ text: "Overall status: " },
{ text: "PASS", bold: true, color: "green" },
{ text: " - reviewed against " },
{ text: "WCAG 2.2 AA", italic: true },
{ text: "." },
],
},
{ type: "heading", text: "Findings", options: { level: 2 } },
{
type: "list",
items: [
"The document language is declared (en-US).",
"Headings follow a logical, gap-free hierarchy.",
{ body: "Images carry alternate text", children: ["Decorative images are marked as artifacts."] },
],
options: { ordered: false, width: 483 },
},
{ type: "heading", text: "Scores", options: { level: 2 } },
{
type: "table",
rows: [
[
{ text: "Criterion", header: true },
{ text: "Result", header: true },
{ text: "Notes", header: true },
],
["Tagged structure", "Pass", "Complete structure tree"],
["Color contrast", "Pass", "4.8:1 body text"],
["Reading order", "Pass", "Matches visual order"],
],
options: { headerRows: 1 },
},
{
type: "section",
role: "Sect",
blocks: [
{ type: "heading", text: "Evidence", options: { level: 3 } },
{ type: "paragraph", text: "The figure below summarizes the four checks; all passed." },
],
},
// Image blocks use `src` at the top level of `blocks`. A 1x1 BMP is stretched
// into a bar; `altText` + `tag: "Figure"` make it accessible.
{
type: "bmp",
src: { base64: solidBmpBase64(34, 139, 87) },
options: {
width: 160,
height: 36,
altText: "Green bar indicating all four accessibility checks passed",
tag: "Figure",
},
},
{ type: "note", options: { contents: "Re-audit scheduled for the next quarter." } },
{ type: "link", text: "Read the WCAG 2.2 specification", url: "https://www.w3.org/TR/WCAG22/" },
{ type: "pageBreak" },
{ type: "heading", text: "Sign-off", options: { level: 2 } },
{ type: "paragraph", text: "Reviewed and approved by the accessibility team." },
// ── Absolute-positioned blocks (coordinate system) ──────────────────────
{
type: "rect",
width: 150,
height: 54,
style: { paint: "stroke", stroke: { color: "green", width: 2 } },
position: { x: 360, y: 660 },
},
{
type: "paragraph",
text: "APPROVED",
options: { fontSize: 22, color: "green" },
position: { x: 378, y: 678 },
},
{
type: "textField",
name: "reviewerSignature",
options: { description: "Reviewer signature", width: 240, height: 16 },
position: { x: 56, y: 640 },
},
],
};
const bytes = await renderDocumentJson(report);
await writeFile("examples/json-to-pdf.pdf", bytes);
console.log(`Wrote examples/json-to-pdf.pdf (${bytes.length} bytes)`);
Multi-column
A two-column newspaper layout that flows paragraphs across columns.
import { createDocument } from "@crstnmac/sempdf";
const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ".repeat(8);
const doc = createDocument({
title: "Newspaper Layout Demo",
language: "en-US",
xmpMetadata: {
creator: "sempdf playground"
}
});
doc.renderTemplate({
page: { size: "A4", margin: 48, columns: { count: 2, gap: 24 } },
styles: {
heading1: { fontSize: 22 },
paragraph: { fontSize: 11, lineHeight: 16 }
},
blocks: [
{ type: "heading", text: "Newspaper layout", options: { level: 1 } },
{ type: "paragraph", text: lorem },
{ type: "paragraph", text: lorem }
]
});
export default doc;
Color spaces
Every supported color model: hex, rgb, gray, cmyk, plus separation spot
colors and DeviceN multi-ink.
import { createDocument, hex, rgb, gray, cmyk } from "@crstnmac/sempdf";
const doc = createDocument({
language: "en-US",
info: {
title: "Color Spaces Demo",
author: "sempdf"
}
});
doc.renderTemplate({
info: { title: "Color Spaces Demo", author: "sempdf" },
page: { size: "A4" },
blocks: [
{
type: "heading",
text: "PDF Color Spaces",
options: { level: 1, color: hex("#1c2f6b") }
},
{
type: "paragraph",
text: "hex('#1565c0') - DeviceRGB hex color. Useful for web-originated palettes and design tokens.",
options: { color: hex("#1565c0"), fontSize: 12, marginTop: 8 }
},
{
type: "paragraph",
text: "rgb(0.08, 0.40, 0.75) - DeviceRGB float. Programmatic color mixing with 0-1 range per channel.",
options: { color: rgb(0.08, 0.4, 0.75), fontSize: 12, marginTop: 4 }
},
{
type: "paragraph",
text: "rgb(0.09, 0.67, 0.36) - Another RGB example, demonstrating a green tone.",
options: { color: rgb(0.09, 0.67, 0.36), fontSize: 12, marginTop: 4 }
},
{
type: "paragraph",
text: "gray(0.5) - DeviceGray mid-tone. Single-channel lightness, ideal for monochrome content.",
options: { color: gray(0.5), fontSize: 12, marginTop: 8 }
},
{
type: "paragraph",
text: "gray(0.2) - DeviceGray dark. Lower values produce deeper shades.",
options: { color: gray(0.2), fontSize: 12, marginTop: 4 }
},
{
type: "paragraph",
text: "gray(0.8) - DeviceGray light. Higher values produce lighter shades.",
options: { color: gray(0.8), fontSize: 12, marginTop: 4 }
},
{
type: "paragraph",
text: "cmyk(0.80, 0.20, 0.00, 0.15) - DeviceCMYK. Process color for professional print workflows.",
options: { color: cmyk(0.8, 0.2, 0, 0.15), fontSize: 12, marginTop: 8 }
},
{
type: "paragraph",
text: "cmyk(0.00, 1.00, 1.00, 0.00) - Bright red in CMYK process.",
options: { color: cmyk(0, 1, 1, 0), fontSize: 12, marginTop: 4 }
},
{
type: "paragraph",
text: "cmyk(0.00, 0.00, 1.00, 0.00) - Bright yellow in CMYK process.",
options: { color: cmyk(0, 0, 1, 0), fontSize: 12, marginTop: 4 }
},
{
type: "heading",
text: "Advanced Color Spaces",
options: { level: 2, color: hex("#1565c0"), marginTop: 16 }
},
{
type: "paragraph",
text: "Separation spot color - Pantone 286 C. Named ink with a tint value (0-1) and CMYK fallback for on-screen previews and composite printing.",
options: {
fontSize: 12,
color: {
kind: "separation",
name: "Pantone 286 C",
tint: 1,
alternate: cmyk(1, 0.66, 0, 0.04)
}
}
},
{
type: "paragraph",
text: "DeviceN multi-ink - Pantone 286 C + Black. Combines multiple named colorants into a single color specification. Each ink has its own tint, with a CMYK alternate for fallback.",
options: {
fontSize: 12,
marginTop: 4,
color: {
kind: "deviceN",
names: ["Pantone 286 C", "Black"],
tints: [1, 0.8],
alternate: cmyk(1, 0.66, 0, 0.04)
}
}
}
]
});
export default doc;
Graphics state & transforms
Low-level vector graphics: saveState/restoreState, translate, scale,
rotate, arbitrary transform, alpha transparency, and blend modes.
import { createDocument, hex } from "@crstnmac/sempdf";
const doc = createDocument({
info: {
title: "Graphics State Demo",
author: "sempdf"
}
});
doc.renderTemplate({
page: { size: "A4", margin: 72 },
blocks: [
{
type: "heading",
text: "Graphics State",
options: { level: 1, color: hex("#1a1a2e") }
},
{
type: "paragraph",
text: "The document shell is rendered as a template. The following page uses the low-level graphics-state API because transforms and blend modes operate directly on a page canvas."
}
]
});
const page = doc.addPage({ size: "A4" });
page.text("Graphics State: Transforms, Alpha & Blend Modes", {
x: 72, y: 800, fontSize: 16
});
// Save / restore state
page.text("saveState / restoreState", { x: 72, y: 770, fontSize: 12 });
page.saveState();
page.text("Blue text inside saved state", {
x: 72, y: 740, fontSize: 14, color: hex("#1565c0")
});
page.path(
[{ type: "rect", x: 72, y: 718, width: 200, height: 16 }],
{ paint: "stroke", stroke: { width: 1, color: hex("#1565c0") } }
);
page.restoreState();
page.text("Back to default graphics state", {
x: 72, y: 690, fontSize: 11
});
// Translate
page.text("translate(100, 0) - two rectangles side by side", { x: 72, y: 660, fontSize: 12 });
page.saveState();
page.translate(100, 0);
page.path(
[{ type: "rect", x: 72, y: 630, width: 80, height: 20 }],
{ paint: "fill", fill: { color: hex("#e3f2fd") } }
);
page.restoreState();
page.saveState();
page.translate(188, 0);
page.path(
[{ type: "rect", x: 72, y: 630, width: 80, height: 20 }],
{ paint: "fill", fill: { color: hex("#bbdefb") } }
);
page.restoreState();
// Scale
page.text("scale(1.5, 1.5)", { x: 72, y: 600, fontSize: 12 });
page.saveState();
page.scale(1.5, 1.5);
page.text("SCALED TEXT", { x: 72, y: 380, fontSize: 16, color: hex("#c62828") });
page.restoreState();
// Rotate
page.text("rotate(15 degrees)", { x: 72, y: 570, fontSize: 12 });
page.saveState();
page.rotate(15);
page.text("Rotated text at 15 degrees", { x: 72, y: 530, fontSize: 14, color: hex("#2e7d32") });
page.restoreState();
// Alpha transparency with setExtGState
page.text("Transparency (fillAlpha = 0.3)", { x: 72, y: 500, fontSize: 12 });
page.saveState();
page.setExtGState({ fillAlpha: 0.3 });
page.path(
[{ type: "rect", x: 72, y: 460, width: 100, height: 30 }],
{ paint: "fill", fill: { color: hex("#ff5722") } }
);
page.text("Semi-transparent overlay", { x: 66, y: 472, fontSize: 10, color: hex("#ffffff") });
page.restoreState();
// Blend modes
page.text("Blend mode: 'Multiply' on overlapping rects", { x: 72, y: 440, fontSize: 12 });
page.saveState();
page.setExtGState({ blendMode: "Multiply" });
page.path(
[{ type: "rect", x: 72, y: 390, width: 60, height: 30 }],
{ paint: "fill", fill: { color: hex("#ff0000") } }
);
page.path(
[{ type: "rect", x: 86, y: 380, width: 60, height: 30 }],
{ paint: "fill", fill: { color: hex("#0000ff") } }
);
page.restoreState();
// Transform (skew)
page.text("transform() - skewed parallelogram", { x: 72, y: 360, fontSize: 12 });
page.saveState();
page.transform({ a: 1, b: 0, c: 0.3, d: 1, e: 0, f: 0 });
page.path(
[{ type: "rect", x: 72, y: 320, width: 120, height: 25 }],
{ paint: "fill", fill: { color: hex("#7b1fa2") } }
);
page.restoreState();
export default doc;
Outlines (bookmarks)
Build a nested outline (bookmarks) tree automatically from template headings.
import { createDocument, hex } from "@crstnmac/sempdf";
const doc = createDocument({
info: {
title: "PDF Outlines (Bookmarks) Demo",
author: "sempdf"
}
});
doc.renderTemplate({
page: { size: "A4", margin: 72 },
autoOutline: { maxLevel: 2 },
styles: {
heading1: { fontSize: 20, color: hex("#1a1a2e") },
heading2: { fontSize: 14 }
},
blocks: [
{ type: "heading", text: "Chapter 1: Introduction", options: { level: 1 } },
{ type: "heading", text: "Overview", options: { level: 2 } },
{ type: "paragraph", text: "This is the first chapter covering basic concepts.", options: { fontSize: 12 } },
{ type: "pageBreak" },
{ type: "heading", text: "Chapter 2: Advanced Topics", options: { level: 1 } },
{ type: "heading", text: "Section 2.1 - Deep Dive", options: { level: 2 } },
{ type: "heading", text: "Section 2.2 - Examples", options: { level: 2 } },
{ type: "pageBreak" },
{ type: "heading", text: "Appendix A: Reference", options: { level: 1 } }
]
});
export default doc;
Annotations
Highlight, sticky note, free text, URI link, and page-to-page link annotations.
import { createDocument, hex } from "@crstnmac/sempdf";
const doc = createDocument({
language: "en-US",
info: {
title: "PDF Annotations Demo",
author: "sempdf"
}
});
doc.renderTemplate({
info: { title: "PDF Annotations Demo", author: "sempdf" },
page: { size: "A4" },
blocks: [
{
type: "heading",
text: "PDF Annotations",
options: { level: 1, marginBottom: 6 }
},
{
type: "paragraph",
text: "This document demonstrates PDF annotation types. The document layout uses renderTemplate, while annotations are placed with the low-level page API for precise positioning.",
options: { fontSize: 11, marginBottom: 14 }
},
{
type: "paragraph",
text: "Annotations below include highlight, sticky note, free text, URI link, and page-to-page link.",
options: { fontSize: 11 }
}
]
});
const page = doc.addPage({ size: "A4" });
page.text("PDF Annotations", { x: 72, y: 800, fontSize: 20 });
// Highlight annotation
page.text("This sentence has a highlight annotation on it.", { x: 72, y: 760, fontSize: 12 });
page.highlight({
x: 72,
y: 760,
width: 350,
height: 14,
color: hex("#ffeb3b")
});
// Note annotation (sticky note)
page.text("Click the note icon to read the comment.", { x: 72, y: 720, fontSize: 12 });
page.note({
x: 72,
y: 720,
width: 20,
height: 20,
contents: "This is a sticky note annotation with reviewer comments.",
open: true
});
// FreeText annotation
page.freeText({
x: 300,
y: 680,
width: 200,
height: 60,
text: "Approved by reviewer on May 8, 2026",
fontSize: 10
});
// URI link
page.text("Visit our website for documentation", { x: 72, y: 640, fontSize: 12 });
page.link("https://example.com/docs", {
x: 72,
y: 640,
width: 220,
height: 14
});
// Internal page link
const page2 = doc.addPage({ size: "A4" });
page2.text("You arrived via the 'Go to Page 2' link on Page 1.", {
x: 72,
y: 800,
fontSize: 14
});
page.text("Go to Page 2", { x: 72, y: 610, fontSize: 12 });
page.linkToPage(page2, {
x: 72,
y: 610,
width: 80,
height: 14
});
export default doc;
Form fields
Interactive AcroForm fields — text, combo/list choice, checkbox, push button,
and signature — laid out with renderTemplate() so the labels and fields flow
down the page without overlapping (the field blocks omit x/y).
import { createDocument, hex } from "@crstnmac/sempdf";
const doc = createDocument({
language: "en-US",
info: {
title: "Registration Form",
author: "sempdf"
}
});
// renderTemplate auto-flows each block down the page, so the labels and form
// fields stack without overlapping. Field blocks (textField, choiceField,
// checkBox, pushButton, signatureField) omit x/y - the template positions them.
//
// Each field sets a `description`, emitted as /TU - the field's accessible name
// (read by screen readers, and required by the "form fields have a description"
// accessibility check). renderTemplate tags the field widgets into the
// structure tree automatically, so no per-field tag is needed.
doc.renderTemplate({
info: { title: "Registration Form", author: "sempdf" },
page: { size: "A4" },
pageNumber: { region: "footer", align: "center" },
blocks: [
{
type: "heading",
text: "Registration Form",
options: { level: 1, color: hex("#1c2f6b"), marginBottom: 8 }
},
{
type: "paragraph",
text: "Please fill out the following form to register for the event. All fields marked with an asterisk (*) are required.",
options: { fontSize: 11, marginBottom: 14 }
},
{
type: "paragraph",
text: "Full Name *",
options: { fontSize: 10, marginBottom: 4 }
},
{
type: "textField",
name: "name",
options: { width: 300, height: 24, value: "John Doe", fontSize: 11, required: true, description: "Full name" }
},
{
type: "paragraph",
text: "Email Address *",
options: { fontSize: 10, marginBottom: 4, marginTop: 10 }
},
{
type: "textField",
name: "email",
options: { width: 300, height: 24, fontSize: 11, required: true, description: "Email address" }
},
{
type: "paragraph",
text: "Country",
options: { fontSize: 10, marginBottom: 4, marginTop: 10 }
},
{
type: "choiceField",
name: "country",
options: {
width: 250,
height: 24,
options: ["United States", "Canada", "United Kingdom", "Germany", "Japan", "Australia"],
value: "United States",
mode: "combo",
fontSize: 10,
description: "Country"
}
},
{
type: "paragraph",
text: "Skills (list box)",
options: { fontSize: 10, marginBottom: 4, marginTop: 10 }
},
{
type: "choiceField",
name: "skills",
options: {
width: 250,
height: 60,
options: ["TypeScript", "JavaScript", "Python", "Rust", "Go"],
mode: "list",
fontSize: 10,
description: "Skills"
}
},
{
type: "checkBox",
name: "subscribe",
options: { width: 16, height: 16, checked: true, description: "Subscribe to newsletter" }
},
{
type: "paragraph",
text: "Subscribe to newsletter",
options: { fontSize: 10, marginTop: 8, marginBottom: 16 }
},
{
type: "pushButton",
name: "submit",
options: { width: 140, height: 32, label: "Submit", fontSize: 12, color: hex("#ffffff"), description: "Submit form" }
},
{
type: "paragraph",
text: "Signature *",
options: { fontSize: 10, marginTop: 16, marginBottom: 4 }
},
{
type: "signatureField",
name: "signature",
options: { width: 300, height: 60, required: true, description: "Signature" }
}
]
});
export default doc;
Incremental editing
Re-open template-generated bytes with editDocument(), append a page, update a
field value, and patch document info — all as an incremental update.
import { createDocument, editDocument } from "@crstnmac/sempdf";
// Step 1: Create a base document with form fields.
const baseDoc = createDocument({
info: { title: "Base Document", author: "sempdf" }
});
baseDoc.renderTemplate({
page: { size: "A4", margin: 72 },
blocks: [
{
type: "heading",
text: "Original document",
options: { level: 1, fontSize: 20 }
},
{
type: "paragraph",
text: "Original content - this document will be edited incrementally.",
options: { fontSize: 14 }
},
{
type: "textField",
name: "name",
options: { width: 300, height: 22, value: "Original", fontSize: 11 }
},
{
type: "checkBox",
name: "agree",
options: { width: 16, height: 16 }
}
]
});
const baseBytes = baseDoc.toUint8Array();
// Step 2: Re-open the saved bytes and edit them as an incremental update.
const editable = editDocument(baseBytes);
// Add a new page.
const newPage = editable.addPage({ size: "A4" });
newPage.text("Added via incremental editing", { x: 72, y: 780, fontSize: 16 });
newPage.text("This page did not exist in the original document.", { x: 72, y: 750, fontSize: 12 });
// Update a form field value.
editable.setFieldValue("name", "Updated via incremental edit");
// Update document info.
editable.updateInfo({ title: "Edited Document", subject: "Demonstrates incremental editing" });
// The edited document is what we render.
export default editable;
Browser AES encryption
AES-256 encrypt a document entirely client-side — no Node runtime. The library
uses a built-in AES/SHA-2 fallback when node:crypto is unavailable, so the
output opens with the same password in any reader. See
Encryption and Signatures → Browser support.
import { createDocument, rgb } from "@crstnmac/sempdf";
// AES encryption runs entirely in the browser — no server round-trip. When
// node:crypto is unavailable the library uses a built-in, zero-dependency
// AES/SHA-2 implementation whose output matches Node byte-for-byte, so the
// password works in any reader.
//
// This document is encrypted as the playground serializes it. Open the
// downloaded PDF and you will be prompted for the password below.
const doc = createDocument({
title: "Encrypted in the Browser",
language: "en-US",
encryption: {
algorithm: "aes-256-r6", // PDF 2.0 hardened key derivation (recommended)
userPassword: "reader", // password required to open
ownerPassword: "owner-secret",
permissions: {
printing: true,
copying: false,
modifying: false
}
}
});
doc.renderTemplate({
page: { size: "A4", margin: 56 },
blocks: [
{
type: "heading",
text: "Confidential",
options: { level: 1, fontSize: 28, color: rgb(0.6, 0.1, 0.1) }
},
{
type: "paragraph",
text: "This PDF was AES-256 encrypted client-side.",
options: { fontSize: 12 }
},
{
type: "paragraph",
text: "Open password: reader",
options: { fontSize: 12 }
}
]
});
export default doc;