h17-sspdf 0.4.1 → 1.0.0
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/DOCUMENTATION.md +19 -3
- package/README.md +2 -1
- package/core/pdf-core.js +54 -6
- package/core/plugin-chart.js +19 -3
- package/core/render-document.js +20 -10
- package/examples/sources/og-sspdf.png +0 -0
- package/examples/sources/source-presentation.json +634 -0
- package/examples/themes/theme-presentation.js +276 -0
- package/package.json +1 -1
- package/vendor/jspdf/jspdf.node.js +150 -57
package/DOCUMENTATION.md
CHANGED
|
@@ -30,9 +30,11 @@ Controls page geometry, background, and baseline rendering state.
|
|
|
30
30
|
```js
|
|
31
31
|
page: {
|
|
32
32
|
// Page geometry
|
|
33
|
-
format: "a4", //
|
|
33
|
+
format: "a4", // named format: "a4", "letter", etc.
|
|
34
34
|
orientation: "portrait", // "portrait" or "landscape"
|
|
35
35
|
unit: "mm", // only "mm" is supported
|
|
36
|
+
pageWidthMm: 338, // custom width in mm (overrides format)
|
|
37
|
+
pageHeightMm: 190, // custom height in mm (overrides format)
|
|
36
38
|
compress: true, // PDF compression (default true)
|
|
37
39
|
|
|
38
40
|
// Margins - individual margins override the shared `margin` value
|
|
@@ -76,7 +78,9 @@ page: {
|
|
|
76
78
|
}
|
|
77
79
|
```
|
|
78
80
|
|
|
79
|
-
The content area runs from `marginTop` to `pageHeight - marginBottom` vertically, and `marginLeft` to `pageWidth - marginRight` horizontally. On A4 portrait, the page is 210
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
When `pageWidthMm` and `pageHeightMm` are both set, they override the `format` field and create a page with those exact dimensions. This is useful for non-standard formats like 16:9 presentations (e.g. 338 x 190 mm). All layout math -- margins, content area, pagination -- adapts automatically.
|
|
80
84
|
|
|
81
85
|
### `labels` (required)
|
|
82
86
|
|
|
@@ -235,6 +239,7 @@ Shared layout defaults read by operation handlers.
|
|
|
235
239
|
```js
|
|
236
240
|
layout: {
|
|
237
241
|
bulletIndentMm: 4.5, // text indent after bullet marker (default: 4)
|
|
242
|
+
chartAlign: "center", // chart horizontal alignment: "left" (default) or "center"
|
|
238
243
|
}
|
|
239
244
|
```
|
|
240
245
|
|
|
@@ -553,6 +558,16 @@ Three forms (use exactly one):
|
|
|
553
558
|
| `px` | number | Fixed space in CSS px |
|
|
554
559
|
| `label` | string | Theme label with `spaceMm` or `spacePx` |
|
|
555
560
|
|
|
561
|
+
#### `pageBreak`
|
|
562
|
+
|
|
563
|
+
Forces a new page. The cursor resets to the top content area. Page templates (headers/footers) are applied to the new page automatically.
|
|
564
|
+
|
|
565
|
+
```json
|
|
566
|
+
{ "type": "pageBreak" }
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
No fields required. Useful for presentation-style layouts where each section should start on its own page.
|
|
570
|
+
|
|
556
571
|
#### `hiddenText`
|
|
557
572
|
|
|
558
573
|
Renders text in the background color so it's invisible but present in the PDF text layer (for ATS keyword injection).
|
|
@@ -1714,11 +1729,12 @@ main();
|
|
|
1714
1729
|
| `chartType` | yes | string | Chart.js type: `"bar"`, `"line"`, `"doughnut"`, etc. |
|
|
1715
1730
|
| `data` | yes | object | Chart.js `data` config (`labels` + `datasets`) |
|
|
1716
1731
|
| `widthMm` | no | number | Width in the PDF in mm (default: content area width) |
|
|
1717
|
-
| `heightMm` | no | number | Height in
|
|
1732
|
+
| `heightMm` | no | number or `"fill"` | Height in mm (default: 80). `"fill"` uses remaining page space. |
|
|
1718
1733
|
| `canvasWidth` | no | number | Canvas render width in pixels (default: 1600) |
|
|
1719
1734
|
| `canvasHeight` | no | number | Canvas render height in pixels (default: 800) |
|
|
1720
1735
|
| `options` | no | object | Chart.js `options` object. `responsive: false` and `animation: false` are injected automatically. |
|
|
1721
1736
|
| `xMm` | no | number | Left edge in mm (default: content left margin) |
|
|
1737
|
+
| `align` | no | string | `"left"` (default) or `"center"`. Can also be set globally via `layout.chartAlign` in the theme. |
|
|
1722
1738
|
|
|
1723
1739
|
### Resolution
|
|
1724
1740
|
|
package/README.md
CHANGED
|
@@ -263,6 +263,7 @@ labels: {
|
|
|
263
263
|
| `divider` | Horizontal rule | `label`, `x1Mm`, `x2Mm` |
|
|
264
264
|
| `image` | Embedded PNG/JPEG | `src`, `width` (percentage or mm), `caption` |
|
|
265
265
|
| `spacer` | Vertical gap | `mm`, `px`, or `label` |
|
|
266
|
+
| `pageBreak` | Force new page | (none) |
|
|
266
267
|
| `hiddenText` | Invisible text | `label`, `text` |
|
|
267
268
|
| `quote` | Blockquote with attribution | `label`, `text`, `attribution` |
|
|
268
269
|
| `block` | Group children, optional background + border | `children`, `keepTogether` |
|
|
@@ -466,7 +467,7 @@ Claude Code skills for generating PDFs and themes are available in the `skills/`
|
|
|
466
467
|
|
|
467
468
|
## Constraints
|
|
468
469
|
|
|
469
|
-
- A4
|
|
470
|
+
- Page format defaults to A4; custom dimensions supported via `pageWidthMm`/`pageHeightMm` (e.g. 16:9 presentations)
|
|
470
471
|
- Single-line `row` cells, no multi-line column pairs
|
|
471
472
|
- `{{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
|
|
472
473
|
- Charts require the `canvas` npm package (native C++ addon) for server-side rendering; everything else is zero native dependencies
|
package/core/pdf-core.js
CHANGED
|
@@ -77,6 +77,8 @@ class PDFCore {
|
|
|
77
77
|
* @param {number} [theme.page.margin]
|
|
78
78
|
* @param {string} [theme.page.format]
|
|
79
79
|
* @param {string} [theme.page.orientation]
|
|
80
|
+
* @param {number} [theme.page.pageWidthMm]
|
|
81
|
+
* @param {number} [theme.page.pageHeightMm]
|
|
80
82
|
* @param {string} [theme.page.unit]
|
|
81
83
|
* @param {boolean} [theme.page.compress]
|
|
82
84
|
* @param {number[]} [theme.page.backgroundColor]
|
|
@@ -102,11 +104,13 @@ class PDFCore {
|
|
|
102
104
|
this.margin = this.marginLeftMm;
|
|
103
105
|
this.headerHeightMm = Number(this.page.headerHeightMm) || 0;
|
|
104
106
|
this.footerHeightMm = Number(this.page.footerHeightMm) || 0;
|
|
105
|
-
|
|
107
|
+
const customW = Number(this.page.pageWidthMm) || 0;
|
|
108
|
+
const customH = Number(this.page.pageHeightMm) || 0;
|
|
109
|
+
const hasCustomDimensions = customW > 0 && customH > 0;
|
|
106
110
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
this.format = hasCustomDimensions
|
|
112
|
+
? [customW, customH]
|
|
113
|
+
: String(this.page.format || "a4").toLowerCase();
|
|
110
114
|
|
|
111
115
|
this.doc = new jsPDF({
|
|
112
116
|
orientation: this.page.orientation || "portrait",
|
|
@@ -119,6 +123,8 @@ class PDFCore {
|
|
|
119
123
|
this.pageHeight = this.doc.internal.pageSize.getHeight();
|
|
120
124
|
this.backgroundColor = this._resolveColor(this.page.backgroundColor);
|
|
121
125
|
this.lastDrawnBounds = null;
|
|
126
|
+
this.documentStateDepth = 0;
|
|
127
|
+
this.hasDeferredInitialRenderState = Array.isArray(theme.customFonts) && theme.customFonts.length > 0;
|
|
122
128
|
this.defaultRenderState = this._buildDefaultRenderState(this.page);
|
|
123
129
|
this.contentTopY = this.marginTopMm + this.headerHeightMm;
|
|
124
130
|
this.contentBottomY = this.pageHeight - this.marginBottomMm - this.footerHeightMm;
|
|
@@ -134,7 +140,12 @@ class PDFCore {
|
|
|
134
140
|
});
|
|
135
141
|
|
|
136
142
|
this.paintBackground();
|
|
137
|
-
|
|
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
|
+
}
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
/**
|
|
@@ -174,9 +185,11 @@ class PDFCore {
|
|
|
174
185
|
* Force a new page and reset cursor to top margin.
|
|
175
186
|
*/
|
|
176
187
|
addPage() {
|
|
188
|
+
const reopenedStateDepth = this._closeDocumentStatesForPageBreak();
|
|
177
189
|
this.doc.addPage();
|
|
178
190
|
this.paintBackground();
|
|
179
191
|
this.applyDefaultRenderState();
|
|
192
|
+
this._reopenDocumentStatesAfterPageBreak(reopenedStateDepth);
|
|
180
193
|
this.cursorY = this.contentTopY;
|
|
181
194
|
}
|
|
182
195
|
|
|
@@ -194,6 +207,7 @@ class PDFCore {
|
|
|
194
207
|
&& typeof this.doc.restoreGraphicsState === "function";
|
|
195
208
|
if (canSaveGraphicsState) {
|
|
196
209
|
this.doc.saveGraphicsState();
|
|
210
|
+
this.documentStateDepth += 1;
|
|
197
211
|
}
|
|
198
212
|
|
|
199
213
|
try {
|
|
@@ -201,6 +215,7 @@ class PDFCore {
|
|
|
201
215
|
} finally {
|
|
202
216
|
if (canSaveGraphicsState) {
|
|
203
217
|
this.doc.restoreGraphicsState();
|
|
218
|
+
this.documentStateDepth = Math.max(0, this.documentStateDepth - 1);
|
|
204
219
|
}
|
|
205
220
|
this.applyDefaultRenderState();
|
|
206
221
|
}
|
|
@@ -494,11 +509,21 @@ class PDFCore {
|
|
|
494
509
|
const baseline = y + baselineOffsetMm;
|
|
495
510
|
|
|
496
511
|
if (markerStyle.shape) {
|
|
497
|
-
// 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.
|
|
498
517
|
const { renderShape, getShapeWidth } = require("./shapes");
|
|
499
518
|
const shapeColor = markerStyle.shapeColor || markerStyle.color || [0, 0, 0];
|
|
500
519
|
const shapeSize = markerStyle.shapeSize || 1;
|
|
520
|
+
if (typeof this.doc.saveGraphicsState === "function") {
|
|
521
|
+
this.doc.saveGraphicsState();
|
|
522
|
+
}
|
|
501
523
|
renderShape(markerStyle.shape, this.doc, x, baseline, shapeColor, shapeSize, textFontSize);
|
|
524
|
+
if (typeof this.doc.restoreGraphicsState === "function") {
|
|
525
|
+
this.doc.restoreGraphicsState();
|
|
526
|
+
}
|
|
502
527
|
this.applyDefaultRenderState();
|
|
503
528
|
} else {
|
|
504
529
|
// Text-based marker (existing behavior)
|
|
@@ -1066,6 +1091,29 @@ class PDFCore {
|
|
|
1066
1091
|
fillColor: this._resolveColor(page.defaultFillColor),
|
|
1067
1092
|
};
|
|
1068
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
|
+
}
|
|
1069
1117
|
}
|
|
1070
1118
|
|
|
1071
1119
|
module.exports = {
|
package/core/plugin-chart.js
CHANGED
|
@@ -132,14 +132,30 @@ module.exports = {
|
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
135
|
+
const { theme } = ctx;
|
|
136
|
+
const contentWidth = bounds.right - bounds.left;
|
|
137
|
+
const widthMm = operation.widthMm || contentWidth;
|
|
138
|
+
const heightMm = operation.heightMm === "fill"
|
|
139
|
+
? Math.max(0, core.contentBottomY - core.getCursorY())
|
|
140
|
+
: (operation.heightMm || 80);
|
|
141
|
+
const align = operation.align || (theme && theme.layout && theme.layout.chartAlign) || "left";
|
|
142
|
+
|
|
143
|
+
let x;
|
|
144
|
+
if (operation.xMm !== undefined) {
|
|
145
|
+
x = operation.xMm;
|
|
146
|
+
} else if (align === "center") {
|
|
147
|
+
x = bounds.left + (contentWidth - widthMm) / 2;
|
|
148
|
+
} else {
|
|
149
|
+
x = bounds.left;
|
|
150
|
+
}
|
|
138
151
|
|
|
139
152
|
core.drawImage({ data: operation._buf, format: 'PNG', x, widthMm, heightMm });
|
|
140
153
|
},
|
|
141
154
|
|
|
142
155
|
estimateHeight(ctx) {
|
|
156
|
+
if (ctx.operation.heightMm === "fill") {
|
|
157
|
+
return ctx.core.contentBottomY - ctx.core.getCursorY();
|
|
158
|
+
}
|
|
143
159
|
return (ctx.operation.heightMm || 80) + 4;
|
|
144
160
|
},
|
|
145
161
|
|
package/core/render-document.js
CHANGED
|
@@ -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
|
|
|
@@ -221,17 +222,21 @@ function executeOperations(ctx) {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
225
|
+
if (operation.type === "pageBreak") {
|
|
226
|
+
core.addPage();
|
|
227
|
+
} else {
|
|
228
|
+
core.withDocumentState(() => {
|
|
229
|
+
executeOperation({
|
|
230
|
+
core,
|
|
231
|
+
theme,
|
|
232
|
+
operation,
|
|
233
|
+
index,
|
|
234
|
+
templateMode,
|
|
235
|
+
templateBypassMargins,
|
|
236
|
+
insideContainer,
|
|
237
|
+
});
|
|
233
238
|
});
|
|
234
|
-
}
|
|
239
|
+
}
|
|
235
240
|
}
|
|
236
241
|
}
|
|
237
242
|
|
|
@@ -335,6 +340,7 @@ function isOperationType(type) {
|
|
|
335
340
|
|| type === "bullet"
|
|
336
341
|
|| type === "divider"
|
|
337
342
|
|| type === "spacer"
|
|
343
|
+
|| type === "pageBreak"
|
|
338
344
|
|| type === "hiddenText"
|
|
339
345
|
|| type === "table"
|
|
340
346
|
|| type === "image"
|
|
@@ -1026,6 +1032,10 @@ function estimateOperationHeight(ctx) {
|
|
|
1026
1032
|
throw new Error(`Spacer operation at index ${index} must provide mm, px, or label with spaceMm/spacePx`);
|
|
1027
1033
|
}
|
|
1028
1034
|
|
|
1035
|
+
if (operation.type === "pageBreak") {
|
|
1036
|
+
return 0;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1029
1039
|
if (operation.type === "hiddenText") {
|
|
1030
1040
|
return 0;
|
|
1031
1041
|
}
|
|
Binary file
|