pdfmake 0.3.4 → 0.3.5
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/CHANGELOG.md +11 -1
- package/README.md +1 -0
- package/build/pdfmake.js +514 -28
- package/build/pdfmake.js.map +1 -1
- package/build/pdfmake.min.js +2 -2
- package/build/pdfmake.min.js.map +1 -1
- package/js/DocMeasure.js +3 -0
- package/js/DocumentContext.js +228 -4
- package/js/LayoutBuilder.js +136 -8
- package/js/PDFDocument.js +3 -1
- package/js/PageElementWriter.js +109 -2
- package/js/Renderer.js +13 -0
- package/js/SVGMeasure.js +4 -2
- package/js/TableProcessor.js +1 -5
- package/js/URLResolver.js +3 -1
- package/package.json +5 -5
- package/src/DocMeasure.js +4 -0
- package/src/DocumentContext.js +243 -9
- package/src/LayoutBuilder.js +145 -11
- package/src/PDFDocument.js +1 -1
- package/src/PageElementWriter.js +121 -2
- package/src/Renderer.js +13 -0
- package/src/SVGMeasure.js +2 -2
- package/src/TableProcessor.js +1 -5
- package/src/URLResolver.js +1 -1
package/js/SVGMeasure.js
CHANGED
|
@@ -28,8 +28,10 @@ const parseSVG = svgString => {
|
|
|
28
28
|
let doc;
|
|
29
29
|
try {
|
|
30
30
|
doc = new _xmldoc.XmlDocument(svgString);
|
|
31
|
-
} catch (
|
|
32
|
-
throw new Error('Invalid svg document (' +
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error('Invalid svg document (' + error + ')', {
|
|
33
|
+
cause: error
|
|
34
|
+
});
|
|
33
35
|
}
|
|
34
36
|
if (doc.name !== "svg") {
|
|
35
37
|
throw new Error('Invalid svg document (expected <svg>)');
|
package/js/TableProcessor.js
CHANGED
|
@@ -20,7 +20,7 @@ class TableProcessor {
|
|
|
20
20
|
const prepareRowSpanData = () => {
|
|
21
21
|
let rsd = [];
|
|
22
22
|
let x = 0;
|
|
23
|
-
let lastWidth
|
|
23
|
+
let lastWidth;
|
|
24
24
|
rsd.push({
|
|
25
25
|
left: 0,
|
|
26
26
|
rowSpan: 0
|
|
@@ -250,7 +250,6 @@ class TableProcessor {
|
|
|
250
250
|
lineColor: borderColor
|
|
251
251
|
}, false, (0, _variableType.isNumber)(overrideY), null, forcePage);
|
|
252
252
|
currentLine = null;
|
|
253
|
-
borderColor = null;
|
|
254
253
|
cellAbove = null;
|
|
255
254
|
currentCell = null;
|
|
256
255
|
rowCellAbove = null;
|
|
@@ -325,9 +324,6 @@ class TableProcessor {
|
|
|
325
324
|
dash: dash,
|
|
326
325
|
lineColor: borderColor
|
|
327
326
|
}, false, true);
|
|
328
|
-
cellBefore = null;
|
|
329
|
-
currentCell = null;
|
|
330
|
-
borderColor = null;
|
|
331
327
|
}
|
|
332
328
|
endTable(writer) {
|
|
333
329
|
if (this.cleanUpRepeatables) {
|
package/js/URLResolver.js
CHANGED
|
@@ -12,7 +12,9 @@ async function fetchUrl(url, headers = {}) {
|
|
|
12
12
|
}
|
|
13
13
|
return await response.arrayBuffer();
|
|
14
14
|
} catch (error) {
|
|
15
|
-
throw new Error(`Network request failed (url: "${url}", error: ${error.message})
|
|
15
|
+
throw new Error(`Network request failed (url: "${url}", error: ${error.message})`, {
|
|
16
|
+
cause: error
|
|
17
|
+
});
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
class URLResolver {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdfmake",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Client/server side PDF printing in pure JavaScript",
|
|
5
5
|
"main": "js/index.js",
|
|
6
6
|
"esnext": "src/index.js",
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
"@babel/core": "^7.29.0",
|
|
19
19
|
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
|
|
20
20
|
"@babel/preset-env": "^7.29.0",
|
|
21
|
-
"@eslint/js": "^
|
|
21
|
+
"@eslint/js": "^10.0.1",
|
|
22
22
|
"assert": "^2.1.0",
|
|
23
23
|
"babel-loader": "^10.0.0",
|
|
24
24
|
"brfs": "^2.0.2",
|
|
25
25
|
"browserify-zlib": "^0.2.0",
|
|
26
26
|
"buffer": "^6.0.3",
|
|
27
27
|
"core-js": "^3.48.0",
|
|
28
|
-
"eslint": "^
|
|
29
|
-
"eslint-plugin-jsdoc": "^62.
|
|
28
|
+
"eslint": "^10.0.1",
|
|
29
|
+
"eslint-plugin-jsdoc": "^62.7.0",
|
|
30
30
|
"expose-loader": "^5.0.1",
|
|
31
31
|
"file-saver": "^2.0.5",
|
|
32
32
|
"globals": "^17.3.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"terser-webpack-plugin": "^5.3.16",
|
|
44
44
|
"transform-loader": "^0.2.4",
|
|
45
45
|
"util": "^0.12.5",
|
|
46
|
-
"webpack": "^5.105.
|
|
46
|
+
"webpack": "^5.105.2",
|
|
47
47
|
"webpack-cli": "^6.0.1"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
package/src/DocMeasure.js
CHANGED
|
@@ -218,6 +218,10 @@ class DocMeasure {
|
|
|
218
218
|
{ text: item._textNodeRef.text, linkToDestination: destination, alignment: 'left', style: lineStyle, margin: lineMargin },
|
|
219
219
|
{ text: '00000', linkToDestination: destination, alignment: 'right', _tocItemRef: item._nodeRef, style: lineNumberStyle, margin: [0, lineMargin[1], 0, lineMargin[3]] }
|
|
220
220
|
]);
|
|
221
|
+
|
|
222
|
+
if (node.toc.outlines) {
|
|
223
|
+
item._textNodeRef.outline = item._textNodeRef.outline || true;
|
|
224
|
+
}
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
node.toc._table = {
|
package/src/DocumentContext.js
CHANGED
|
@@ -19,7 +19,7 @@ class DocumentContext extends EventEmitter {
|
|
|
19
19
|
this.backgroundLength = [];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
beginColumnGroup(marginXTopParent, bottomByPage = {}) {
|
|
22
|
+
beginColumnGroup(marginXTopParent, bottomByPage = {}, snakingColumns = false, columnGap = 0, columnWidths = null) {
|
|
23
23
|
this.snapshots.push({
|
|
24
24
|
x: this.x,
|
|
25
25
|
y: this.y,
|
|
@@ -34,7 +34,10 @@ class DocumentContext extends EventEmitter {
|
|
|
34
34
|
availableWidth: this.availableWidth,
|
|
35
35
|
page: this.page
|
|
36
36
|
},
|
|
37
|
-
lastColumnWidth: this.lastColumnWidth
|
|
37
|
+
lastColumnWidth: this.lastColumnWidth,
|
|
38
|
+
snakingColumns: snakingColumns,
|
|
39
|
+
gap: columnGap,
|
|
40
|
+
columnWidths: columnWidths
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
this.lastColumnWidth = 0;
|
|
@@ -45,20 +48,74 @@ class DocumentContext extends EventEmitter {
|
|
|
45
48
|
|
|
46
49
|
updateBottomByPage() {
|
|
47
50
|
const lastSnapshot = this.snapshots[this.snapshots.length - 1];
|
|
51
|
+
if (!lastSnapshot) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
48
54
|
const lastPage = this.page;
|
|
49
55
|
let previousBottom = -Number.MIN_VALUE;
|
|
50
|
-
if (lastSnapshot.bottomByPage[lastPage]) {
|
|
56
|
+
if (lastSnapshot.bottomByPage && lastSnapshot.bottomByPage[lastPage]) {
|
|
51
57
|
previousBottom = lastSnapshot.bottomByPage[lastPage];
|
|
52
58
|
}
|
|
53
|
-
lastSnapshot.bottomByPage
|
|
59
|
+
if (lastSnapshot.bottomByPage) {
|
|
60
|
+
lastSnapshot.bottomByPage[lastPage] = Math.max(previousBottom, this.y);
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
resetMarginXTopParent() {
|
|
57
65
|
this.marginXTopParent = null;
|
|
58
66
|
}
|
|
59
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Find the most recent (deepest) snaking column group snapshot.
|
|
70
|
+
* @returns {object|null}
|
|
71
|
+
*/
|
|
72
|
+
getSnakingSnapshot() {
|
|
73
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
74
|
+
if (this.snapshots[i].snakingColumns) {
|
|
75
|
+
return this.snapshots[i];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
inSnakingColumns() {
|
|
82
|
+
return !!this.getSnakingSnapshot();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if we're inside a nested non-snaking column group (e.g., a table row)
|
|
87
|
+
* within an outer snaking column group. This is used to prevent snaking-specific
|
|
88
|
+
* breaks inside table cells — the table's own page break mechanism should handle
|
|
89
|
+
* row breaks, and column breaks should happen between rows.
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
isInNestedNonSnakingGroup() {
|
|
93
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
94
|
+
let snap = this.snapshots[i];
|
|
95
|
+
if (snap.snakingColumns) {
|
|
96
|
+
return false; // Reached snaking snapshot without finding inner group
|
|
97
|
+
}
|
|
98
|
+
if (!snap.overflowed) {
|
|
99
|
+
return true; // Found non-snaking, non-overflowed inner group
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
60
105
|
beginColumn(width, offset, endingCell) {
|
|
106
|
+
// Find the correct snapshot for this column group.
|
|
107
|
+
// When a snaking column break (moveToNextColumn) occurs during inner column
|
|
108
|
+
// processing, overflowed snapshots may sit above this column group's snapshot.
|
|
109
|
+
// We need to skip past those to find the one from our beginColumnGroup call.
|
|
61
110
|
let saved = this.snapshots[this.snapshots.length - 1];
|
|
111
|
+
if (saved && saved.overflowed) {
|
|
112
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
113
|
+
if (!this.snapshots[i].overflowed) {
|
|
114
|
+
saved = this.snapshots[i];
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
62
119
|
|
|
63
120
|
this.calculateBottomMost(saved, endingCell);
|
|
64
121
|
|
|
@@ -102,6 +159,48 @@ class DocumentContext extends EventEmitter {
|
|
|
102
159
|
completeColumnGroup(height, endingCell) {
|
|
103
160
|
let saved = this.snapshots.pop();
|
|
104
161
|
|
|
162
|
+
// Track the maximum bottom position across all columns (including overflowed).
|
|
163
|
+
// Critical for snaking: content after columns must appear below the tallest column.
|
|
164
|
+
let maxBottomY = this.y;
|
|
165
|
+
let maxBottomPage = this.page;
|
|
166
|
+
let maxBottomAvailableHeight = this.availableHeight;
|
|
167
|
+
|
|
168
|
+
// Pop overflowed snapshots created by moveToNextColumn (snaking columns).
|
|
169
|
+
// Merge their bottomMost values to find the true maximum.
|
|
170
|
+
while (saved && saved.overflowed) {
|
|
171
|
+
let bm = bottomMostContext(
|
|
172
|
+
{
|
|
173
|
+
page: maxBottomPage,
|
|
174
|
+
y: maxBottomY,
|
|
175
|
+
availableHeight: maxBottomAvailableHeight
|
|
176
|
+
},
|
|
177
|
+
saved.bottomMost || {}
|
|
178
|
+
);
|
|
179
|
+
maxBottomPage = bm.page;
|
|
180
|
+
maxBottomY = bm.y;
|
|
181
|
+
maxBottomAvailableHeight = bm.availableHeight;
|
|
182
|
+
saved = this.snapshots.pop();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!saved) {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Apply the max bottom from all overflowed columns to this base snapshot
|
|
190
|
+
if (
|
|
191
|
+
maxBottomPage > saved.bottomMost.page ||
|
|
192
|
+
(maxBottomPage === saved.bottomMost.page &&
|
|
193
|
+
maxBottomY > saved.bottomMost.y)
|
|
194
|
+
) {
|
|
195
|
+
saved.bottomMost = {
|
|
196
|
+
x: saved.x,
|
|
197
|
+
y: maxBottomY,
|
|
198
|
+
page: maxBottomPage,
|
|
199
|
+
availableHeight: maxBottomAvailableHeight,
|
|
200
|
+
availableWidth: saved.availableWidth
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
105
204
|
this.calculateBottomMost(saved, endingCell);
|
|
106
205
|
|
|
107
206
|
this.x = saved.x;
|
|
@@ -125,16 +224,152 @@ class DocumentContext extends EventEmitter {
|
|
|
125
224
|
this.availableHeight -= (y - saved.bottomMost.y);
|
|
126
225
|
}
|
|
127
226
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
227
|
+
if (height && (saved.bottomMost.y - saved.y < height)) {
|
|
228
|
+
this.height = height;
|
|
229
|
+
} else {
|
|
131
230
|
this.height = saved.bottomMost.y - saved.y;
|
|
132
|
-
|
|
231
|
+
}
|
|
133
232
|
|
|
134
233
|
this.lastColumnWidth = saved.lastColumnWidth;
|
|
135
234
|
return saved.bottomByPage;
|
|
136
235
|
}
|
|
137
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Move to the next column in a column group (snaking columns).
|
|
239
|
+
* Creates an overflowed snapshot to track that we've moved to the next column.
|
|
240
|
+
* @returns {object} Position info for the new column
|
|
241
|
+
*/
|
|
242
|
+
moveToNextColumn() {
|
|
243
|
+
let prevY = this.y;
|
|
244
|
+
let snakingSnapshot = this.getSnakingSnapshot();
|
|
245
|
+
|
|
246
|
+
if (!snakingSnapshot) {
|
|
247
|
+
return { prevY: prevY, y: this.y };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Update snaking snapshot's bottomMost with current position BEFORE resetting.
|
|
251
|
+
// This captures where content reached in the current column (overflow point).
|
|
252
|
+
this.calculateBottomMost(snakingSnapshot, null);
|
|
253
|
+
|
|
254
|
+
// Calculate new X position: move right by current column width + gap
|
|
255
|
+
let overflowCount = 0;
|
|
256
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
257
|
+
if (this.snapshots[i].overflowed) {
|
|
258
|
+
overflowCount++;
|
|
259
|
+
} else {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let currentColumnWidth = (snakingSnapshot.columnWidths && snakingSnapshot.columnWidths[overflowCount]) || this.lastColumnWidth || this.availableWidth;
|
|
265
|
+
let nextColumnWidth = (snakingSnapshot.columnWidths && snakingSnapshot.columnWidths[overflowCount + 1]) || currentColumnWidth;
|
|
266
|
+
|
|
267
|
+
this.lastColumnWidth = nextColumnWidth;
|
|
268
|
+
|
|
269
|
+
let newX = this.x + (currentColumnWidth || 0) + (snakingSnapshot.gap || 0);
|
|
270
|
+
let newY = snakingSnapshot.y;
|
|
271
|
+
|
|
272
|
+
this.snapshots.push({
|
|
273
|
+
x: newX,
|
|
274
|
+
y: newY,
|
|
275
|
+
availableHeight: snakingSnapshot.availableHeight,
|
|
276
|
+
availableWidth: nextColumnWidth,
|
|
277
|
+
page: this.page,
|
|
278
|
+
overflowed: true,
|
|
279
|
+
bottomMost: {
|
|
280
|
+
x: newX,
|
|
281
|
+
y: newY,
|
|
282
|
+
availableHeight: snakingSnapshot.availableHeight,
|
|
283
|
+
availableWidth: nextColumnWidth,
|
|
284
|
+
page: this.page
|
|
285
|
+
},
|
|
286
|
+
lastColumnWidth: nextColumnWidth,
|
|
287
|
+
snakingColumns: true,
|
|
288
|
+
gap: snakingSnapshot.gap,
|
|
289
|
+
columnWidths: snakingSnapshot.columnWidths
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
this.x = newX;
|
|
293
|
+
this.y = newY;
|
|
294
|
+
this.availableHeight = snakingSnapshot.availableHeight;
|
|
295
|
+
this.availableWidth = nextColumnWidth;
|
|
296
|
+
|
|
297
|
+
// Sync non-overflowed inner snapshots (e.g. inner column groups for
|
|
298
|
+
// product/price rows) with the new snaking column position.
|
|
299
|
+
// Without this, inner beginColumn would read stale y/page/x values.
|
|
300
|
+
for (let i = this.snapshots.length - 2; i >= 0; i--) {
|
|
301
|
+
let snapshot = this.snapshots[i];
|
|
302
|
+
if (snapshot.overflowed || snapshot.snakingColumns) {
|
|
303
|
+
break; // Stop at first overflowed or snaking snapshot
|
|
304
|
+
}
|
|
305
|
+
snapshot.x = newX;
|
|
306
|
+
snapshot.y = newY;
|
|
307
|
+
snapshot.page = this.page;
|
|
308
|
+
snapshot.availableHeight = snakingSnapshot.availableHeight;
|
|
309
|
+
if (snapshot.bottomMost) {
|
|
310
|
+
snapshot.bottomMost.x = newX;
|
|
311
|
+
snapshot.bottomMost.y = newY;
|
|
312
|
+
snapshot.bottomMost.page = this.page;
|
|
313
|
+
snapshot.bottomMost.availableHeight = snakingSnapshot.availableHeight;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { prevY: prevY, y: this.y };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Reset snaking column state when moving to a new page.
|
|
322
|
+
* Clears overflowed snapshots, resets X to left margin, sets width to first column,
|
|
323
|
+
* and syncs all snapshots to new page coordinates.
|
|
324
|
+
*/
|
|
325
|
+
resetSnakingColumnsForNewPage() {
|
|
326
|
+
let snakingSnapshot = this.getSnakingSnapshot();
|
|
327
|
+
if (!snakingSnapshot) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let pageTopY = this.pageMargins.top;
|
|
332
|
+
let pageInnerHeight = this.getCurrentPage().pageSize.height - this.pageMargins.top - this.pageMargins.bottom;
|
|
333
|
+
|
|
334
|
+
// When moving to new page, start at first column.
|
|
335
|
+
// Reset width to FIRST column width, not last column from previous page.
|
|
336
|
+
let firstColumnWidth = snakingSnapshot.columnWidths ? snakingSnapshot.columnWidths[0] : (this.lastColumnWidth || this.availableWidth);
|
|
337
|
+
|
|
338
|
+
// Clean up overflowed snapshots
|
|
339
|
+
while (this.snapshots.length > 1 && this.snapshots[this.snapshots.length - 1].overflowed) {
|
|
340
|
+
this.snapshots.pop();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Reset X to start of first column (left margin)
|
|
344
|
+
if (this.marginXTopParent) {
|
|
345
|
+
this.x = this.pageMargins.left + this.marginXTopParent[0];
|
|
346
|
+
} else {
|
|
347
|
+
this.x = this.pageMargins.left;
|
|
348
|
+
}
|
|
349
|
+
this.availableWidth = firstColumnWidth;
|
|
350
|
+
this.lastColumnWidth = firstColumnWidth;
|
|
351
|
+
|
|
352
|
+
// Sync all snapshots to new page state.
|
|
353
|
+
// When page break occurs within snaking columns, update ALL snapshots
|
|
354
|
+
// (not just snaking column snapshots) to reflect new page coordinates.
|
|
355
|
+
// This ensures nested structures (like inner product/price columns)
|
|
356
|
+
// don't retain stale values that would cause layout corruption.
|
|
357
|
+
for (let i = 0; i < this.snapshots.length; i++) {
|
|
358
|
+
let snapshot = this.snapshots[i];
|
|
359
|
+
let isSnakingSnapshot = !!snapshot.snakingColumns;
|
|
360
|
+
snapshot.x = this.x;
|
|
361
|
+
snapshot.y = isSnakingSnapshot ? pageTopY : this.y;
|
|
362
|
+
snapshot.availableHeight = isSnakingSnapshot ? pageInnerHeight : this.availableHeight;
|
|
363
|
+
snapshot.page = this.page;
|
|
364
|
+
if (snapshot.bottomMost) {
|
|
365
|
+
snapshot.bottomMost.x = this.x;
|
|
366
|
+
snapshot.bottomMost.y = isSnakingSnapshot ? pageTopY : this.y;
|
|
367
|
+
snapshot.bottomMost.availableHeight = isSnakingSnapshot ? pageInnerHeight : this.availableHeight;
|
|
368
|
+
snapshot.bottomMost.page = this.page;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
138
373
|
addMargin(left, right) {
|
|
139
374
|
this.x += left;
|
|
140
375
|
this.availableWidth -= left + (right || 0);
|
|
@@ -318,7 +553,6 @@ const getPageSize = (currentPage, newPageOrientation) => {
|
|
|
318
553
|
height: currentPage.pageSize.height
|
|
319
554
|
};
|
|
320
555
|
}
|
|
321
|
-
|
|
322
556
|
};
|
|
323
557
|
|
|
324
558
|
|
package/src/LayoutBuilder.js
CHANGED
|
@@ -420,7 +420,11 @@ class LayoutBuilder {
|
|
|
420
420
|
if (availableHeight - margin[1] < 0) {
|
|
421
421
|
// Consume the whole available space
|
|
422
422
|
this.writer.context().moveDown(availableHeight);
|
|
423
|
-
this.writer.
|
|
423
|
+
if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
|
|
424
|
+
this.snakingAwarePageBreak(node.pageOrientation);
|
|
425
|
+
} else {
|
|
426
|
+
this.writer.moveToNextPage(node.pageOrientation);
|
|
427
|
+
}
|
|
424
428
|
/**
|
|
425
429
|
* TODO - Something to consider:
|
|
426
430
|
* Right now the node starts at the top of next page (after header)
|
|
@@ -442,7 +446,11 @@ class LayoutBuilder {
|
|
|
442
446
|
// Necessary for nodes inside tables
|
|
443
447
|
if (availableHeight - margin[3] < 0) {
|
|
444
448
|
this.writer.context().moveDown(availableHeight);
|
|
445
|
-
this.writer.
|
|
449
|
+
if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
|
|
450
|
+
this.snakingAwarePageBreak(node.pageOrientation);
|
|
451
|
+
} else {
|
|
452
|
+
this.writer.moveToNextPage(node.pageOrientation);
|
|
453
|
+
}
|
|
446
454
|
/**
|
|
447
455
|
* TODO - Something to consider:
|
|
448
456
|
* Right now next node starts at the top of next page (after header)
|
|
@@ -553,6 +561,37 @@ class LayoutBuilder {
|
|
|
553
561
|
}
|
|
554
562
|
}
|
|
555
563
|
|
|
564
|
+
/**
|
|
565
|
+
* Helper for page breaks that respects snaking column context.
|
|
566
|
+
* When in snaking columns, first tries moving to next column.
|
|
567
|
+
* If no columns available, moves to next page and resets x to left margin.
|
|
568
|
+
* @param {string} pageOrientation - Optional page orientation for the new page
|
|
569
|
+
*/
|
|
570
|
+
snakingAwarePageBreak(pageOrientation) {
|
|
571
|
+
let ctx = this.writer.context();
|
|
572
|
+
let snakingSnapshot = ctx.getSnakingSnapshot();
|
|
573
|
+
if (!snakingSnapshot) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Try flowing to next column first
|
|
578
|
+
if (this.writer.canMoveToNextColumn()) {
|
|
579
|
+
this.writer.moveToNextColumn();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// No more columns available, move to new page
|
|
584
|
+
this.writer.moveToNextPage(pageOrientation);
|
|
585
|
+
|
|
586
|
+
// Reset snaking column state for the new page
|
|
587
|
+
// Save lastColumnWidth before reset — if we're inside a nested
|
|
588
|
+
// column group (e.g. product/price row), the reset would overwrite
|
|
589
|
+
// it with the snaking column width, corrupting inner column layout.
|
|
590
|
+
let savedLastColumnWidth = ctx.lastColumnWidth;
|
|
591
|
+
ctx.resetSnakingColumnsForNewPage();
|
|
592
|
+
ctx.lastColumnWidth = savedLastColumnWidth;
|
|
593
|
+
}
|
|
594
|
+
|
|
556
595
|
// vertical container
|
|
557
596
|
processVerticalContainer(node) {
|
|
558
597
|
node.stack.forEach(item => {
|
|
@@ -648,7 +687,8 @@ class LayoutBuilder {
|
|
|
648
687
|
marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
|
|
649
688
|
cells: columns,
|
|
650
689
|
widths: columns,
|
|
651
|
-
gaps
|
|
690
|
+
gaps,
|
|
691
|
+
snakingColumns: columnNode.snakingColumns
|
|
652
692
|
});
|
|
653
693
|
addAll(columnNode.positions, result.positions);
|
|
654
694
|
this.nestedLevel--;
|
|
@@ -838,7 +878,7 @@ class LayoutBuilder {
|
|
|
838
878
|
return null;
|
|
839
879
|
}
|
|
840
880
|
|
|
841
|
-
processRow({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height }) {
|
|
881
|
+
processRow({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height, snakingColumns = false }) {
|
|
842
882
|
const isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
|
|
843
883
|
let pageBreaks = [];
|
|
844
884
|
let pageBreaksByRowSpan = [];
|
|
@@ -855,8 +895,19 @@ class LayoutBuilder {
|
|
|
855
895
|
// Use the marginX if we are in a top level table/column (not nested)
|
|
856
896
|
const marginXParent = this.nestedLevel === 1 ? marginX : null;
|
|
857
897
|
const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
|
|
858
|
-
|
|
859
|
-
|
|
898
|
+
// Pass column gap and widths to context snapshot for snaking columns
|
|
899
|
+
// to advance correctly and reset to first-column width on new pages.
|
|
900
|
+
const columnGapForGroup = (gaps && gaps.length > 1) ? gaps[1] : 0;
|
|
901
|
+
const columnWidthsForContext = widths.map(w => w._calcWidth);
|
|
902
|
+
this.writer.context().beginColumnGroup(marginXParent, _bottomByPage,
|
|
903
|
+
snakingColumns, columnGapForGroup, columnWidthsForContext);
|
|
904
|
+
|
|
905
|
+
// IMPORTANT: We iterate ALL columns even when snakingColumns is enabled.
|
|
906
|
+
// This is intentional — beginColumn() must be called for each column to set up
|
|
907
|
+
// proper geometry (widths, offsets) and rowspan/colspan tracking. The
|
|
908
|
+
// completeColumnGroup() call at the end depends on this bookkeeping to compute
|
|
909
|
+
// heights correctly. Content processing is skipped for columns > 0 via
|
|
910
|
+
// skipForSnaking below, but the column structure must still be established.
|
|
860
911
|
for (let i = 0, l = cells.length; i < l; i++) {
|
|
861
912
|
let cell = cells[i];
|
|
862
913
|
let cellIndexBegin = i;
|
|
@@ -913,7 +964,13 @@ class LayoutBuilder {
|
|
|
913
964
|
// We pass the endingSpanCell reference to store the context just after processing rowspan cell
|
|
914
965
|
this.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
|
|
915
966
|
|
|
916
|
-
|
|
967
|
+
// When snaking, only process content from the first column (i === 0).
|
|
968
|
+
// Content overflows into subsequent columns via moveToNextColumn().
|
|
969
|
+
// We skip content processing here but NOT the beginColumn() call above —
|
|
970
|
+
// the column geometry setup is still needed for proper layout bookkeeping.
|
|
971
|
+
const skipForSnaking = snakingColumns && i > 0;
|
|
972
|
+
|
|
973
|
+
if (!cell._span && !skipForSnaking) {
|
|
917
974
|
this.processNode(cell, true);
|
|
918
975
|
this.writer.context().updateBottomByPage();
|
|
919
976
|
|
|
@@ -968,7 +1025,11 @@ class LayoutBuilder {
|
|
|
968
1025
|
// If content did not break page, check if we should break by height
|
|
969
1026
|
if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
|
|
970
1027
|
this.writer.context().moveDown(this.writer.context().availableHeight);
|
|
971
|
-
|
|
1028
|
+
if (snakingColumns) {
|
|
1029
|
+
this.snakingAwarePageBreak();
|
|
1030
|
+
} else {
|
|
1031
|
+
this.writer.moveToNextPage();
|
|
1032
|
+
}
|
|
972
1033
|
}
|
|
973
1034
|
|
|
974
1035
|
const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
|
|
@@ -989,7 +1050,7 @@ class LayoutBuilder {
|
|
|
989
1050
|
itemBegin.cell = cell;
|
|
990
1051
|
itemBegin.bottomY = this.writer.context().y;
|
|
991
1052
|
itemBegin.isCellContentMultiPage = !itemBegin.cell.positions.every(item => item.pageNumber === itemBegin.cell.positions[0].pageNumber);
|
|
992
|
-
itemBegin.getViewHeight = function() {
|
|
1053
|
+
itemBegin.getViewHeight = function () {
|
|
993
1054
|
if (this.cell._willBreak) {
|
|
994
1055
|
return this.cell._bottomY - this.cell._rowTopPageY;
|
|
995
1056
|
}
|
|
@@ -1009,7 +1070,7 @@ class LayoutBuilder {
|
|
|
1009
1070
|
|
|
1010
1071
|
return this.viewHeight;
|
|
1011
1072
|
};
|
|
1012
|
-
itemBegin.getNodeHeight = function() {
|
|
1073
|
+
itemBegin.getNodeHeight = function () {
|
|
1013
1074
|
return this.nodeHeight;
|
|
1014
1075
|
};
|
|
1015
1076
|
|
|
@@ -1077,7 +1138,32 @@ class LayoutBuilder {
|
|
|
1077
1138
|
processor.beginTable(this.writer);
|
|
1078
1139
|
|
|
1079
1140
|
let rowHeights = tableNode.table.heights;
|
|
1141
|
+
let lastRowHeight = 0;
|
|
1142
|
+
|
|
1080
1143
|
for (let i = 0, l = tableNode.table.body.length; i < l; i++) {
|
|
1144
|
+
// Between table rows: check if we should move to the next snaking column.
|
|
1145
|
+
// This must happen AFTER the previous row's endRow (borders drawn) and
|
|
1146
|
+
// BEFORE this row's beginRow. At this point, the table row column group
|
|
1147
|
+
// has been completed, so canMoveToNextColumn() works correctly.
|
|
1148
|
+
if (i > 0 && this.writer.context().inSnakingColumns()) {
|
|
1149
|
+
// Estimate minimum space for next row: use last row's height as heuristic,
|
|
1150
|
+
// or fall back to a minimum of padding + line height + border
|
|
1151
|
+
let minRowHeight = lastRowHeight > 0 ? lastRowHeight : (
|
|
1152
|
+
processor.rowPaddingTop + 14 + processor.rowPaddingBottom +
|
|
1153
|
+
processor.bottomLineWidth + processor.topLineWidth
|
|
1154
|
+
);
|
|
1155
|
+
if (this.writer.context().availableHeight < minRowHeight) {
|
|
1156
|
+
this.snakingAwarePageBreak();
|
|
1157
|
+
|
|
1158
|
+
// Skip border when headerRows present (header repeat includes it)
|
|
1159
|
+
if (processor.layout.hLineWhenBroken !== false && !processor.headerRows) {
|
|
1160
|
+
processor.drawHorizontalLine(i, this.writer);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
let rowYBefore = this.writer.context().y;
|
|
1166
|
+
|
|
1081
1167
|
// if dontBreakRows and row starts a rowspan
|
|
1082
1168
|
// we store the 'y' of the beginning of each rowSpan
|
|
1083
1169
|
if (processor.dontBreakRows) {
|
|
@@ -1130,6 +1216,12 @@ class LayoutBuilder {
|
|
|
1130
1216
|
}
|
|
1131
1217
|
|
|
1132
1218
|
processor.endRow(i, this.writer, result.pageBreaks);
|
|
1219
|
+
|
|
1220
|
+
// Track the height of the completed row for the next row's estimate
|
|
1221
|
+
let rowYAfter = this.writer.context().y;
|
|
1222
|
+
if (this.writer.context().page === pageBeforeProcessing) {
|
|
1223
|
+
lastRowHeight = rowYAfter - rowYBefore;
|
|
1224
|
+
}
|
|
1133
1225
|
}
|
|
1134
1226
|
|
|
1135
1227
|
processor.endTable(this.writer);
|
|
@@ -1155,6 +1247,27 @@ class LayoutBuilder {
|
|
|
1155
1247
|
}
|
|
1156
1248
|
}
|
|
1157
1249
|
|
|
1250
|
+
if (node.outline) {
|
|
1251
|
+
line._outline = {
|
|
1252
|
+
id: node.id,
|
|
1253
|
+
parentId: node.outlineParentId,
|
|
1254
|
+
text: node.outlineText || node.text,
|
|
1255
|
+
expanded: node.outlineExpanded || false
|
|
1256
|
+
};
|
|
1257
|
+
} else if (Array.isArray(node.text)) {
|
|
1258
|
+
for (let i = 0, l = node.text.length; i < l; i++) {
|
|
1259
|
+
let item = node.text[i];
|
|
1260
|
+
if (item.outline) {
|
|
1261
|
+
line._outline = {
|
|
1262
|
+
id: item.id,
|
|
1263
|
+
parentId: item.outlineParentId,
|
|
1264
|
+
text: item.outlineText || item.text,
|
|
1265
|
+
expanded: item.outlineExpanded || false
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1158
1271
|
if (node._tocItemRef) {
|
|
1159
1272
|
line._pageNodeRef = node._tocItemRef;
|
|
1160
1273
|
}
|
|
@@ -1176,6 +1289,28 @@ class LayoutBuilder {
|
|
|
1176
1289
|
}
|
|
1177
1290
|
|
|
1178
1291
|
while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
|
|
1292
|
+
// Check if line fits vertically in current context
|
|
1293
|
+
if (line.getHeight() > this.writer.context().availableHeight && this.writer.context().y > this.writer.context().pageMargins.top) {
|
|
1294
|
+
// Line doesn't fit, forced move to next page/column
|
|
1295
|
+
// Only do snaking-specific break if we're in snaking columns AND NOT inside
|
|
1296
|
+
// a nested non-snaking group (like a table row). Table cells should use
|
|
1297
|
+
// standard page breaks — column breaks happen between table rows instead.
|
|
1298
|
+
if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
|
|
1299
|
+
this.snakingAwarePageBreak(node.pageOrientation);
|
|
1300
|
+
|
|
1301
|
+
// Always reflow text after a snaking break (column or page).
|
|
1302
|
+
// This ensures text adapts to the new column width, whether it's narrower or wider.
|
|
1303
|
+
if (line.inlines && line.inlines.length > 0) {
|
|
1304
|
+
node._inlines.unshift(...line.inlines);
|
|
1305
|
+
}
|
|
1306
|
+
// Rebuild line with new width
|
|
1307
|
+
line = this.buildNextLine(node);
|
|
1308
|
+
continue;
|
|
1309
|
+
} else {
|
|
1310
|
+
this.writer.moveToNextPage(node.pageOrientation);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1179
1314
|
let positions = this.writer.addLine(line);
|
|
1180
1315
|
node.positions.push(positions);
|
|
1181
1316
|
line = this.buildNextLine(node);
|
|
@@ -1241,7 +1376,6 @@ class LayoutBuilder {
|
|
|
1241
1376
|
(line.hasEnoughSpaceForInline(textNode._inlines[0], textNode._inlines.slice(1)) || isForceContinue)) {
|
|
1242
1377
|
let isHardWrap = false;
|
|
1243
1378
|
let inline = textNode._inlines.shift();
|
|
1244
|
-
isForceContinue = false;
|
|
1245
1379
|
|
|
1246
1380
|
if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
|
|
1247
1381
|
let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), (txt) =>
|
package/src/PDFDocument.js
CHANGED
|
@@ -114,7 +114,7 @@ class PDFDocument extends PDFKit {
|
|
|
114
114
|
throw new Error('No image');
|
|
115
115
|
}
|
|
116
116
|
} catch (error) {
|
|
117
|
-
throw new Error(`Invalid image: ${error.toString()}\nImages dictionary should contain dataURL entries (or local file paths in node.js)
|
|
117
|
+
throw new Error(`Invalid image: ${error.toString()}\nImages dictionary should contain dataURL entries (or local file paths in node.js)`, { cause: error });
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
image.embed(this);
|