h17-sspdf 0.5.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,8 @@ Source JSON + Theme = PDF
20
20
  npm install h17-sspdf
21
21
  ```
22
22
 
23
+ Requires **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 only install-time dependency is [`canvas`](https://www.npmjs.com/package/canvas) (native C++ addon, used by the chart plugin) — no transitive dependency tree beyond that.
24
+
23
25
  ## The problem it solves
24
26
 
25
27
  Generating PDFs imperatively means tracking the cursor yourself. Every element you place shifts everything below it. Line wrapping, page breaks, font resets, all manual.
@@ -471,6 +473,7 @@ Claude Code skills for generating PDFs and themes are available in the `skills/`
471
473
  - Single-line `row` cells, no multi-line column pairs
472
474
  - `{{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
473
475
  - Charts require the `canvas` npm package (native C++ addon) for server-side rendering; everything else is zero native dependencies
476
+ - 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
474
477
 
475
478
  ---
476
479
 
@@ -480,7 +483,7 @@ Hugo Palma, 2026
480
483
 
481
484
  This project vendors the following MIT-licensed libraries:
482
485
 
483
- - [jsPDF](https://github.com/parallax/jsPDF) - PDF generation. Copyright (c) 2010-2025 James Hall, yWorks GmbH.
486
+ - [jsPDF](https://github.com/parallax/jsPDF) - PDF generation (UMD build, bundles fflate, fast-png, iobuffer internally). Copyright (c) 2010-2025 James Hall, yWorks GmbH.
484
487
  - [Chart.js](https://github.com/chartjs/Chart.js) - Chart rendering. Copyright (c) 2014-2024 Chart.js Contributors.
485
488
  - [chartjs-node-canvas](https://github.com/SeanSobey/ChartjsNodeCanvas) - Server-side Chart.js rendering. Copyright (c) 2018 Sean Sobey.
486
489
 
package/core/pdf-core.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const fs = require("fs");
2
- const { jsPDF } = require("../vendor/jspdf/jspdf.node");
2
+ const { jsPDF } = require("../vendor/jspdf/jspdf.umd.js");
3
3
  const { pxToMm, ptToMm, resolveLineHeightMm } = require("./units");
4
4
 
5
5
  // Style math helpers, shared between core rendering and height estimation.
@@ -123,6 +123,8 @@ class PDFCore {
123
123
  this.pageHeight = this.doc.internal.pageSize.getHeight();
124
124
  this.backgroundColor = this._resolveColor(this.page.backgroundColor);
125
125
  this.lastDrawnBounds = null;
126
+ this.documentStateDepth = 0;
127
+ this.hasDeferredInitialRenderState = Array.isArray(theme.customFonts) && theme.customFonts.length > 0;
126
128
  this.defaultRenderState = this._buildDefaultRenderState(this.page);
127
129
  this.contentTopY = this.marginTopMm + this.headerHeightMm;
128
130
  this.contentBottomY = this.pageHeight - this.marginBottomMm - this.footerHeightMm;
@@ -138,7 +140,12 @@ class PDFCore {
138
140
  });
139
141
 
140
142
  this.paintBackground();
141
- this.applyDefaultRenderState();
143
+ // Custom fonts are registered by renderDocument() after core construction.
144
+ // Deferring the first setFont() avoids jsPDF warnings when page.defaultText
145
+ // uses a custom family that is not yet in the font map.
146
+ if (!this.hasDeferredInitialRenderState) {
147
+ this.applyDefaultRenderState();
148
+ }
142
149
  }
143
150
 
144
151
  /**
@@ -178,9 +185,11 @@ class PDFCore {
178
185
  * Force a new page and reset cursor to top margin.
179
186
  */
180
187
  addPage() {
188
+ const reopenedStateDepth = this._closeDocumentStatesForPageBreak();
181
189
  this.doc.addPage();
182
190
  this.paintBackground();
183
191
  this.applyDefaultRenderState();
192
+ this._reopenDocumentStatesAfterPageBreak(reopenedStateDepth);
184
193
  this.cursorY = this.contentTopY;
185
194
  }
186
195
 
@@ -198,6 +207,7 @@ class PDFCore {
198
207
  && typeof this.doc.restoreGraphicsState === "function";
199
208
  if (canSaveGraphicsState) {
200
209
  this.doc.saveGraphicsState();
210
+ this.documentStateDepth += 1;
201
211
  }
202
212
 
203
213
  try {
@@ -205,6 +215,7 @@ class PDFCore {
205
215
  } finally {
206
216
  if (canSaveGraphicsState) {
207
217
  this.doc.restoreGraphicsState();
218
+ this.documentStateDepth = Math.max(0, this.documentStateDepth - 1);
208
219
  }
209
220
  this.applyDefaultRenderState();
210
221
  }
@@ -498,11 +509,21 @@ class PDFCore {
498
509
  const baseline = y + baselineOffsetMm;
499
510
 
500
511
  if (markerStyle.shape) {
501
- // Vector shape marker: renders via core/shapes.js, no text encoding needed
512
+ // Vector shape marker: renders via core/shapes.js, no text encoding needed.
513
+ // Wrapped in saveGraphicsState/restoreGraphicsState to isolate draw state
514
+ // mutations (setLineCap, setFillColor, etc.) from the main content stream.
515
+ // Without this, accumulated state operators can cause print rendering issues
516
+ // where printer RIPs interpret the stacked state differently than screen viewers.
502
517
  const { renderShape, getShapeWidth } = require("./shapes");
503
518
  const shapeColor = markerStyle.shapeColor || markerStyle.color || [0, 0, 0];
504
519
  const shapeSize = markerStyle.shapeSize || 1;
520
+ if (typeof this.doc.saveGraphicsState === "function") {
521
+ this.doc.saveGraphicsState();
522
+ }
505
523
  renderShape(markerStyle.shape, this.doc, x, baseline, shapeColor, shapeSize, textFontSize);
524
+ if (typeof this.doc.restoreGraphicsState === "function") {
525
+ this.doc.restoreGraphicsState();
526
+ }
506
527
  this.applyDefaultRenderState();
507
528
  } else {
508
529
  // Text-based marker (existing behavior)
@@ -1070,6 +1091,29 @@ class PDFCore {
1070
1091
  fillColor: this._resolveColor(page.defaultFillColor),
1071
1092
  };
1072
1093
  }
1094
+
1095
+ _closeDocumentStatesForPageBreak() {
1096
+ if (this.documentStateDepth <= 0) {
1097
+ return 0;
1098
+ }
1099
+
1100
+ for (let i = 0; i < this.documentStateDepth; i += 1) {
1101
+ this.doc.restoreGraphicsState();
1102
+ }
1103
+
1104
+ return this.documentStateDepth;
1105
+ }
1106
+
1107
+ _reopenDocumentStatesAfterPageBreak(depth) {
1108
+ const count = Number(depth) || 0;
1109
+ if (count <= 0) {
1110
+ return;
1111
+ }
1112
+
1113
+ for (let i = 0; i < count; i += 1) {
1114
+ this.doc.saveGraphicsState();
1115
+ }
1116
+ }
1073
1117
  }
1074
1118
 
1075
1119
  module.exports = {
@@ -47,6 +47,7 @@ function renderDocument(input) {
47
47
 
48
48
  const core = new PDFCore(runtimeTheme);
49
49
  registerThemeFonts(core, runtimeTheme);
50
+ core.applyDefaultRenderState();
50
51
 
51
52
  installPageTemplates(core, runtimeTheme, built.pageTemplates);
52
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h17-sspdf",
3
- "version": "0.5.0",
3
+ "version": "1.0.1",
4
4
  "description": "Declarative PDF engine - define layout once, feed it JSON, get consistent PDFs",
5
5
  "main": "index.js",
6
6
  "author": "Hugo Palma",
@@ -20,7 +20,9 @@
20
20
  "files": [
21
21
  "core",
22
22
  "fonts",
23
- "vendor",
23
+ "vendor/jspdf",
24
+ "vendor/chart.js",
25
+ "vendor/chartjs-node-canvas",
24
26
  "examples/themes",
25
27
  "examples/sources",
26
28
  "index.js",