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/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 (err) {
32
- throw new Error('Invalid svg document (' + err + ')');
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>)');
@@ -20,7 +20,7 @@ class TableProcessor {
20
20
  const prepareRowSpanData = () => {
21
21
  let rsd = [];
22
22
  let x = 0;
23
- let lastWidth = 0;
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.4",
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": "^9.39.2",
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": "^9.39.2",
29
- "eslint-plugin-jsdoc": "^62.5.4",
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.0",
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 = {
@@ -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[lastPage] = Math.max(previousBottom, this.y);
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
- if (height && (saved.bottomMost.y - saved.y < height)) {
129
- this.height = height;
130
- } else {
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
 
@@ -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.moveToNextPage(node.pageOrientation);
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.moveToNextPage(node.pageOrientation);
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
- this.writer.context().beginColumnGroup(marginXParent, _bottomByPage);
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
- if (!cell._span) {
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
- this.writer.moveToNextPage();
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) =>
@@ -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);