SuperSimplePDF
v0.4.1

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.

Source JSON + Theme = PDF. Describe what to render and how it looks. Cursor, math, and page breaks happen automatically.

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

bash
npm install h17-sspdf

Quick Start

js
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.

text
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.

No inheritance. Every label is self-contained. If a label needs 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.

js
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.

js
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.

js
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)
The 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.

js
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.

js
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.

js
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:

js
"doc.title": {
  fontFamily: "Inter",
  fontStyle: "bold",
  fontSize: 24,
  color: [20, 20, 20],
  lineHeight: 1.2,
}
Font embedding size. Each embedded TTF font adds its full file size to the theme. A single face is typically 100-300 KB as base64. Consider embedding only the faces you actually use (e.g. normal + bold) rather than all four.

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:

js
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:

bash
npx h17-sspdf --fonts
No italic faces ship. Only normal and bold TTFs are included. If you set 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:

js
"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:

json
{
  "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:

bash
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.

json
{ "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.

json
{
  "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.

json
{
  "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.

json
{ "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.

json
{ "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.

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{ "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.

json
{
  "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.

Keep-together. The image and its caption are always rendered as one unit. If the total height does not fit on the current page, the entire block moves to the next page.

Page Templates

Page templates define repeating content (headers and footers) that appear on every page. Defined at the source root alongside the operation array.

json
{
  "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.

json
{
  "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.

js
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.

js
// 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)

json
{
  "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

json
{
  "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

json
{
  "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

json
{ "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

json
{
  "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

json
{
  "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.

bash
npm install canvas

Registration

js
const { registerPlugin, plugins } = require('h17-sspdf');

registerPlugin('chart', plugins.chart);

Chart operation format

js
{
  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.

bash
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

  1. Read DOCUMENTATION.md for the full operation reference.
  2. Determine what document the user needs from the task description.
  3. Check examples/themes/ for an existing theme that fits. If none fits, generate a custom theme.
  4. Build the source JSON with the correct operations and label references.
  5. Write the source JSON to examples/sources/ or wherever specified.
  6. If using charts, register the chart plugin and pre-render before calling renderDocument.
  7. Render the PDF and verify the output.

CLI rendering

bash
# 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

js
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.

js
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:

Source JSON rules

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

  1. Self-contained labels. No inheritance between labels. If a label needs fontFamily, write fontFamily.
  2. RGB arrays. Colors are always [R, G, B] arrays, 0-255. Never hex strings.
  3. A4 only. Only "a4" format and "mm" units are supported.
  4. Required defaults. The page must include defaultText, defaultStroke, and defaultFillColor, all fully specified.
  5. Dot-namespace convention. Label names use dots: invoice.title, report.body, news.headline.
  6. Built-in fonts. helvetica, courier, times. For anything else, embed TTF via customFonts.
  7. Table labels need cellPaddingMm, border properties, and optionally altRowColor.
  8. 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

  1. Read DOCUMENTATION.md for the full property reference.
  2. Read at least one existing theme in examples/themes/ for conventions.
  3. Identify every visual element the document will have. Each one needs a label.
  4. Generate the theme file with all labels fully specified.
  5. If the document uses tables, use the shared constants pattern from examples/themes/table.js.
  6. Render a test PDF to verify: npx h17-sspdf -s source.json -t theme-path -o output/test.pdf

Validation checklist

Page section:

Labels:

If using custom fonts:

Example theme structure

js
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
bash
# 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:

bash
ls $(node -e "console.log(require('path').dirname(require.resolve('h17-sspdf')))")/examples/themes/

Constraints

License

Apache 2.0.

Third-party

This project vendors the following MIT-licensed libraries:

Full license texts are in vendor/*/LICENSE.