{
  "markdown": "# SuperSimplePDF\n\n[![npm](https://img.shields.io/npm/v/h17-sspdf)](https://www.npmjs.com/package/h17-sspdf)\n[![Socket Badge](https://socket.dev/api/badge/npm/package/h17-sspdf)](https://socket.dev/npm/package/h17-sspdf)\n[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)\n[![Node](https://img.shields.io/node/v/h17-sspdf)](https://www.npmjs.com/package/h17-sspdf)\n[![Publish](https://github.com/hugopalma17/sspdf/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/hugopalma17/sspdf/actions/workflows/npm-publish.yml)\n\nDefine the layout once. Feed it JSON. The core is blind to both and does all the math.\n\nThe 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.\n\n```\nSource JSON  +  Theme  =  PDF\n```\n\n## Install\n\n```bash\nnpm install h17-sspdf\n```\n\nRequires **Node.js 18 or newer**. The engine vendors a single self-contained UMD build of jsPDF (which bundles fflate, fast-png, iobuffer internally) plus Chart.js and chartjs-node-canvas. The base install has no runtime dependencies. The optional chart plugin uses [`canvas`](https://www.npmjs.com/package/canvas) as an optional peer dependency.\n\n## The problem it solves\n\nGenerating PDFs imperatively means tracking the cursor yourself. Every element you place shifts everything below it. Line wrapping, page breaks, font resets, all manual.\n\nThis engine inverts that. You describe *what* to render and *how it looks*. The cursor, the math, the page breaks happen automatically.\n\n## How it works\n\nEvery 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.\n\n```\noperation → label → theme style → layout → cursor advance → next operation\n```\n\nPage breaks happen automatically when content reaches the bottom margin. Style resets after every operation, nothing leaks.\n\n## Quick start\n\n```js\nconst { renderDocument } = require('h17-sspdf');\n\nrenderDocument({\n  source: {\n    operations: [\n      { type: 'text', label: 'doc.title', text: 'My Document' },\n      { type: 'divider', label: 'doc.rule' },\n      { type: 'text', label: 'doc.body', text: 'First paragraph.' }\n    ]\n  },\n  theme: {\n    name: 'My Theme',\n    page: {\n      format: 'a4',\n      orientation: 'portrait',\n      unit: 'mm',\n      marginTopMm: 20,\n      marginBottomMm: 20,\n      marginLeftMm: 20,\n      marginRightMm: 20,\n      backgroundColor: [255, 255, 255],\n      defaultText:      { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 10, color: [0,0,0], lineHeight: 1.4 },\n      defaultStroke:    { color: [200,200,200], lineWidth: 0.3, lineCap: 'butt', lineJoin: 'miter' },\n      defaultFillColor: [255, 255, 255],\n    },\n    labels: {\n      'doc.title': { fontFamily: 'helvetica', fontStyle: 'bold', fontSize: 22, color: [0,0,0], lineHeight: 1.2, marginBottomMm: 4 },\n      'doc.rule':  { color: [200,200,200], lineWidth: 0.3, marginBottomMm: 4 },\n      'doc.body':  { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 10, color: [40,40,40], lineHeight: 1.5 },\n    }\n  },\n  outputPath: 'output/doc.pdf'\n});\n```\n\n## Building a layout\n\n### The newspaper front page\n\nThe most complex built-in layout is a newspaper front page. It has a masthead, edition line, heavy rule, headline hierarchy, byline, multi-paragraph body, pull quote, stat scoreboard, and a footer that repeats on every page. Here is how it is built.\n\n#### Page template (repeating footer)\n\nDeclare it once. The engine stamps it on every page at the specified Y position.\n\n```json\n{\n  \"pageTemplates\": {\n    \"footer\": [\n      { \"type\": \"divider\", \"label\": \"news.footer.rule\", \"x1Mm\": 18, \"x2Mm\": 192 },\n      {\n        \"type\": \"row\",\n        \"leftLabel\":  \"news.footer.left\",\n        \"rightLabel\": \"news.footer.right\",\n        \"leftText\":   \"The Meridian Times | Civic Desk\",\n        \"rightText\":  \"Page {{page}}\",\n        \"xLeftMm\": 18,\n        \"xRightMm\": 192\n      }\n    ],\n    \"footerHeightMm\": 8,\n    \"footerStartMm\": 284\n  }\n}\n```\n\n`{{page}}` is replaced with the current page number at render time.\n\n#### Masthead block\n\nThe masthead, edition row, heavy rule, kicker, headline, deck, and byline are all inside a `section`. A section allows page breaks inside it but groups the content logically.\n\n```json\n{\n  \"type\": \"section\",\n  \"content\": [\n    { \"type\": \"text\",  \"label\": \"news.masthead\", \"text\": \"The Meridian Times\", \"xMm\": 18, \"maxWidthMm\": 174 },\n    {\n      \"type\": \"row\",\n      \"leftLabel\":  \"news.edition.left\",\n      \"rightLabel\": \"news.edition.right\",\n      \"leftText\":   \"Saturday, March 7, 2026\",\n      \"rightText\":  \"Late City Edition\",\n      \"xLeftMm\": 18,\n      \"xRightMm\": 192\n    },\n    { \"type\": \"divider\", \"label\": \"news.rule.heavy\", \"x1Mm\": 18, \"x2Mm\": 192 },\n    { \"type\": \"text\", \"label\": \"news.kicker\",   \"text\": \"Infrastructure & Society\", \"xMm\": 18, \"maxWidthMm\": 174 },\n    { \"type\": \"text\", \"label\": \"news.headline\", \"text\": \"Local Governments Are Finally Rewriting How They Publish Public Records\", \"xMm\": 18, \"maxWidthMm\": 174 },\n    { \"type\": \"text\", \"label\": \"news.deck\",     \"text\": \"A quiet wave of procurement reform...\", \"xMm\": 18, \"maxWidthMm\": 174 },\n    {\n      \"type\": \"row\",\n      \"leftLabel\":  \"news.byline\",\n      \"rightLabel\": \"news.timestamp\",\n      \"leftText\":   \"By Marta Ruiz\",\n      \"rightText\":  \"Updated 6:40 PM\",\n      \"xLeftMm\": 18,\n      \"xRightMm\": 192\n    },\n    { \"type\": \"divider\", \"label\": \"news.rule.light\", \"x1Mm\": 18, \"x2Mm\": 192 }\n  ]\n}\n```\n\n`xMm` and `maxWidthMm` override the page margins for this operation. This is how you position content independently of the theme margins.\n\n#### Multi-paragraph body text\n\nPass `text` as an array. Each string becomes a paragraph with the label's spacing applied between them.\n\n```json\n{\n  \"type\": \"text\",\n  \"label\": \"news.body\",\n  \"text\": [\n    \"First paragraph.\",\n    \"Second paragraph.\",\n    \"Third paragraph.\"\n  ],\n  \"xMm\": 18,\n  \"maxWidthMm\": 174\n}\n```\n\n#### Keeping a heading with its content\n\n`keepWithNext: N` 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.\n\n```json\n{ \"type\": \"text\", \"label\": \"news.section.title\", \"text\": \"Inside the shift\", \"keepWithNext\": 3 }\n```\n\n#### Stat scoreboard\n\nA repeating pattern of `row` + `text` pairs. The row carries the label/value, the text below carries the annotation.\n\n```json\n{\n  \"type\": \"row\",\n  \"leftLabel\":  \"news.stat.label\",\n  \"rightLabel\": \"news.stat.value\",\n  \"leftText\":   \"Harbor City planning notices\",\n  \"rightText\":  \"42% faster\"\n},\n{\n  \"type\": \"text\",\n  \"label\": \"news.stat.note\",\n  \"text\": \"Review time fell after zoning notices moved to a single contract.\"\n}\n```\n\n#### Pull quote\n\n```json\n{\n  \"type\": \"quote\",\n  \"label\": \"news.pullquote\",\n  \"text\": \"When the format becomes a system instead of a template, agencies stop re-solving the same layout problem every week.\",\n  \"attribution\": \"- Elena Ward, public records modernization lead\",\n  \"xMm\": 22,\n  \"maxWidthMm\": 166\n}\n```\n\n`xMm` and `maxWidthMm` indent it from the body column, the indentation is in the source, not the theme.\n\n#### Hidden text (ATS / search metadata)\n\nInvisible in the rendered PDF, present in text extraction.\n\n```json\n{\n  \"type\": \"hiddenText\",\n  \"label\": \"news.hidden.tags\",\n  \"text\": \"public records procurement modernization searchable notices\"\n}\n```\n\n---\n\n### The theme labels for this layout\n\nEach label the source uses must exist in the theme. These are the ones the newspaper source uses:\n\n```js\nlabels: {\n  'news.masthead':     { fontFamily: 'custom', fontStyle: 'bold', fontSize: 36, color: [0,0,0], lineHeight: 1.1, marginBottomMm: 2 },\n  'news.edition.left': { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 8, color: [80,80,80], lineHeight: 1 },\n  'news.edition.right':{ fontFamily: 'helvetica', fontStyle: 'italic', fontSize: 8, color: [80,80,80], lineHeight: 1, marginBottomMm: 1 },\n  'news.rule.heavy':   { color: [0,0,0], lineWidth: 1.2, marginBottomMm: 2 },\n  'news.rule.light':   { color: [160,160,160], lineWidth: 0.3, marginBottomMm: 3 },\n  'news.kicker':       { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 9, color: [100,100,100], lineHeight: 1, textTransform: 'uppercase', marginBottomMm: 1 },\n  'news.headline':     { fontFamily: 'custom', fontStyle: 'bold', fontSize: 28, color: [0,0,0], lineHeight: 1.15, marginBottomMm: 3 },\n  'news.deck':         { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 11, color: [40,40,40], lineHeight: 1.4, marginBottomMm: 2 },\n  'news.byline':       { fontFamily: 'helvetica', fontStyle: 'bold', fontSize: 8, color: [0,0,0], lineHeight: 1 },\n  'news.timestamp':    { fontFamily: 'helvetica', fontStyle: 'italic', fontSize: 8, color: [80,80,80], lineHeight: 1, marginBottomMm: 2 },\n  'news.body':         { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 10, color: [20,20,20], lineHeight: 1.55, marginBottomMm: 3 },\n  'news.section.title':{ fontFamily: 'helvetica', fontStyle: 'bold', fontSize: 11, color: [0,0,0], lineHeight: 1.2, marginTopMm: 3, marginBottomMm: 1 },\n  'news.pullquote':    { fontFamily: 'custom', fontStyle: 'italic', fontSize: 13, color: [30,30,30], lineHeight: 1.5,\n                         leftBorder: { widthMm: 1.5, color: [0,0,0], paddingMm: 4 }, marginTopMm: 4, marginBottomMm: 4 },\n  'news.stat.label':   { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 9, color: [40,40,40], lineHeight: 1 },\n  'news.stat.value':   { fontFamily: 'helvetica', fontStyle: 'bold', fontSize: 9, color: [0,0,0], lineHeight: 1 },\n  'news.stat.note':    { fontFamily: 'helvetica', fontStyle: 'italic', fontSize: 8, color: [100,100,100], lineHeight: 1.3, marginBottomMm: 3 },\n  'news.brief.text':   { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 10, color: [20,20,20], lineHeight: 1.5 },\n  'news.brief.marker': { color: [0,0,0] },\n  'news.footer.rule':  { color: [160,160,160], lineWidth: 0.3, marginBottomMm: 1 },\n  'news.footer.left':  { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 7, color: [120,120,120], lineHeight: 1 },\n  'news.footer.right': { fontFamily: 'helvetica', fontStyle: 'normal', fontSize: 7, color: [120,120,120], lineHeight: 1 },\n  'news.hidden.tags':  { fontSize: 0.1, color: [255,255,255] },\n}\n```\n\n---\n\n## Operation types\n\n| Type | Purpose | Key fields |\n|------|---------|------------|\n| `text` | Wrapped text block | `label`, `text` (string or array of strings) |\n| `row` | Left/right pair on one line | `leftLabel`, `rightLabel`, `leftText`, `rightText` |\n| `bullet` | Marker + wrapped text | `label`, `markerLabel`, `bullets` (array) |\n| `divider` | Horizontal rule | `label`, `x1Mm`, `x2Mm` |\n| `image` | Embedded PNG/JPEG | `src`, `width` (percentage or mm), `caption` |\n| `spacer` | Vertical gap | `mm`, `px`, or `label` |\n| `pageBreak` | Force new page | (none) |\n| `hiddenText` | Invisible text | `label`, `text` |\n| `quote` | Blockquote with attribution | `label`, `text`, `attribution` |\n| `block` | Group children, optional background + border | `children`, `keepTogether` |\n| `section` | Logical group, allows breaks inside | `content` |\n| `columns` | Two-column side-by-side layout | `column1`, `column2`, `gutterMm` |\n\n### Position overrides\n\nAny operation accepts `xMm` and `maxWidthMm` to override the theme margins for that operation only.\n\n### Page break control\n\n- `keepWithNext: N` - keep this operation on the same page as the next N operations\n- `block` with `keepTogether: true` - all children stay on the same page\n\n---\n\n## Label style properties\n\n```js\n{\n  // Typography\n  fontFamily: 'helvetica',       // or any registered custom font family\n  fontStyle: 'normal',           // 'normal' | 'bold' | 'italic' | 'bolditalic'\n  fontSize: 10,                  // pt\n  color: [0, 0, 0],              // RGB\n  lineHeight: 1.4,               // multiplier\n  textTransform: 'upper',        // 'upper' | 'lower' | undefined\n\n  // Spacing\n  marginTopMm: 0,\n  marginBottomMm: 0,\n  marginTopPx: 0,\n  marginBottomPx: 0,\n  paddingTopMm: 0,\n  paddingBottomMm: 0,\n  paddingTopPx: 0,\n  paddingBottomPx: 0,\n\n  // Dividers\n  lineWidth: 0.3,\n\n  // Container (block/section)\n  backgroundColor: [245, 245, 245],\n  borderColor: [200, 200, 200],\n  borderWidthMm: 0.3,\n  paddingMm: 4,\n\n  // Left border accent (quote, callout)\n  leftBorder: {\n    widthMm: 1.5,\n    color: [0, 0, 0],\n    paddingMm: 4,\n  },\n}\n```\n\n---\n\n## Built-in fonts\n\n20 Google Fonts ship with the package as base64 TTF. Each exports `{ Regular, Bold }`.\n\n**Sans-serif:** Inter, Roboto, Open Sans, Montserrat, Lato, Raleway, Nunito, Work Sans, IBM Plex Sans, PT Sans, Oswald\n\n**Serif:** Merriweather, Lora, Playfair Display, Crimson Text, Libre Baskerville, Source Serif 4\n\n**Monospace:** Fira Code, JetBrains Mono, Source Code Pro\n\n```js\nconst INTER = require('h17-sspdf/fonts/inter.js');\n\ncustomFonts: [{\n  family: 'Inter',\n  faces: [\n    { style: 'normal', fileName: 'Inter-Regular.ttf', data: INTER.Regular },\n    { style: 'bold',   fileName: 'Inter-Bold.ttf',    data: INTER.Bold },\n  ],\n}],\n```\n\nList all fonts: `npx h17-sspdf --fonts`\n\n---\n\n## Vector shapes\n\n20 built-in vector shapes rendered via jsPDF drawing primitives. No text encoding, no font dependencies.\n\nUse as bullet markers by setting `shape` on a marker label:\n\n```js\n// Theme\n'bullet.arrow': { shape: 'arrow', shapeColor: [0, 128, 255], shapeSize: 0.8 }\n\n// Source JSON (same bullet operation as always)\n{ \"type\": \"bullet\", \"label\": \"doc.body\", \"markerLabel\": \"bullet.arrow\", \"bullets\": [\"Point one\"] }\n```\n\nAvailable: `arrow`, `circle`, `square`, `diamond`, `triangle`, `dash`, `chevron`, `doubleColon`, `commentSlash`, `hashComment`, `bracketChevron`, `treeBranch`, `terminalPrompt`, `checkmark`, `cross`, `star`, `plus`, `minus`, `warning`, `infoCircle`\n\nList all shapes: `npx h17-sspdf --shapes`\n\n---\n\n## Custom fonts\n\nEmbed your own TTF as base64 and register in the theme:\n\n```js\ncustomFonts: [\n  {\n    family: 'MyFont',\n    faces: [\n      { style: 'normal', fileName: 'MyFont-Regular.ttf', data: '<base64>' },\n      { style: 'bold',   fileName: 'MyFont-Bold.ttf',    data: '<base64>' },\n    ],\n  },\n],\n```\n\nThen use `fontFamily: 'MyFont'` in any label.\n\n---\n\n## Chart plugin\n\nRenders any Chart.js configuration to a PNG and embeds it in the PDF.\n\n### Requirements\n\nThe chart plugin requires the `canvas` npm package (native C++ addon). Chart.js and chartjs-node-canvas are vendored and ship with the engine.\n\n```bash\nnpm install canvas\n```\n\n### Register\n\n```js\nconst { registerPlugin, plugins } = require('h17-sspdf');\nregisterPlugin('chart', plugins.chart);\n```\n\n### Operation format\n\n```json\n{\n  \"type\": \"chart\",\n  \"chartType\": \"bar\",\n  \"widthMm\": 160,\n  \"heightMm\": 80,\n  \"canvasWidth\": 1600,\n  \"canvasHeight\": 800,\n  \"data\": {\n    \"labels\": [\"Q1\", \"Q2\", \"Q3\", \"Q4\"],\n    \"datasets\": [\n      {\n        \"label\": \"Revenue\",\n        \"data\": [120000, 145000, 138000, 172000],\n        \"backgroundColor\": \"rgba(110, 158, 210, 0.80)\"\n      }\n    ]\n  },\n  \"options\": {\n    \"scales\": {\n      \"y\": { \"beginAtZero\": true }\n    }\n  }\n}\n```\n\n`data` and `options` are passed directly to Chart.js, the plugin does not abstract the Chart.js API. `canvasWidth`/`canvasHeight` control render resolution (default 1600×800). `widthMm`/`heightMm` control the slot size in the PDF.\n\n---\n\n## CLI\n\n```bash\nnpx h17-sspdf -s source.json -t theme.js -o output.pdf\n```\n\n| Flag | Short | Description |\n|------|-------|-------------|\n| `--source` | `-s` | Path to source JSON (or pipe via stdin) |\n| `--theme` | `-t` | Path to theme `.js` file or built-in name |\n| `--output` | `-o` | Output PDF path |\n| `--fonts` | | List built-in fonts |\n| `--shapes` | | List built-in vector shapes |\n| `--help` | `-h` | Show help |\n\n---\n\n## AI skills\n\nClaude Code skills for generating PDFs and themes are available in the `skills/` directory of the [GitHub repository](https://github.com/hugopalma17/sspdf):\n\n- `skills/sspdf/` - Generate PDF documents from a task description\n- `skills/sspdf-theme-generator/` - Generate theme files from brand specs\n\n---\n\n## Security model\n\nSuperSimplePDF turns data into a document. The split between data and code matters for how you treat each input.\n\n- Source JSON is data. You can generate it from untrusted input. The engine reads it as a description of what to render, never as code.\n- Theme files are code. A `.js` theme is loaded with `require`, so it runs with your process privileges. Treat a theme like any other module you import. Do not load theme files you do not trust. Built-in themes referenced by name are safe.\n- Image paths are contained. The `image` operation reads files from disk via `src`. To stop an untrusted source document from reading arbitrary files into a PDF, `src` must be a relative path that stays inside the working directory. Absolute paths, parent directory traversal with `..`, and null bytes are rejected.\n\nThe engine has no network access and no runtime dependencies in the base install, so a source document cannot trigger an outbound request. The optional chart plugin adds the `canvas` native module, which renders locally and does not phone home.\n\nIf you find a security issue, see SECURITY.md.\n\n## Constraints\n\n- Page format defaults to A4; custom dimensions supported via `pageWidthMm`/`pageHeightMm` (e.g. 16:9 presentations)\n- Single-line `row` cells, no multi-line column pairs\n- `{{page}}` gives the current page number; `{{pages}}` (total page count) is not supported because keep-together rules make the final page count unpredictable until the last operation is laid out\n- Charts require the `canvas` npm package (native C++ addon) for server-side rendering; everything else is zero native dependencies\n- A `jspdf.umd.js` build is vendored for client-side/browser use. It bundles all dependencies internally but requires wiring up your own entry point; `pdf-core.js` uses the Node build by default\n\n---\n\nHugo Palma, 2026\n\n## Third-party\n\nThis project vendors the following MIT-licensed libraries:\n\n- [jsPDF](https://github.com/parallax/jsPDF) - PDF generation (UMD build, bundles fflate, fast-png, iobuffer internally). Copyright (c) 2010-2025 James Hall, yWorks GmbH.\n- [Chart.js](https://github.com/chartjs/Chart.js) - Chart rendering. Copyright (c) 2014-2024 Chart.js Contributors.\n- [chartjs-node-canvas](https://github.com/SeanSobey/ChartjsNodeCanvas) - Server-side Chart.js rendering. Copyright (c) 2018 Sean Sobey.\n\nFull license texts are in `vendor/*/LICENSE`.\n",
  "bytes": 18635,
  "sha": "6d42f9e659d69132049824a8c6ff36006f731680be0ad2f836d567e7219ec184",
  "repo_slug": "hugopalma17/sspdf",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_hugopalma17_sspdf_038ea78b/readme"
}