| name | sspdf-theme-generator |
| description | Generate sspdf theme files from brand specs (colors, fonts, document type). Use when asked to create a theme, style a document, or design a PDF layout for sspdf. |
| user-invocable | true |
| argument-hint | brand colors, fonts, and document type (e.g. 'navy headers, Arial, financial tear sheet') |
| metadata | {"author":"Hugo Palma","version":"1.2.0","tags":["pdf","theme","styling","design","sspdf"],"input_format":"brand specs (plain text)","output_format":"theme.js file"} |
| license | Apache-2.0 |
Skill: sspdf Theme Generator
You generate theme files for the sspdf PDF engine. A theme is a JS object that controls every visual decision in a document. You know the full label property schema and produce themes that work on first render.
GitHub repository
The full source, examples, tests, and additional resources are at:
https://github.com/hugopalma17/sspdf
The npm package includes core, fonts, vendor, example themes, and example sources. The GitHub repo also contains test suites, skills, and development history.
Step 0: Locate the engine
Start from the document the user wants, then build the theme. Only verify installation when rendering or resolving examples:
SSPDF_DIR=$(node -e "console.log(require('path').dirname(require.resolve('h17-sspdf')))")
If this fails, install it:
npm install h17-sspdf
Context
The sspdf engine takes two inputs: a theme (styling rules) and a source (content). The theme controls page geometry, baseline state, and label styles. The source references labels by name. If a label is missing, the engine throws.
Resolve the package location:
SSPDF_DIR=$(node -e "console.log(require('path').dirname(require.resolve('h17-sspdf')))")
If working inside the sspdf repo itself, use the current working directory instead.
What you produce
A single .js file that exports a valid theme object. The file goes wherever the user specifies.
Fast path
When asked for a theme, build immediately from the source label inventory:
- If a source JSON exists, scan labels first:
node -e "const s=require('./source.json'); const labels=new Set(); const walk=o=>{ if(!o||typeof o!=='object') return; if(Array.isArray(o)) return o.forEach(walk); for(const k of ['label','leftLabel','rightLabel','markerLabel','headerLabel','captionLabel','spaceAfterLabel']) if(o[k]) labels.add(o[k]); for(const k of ['operations','sections','content','items','children','column1','column2']) walk(o[k]); if(o.pageTemplates){walk(o.pageTemplates.header); walk(o.pageTemplates.footer);} }; walk(s); console.log([...labels].sort().join('\\n'))"
- If no source exists, infer 6 to 12 labels from the document type:
doc.title, doc.subtitle, doc.body, doc.rule, doc.row.left, doc.row.right, doc.bullet, doc.bullet.marker, doc.table.cell, doc.table.header, doc.footer.left, doc.footer.right.
- Write the page section and every label explicitly. No inheritance between labels.
- Render once with the matching source if available.
- Add missing labels or tune spacing from the render result.
Minimal theme:
module.exports = {
name: "Quick Theme",
page: {
format: "a4",
orientation: "portrait",
unit: "mm",
marginTopMm: 18,
marginBottomMm: 18,
marginLeftMm: 18,
marginRightMm: 18,
backgroundColor: [255, 255, 255],
defaultText: { fontFamily: "helvetica", fontStyle: "normal", fontSize: 10, color: [35, 35, 35], lineHeight: 1.35 },
defaultStroke: { color: [35, 35, 35], lineWidth: 0.2, lineCap: "butt", lineJoin: "miter" },
defaultFillColor: [255, 255, 255],
},
layout: { bulletIndentMm: 4, columnGutterMm: 6, chartAlign: "center" },
labels: {
"doc.title": { fontFamily: "helvetica", fontStyle: "bold", fontSize: 22, color: [20, 28, 38], lineHeight: 1.1, marginBottomMm: 4 },
"doc.subtitle": { fontFamily: "helvetica", fontStyle: "normal", fontSize: 11, color: [91, 99, 112], lineHeight: 1.3, marginBottomMm: 5 },
"doc.body": { fontFamily: "helvetica", fontStyle: "normal", fontSize: 10, color: [45, 50, 58], lineHeight: 1.35, marginBottomMm: 3 },
"doc.rule": { color: [180, 185, 194], lineWidth: 0.25, marginTopMm: 1, marginBottomMm: 4 },
},
};
Read on demand
Do not read the full docs before producing a normal theme. Read targeted sections only when needed:
rg -n "Theme structure|Label property quick reference|Table labels|Page templates|customFonts|columns" $SSPDF_DIR/DOCUMENTATION.md
Use DOCUMENTATION.md as the source of truth for exact property names, but do not let it delay the first draft.
Also check existing themes for patterns:
ls $SSPDF_DIR/examples/themes/
Read at least one existing theme to match the project's conventions.
Theme structure
module.exports = {
name: "Theme Name",
page: {
format: "a4",
orientation: "portrait",
unit: "mm",
pageWidthMm: 338,
pageHeightMm: 190,
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],
},
layout: {
chartAlign: "center",
bulletIndentMm: 4,
},
labels: {
},
};
When pageWidthMm and pageHeightMm are both set, they override the format field and create a page with those exact dimensions. All layout math adapts automatically. For 16:9 presentations, use 338x190mm. See examples/themes/theme-presentation.js for a complete example.
Built-in fonts
The package ships with 20 Google Fonts as base64 TTF files. Each exports { Regular, Bold } (capitalized). Only normal and bold faces ship. No italic TTFs included.
Sans-serif: Inter (inter), Roboto (roboto), Open Sans (open-sans), Montserrat (montserrat), Lato (lato), Raleway (raleway), Nunito (nunito), Work Sans (work-sans), IBM Plex Sans (ibm-plex-sans), PT Sans (pt-sans), Oswald (oswald)
Serif: Merriweather (merriweather), Lora (lora), Playfair Display (playfair-display), Crimson Text (crimson-text), Libre Baskerville (libre-baskerville), Source Serif 4 (source-serif-4)
Monospace: Fira Code (fira-code), JetBrains Mono (jetbrains-mono), Source Code Pro (source-code-pro)
Require path: h17-sspdf/fonts/<name>.js where <name> is the value in parentheses above.
const INTER = require("h17-sspdf/fonts/inter.js");
const MERRIWEATHER = require("h17-sspdf/fonts/merriweather.js");
customFonts: [
{
family: "Inter",
faces: [
{ style: "normal", fileName: "Inter-Regular.ttf", data: INTER.Regular },
{ style: "bold", fileName: "Inter-Bold.ttf", data: INTER.Bold },
],
},
{
family: "Merriweather",
faces: [
{ style: "normal", fileName: "Merriweather-Regular.ttf", data: MERRIWEATHER.Regular },
{ style: "bold", fileName: "Merriweather-Bold.ttf", data: MERRIWEATHER.Bold },
],
},
],
If you set fontStyle: "italic" without a matching TTF, jsPDF throws: Unable to look up font label for font 'Inter', 'italic'.
Shape-based bullet markers
Instead of text markers that may have unicode encoding issues, use vector shapes:
"doc.marker.arrow": {
shape: "arrow",
shapeColor: [0, 128, 255],
shapeSize: 0.8,
textIndentMm: 2,
}
List available shapes: npx h17-sspdf --shapes
The source JSON uses the same bullet operation with markerLabel pointing to this label. No changes needed on the source side.
Rules
- Every label is self-contained. No inheritance between labels. If a label needs
fontFamily, write fontFamily.
- Colors are always
[R, G, B] arrays, 0-255.
- Default format is
"a4". Custom dimensions are supported via pageWidthMm/pageHeightMm (e.g. 338x190mm for 16:9 presentations). Only "mm" units are supported.
- The
page section must include defaultText, defaultStroke, and defaultFillColor, all fully specified. These reset after every operation to prevent style leaks.
- Label names are arbitrary strings. Use a dot-namespace convention:
invoice.title, report.body, news.headline.
- Built-in jsPDF font families:
helvetica, courier, times. For better typography, use the 20 shipped Google Fonts listed in the Built-in fonts section above, or embed your own TTF via customFonts.
- Table labels need
cellPaddingMm, border properties, and optionally altRowColor. Use the shared constants pattern from examples/themes/table.js if the document includes tables.
- Do not hardcode positions or sizes in labels that belong in the source JSON.
- Only
"normal" and "bold" font styles are available for built-in fonts. Do not use "italic" or "bolditalic" unless the font includes those TTF files.
Label property quick reference
Text labels: fontFamily, fontStyle, fontSize, color, lineHeight, lineHeightMm, align, textTransform
Spacing: marginTopMm, marginTopPx, marginBottomMm, marginBottomPx
Padding: paddingMm, paddingPx, paddingTopMm, paddingBottomMm, paddingLeftMm, paddingRightMm (and Px variants)
Container: backgroundColor, borderWidthMm, borderColor, borderRadiusMm
Left border accent: leftBorder: { color, widthMm, gapMm, heightMm, topOffsetMm }
Divider labels: color, lineWidth, opacity, dashPattern, spacing props
Bullet marker (text): fontFamily, fontStyle, fontSize, color, lineHeight, marker
Bullet marker (shape): shape, shapeColor, shapeSize, textIndentMm
Spacer labels: spaceMm, spacePx
Image labels: paddingTopMm, paddingBottomMm, paddingLeftMm, paddingRightMm, marginTopMm, marginBottomMm (controls spacing around the image block, not typography)
Image caption labels: fontFamily, fontStyle, fontSize, color, lineHeight, align (always center-aligned). If not declared in the theme, the engine applies defaults: same font family as page.defaultText, italic, 2pt smaller, centered, 1.5mm gap. Color inherited from defaultText. To override, declare the caption label in the theme.
Table cell labels: fontFamily, fontStyle, fontSize, color, lineHeight, cellPaddingMm, backgroundColor, altRowColor, borderColor, borderTopMm, borderBottomMm, borderLeftMm, borderRightMm, per-edge color overrides
Workflow
- Infer the document type and label inventory from the user request or source JSON.
- Generate the theme file immediately with a complete page section and explicit labels.
- Use built-in fonts or shipped Google Fonts. Avoid italic unless a matching TTF is registered.
- If the document uses tables, read
examples/themes/table.js and use the shared constants pattern.
- If the document uses page templates, make sure the source JSON sets
headerBypassMargins: false / footerBypassMargins: false for margin-aligned template text.
- Render with the matching source if available, then tune spacing.
- Write the file to the specified path.
Theme validation checklist
Before finalizing a theme, verify:
Page section:
format: "a4" (default) or custom via pageWidthMm/pageHeightMm. unit: "mm" required.
orientation: "portrait" or "landscape"
- All four margins set (
marginTopMm, marginBottomMm, marginLeftMm, marginRightMm)
defaultText fully specified (fontFamily, fontStyle, fontSize, color, lineHeight)
defaultStroke fully specified (color, lineWidth, lineCap, lineJoin)
defaultFillColor set
Page-template margins (CRITICAL: source JSON, not theme labels):
Header and footer operation bounds are controlled by the source JSON pageTemplates flags:
{
"pageTemplates": {
"header": [{ "type": "row", "leftLabel": "doc.header.left", "rightLabel": "doc.header.right", "leftText": "Title", "rightText": "Page {{page}}" }],
"footer": [{ "type": "row", "leftLabel": "doc.footer.left", "rightLabel": "doc.footer.right", "leftText": "Document", "rightText": "Page {{page}}" }],
"headerHeightMm": 12,
"footerHeightMm": 10,
"headerBypassMargins": false,
"footerBypassMargins": false
}
}
Use headerBypassMargins: false / footerBypassMargins: false for text that should align to the normal page content area. Do not try to fix header/footer text by adding marginLeftMm or marginRightMm to theme labels; the renderer chooses template bounds before label styling.
Leave a bypass flag true only for intentional full-bleed rules, backgrounds, or bands. If a design needs both edge-to-edge graphics and margin-aligned text, put that structure in the source JSON with separate operations/templates rather than pushing side margins into the theme.
Also size the reserved band correctly via the source's headerHeightMm / footerHeightMm. The body cursor starts below headerHeightMm and stops above pageHeightMm - footerHeightMm. Undersize either and body text bleeds into the band. Rule of thumb: headerHeightMm = (header fontSize in mm * lineHeight) + 4mm padding; same for footer (add an extra 1-2mm if the footer carries a divider rule above the text).
Labels:
- Every label has
fontFamily explicitly set (no inheritance)
- Table labels have
cellPaddingMm, borderColor, altRowColor if using alternating rows
- Divider labels have
color and lineWidth
- Bullet text marker labels have
marker character
- Bullet shape marker labels have
shape name
- Image labels use padding/margin props only (not font props)
- Image caption labels have
fontFamily, fontSize, color, align: "center"
- Colors are
[R, G, B] arrays, not hex strings
If using custom fonts:
customFonts array includes all fonts used in labels
- Each face has
style, fileName (ending in .ttf), and data (base64)
fontFamily in labels matches family in customFonts exactly
Verification
If the user has a source JSON ready, render it:
npx h17-sspdf -s <source.json> -t <theme-path> -o output/test.pdf