pdfmake 0.3.4 → 0.3.6
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 +26 -1
- package/README.md +1 -0
- package/build/pdfmake.js +612 -104
- 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 +7 -2
- 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/Printer.js +1 -19
- package/js/Renderer.js +13 -0
- package/js/SVGMeasure.js +4 -2
- package/js/TableProcessor.js +1 -5
- package/js/URLResolver.js +14 -1
- package/js/base.js +19 -2
- package/js/browser-extensions/index.js +0 -2
- package/js/index.js +0 -2
- package/package.json +5 -5
- package/src/DocMeasure.js +8 -2
- 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/Printer.js +1 -20
- package/src/Renderer.js +13 -0
- package/src/SVGMeasure.js +2 -2
- package/src/TableProcessor.js +1 -5
- package/src/URLResolver.js +14 -1
- package/src/base.js +24 -2
- package/src/browser-extensions/index.js +0 -2
- package/src/index.js +0 -2
package/js/DocMeasure.js
CHANGED
|
@@ -86,9 +86,11 @@ class DocMeasure {
|
|
|
86
86
|
node._width = node._minWidth = node._maxWidth = node.cover.width;
|
|
87
87
|
node._height = node._minHeight = node._maxHeight = node.cover.height;
|
|
88
88
|
} else {
|
|
89
|
+
let nodeWidth = (0, _variableType.isNumber)(node.width) ? node.width : undefined;
|
|
90
|
+
let nodeHeight = (0, _variableType.isNumber)(node.height) ? node.height : undefined;
|
|
89
91
|
let ratio = dimensions.width / dimensions.height;
|
|
90
|
-
node._width = node._minWidth = node._maxWidth =
|
|
91
|
-
node._height =
|
|
92
|
+
node._width = node._minWidth = node._maxWidth = nodeWidth || (nodeHeight ? nodeHeight * ratio : dimensions.width);
|
|
93
|
+
node._height = nodeHeight || (nodeWidth ? nodeWidth / ratio : dimensions.height);
|
|
92
94
|
if ((0, _variableType.isNumber)(node.maxWidth) && node.maxWidth < node._width) {
|
|
93
95
|
node._width = node._minWidth = node._maxWidth = node.maxWidth;
|
|
94
96
|
node._height = node._width * dimensions.height / dimensions.width;
|
|
@@ -204,6 +206,9 @@ class DocMeasure {
|
|
|
204
206
|
style: lineNumberStyle,
|
|
205
207
|
margin: [0, lineMargin[1], 0, lineMargin[3]]
|
|
206
208
|
}]);
|
|
209
|
+
if (node.toc.outlines) {
|
|
210
|
+
item._textNodeRef.outline = item._textNodeRef.outline || true;
|
|
211
|
+
}
|
|
207
212
|
}
|
|
208
213
|
node.toc._table = {
|
|
209
214
|
table: {
|
package/js/DocumentContext.js
CHANGED
|
@@ -20,7 +20,7 @@ class DocumentContext extends _events.EventEmitter {
|
|
|
20
20
|
this.snapshots = [];
|
|
21
21
|
this.backgroundLength = [];
|
|
22
22
|
}
|
|
23
|
-
beginColumnGroup(marginXTopParent, bottomByPage = {}) {
|
|
23
|
+
beginColumnGroup(marginXTopParent, bottomByPage = {}, snakingColumns = false, columnGap = 0, columnWidths = null) {
|
|
24
24
|
this.snapshots.push({
|
|
25
25
|
x: this.x,
|
|
26
26
|
y: this.y,
|
|
@@ -35,7 +35,10 @@ class DocumentContext extends _events.EventEmitter {
|
|
|
35
35
|
availableWidth: this.availableWidth,
|
|
36
36
|
page: this.page
|
|
37
37
|
},
|
|
38
|
-
lastColumnWidth: this.lastColumnWidth
|
|
38
|
+
lastColumnWidth: this.lastColumnWidth,
|
|
39
|
+
snakingColumns: snakingColumns,
|
|
40
|
+
gap: columnGap,
|
|
41
|
+
columnWidths: columnWidths
|
|
39
42
|
});
|
|
40
43
|
this.lastColumnWidth = 0;
|
|
41
44
|
if (marginXTopParent) {
|
|
@@ -44,18 +47,71 @@ class DocumentContext extends _events.EventEmitter {
|
|
|
44
47
|
}
|
|
45
48
|
updateBottomByPage() {
|
|
46
49
|
const lastSnapshot = this.snapshots[this.snapshots.length - 1];
|
|
50
|
+
if (!lastSnapshot) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
47
53
|
const lastPage = this.page;
|
|
48
54
|
let previousBottom = -Number.MIN_VALUE;
|
|
49
|
-
if (lastSnapshot.bottomByPage[lastPage]) {
|
|
55
|
+
if (lastSnapshot.bottomByPage && lastSnapshot.bottomByPage[lastPage]) {
|
|
50
56
|
previousBottom = lastSnapshot.bottomByPage[lastPage];
|
|
51
57
|
}
|
|
52
|
-
lastSnapshot.bottomByPage
|
|
58
|
+
if (lastSnapshot.bottomByPage) {
|
|
59
|
+
lastSnapshot.bottomByPage[lastPage] = Math.max(previousBottom, this.y);
|
|
60
|
+
}
|
|
53
61
|
}
|
|
54
62
|
resetMarginXTopParent() {
|
|
55
63
|
this.marginXTopParent = null;
|
|
56
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Find the most recent (deepest) snaking column group snapshot.
|
|
68
|
+
* @returns {object|null}
|
|
69
|
+
*/
|
|
70
|
+
getSnakingSnapshot() {
|
|
71
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
72
|
+
if (this.snapshots[i].snakingColumns) {
|
|
73
|
+
return this.snapshots[i];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
inSnakingColumns() {
|
|
79
|
+
return !!this.getSnakingSnapshot();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if we're inside a nested non-snaking column group (e.g., a table row)
|
|
84
|
+
* within an outer snaking column group. This is used to prevent snaking-specific
|
|
85
|
+
* breaks inside table cells — the table's own page break mechanism should handle
|
|
86
|
+
* row breaks, and column breaks should happen between rows.
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
isInNestedNonSnakingGroup() {
|
|
90
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
91
|
+
let snap = this.snapshots[i];
|
|
92
|
+
if (snap.snakingColumns) {
|
|
93
|
+
return false; // Reached snaking snapshot without finding inner group
|
|
94
|
+
}
|
|
95
|
+
if (!snap.overflowed) {
|
|
96
|
+
return true; // Found non-snaking, non-overflowed inner group
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
57
101
|
beginColumn(width, offset, endingCell) {
|
|
102
|
+
// Find the correct snapshot for this column group.
|
|
103
|
+
// When a snaking column break (moveToNextColumn) occurs during inner column
|
|
104
|
+
// processing, overflowed snapshots may sit above this column group's snapshot.
|
|
105
|
+
// We need to skip past those to find the one from our beginColumnGroup call.
|
|
58
106
|
let saved = this.snapshots[this.snapshots.length - 1];
|
|
107
|
+
if (saved && saved.overflowed) {
|
|
108
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
109
|
+
if (!this.snapshots[i].overflowed) {
|
|
110
|
+
saved = this.snapshots[i];
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
59
115
|
this.calculateBottomMost(saved, endingCell);
|
|
60
116
|
this.page = saved.page;
|
|
61
117
|
this.x = this.x + this.lastColumnWidth + (offset || 0);
|
|
@@ -91,6 +147,40 @@ class DocumentContext extends _events.EventEmitter {
|
|
|
91
147
|
}
|
|
92
148
|
completeColumnGroup(height, endingCell) {
|
|
93
149
|
let saved = this.snapshots.pop();
|
|
150
|
+
|
|
151
|
+
// Track the maximum bottom position across all columns (including overflowed).
|
|
152
|
+
// Critical for snaking: content after columns must appear below the tallest column.
|
|
153
|
+
let maxBottomY = this.y;
|
|
154
|
+
let maxBottomPage = this.page;
|
|
155
|
+
let maxBottomAvailableHeight = this.availableHeight;
|
|
156
|
+
|
|
157
|
+
// Pop overflowed snapshots created by moveToNextColumn (snaking columns).
|
|
158
|
+
// Merge their bottomMost values to find the true maximum.
|
|
159
|
+
while (saved && saved.overflowed) {
|
|
160
|
+
let bm = bottomMostContext({
|
|
161
|
+
page: maxBottomPage,
|
|
162
|
+
y: maxBottomY,
|
|
163
|
+
availableHeight: maxBottomAvailableHeight
|
|
164
|
+
}, saved.bottomMost || {});
|
|
165
|
+
maxBottomPage = bm.page;
|
|
166
|
+
maxBottomY = bm.y;
|
|
167
|
+
maxBottomAvailableHeight = bm.availableHeight;
|
|
168
|
+
saved = this.snapshots.pop();
|
|
169
|
+
}
|
|
170
|
+
if (!saved) {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Apply the max bottom from all overflowed columns to this base snapshot
|
|
175
|
+
if (maxBottomPage > saved.bottomMost.page || maxBottomPage === saved.bottomMost.page && maxBottomY > saved.bottomMost.y) {
|
|
176
|
+
saved.bottomMost = {
|
|
177
|
+
x: saved.x,
|
|
178
|
+
y: maxBottomY,
|
|
179
|
+
page: maxBottomPage,
|
|
180
|
+
availableHeight: maxBottomAvailableHeight,
|
|
181
|
+
availableWidth: saved.availableWidth
|
|
182
|
+
};
|
|
183
|
+
}
|
|
94
184
|
this.calculateBottomMost(saved, endingCell);
|
|
95
185
|
this.x = saved.x;
|
|
96
186
|
let y = saved.bottomMost.y;
|
|
@@ -118,6 +208,140 @@ class DocumentContext extends _events.EventEmitter {
|
|
|
118
208
|
this.lastColumnWidth = saved.lastColumnWidth;
|
|
119
209
|
return saved.bottomByPage;
|
|
120
210
|
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Move to the next column in a column group (snaking columns).
|
|
214
|
+
* Creates an overflowed snapshot to track that we've moved to the next column.
|
|
215
|
+
* @returns {object} Position info for the new column
|
|
216
|
+
*/
|
|
217
|
+
moveToNextColumn() {
|
|
218
|
+
let prevY = this.y;
|
|
219
|
+
let snakingSnapshot = this.getSnakingSnapshot();
|
|
220
|
+
if (!snakingSnapshot) {
|
|
221
|
+
return {
|
|
222
|
+
prevY: prevY,
|
|
223
|
+
y: this.y
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Update snaking snapshot's bottomMost with current position BEFORE resetting.
|
|
228
|
+
// This captures where content reached in the current column (overflow point).
|
|
229
|
+
this.calculateBottomMost(snakingSnapshot, null);
|
|
230
|
+
|
|
231
|
+
// Calculate new X position: move right by current column width + gap
|
|
232
|
+
let overflowCount = 0;
|
|
233
|
+
for (let i = this.snapshots.length - 1; i >= 0; i--) {
|
|
234
|
+
if (this.snapshots[i].overflowed) {
|
|
235
|
+
overflowCount++;
|
|
236
|
+
} else {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
let currentColumnWidth = snakingSnapshot.columnWidths && snakingSnapshot.columnWidths[overflowCount] || this.lastColumnWidth || this.availableWidth;
|
|
241
|
+
let nextColumnWidth = snakingSnapshot.columnWidths && snakingSnapshot.columnWidths[overflowCount + 1] || currentColumnWidth;
|
|
242
|
+
this.lastColumnWidth = nextColumnWidth;
|
|
243
|
+
let newX = this.x + (currentColumnWidth || 0) + (snakingSnapshot.gap || 0);
|
|
244
|
+
let newY = snakingSnapshot.y;
|
|
245
|
+
this.snapshots.push({
|
|
246
|
+
x: newX,
|
|
247
|
+
y: newY,
|
|
248
|
+
availableHeight: snakingSnapshot.availableHeight,
|
|
249
|
+
availableWidth: nextColumnWidth,
|
|
250
|
+
page: this.page,
|
|
251
|
+
overflowed: true,
|
|
252
|
+
bottomMost: {
|
|
253
|
+
x: newX,
|
|
254
|
+
y: newY,
|
|
255
|
+
availableHeight: snakingSnapshot.availableHeight,
|
|
256
|
+
availableWidth: nextColumnWidth,
|
|
257
|
+
page: this.page
|
|
258
|
+
},
|
|
259
|
+
lastColumnWidth: nextColumnWidth,
|
|
260
|
+
snakingColumns: true,
|
|
261
|
+
gap: snakingSnapshot.gap,
|
|
262
|
+
columnWidths: snakingSnapshot.columnWidths
|
|
263
|
+
});
|
|
264
|
+
this.x = newX;
|
|
265
|
+
this.y = newY;
|
|
266
|
+
this.availableHeight = snakingSnapshot.availableHeight;
|
|
267
|
+
this.availableWidth = nextColumnWidth;
|
|
268
|
+
|
|
269
|
+
// Sync non-overflowed inner snapshots (e.g. inner column groups for
|
|
270
|
+
// product/price rows) with the new snaking column position.
|
|
271
|
+
// Without this, inner beginColumn would read stale y/page/x values.
|
|
272
|
+
for (let i = this.snapshots.length - 2; i >= 0; i--) {
|
|
273
|
+
let snapshot = this.snapshots[i];
|
|
274
|
+
if (snapshot.overflowed || snapshot.snakingColumns) {
|
|
275
|
+
break; // Stop at first overflowed or snaking snapshot
|
|
276
|
+
}
|
|
277
|
+
snapshot.x = newX;
|
|
278
|
+
snapshot.y = newY;
|
|
279
|
+
snapshot.page = this.page;
|
|
280
|
+
snapshot.availableHeight = snakingSnapshot.availableHeight;
|
|
281
|
+
if (snapshot.bottomMost) {
|
|
282
|
+
snapshot.bottomMost.x = newX;
|
|
283
|
+
snapshot.bottomMost.y = newY;
|
|
284
|
+
snapshot.bottomMost.page = this.page;
|
|
285
|
+
snapshot.bottomMost.availableHeight = snakingSnapshot.availableHeight;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
prevY: prevY,
|
|
290
|
+
y: this.y
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Reset snaking column state when moving to a new page.
|
|
296
|
+
* Clears overflowed snapshots, resets X to left margin, sets width to first column,
|
|
297
|
+
* and syncs all snapshots to new page coordinates.
|
|
298
|
+
*/
|
|
299
|
+
resetSnakingColumnsForNewPage() {
|
|
300
|
+
let snakingSnapshot = this.getSnakingSnapshot();
|
|
301
|
+
if (!snakingSnapshot) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
let pageTopY = this.pageMargins.top;
|
|
305
|
+
let pageInnerHeight = this.getCurrentPage().pageSize.height - this.pageMargins.top - this.pageMargins.bottom;
|
|
306
|
+
|
|
307
|
+
// When moving to new page, start at first column.
|
|
308
|
+
// Reset width to FIRST column width, not last column from previous page.
|
|
309
|
+
let firstColumnWidth = snakingSnapshot.columnWidths ? snakingSnapshot.columnWidths[0] : this.lastColumnWidth || this.availableWidth;
|
|
310
|
+
|
|
311
|
+
// Clean up overflowed snapshots
|
|
312
|
+
while (this.snapshots.length > 1 && this.snapshots[this.snapshots.length - 1].overflowed) {
|
|
313
|
+
this.snapshots.pop();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Reset X to start of first column (left margin)
|
|
317
|
+
if (this.marginXTopParent) {
|
|
318
|
+
this.x = this.pageMargins.left + this.marginXTopParent[0];
|
|
319
|
+
} else {
|
|
320
|
+
this.x = this.pageMargins.left;
|
|
321
|
+
}
|
|
322
|
+
this.availableWidth = firstColumnWidth;
|
|
323
|
+
this.lastColumnWidth = firstColumnWidth;
|
|
324
|
+
|
|
325
|
+
// Sync all snapshots to new page state.
|
|
326
|
+
// When page break occurs within snaking columns, update ALL snapshots
|
|
327
|
+
// (not just snaking column snapshots) to reflect new page coordinates.
|
|
328
|
+
// This ensures nested structures (like inner product/price columns)
|
|
329
|
+
// don't retain stale values that would cause layout corruption.
|
|
330
|
+
for (let i = 0; i < this.snapshots.length; i++) {
|
|
331
|
+
let snapshot = this.snapshots[i];
|
|
332
|
+
let isSnakingSnapshot = !!snapshot.snakingColumns;
|
|
333
|
+
snapshot.x = this.x;
|
|
334
|
+
snapshot.y = isSnakingSnapshot ? pageTopY : this.y;
|
|
335
|
+
snapshot.availableHeight = isSnakingSnapshot ? pageInnerHeight : this.availableHeight;
|
|
336
|
+
snapshot.page = this.page;
|
|
337
|
+
if (snapshot.bottomMost) {
|
|
338
|
+
snapshot.bottomMost.x = this.x;
|
|
339
|
+
snapshot.bottomMost.y = isSnakingSnapshot ? pageTopY : this.y;
|
|
340
|
+
snapshot.bottomMost.availableHeight = isSnakingSnapshot ? pageInnerHeight : this.availableHeight;
|
|
341
|
+
snapshot.bottomMost.page = this.page;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
121
345
|
addMargin(left, right) {
|
|
122
346
|
this.x += left;
|
|
123
347
|
this.availableWidth -= left + (right || 0);
|
package/js/LayoutBuilder.js
CHANGED
|
@@ -356,7 +356,11 @@ class LayoutBuilder {
|
|
|
356
356
|
if (availableHeight - margin[1] < 0) {
|
|
357
357
|
// Consume the whole available space
|
|
358
358
|
this.writer.context().moveDown(availableHeight);
|
|
359
|
-
this.writer.
|
|
359
|
+
if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
|
|
360
|
+
this.snakingAwarePageBreak(node.pageOrientation);
|
|
361
|
+
} else {
|
|
362
|
+
this.writer.moveToNextPage(node.pageOrientation);
|
|
363
|
+
}
|
|
360
364
|
/**
|
|
361
365
|
* TODO - Something to consider:
|
|
362
366
|
* Right now the node starts at the top of next page (after header)
|
|
@@ -378,7 +382,11 @@ class LayoutBuilder {
|
|
|
378
382
|
// Necessary for nodes inside tables
|
|
379
383
|
if (availableHeight - margin[3] < 0) {
|
|
380
384
|
this.writer.context().moveDown(availableHeight);
|
|
381
|
-
this.writer.
|
|
385
|
+
if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
|
|
386
|
+
this.snakingAwarePageBreak(node.pageOrientation);
|
|
387
|
+
} else {
|
|
388
|
+
this.writer.moveToNextPage(node.pageOrientation);
|
|
389
|
+
}
|
|
382
390
|
/**
|
|
383
391
|
* TODO - Something to consider:
|
|
384
392
|
* Right now next node starts at the top of next page (after header)
|
|
@@ -477,6 +485,37 @@ class LayoutBuilder {
|
|
|
477
485
|
}
|
|
478
486
|
}
|
|
479
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Helper for page breaks that respects snaking column context.
|
|
490
|
+
* When in snaking columns, first tries moving to next column.
|
|
491
|
+
* If no columns available, moves to next page and resets x to left margin.
|
|
492
|
+
* @param {string} pageOrientation - Optional page orientation for the new page
|
|
493
|
+
*/
|
|
494
|
+
snakingAwarePageBreak(pageOrientation) {
|
|
495
|
+
let ctx = this.writer.context();
|
|
496
|
+
let snakingSnapshot = ctx.getSnakingSnapshot();
|
|
497
|
+
if (!snakingSnapshot) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Try flowing to next column first
|
|
502
|
+
if (this.writer.canMoveToNextColumn()) {
|
|
503
|
+
this.writer.moveToNextColumn();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// No more columns available, move to new page
|
|
508
|
+
this.writer.moveToNextPage(pageOrientation);
|
|
509
|
+
|
|
510
|
+
// Reset snaking column state for the new page
|
|
511
|
+
// Save lastColumnWidth before reset — if we're inside a nested
|
|
512
|
+
// column group (e.g. product/price row), the reset would overwrite
|
|
513
|
+
// it with the snaking column width, corrupting inner column layout.
|
|
514
|
+
let savedLastColumnWidth = ctx.lastColumnWidth;
|
|
515
|
+
ctx.resetSnakingColumnsForNewPage();
|
|
516
|
+
ctx.lastColumnWidth = savedLastColumnWidth;
|
|
517
|
+
}
|
|
518
|
+
|
|
480
519
|
// vertical container
|
|
481
520
|
processVerticalContainer(node) {
|
|
482
521
|
node.stack.forEach(item => {
|
|
@@ -557,7 +596,8 @@ class LayoutBuilder {
|
|
|
557
596
|
marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
|
|
558
597
|
cells: columns,
|
|
559
598
|
widths: columns,
|
|
560
|
-
gaps
|
|
599
|
+
gaps,
|
|
600
|
+
snakingColumns: columnNode.snakingColumns
|
|
561
601
|
});
|
|
562
602
|
addAll(columnNode.positions, result.positions);
|
|
563
603
|
this.nestedLevel--;
|
|
@@ -743,7 +783,8 @@ class LayoutBuilder {
|
|
|
743
783
|
tableNode,
|
|
744
784
|
tableBody,
|
|
745
785
|
rowIndex,
|
|
746
|
-
height
|
|
786
|
+
height,
|
|
787
|
+
snakingColumns = false
|
|
747
788
|
}) {
|
|
748
789
|
const isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
|
|
749
790
|
let pageBreaks = [];
|
|
@@ -761,7 +802,18 @@ class LayoutBuilder {
|
|
|
761
802
|
// Use the marginX if we are in a top level table/column (not nested)
|
|
762
803
|
const marginXParent = this.nestedLevel === 1 ? marginX : null;
|
|
763
804
|
const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
|
|
764
|
-
|
|
805
|
+
// Pass column gap and widths to context snapshot for snaking columns
|
|
806
|
+
// to advance correctly and reset to first-column width on new pages.
|
|
807
|
+
const columnGapForGroup = gaps && gaps.length > 1 ? gaps[1] : 0;
|
|
808
|
+
const columnWidthsForContext = widths.map(w => w._calcWidth);
|
|
809
|
+
this.writer.context().beginColumnGroup(marginXParent, _bottomByPage, snakingColumns, columnGapForGroup, columnWidthsForContext);
|
|
810
|
+
|
|
811
|
+
// IMPORTANT: We iterate ALL columns even when snakingColumns is enabled.
|
|
812
|
+
// This is intentional — beginColumn() must be called for each column to set up
|
|
813
|
+
// proper geometry (widths, offsets) and rowspan/colspan tracking. The
|
|
814
|
+
// completeColumnGroup() call at the end depends on this bookkeeping to compute
|
|
815
|
+
// heights correctly. Content processing is skipped for columns > 0 via
|
|
816
|
+
// skipForSnaking below, but the column structure must still be established.
|
|
765
817
|
for (let i = 0, l = cells.length; i < l; i++) {
|
|
766
818
|
let cell = cells[i];
|
|
767
819
|
let cellIndexBegin = i;
|
|
@@ -814,7 +866,13 @@ class LayoutBuilder {
|
|
|
814
866
|
|
|
815
867
|
// We pass the endingSpanCell reference to store the context just after processing rowspan cell
|
|
816
868
|
this.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
|
|
817
|
-
|
|
869
|
+
|
|
870
|
+
// When snaking, only process content from the first column (i === 0).
|
|
871
|
+
// Content overflows into subsequent columns via moveToNextColumn().
|
|
872
|
+
// We skip content processing here but NOT the beginColumn() call above —
|
|
873
|
+
// the column geometry setup is still needed for proper layout bookkeeping.
|
|
874
|
+
const skipForSnaking = snakingColumns && i > 0;
|
|
875
|
+
if (!cell._span && !skipForSnaking) {
|
|
818
876
|
this.processNode(cell, true);
|
|
819
877
|
this.writer.context().updateBottomByPage();
|
|
820
878
|
if (cell.verticalAlignment) {
|
|
@@ -867,7 +925,11 @@ class LayoutBuilder {
|
|
|
867
925
|
// If content did not break page, check if we should break by height
|
|
868
926
|
if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
|
|
869
927
|
this.writer.context().moveDown(this.writer.context().availableHeight);
|
|
870
|
-
|
|
928
|
+
if (snakingColumns) {
|
|
929
|
+
this.snakingAwarePageBreak();
|
|
930
|
+
} else {
|
|
931
|
+
this.writer.moveToNextPage();
|
|
932
|
+
}
|
|
871
933
|
}
|
|
872
934
|
const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
|
|
873
935
|
if (tableNode) {
|
|
@@ -957,7 +1019,27 @@ class LayoutBuilder {
|
|
|
957
1019
|
let processor = new _TableProcessor.default(tableNode);
|
|
958
1020
|
processor.beginTable(this.writer);
|
|
959
1021
|
let rowHeights = tableNode.table.heights;
|
|
1022
|
+
let lastRowHeight = 0;
|
|
960
1023
|
for (let i = 0, l = tableNode.table.body.length; i < l; i++) {
|
|
1024
|
+
// Between table rows: check if we should move to the next snaking column.
|
|
1025
|
+
// This must happen AFTER the previous row's endRow (borders drawn) and
|
|
1026
|
+
// BEFORE this row's beginRow. At this point, the table row column group
|
|
1027
|
+
// has been completed, so canMoveToNextColumn() works correctly.
|
|
1028
|
+
if (i > 0 && this.writer.context().inSnakingColumns()) {
|
|
1029
|
+
// Estimate minimum space for next row: use last row's height as heuristic,
|
|
1030
|
+
// or fall back to a minimum of padding + line height + border
|
|
1031
|
+
let minRowHeight = lastRowHeight > 0 ? lastRowHeight : processor.rowPaddingTop + 14 + processor.rowPaddingBottom + processor.bottomLineWidth + processor.topLineWidth;
|
|
1032
|
+
if (this.writer.context().availableHeight < minRowHeight) {
|
|
1033
|
+
this.snakingAwarePageBreak();
|
|
1034
|
+
|
|
1035
|
+
// Skip border when headerRows present (header repeat includes it)
|
|
1036
|
+
if (processor.layout.hLineWhenBroken !== false && !processor.headerRows) {
|
|
1037
|
+
processor.drawHorizontalLine(i, this.writer);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
let rowYBefore = this.writer.context().y;
|
|
1042
|
+
|
|
961
1043
|
// if dontBreakRows and row starts a rowspan
|
|
962
1044
|
// we store the 'y' of the beginning of each rowSpan
|
|
963
1045
|
if (processor.dontBreakRows) {
|
|
@@ -1002,6 +1084,12 @@ class LayoutBuilder {
|
|
|
1002
1084
|
}
|
|
1003
1085
|
}
|
|
1004
1086
|
processor.endRow(i, this.writer, result.pageBreaks);
|
|
1087
|
+
|
|
1088
|
+
// Track the height of the completed row for the next row's estimate
|
|
1089
|
+
let rowYAfter = this.writer.context().y;
|
|
1090
|
+
if (this.writer.context().page === pageBeforeProcessing) {
|
|
1091
|
+
lastRowHeight = rowYAfter - rowYBefore;
|
|
1092
|
+
}
|
|
1005
1093
|
}
|
|
1006
1094
|
processor.endTable(this.writer);
|
|
1007
1095
|
this.nestedLevel--;
|
|
@@ -1024,6 +1112,26 @@ class LayoutBuilder {
|
|
|
1024
1112
|
line.id = nodeId;
|
|
1025
1113
|
}
|
|
1026
1114
|
}
|
|
1115
|
+
if (node.outline) {
|
|
1116
|
+
line._outline = {
|
|
1117
|
+
id: node.id,
|
|
1118
|
+
parentId: node.outlineParentId,
|
|
1119
|
+
text: node.outlineText || node.text,
|
|
1120
|
+
expanded: node.outlineExpanded || false
|
|
1121
|
+
};
|
|
1122
|
+
} else if (Array.isArray(node.text)) {
|
|
1123
|
+
for (let i = 0, l = node.text.length; i < l; i++) {
|
|
1124
|
+
let item = node.text[i];
|
|
1125
|
+
if (item.outline) {
|
|
1126
|
+
line._outline = {
|
|
1127
|
+
id: item.id,
|
|
1128
|
+
parentId: item.outlineParentId,
|
|
1129
|
+
text: item.outlineText || item.text,
|
|
1130
|
+
expanded: item.outlineExpanded || false
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1027
1135
|
if (node._tocItemRef) {
|
|
1028
1136
|
line._pageNodeRef = node._tocItemRef;
|
|
1029
1137
|
}
|
|
@@ -1041,6 +1149,27 @@ class LayoutBuilder {
|
|
|
1041
1149
|
}
|
|
1042
1150
|
}
|
|
1043
1151
|
while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
|
|
1152
|
+
// Check if line fits vertically in current context
|
|
1153
|
+
if (line.getHeight() > this.writer.context().availableHeight && this.writer.context().y > this.writer.context().pageMargins.top) {
|
|
1154
|
+
// Line doesn't fit, forced move to next page/column
|
|
1155
|
+
// Only do snaking-specific break if we're in snaking columns AND NOT inside
|
|
1156
|
+
// a nested non-snaking group (like a table row). Table cells should use
|
|
1157
|
+
// standard page breaks — column breaks happen between table rows instead.
|
|
1158
|
+
if (this.writer.context().inSnakingColumns() && !this.writer.context().isInNestedNonSnakingGroup()) {
|
|
1159
|
+
this.snakingAwarePageBreak(node.pageOrientation);
|
|
1160
|
+
|
|
1161
|
+
// Always reflow text after a snaking break (column or page).
|
|
1162
|
+
// This ensures text adapts to the new column width, whether it's narrower or wider.
|
|
1163
|
+
if (line.inlines && line.inlines.length > 0) {
|
|
1164
|
+
node._inlines.unshift(...line.inlines);
|
|
1165
|
+
}
|
|
1166
|
+
// Rebuild line with new width
|
|
1167
|
+
line = this.buildNextLine(node);
|
|
1168
|
+
continue;
|
|
1169
|
+
} else {
|
|
1170
|
+
this.writer.moveToNextPage(node.pageOrientation);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1044
1173
|
let positions = this.writer.addLine(line);
|
|
1045
1174
|
node.positions.push(positions);
|
|
1046
1175
|
line = this.buildNextLine(node);
|
|
@@ -1094,7 +1223,6 @@ class LayoutBuilder {
|
|
|
1094
1223
|
while (textNode._inlines && textNode._inlines.length > 0 && (line.hasEnoughSpaceForInline(textNode._inlines[0], textNode._inlines.slice(1)) || isForceContinue)) {
|
|
1095
1224
|
let isHardWrap = false;
|
|
1096
1225
|
let inline = textNode._inlines.shift();
|
|
1097
|
-
isForceContinue = false;
|
|
1098
1226
|
if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
|
|
1099
1227
|
let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), txt => textInlines.widthOfText(txt, inline));
|
|
1100
1228
|
if (maxChars < inline.text.length) {
|
package/js/PDFDocument.js
CHANGED
|
@@ -95,7 +95,9 @@ class PDFDocument extends _pdfkit.default {
|
|
|
95
95
|
throw new Error('No image');
|
|
96
96
|
}
|
|
97
97
|
} catch (error) {
|
|
98
|
-
throw new Error(`Invalid image: ${error.toString()}\nImages dictionary should contain dataURL entries (or local file paths in node.js)
|
|
98
|
+
throw new Error(`Invalid image: ${error.toString()}\nImages dictionary should contain dataURL entries (or local file paths in node.js)`, {
|
|
99
|
+
cause: error
|
|
100
|
+
});
|
|
99
101
|
}
|
|
100
102
|
image.embed(this);
|
|
101
103
|
this._imageRegistry[src] = image;
|
package/js/PageElementWriter.js
CHANGED
|
@@ -149,11 +149,118 @@ class PageElementWriter extends _ElementWriter.default {
|
|
|
149
149
|
popFromRepeatables() {
|
|
150
150
|
this.repeatables.pop();
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Move to the next column in a column group (snaking columns).
|
|
155
|
+
* Handles repeatables and emits columnChanged event.
|
|
156
|
+
*/
|
|
157
|
+
moveToNextColumn() {
|
|
158
|
+
let nextColumn = this.context().moveToNextColumn();
|
|
159
|
+
|
|
160
|
+
// Handle repeatables (like table headers) for the new column
|
|
161
|
+
this.repeatables.forEach(function (rep) {
|
|
162
|
+
// In snaking columns, we WANT headers to repeat.
|
|
163
|
+
// However, in Standard Page Breaks, headers are drawn using useBlockXOffset=true (original absolute X).
|
|
164
|
+
// This works for page breaks because margins are consistent.
|
|
165
|
+
// In Snaking Columns, the X position changes for each column.
|
|
166
|
+
// If we use true, the header is drawn at the *original* X position (Col 1), overlapping/invisible.
|
|
167
|
+
// We MUST use false to force drawing relative to the CURRENT context X (new column start).
|
|
168
|
+
this.addFragment(rep, false);
|
|
169
|
+
}, this);
|
|
170
|
+
this.emit('columnChanged', {
|
|
171
|
+
prevY: nextColumn.prevY,
|
|
172
|
+
y: this.context().y
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if currently in a column group that can move to next column.
|
|
178
|
+
* Only returns true if snakingColumns is enabled for the column group.
|
|
179
|
+
* @returns {boolean}
|
|
180
|
+
*/
|
|
181
|
+
canMoveToNextColumn() {
|
|
182
|
+
let ctx = this.context();
|
|
183
|
+
let snakingSnapshot = ctx.getSnakingSnapshot();
|
|
184
|
+
if (snakingSnapshot) {
|
|
185
|
+
// Check if we're inside a nested (non-snaking) column group.
|
|
186
|
+
// If so, don't allow a snaking column move — it would corrupt
|
|
187
|
+
// the inner row's layout (e.g. product name in col 1, price in col 2).
|
|
188
|
+
// The inner row should complete via normal page break instead.
|
|
189
|
+
for (let i = ctx.snapshots.length - 1; i >= 0; i--) {
|
|
190
|
+
let snap = ctx.snapshots[i];
|
|
191
|
+
if (snap.snakingColumns) {
|
|
192
|
+
break; // Reached the snaking snapshot, no inner groups found
|
|
193
|
+
}
|
|
194
|
+
if (!snap.overflowed) {
|
|
195
|
+
return false; // Found a non-snaking, non-overflowed inner group
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
let overflowCount = 0;
|
|
199
|
+
for (let i = ctx.snapshots.length - 1; i >= 0; i--) {
|
|
200
|
+
if (ctx.snapshots[i].overflowed) {
|
|
201
|
+
overflowCount++;
|
|
202
|
+
} else {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (snakingSnapshot.columnWidths && overflowCount >= snakingSnapshot.columnWidths.length - 1) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
let currentColumnWidth = ctx.availableWidth || ctx.lastColumnWidth || 0;
|
|
210
|
+
let nextColumnWidth = snakingSnapshot.columnWidths ? snakingSnapshot.columnWidths[overflowCount + 1] : currentColumnWidth;
|
|
211
|
+
let nextX = ctx.x + currentColumnWidth + (snakingSnapshot.gap || 0);
|
|
212
|
+
let page = ctx.getCurrentPage();
|
|
213
|
+
let pageWidth = page.pageSize.width;
|
|
214
|
+
let rightMargin = page.pageMargins ? page.pageMargins.right : 0;
|
|
215
|
+
let parentRightMargin = ctx.marginXTopParent ? ctx.marginXTopParent[1] : 0;
|
|
216
|
+
let rightBoundary = pageWidth - rightMargin - parentRightMargin;
|
|
217
|
+
return nextX + nextColumnWidth <= rightBoundary + 1;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
152
221
|
_fitOnPage(addFct) {
|
|
153
222
|
let position = addFct();
|
|
154
223
|
if (!position) {
|
|
155
|
-
this.
|
|
156
|
-
|
|
224
|
+
if (this.canMoveToNextColumn()) {
|
|
225
|
+
this.moveToNextColumn();
|
|
226
|
+
position = addFct();
|
|
227
|
+
}
|
|
228
|
+
if (!position) {
|
|
229
|
+
let ctx = this.context();
|
|
230
|
+
let snakingSnapshot = ctx.getSnakingSnapshot();
|
|
231
|
+
if (snakingSnapshot) {
|
|
232
|
+
if (ctx.isInNestedNonSnakingGroup()) {
|
|
233
|
+
// Inside a table cell within snaking columns — use standard page break.
|
|
234
|
+
// Don't reset snaking state; the table handles its own breaks.
|
|
235
|
+
// Column breaks happen between rows in processTable instead.
|
|
236
|
+
this.moveToNextPage();
|
|
237
|
+
} else {
|
|
238
|
+
this.moveToNextPage();
|
|
239
|
+
|
|
240
|
+
// Save lastColumnWidth before reset — if we're inside a nested
|
|
241
|
+
// column group (e.g. product/price row), the reset would overwrite
|
|
242
|
+
// it with the snaking column width, corrupting inner column layout.
|
|
243
|
+
let savedLastColumnWidth = ctx.lastColumnWidth;
|
|
244
|
+
ctx.resetSnakingColumnsForNewPage();
|
|
245
|
+
ctx.lastColumnWidth = savedLastColumnWidth;
|
|
246
|
+
}
|
|
247
|
+
position = addFct();
|
|
248
|
+
} else {
|
|
249
|
+
while (ctx.snapshots.length > 0 && ctx.snapshots[ctx.snapshots.length - 1].overflowed) {
|
|
250
|
+
let popped = ctx.snapshots.pop();
|
|
251
|
+
let prevSnapshot = ctx.snapshots[ctx.snapshots.length - 1];
|
|
252
|
+
if (prevSnapshot) {
|
|
253
|
+
ctx.x = prevSnapshot.x;
|
|
254
|
+
ctx.y = prevSnapshot.y;
|
|
255
|
+
ctx.availableHeight = prevSnapshot.availableHeight;
|
|
256
|
+
ctx.availableWidth = popped.availableWidth;
|
|
257
|
+
ctx.lastColumnWidth = prevSnapshot.lastColumnWidth;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
this.moveToNextPage();
|
|
261
|
+
position = addFct();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
157
264
|
}
|
|
158
265
|
return position;
|
|
159
266
|
}
|