SuperSimplePDF
Define the layout once. Feed it JSON. The core is blind to both and does all the math.
The theme does not know what the content says. The JSON does not know how it looks. The core does not know it is rendering a newspaper, an invoice, or a certificate. Three blind components, one coherent output.
The problem it solves
Generating PDFs imperatively means tracking the cursor yourself. Every element shifts everything below. Line wrapping, page breaks, font resets: all manual. This engine inverts that. Describe what to render and how it looks. Cursor, math, and page breaks happen automatically.
Install
npm install h17-sspdf
Quick Start
const { renderDocument } = require('h17-sspdf'); renderDocument({ source: { operations: [ { type: 'text', label: 'doc.title', text: 'My Document' }, { type: 'divider', label: 'doc.rule' }, { type: 'text', label: 'doc.body', text: 'First paragraph.' } ] }, theme: { name: 'My Theme', page: { format: 'a4', orientation: 'portrait', unit: 'mm', marginTopMm: 20, marginBottomMm: 20, marginLeftMm: 20, marginRightMm: 20, backgroundColor: [255, 255, 255], defaultText: { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 10, color: [0,0,0], lineHeight: 1.4 }, defaultStroke: { color: [200,200,200], lineWidth: 0.3, lineCap: 'butt', lineJoin: 'miter' }, defaultFillColor: [255, 255, 255], }, labels: { 'doc.title': { fontFamily: 'helvetica', fontStyle: 'bold', fontSize: 22, color: [0,0,0], lineHeight: 1.2, marginBottomMm: 4 }, 'doc.rule': { color: [200,200,200], lineWidth: 0.3, marginBottomMm: 4 }, 'doc.body': { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 10, color: [40,40,40], lineHeight: 1.5 }, } }, outputPath: 'output/doc.pdf' });
How It Works
Every operation has a type and a label. The label maps to a style in the theme. The engine looks up the style, lays out the content, advances the cursor by an exact calculated amount, and moves to the next operation.
operation → label → theme style → layout → cursor advance → next operation
Page breaks happen automatically when content reaches the bottom margin. Style resets after every operation; nothing leaks.
Operations
Each operation in the source.operations array represents a renderable element. The type field determines what kind of content is produced.
| Type | Description | Key Fields |
|---|---|---|
text |
Wrapped text block | label, text (string or array) |
row |
Left/right pair on one line | leftLabel, rightLabel, leftText, rightText |
bullet |
Marker + wrapped text | label, markerLabel, bullets array |
divider |
Horizontal rule | label, x1Mm, x2Mm |
image |
Embedded PNG/JPEG | src, width (percentage or mm), caption |
spacer |
Vertical gap | mm, px, or label |
hiddenText |
Invisible text | label, text |
quote |
Blockquote with attribution | label, text, attribution |
block |
Group children, optional background + border | children, keepTogether |
section |
Logical group, allows breaks inside | content |
table |
Data table with headers | label, headerLabel, columns, rows |
Page break control
Use keepWithNext: N on any operation to keep it grouped with the next N operations on the same page. Use keepTogether: true on a block to prevent the entire block from splitting across pages.
Labels & Styling
Every operation references a label that maps to a style definition in the theme. Labels control typography, spacing, dividers, containers, and accents. Label names are arbitrary strings; use a dot-namespace convention like news.headline, resume.title, or invoice.total.
fontFamily, you must write fontFamily. There are no fallbacks or cascades between labels.
Typography
| Property | Type | Description |
|---|---|---|
fontFamily |
string | Font family name ("helvetica", "courier", "times", or custom) |
fontStyle |
string | "normal", "bold", "italic", "bolditalic" |
fontSize |
number | Font size in points |
color |
[R,G,B] | Text color as RGB array (0-255) |
lineHeight |
number | Line height multiplier (e.g. 1.2) |
lineHeightMm |
number | Direct line height in mm (overrides fontSize x lineHeight) |
align |
string | "left", "center", "right", "justify" |
textTransform |
string | "upper" or "lower" |
Spacing
| Property | Type | Description |
|---|---|---|
marginTopMm / marginTopPx |
number | Space above in mm or CSS px (converted to mm) |
marginBottomMm / marginBottomPx |
number | Space below in mm or CSS px |
paddingMm / paddingPx |
number | Shorthand padding for all four sides |
paddingTopMm / paddingTopPx |
number | Top padding in mm or CSS px |
paddingBottomMm / paddingBottomPx |
number | Bottom padding in mm or CSS px |
paddingLeftMm / paddingRightMm |
number | Horizontal padding in mm (individual sides override shorthand) |
For row labels, paddingLeftMm on the left label and paddingRightMm on the right label offset the text inward from the content edge.
Container & Border
| Property | Type | Description |
|---|---|---|
backgroundColor |
[R,G,B] | Fill color behind text |
borderWidthMm |
number | Border stroke width in mm |
borderColor |
[R,G,B] | Border stroke color |
borderRadiusMm |
number | Rounded corners radius |
Left Border Accent
A vertical bar drawn to the left of the text block, useful for pull quotes and callouts.
leftBorder: { color: [13, 148, 136], // [R, G, B] widthMm: 1.4, // bar width gapMm: 2.5, // gap between bar and text heightMm: 10, // optional - defaults to text block height topOffsetMm: 0, // optional - vertical offset from text top }
Divider Labels
| Property | Type | Description |
|---|---|---|
color |
[R,G,B] | Line color |
lineWidth |
number | Line thickness in mm |
opacity |
number | Stroke opacity (0-1) |
dashPattern |
number[] | Dash pattern (e.g. [2, 1]) |
marginTopMm |
number | Space above |
marginBottomMm |
number | Space below |
Also accepts Px variants for margins.
Spacer Labels
| Property | Type | Description |
|---|---|---|
spaceMm |
number | Vertical space in mm |
spacePx |
number | Vertical space in CSS px |
Bullet Marker Labels
A marker can be either a text character or a vector shape. For text markers:
| Property | Type | Description |
|---|---|---|
fontFamily |
string | Marker font |
fontStyle |
string | Marker font style |
fontSize |
number | Marker font size in points |
color |
[R,G,B] | Marker color |
lineHeight |
number | Marker line height multiplier |
marker |
string | The marker character (e.g. "-") |
For shape markers (vector, no text encoding needed):
| Property | Type | Description |
|---|---|---|
shape |
string | Shape name from the built-in library |
shapeColor |
[R,G,B] | Shape fill/stroke color |
shapeSize |
number | Scale factor (default: 1) |
textIndentMm |
number | Gap between shape and bullet text in mm |
Table Cell Labels
Used by table operations. The label controls data cell styling, headerLabel controls header cell styling.
| Property | Type | Description |
|---|---|---|
fontFamily |
string | Cell text font |
fontStyle |
string | Cell text font style |
fontSize |
number | Cell text font size in points |
color |
[R,G,B] | Cell text color |
lineHeight |
number | Cell text line height multiplier |
cellPaddingMm |
number | Padding inside each cell in mm |
backgroundColor |
[R,G,B] | Cell/header background color (even rows use this, odd rows use altRowColor) |
altRowColor |
[R,G,B] | Background for odd data rows (alternating shading) |
borderColor |
[R,G,B] | Default border color for all edges |
borderTopMm / borderBottomMm |
number | Vertical edge border widths in mm |
borderLeftMm / borderRightMm |
number | Horizontal edge border widths in mm |
borderTopColor / borderBottomColor |
[R,G,B] | Per-edge color overrides (top, bottom) |
borderLeftColor / borderRightColor |
[R,G,B] | Per-edge color overrides (left, right) |
Theme Object
A theme is a JS object with four top-level keys. The engine reads this object to control every visual decision in the document.
module.exports = {
name: "My Theme", // used in PDF metadata
page: { /* page config */ }, // required
labels: { /* label styles */ }, // required
// optional
layout: { /* shared layout defaults */ },
customFonts: [ /* embedded fonts */ ],
};
| Key | Required | Description |
|---|---|---|
name |
No | Used in PDF metadata title |
page |
Yes | Page geometry, background, and baseline rendering state |
labels |
Yes | Flat map of label names to style objects |
layout |
No | Shared layout defaults (bullet indent, etc.) |
customFonts |
No | Array of embedded TTF font definitions |
Page Config
The page object controls page geometry, background, and baseline rendering state. Baseline state is applied after every operation to prevent style leakage between operations.
page: { // Page geometry format: "a4", // only "a4" is supported orientation: "portrait", // "portrait" or "landscape" unit: "mm", // only "mm" is supported compress: true, // PDF compression (default true) // Margins margin: 15, // fallback for all four sides (mm) marginTopMm: 20, // overrides margin for top marginBottomMm: 20, // overrides margin for bottom marginLeftMm: 18, // overrides margin for left marginRightMm: 18, // overrides margin for right // Background backgroundColor: [255, 252, 248], // [R, G, B] - painted on every page // Baseline text state (required, every property) defaultText: { fontFamily: "helvetica", fontStyle: "normal", fontSize: 10, color: [0, 0, 0], lineHeight: 1.2, }, // Baseline stroke state (required) defaultStroke: { color: [0, 0, 0], lineWidth: 0.2, lineCap: "butt", lineJoin: "miter", }, // Baseline fill color (required) defaultFillColor: [255, 255, 255], }
The content area runs from marginTop to pageHeight - marginBottom vertically, and marginLeft to pageWidth - marginRight horizontally. On A4 portrait, the page is 210 x 297 mm.
| Property | Type | Description |
|---|---|---|
format |
string | Only "a4" is supported |
orientation |
string | "portrait" or "landscape" |
unit |
string | Only "mm" is supported |
compress |
boolean | PDF compression (default true) |
margin |
number | Fallback for all four sides (mm) |
marginTopMm |
number | Top margin in mm (overrides margin) |
marginBottomMm |
number | Bottom margin in mm |
marginLeftMm |
number | Left margin in mm |
marginRightMm |
number | Right margin in mm |
backgroundColor |
[R,G,B] | Page background color, painted on every page |
defaultText |
object | Baseline text state (all properties required) |
defaultStroke |
object | Baseline stroke state (all properties required) |
defaultFillColor |
[R,G,B] | Baseline fill color (required) |
metadata |
object | Optional PDF metadata (title, subject, author, keywords) |
defaultText, defaultStroke, and defaultFillColor are required and must be fully specified. They reset after every operation to prevent style leakage.
Layout Defaults
The optional layout object provides shared layout defaults read by operation handlers.
layout: { bulletIndentMm: 4.5, // text indent after bullet marker (default: 4) }
Label Properties
The labels object is a flat map of label names to style objects. Every operation in the source references a label by name. If the label does not exist in the theme, the engine throws.
labels: { "doc.title": { fontFamily: "helvetica", fontStyle: "bold", fontSize: 22, color: [30, 30, 30], lineHeight: 1.2, align: "center", marginBottomPx: 8, }, "doc.divider": { color: [200, 200, 200], lineWidth: 0.3, marginBottomPx: 6, }, "doc.body": { fontFamily: "helvetica", fontStyle: "normal", fontSize: 10, color: [50, 50, 50], lineHeight: 1.4, align: "justify", marginBottomPx: 4, }, }
See the Labels & Styling section above for the full property reference for each label type (text, spacing, container, divider, spacer, bullet marker, and table cell labels).
Custom Fonts
Embed TTF fonts as base64 strings in the theme's customFonts array. Each entry defines a font family with one or more faces (normal, bold, italic, bolditalic). Once registered, reference them by fontFamily in any label.
customFonts: [ { family: "Inter", faces: [ { style: "normal", fileName: "Inter-Regular.ttf", data: "<base64 string>" }, { style: "bold", fileName: "Inter-Bold.ttf", data: "<base64 string>" }, ], }, ],
| Field | Type | Description |
|---|---|---|
family |
string | Font family name used in labels' fontFamily |
faces |
array | Array of font face objects |
faces[].style |
string | "normal", "bold", "italic", or "bolditalic" |
faces[].fileName |
string | Original TTF filename (used as an identifier) |
faces[].data |
string | TTF file content encoded as a base64 string |
After registration, use the family name in any label:
"doc.title": { fontFamily: "Inter", fontStyle: "bold", fontSize: 24, color: [20, 20, 20], lineHeight: 1.2, }
Built-in font families that require no embedding: helvetica, courier, times.
Built-in Fonts
Twenty Google Fonts ship with the package as base64 TTF. Each exports { Regular, Bold } (capitalized), ready to drop into customFonts without any manual conversion.
Sans-serif
Inter, Roboto, Open Sans, Montserrat, Lato, Raleway, Nunito, Work Sans, IBM Plex Sans, PT Sans, Oswald
Serif
Merriweather, Lora, Playfair Display, Crimson Text, Libre Baskerville, Source Serif 4
Monospace
Fira Code, JetBrains Mono, Source Code Pro
Usage
Require the font module and wire the exported faces into customFonts:
const INTER = require('h17-sspdf/fonts/inter.js'); customFonts: [{ family: 'Inter', faces: [ { style: 'normal', fileName: 'Inter-Regular.ttf', data: INTER.Regular }, { style: 'bold', fileName: 'Inter-Bold.ttf', data: INTER.Bold }, ], }],
CLI discovery
List all available built-in fonts from the command line:
npx h17-sspdf --fonts
fontStyle: "italic" without a matching TTF, jsPDF throws: "Unable to look up font label for font 'Inter', 'italic'"
Vector Shapes
Twenty built-in vector shapes rendered via jsPDF drawing primitives. No text encoding, no font dependencies. Use them as bullet markers by setting shape on a marker label instead of marker.
Theme definition
Define a shape-based marker label in your theme:
"bullet.arrow": { shape: "arrow", shapeColor: [0, 128, 255], shapeSize: 0.8, textIndentMm: 2, }
Source JSON
The source JSON is unchanged from text markers. Reference the shape marker label via markerLabel:
{
"type": "bullet",
"label": "doc.body",
"markerLabel": "bullet.arrow",
"bullets": ["First point", "Second point"]
}
Available shapes
arrow, circle, square, diamond, triangle, dash, chevron, doubleColon, commentSlash, hashComment, bracketChevron, treeBranch, terminalPrompt, checkmark, cross, star, plus, minus, warning, infoCircle
CLI discovery
List all available shapes from the command line:
npx h17-sspdf --shapes
Source Operations Reference
The source is a JSON object containing content and structure. No colors, no sizes, no positions; those all come from the theme via labels. The source can use several equivalent root keys: operations, sections, content, items, children, or a bare array. All are interchangeable; the engine normalizes them into a flat operation list.
text
Renders wrapped text. When text is an array, each string becomes its own text operation with the same label.
{ "type": "text", "label": "doc.body", "text": "Single paragraph." }
{ "type": "text", "label": "doc.body", "text": [
"First paragraph.",
"Second paragraph.",
"Third paragraph."
]
}
| Field | Required | Description |
|---|---|---|
label |
Yes | Theme label for styling |
text |
Yes | String or string array. Array expands to multiple text operations. |
keepWithNext |
No | Number or true. Keeps this + next N operations on the same page. |
xMm |
No | Override horizontal position (mm from page left) |
maxWidthMm |
No | Override maximum text width in mm |
align |
No | Overrides the label's align |
wrap |
No | Set false to disable wrapping (default true) |
advance |
No | Set false to not move cursor after drawing |
row
Renders two text values on the same line: one left-aligned, one right-aligned. Cursor delta uses the taller of the two sides.
{
"type": "row",
"leftLabel": "meta.left",
"rightLabel": "meta.right",
"leftText": "Author Name",
"rightText": "March 2026"
}
| Field | Required | Description |
|---|---|---|
leftLabel |
Yes | Theme label for left text |
rightLabel |
Yes | Theme label for right text |
leftText |
Yes | Left-aligned text |
rightText |
Yes | Right-aligned text |
bullet
Renders a marker character followed by wrapped text. Array form creates multiple bullet items.
{
"type": "bullet",
"label": "doc.body",
"markerLabel": "doc.marker",
"bullets": [
"First point.",
"Second point.",
"Third point."
]
}
| Field | Required | Description |
|---|---|---|
label |
Yes | Theme label for the bullet text |
text / items / bullets |
Yes | String or string array. Array expands to multiple bullets. |
markerLabel |
No | Theme label for the marker (default: "bullet.marker") |
marker |
No | Override marker character |
textIndentMm |
No | Override indent between marker and text |
divider
Renders a horizontal line. The label must define lineWidth and color.
{ "type": "divider", "label": "doc.divider" }
Optional fields: x1Mm and x2Mm override the line start and end positions. opacity and dashPattern can also be set at the operation level.
spacer
Adds vertical space without drawing anything. Use exactly one of the three forms.
| Field | Type | Description |
|---|---|---|
mm |
number | Fixed space in mm |
px |
number | Fixed space in CSS px |
label |
string | Theme label with spaceMm or spacePx |
hiddenText
Renders text in the background color so it is invisible but present in the PDF text layer. Useful for ATS keyword injection or search indexing. Does not move the cursor.
{ "type": "hiddenText", "label": "doc.hidden", "text": "keywords for ATS parsing" }
quote
Shorthand for a blockquote. Normalizes into a block with keepTogether: true containing the quote text and an optional attribution line.
{
"type": "quote",
"label": "doc.pullquote",
"text": "The quoted text goes here.",
"attribution": "- Author Name"
}
| Field | Required | Description |
|---|---|---|
label |
Yes | Theme label for quote text (also used for block container) |
text / content |
Yes | The quoted text |
attribution / author |
No | Attribution line rendered below the quote |
attributionLabel |
No | Label for attribution (default: label + ".attribution") |
If the quote label has backgroundColor, the background wraps the entire quote + attribution. Use leftBorder in the label for a vertical accent bar.
block
Groups child operations. If the label has backgroundColor or borderWidthMm, a container rect is drawn behind the children. Inside a block with a container, child labels have their container properties (backgroundColor, borderWidthMm, etc.) stripped so they do not draw their own containers.
{
"type": "block",
"label": "doc.callout",
"keepTogether": true,
"children": [
{ "type": "text", "label": "doc.body", "text": "Inside the block." },
{ "type": "text", "label": "doc.body", "text": "Also inside." }
]
}
| Field | Required | Description |
|---|---|---|
label |
No | Theme label for container styling |
children / content / items |
Yes | Child operations array |
keepTogether |
No | If true, all children move to next page if they don't fit |
spaceAfterMm / spaceAfterPx |
No | Extra space after the block |
spaceAfterLabel |
No | Theme label with spaceMm/spacePx for space after |
section
Identical to block but defaults to keepTogether: false. Use sections to group related content without forcing it all onto one page.
{
"type": "section",
"content": [
{ "type": "text", "label": "doc.heading", "text": "Section Title", "keepWithNext": 3 },
{ "type": "text", "label": "doc.body", "text": "Section body text." }
]
}
group
Alias for block. Behaves identically.
Inferred text operations
If a node has label and text (or value) but no type, it is treated as a text operation. Array values expand to multiple text operations.
{ "label": "doc.body", "text": "This works too." }
{ "label": "doc.body", "text": ["Paragraph one.", "Paragraph two."] }
image
Embeds a PNG or JPEG image from a file path. The image and its caption are always kept together on the same page.
{
"type": "image",
"src": "photos/cityscape.jpg",
"width": "100%",
"label": "editorial.photo",
"caption": "Fig. 1 - Downtown skyline at dusk",
"captionLabel": "editorial.photo.caption"
}
| Field | Required | Description |
|---|---|---|
src |
Yes | File path to a PNG or JPEG image (absolute, or relative to CWD) |
width |
No | Percentage of content width (e.g. "100%", "50%"). Height derived from aspect ratio. |
widthMm |
No | Explicit width in mm. If heightMm is omitted, height derived from aspect ratio. |
heightMm |
No | Explicit height in mm. If both widthMm and heightMm are set, the image may distort. |
label |
No | Theme label for padding and margins around the image |
caption |
No | Caption text rendered centered below the image |
captionLabel |
No | Theme label for caption styling (default: label + ".caption") |
Size resolution priority: (1) widthMm + heightMm both set: exact slot, may distort. (2) width: "80%": percentage of content width, height from aspect ratio. (3) widthMm only: fixed width, height from ratio. (4) heightMm only: fixed height, width from ratio. (5) Nothing set: fills content width, height from ratio.
Caption behavior: If caption is set, the text renders centered below the image. The caption label defaults to label + ".caption". If no captionLabel or derived label exists in the theme, the engine applies defaults: same font family as page.defaultText, italic, 2pt smaller, centered, with a 1.5mm gap. Color is inherited from defaultText.
Image label properties: The label on an image operation controls spacing around the image block, not typography. Use it to set paddingTopMm, paddingBottomMm, paddingLeftMm, paddingRightMm, marginTopMm, marginBottomMm.
Page Templates
Page templates define repeating content (headers and footers) that appear on every page. Defined at the source root alongside the operation array.
{
"pageTemplates": {
"header": [ /* operations */ ],
"footer": [ /* operations */ ],
"headerHeightMm": 12,
"footerHeightMm": 10,
"headerStartMm": 5,
"footerStartMm": 280,
"headerBypassMargins": true,
"footerBypassMargins": true
},
"operations": [ ... ]
}
| Field | Description |
|---|---|
header / footer |
Array of operations rendered on every page |
headerHeightMm / footerHeightMm |
Reserves space in the content area so body text does not overlap. Defaults to 12mm / 10mm if operations exist. |
headerStartMm / footerStartMm |
Y position where the template starts rendering. Footer defaults to pageHeight - footerHeightMm. |
headerBypassMargins / footerBypassMargins |
If true (default), template operations use the full page width. |
The {{page}} token in any text value resolves to the current page number at render time.
{{page}} is supported but {{pages}} (total page count) is not. The keepTogether feature makes total page count unpredictable until the full render completes.
Tables
Tables are a first-class operation type. Pure vector rendering, no images. Define columns with width and alignment, provide rows as string arrays.
{
"type": "table",
"label": "invoice.table.cell",
"headerLabel": "invoice.table.header",
"columns": [
{ "header": "Description", "width": "45%", "align": "left" },
{ "header": "Qty", "width": "10%", "align": "right" },
{ "header": "Unit Price", "width": "20%", "align": "right" },
{ "header": "Amount", "width": "25%", "align": "right" }
],
"rows": [
["Automation Workflow", "1", "$3,200.00", "$3,200.00"],
["Monitoring Dashboard", "1", "$1,400.00", "$1,400.00"]
]
}
Table operation fields
| Field | Required | Description |
|---|---|---|
label |
Yes | Theme label for data cell styling |
headerLabel |
No | Theme label for header row. If omitted, no header is rendered. |
columns |
Yes | Array of column definitions |
rows |
Yes | Data rows, each an array of cell strings |
xMm |
No | Left edge in mm (default: content left margin) |
maxWidthMm |
No | Total table width in mm (default: content area width) |
altRowColor |
No | Override the label's altRowColor for this table |
cellPaddingMm |
No | Override the label's cellPaddingMm for this table |
borderColor |
No | Override the label's borderColor for this table |
Column definition
| Field | Type | Description |
|---|---|---|
header |
string | Header text (used when headerLabel is set) |
width |
string or number | "30%" (percentage), 35 (fixed mm), or omitted (auto-divide remaining space) |
align |
string | "left", "right", or "center" (default: "left") |
Page break behavior
When a table spans multiple pages, the header row is automatically re-drawn at the top of each new page. Table rows are never split; if a row does not fit, it moves to the next page.
Style cascade
Table styles cascade in order: engine defaults, then theme label, then source-level overrides. The source operation can override altRowColor, cellPaddingMm, and all border properties directly.
Table Labels Pattern
SSPDF ships shared table label constants you can spread into your theme. This keeps table styling consistent and reduces boilerplate.
const table = require("h17-sspdf/examples/themes/table"); labels: { "report.table.cell": { ...table.cell, color: [51, 65, 85], altRowColor: [248, 249, 252], }, "report.table.header": { ...table.header, backgroundColor: [55, 65, 81], color: [255, 255, 255], }, }
Position Overrides
Any operation accepts xMm and maxWidthMm to override its horizontal position and maximum width. This is useful for indented quotes, narrow columns, or custom positioning within the page margins.
// indent a quote 20mm from the left, max 130mm wide { type: 'quote', label: 'pullQuote', text: 'The future is already here...', attribution: 'William Gibson', xMm: 20, maxWidthMm: 130 }
Building a Layout: Newspaper Example
A newspaper-style PDF demonstrates most features working together. Here is how the pieces fit.
Page template (repeating footer)
{
"pageTemplates": {
"footer": [
{ "type": "divider", "label": "news.footer.rule", "x1Mm": 18, "x2Mm": 192 },
{
"type": "row",
"leftLabel": "news.footer.left",
"rightLabel": "news.footer.right",
"leftText": "The Meridian Times | Civic Desk",
"rightText": "Page {{page}}",
"xLeftMm": 18,
"xRightMm": 192
}
],
"footerHeightMm": 8,
"footerStartMm": 284
}
}
Declare it once. The engine stamps it on every page at the specified Y position. {{page}} is replaced with the current page number at render time.
Masthead block inside a section
{
"type": "section",
"content": [
{ "type": "text", "label": "news.masthead", "text": "The Meridian Times", "xMm": 18, "maxWidthMm": 174 },
{
"type": "row",
"leftLabel": "news.edition.left",
"rightLabel": "news.edition.right",
"leftText": "Saturday, March 7, 2026",
"rightText": "Late City Edition",
"xLeftMm": 18,
"xRightMm": 192
},
{ "type": "divider", "label": "news.rule.heavy", "x1Mm": 18, "x2Mm": 192 }
]
}
xMm and maxWidthMm override the page margins for that operation only. This is how you position content independently of the theme margins.
Multi-paragraph body text
{
"type": "text",
"label": "news.body",
"text": [
"First paragraph.",
"Second paragraph.",
"Third paragraph."
],
"xMm": 18,
"maxWidthMm": 174
}
Pass text as an array. Each string becomes a paragraph with the label's spacing applied between them.
keepWithNext
{ "type": "text", "label": "news.section.title", "text": "Inside the shift", "keepWithNext": 3 }
Tells the engine this operation must stay on the same page as the next N operations. Use it on section headings so they never strand at the bottom of a page.
Pull quote
{
"type": "quote",
"label": "news.pullquote",
"text": "When the format becomes a system instead of a template, agencies stop re-solving the same layout problem every week.",
"attribution": "Elena Ward, public records modernization lead",
"xMm": 22,
"maxWidthMm": 166
}
xMm and maxWidthMm indent it from the body column. The indentation is in the source, not the theme.
Hidden text
{
"type": "hiddenText",
"label": "news.hidden.tags",
"text": "public records procurement modernization searchable notices"
}
Invisible in the rendered PDF, present in text extraction. Useful for ATS keyword matching or search indexing.
Chart Plugin
The chart plugin renders Chart.js configurations to PNG images and embeds them directly in the PDF.
Setup
The chart plugin requires the canvas npm package as a peer dependency.
npm install canvas
Registration
const { registerPlugin, plugins } = require('h17-sspdf'); registerPlugin('chart', plugins.chart);
Chart operation format
{
type: 'chart',
chartType: 'bar',
widthMm: 160,
heightMm: 80,
canvasWidth: 1600,
canvasHeight: 800,
data: { /* Chart.js data config */ },
options: { /* Chart.js options */ }
}
CLI
Run SSPDF from the command line without writing any code.
npx h17-sspdf -s source.json -t theme.js -o output.pdf
| Flag | Alias | Description |
|---|---|---|
--source |
-s |
Path to source JSON (or pipe via stdin) |
--theme |
-t |
Path to theme .js file or built-in name |
--output |
-o |
Output PDF path |
--fonts |
(none) | List all built-in fonts |
--shapes |
(none) | List all built-in vector shapes |
--help |
-h |
Show help |
LLM Document Generation
SSPDF includes a Claude Code skill for generating PDF documents end-to-end. Given a description of what to create (invoice, report, article, tear sheet), the skill reads the documentation, selects or generates a theme, builds the source JSON, and renders the output. The skill is available in the skills/sspdf/ directory of the GitHub repository.
Workflow
- Read
DOCUMENTATION.mdfor the full operation reference. - Determine what document the user needs from the task description.
- Check
examples/themes/for an existing theme that fits. If none fits, generate a custom theme. - Build the source JSON with the correct operations and label references.
- Write the source JSON to
examples/sources/or wherever specified. - If using charts, register the chart plugin and pre-render before calling
renderDocument. - Render the PDF and verify the output.
CLI rendering
# Built-in theme name npx h17-sspdf -s my-source.json -t default -o output/my-doc.pdf # Custom theme file npx h17-sspdf -s my-source.json -t ./my-custom-theme.js -o output/custom.pdf
The CLI auto-detects chart operations and pre-renders them. No extra setup needed.
Programmatic rendering
const { renderDocument } = require("h17-sspdf"); const theme = require("h17-sspdf/examples/themes/theme-default"); const source = require("./my-source.json"); renderDocument({ source, theme, outputPath: "output/my-doc.pdf" });
Chart pre-rendering (async)
When using charts programmatically, you must pre-render chart operations before calling renderDocument. The CLI handles this automatically.
const { renderDocument, registerPlugin, plugins } = require("h17-sspdf"); registerPlugin("chart", plugins.chart); async function main() { const chartOp = { type: "chart", chartType: "bar", data: { /* Chart.js data */ }, widthMm: 160, heightMm: 90 }; await plugins.chart.preRender(chartOp); renderDocument({ source: { operations: [ chartOp ] }, theme, outputPath: "output/chart.pdf", }); } main();
Verification
After rendering, confirm the PDF exists and check for common issues:
- All labels referenced in the source must exist in the theme.
- Table
columnsarray must be non-empty,rowsarray must exist, and each row must match column count. - The
h17-sspdfpackage must be installed (npx h17-sspdf --help).
Source JSON rules
- Every
labelin the source must exist in the theme. - The source never says how to render. No colors, sizes, or font names in the JSON; only content and label references.
- Use
keepWithNexton headings to prevent orphaning. - Use
blockwithkeepTogetherfor cards or grouped content. - Prefer text arrays over repeating the same operation for multiple paragraphs.
Theme Generator
The theme generator skill creates theme files from brand specifications (colors, fonts, document type). It produces a single .js file that exports a valid theme object, ready for first-render success. Available in the skills/sspdf-theme-generator/ directory of the GitHub repository.
What it produces
A single .js file that exports a valid theme object. The file goes wherever specified. The theme includes a complete page section with all required baseline state, and a labels map covering every visual element the document needs.
Theme rules
- Self-contained labels. No inheritance between labels. If a label needs
fontFamily, writefontFamily. - RGB arrays. Colors are always
[R, G, B]arrays, 0-255. Never hex strings. - A4 only. Only
"a4"format and"mm"units are supported. - Required defaults. The
pagemust includedefaultText,defaultStroke, anddefaultFillColor, all fully specified. - Dot-namespace convention. Label names use dots:
invoice.title,report.body,news.headline. - Built-in fonts.
helvetica,courier,times. For anything else, embed TTF viacustomFonts. - Table labels need
cellPaddingMm, border properties, and optionallyaltRowColor. - Do not hardcode positions or sizes in labels; those belong in the source JSON.
Label property quick reference
| Category | Properties |
|---|---|
| Text | fontFamily, fontStyle, fontSize, color, lineHeight, lineHeightMm, align, textTransform |
| Spacing | marginTopMm/Px, marginBottomMm/Px |
| Padding | paddingMm/Px, paddingTopMm, paddingBottomMm, paddingLeftMm, paddingRightMm (and Px variants) |
| Container | backgroundColor, borderWidthMm, borderColor, borderRadiusMm |
| Left border | leftBorder: { color, widthMm, gapMm, heightMm, topOffsetMm } |
| Divider | color, lineWidth, opacity, dashPattern |
| Bullet marker (text) | fontFamily, fontStyle, fontSize, color, lineHeight, marker |
| Bullet marker (shape) | shape, shapeColor, shapeSize, textIndentMm |
| Spacer | spaceMm, spacePx |
| Table cell | cellPaddingMm, altRowColor, borderColor, borderTopMm/BottomMm/LeftMm/RightMm, per-edge color overrides |
Workflow
- Read
DOCUMENTATION.mdfor the full property reference. - Read at least one existing theme in
examples/themes/for conventions. - Identify every visual element the document will have. Each one needs a label.
- Generate the theme file with all labels fully specified.
- If the document uses tables, use the shared constants pattern from
examples/themes/table.js. - Render a test PDF to verify:
npx h17-sspdf -s source.json -t theme-path -o output/test.pdf
Validation checklist
Page section:
format:"a4"andunit:"mm"(only supported values)orientation:"portrait"or"landscape"- All four margins set
defaultTextfully specified (fontFamily,fontStyle,fontSize,color,lineHeight)defaultStrokefully specified (color,lineWidth,lineCap,lineJoin)defaultFillColorset
Labels:
- Every label has
fontFamilyexplicitly set (no inheritance) - Table labels have
cellPaddingMm,borderColor,altRowColorif using alternating rows - Divider labels have
colorandlineWidth - Text bullet markers have
markercharacter - Shape bullet markers have
shapename - Colors are
[R, G, B]arrays, not hex strings
If using custom fonts:
customFontsarray includes all fonts used in labels- Each face has
style,fileName, anddata fontFamilyin labels matchesfamilyincustomFontsexactly
Example theme structure
module.exports = {
name: "Theme Name",
page: {
format: "a4",
orientation: "portrait",
unit: "mm",
compress: true,
marginTopMm: 20,
marginBottomMm: 20,
marginLeftMm: 18,
marginRightMm: 18,
backgroundColor: [255, 255, 255],
defaultText: {
fontFamily: "helvetica",
fontStyle: "normal",
fontSize: 10,
color: [0, 0, 0],
lineHeight: 1.2,
},
defaultStroke: {
color: [0, 0, 0],
lineWidth: 0.2,
lineCap: "butt",
lineJoin: "miter",
},
defaultFillColor: [255, 255, 255],
},
labels: {
// every label the source JSON will reference
},
};
Built-in Themes
SSPDF ships with several built-in themes that can be referenced by name when using the CLI. These cover common document types and visual styles.
| Theme | Description |
|---|---|
default |
Clean baseline theme with Helvetica, generous margins |
editorial |
Magazine-style layout with refined typography |
newsprint |
Newspaper aesthetic with compact spacing |
corporate |
Professional business documents with structured headers |
ceremony |
Formal documents, certificates, invitations |
program |
Event programs with clear section hierarchy |
financial |
Financial reports and invoices with table support |
# Use a built-in theme by name
npx h17-sspdf -s source.json -t editorial -o output.pdf
To see all available themes, list the themes directory:
ls $(node -e "console.log(require('path').dirname(require.resolve('h17-sspdf')))")/examples/themes/
Constraints
- A4 only. Page size is fixed to A4 (210 x 297 mm).
- Single-line row cells.
rowoperations render each side on a single line; text does not wrap within a row cell. {{page}}supported,{{pages}}not. ThekeepTogetherfeature makes total page count unpredictable until the full render completes.- Charts need
canvas. The chart plugin requires thecanvasnpm package (native C++ addon) for server-side rendering. Everything else has zero native dependencies.
License
Apache 2.0.
Third-party
This project vendors the following MIT-licensed libraries:
- jsPDF: PDF generation. Copyright (c) 2010-2025 James Hall, yWorks GmbH.
- Chart.js: Chart rendering. Copyright (c) 2014-2024 Chart.js Contributors.
- chartjs-node-canvas: Server-side Chart.js rendering. Copyright (c) 2018 Sean Sobey.
Full license texts are in vendor/*/LICENSE.