pdfmake 0.3.0-beta.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +7 -48
  2. package/LICENSE +1 -1
  3. package/README.md +78 -85
  4. package/build/pdfmake.js +66833 -75014
  5. package/build/pdfmake.js.map +1 -1
  6. package/build/pdfmake.min.js +2 -2
  7. package/build/pdfmake.min.js.map +1 -1
  8. package/build/vfs_fonts.js +4 -4
  9. package/fonts/Roboto/Roboto-Italic.ttf +0 -0
  10. package/fonts/Roboto/Roboto-Medium.ttf +0 -0
  11. package/fonts/Roboto/Roboto-MediumItalic.ttf +0 -0
  12. package/fonts/Roboto/Roboto-Regular.ttf +0 -0
  13. package/js/DocMeasure.js +20 -1
  14. package/js/DocPreprocessor.js +21 -6
  15. package/js/DocumentContext.js +64 -17
  16. package/js/ElementWriter.js +31 -8
  17. package/js/LayoutBuilder.js +488 -127
  18. package/js/OutputDocument.js +22 -34
  19. package/js/OutputDocumentServer.js +6 -11
  20. package/js/PDFDocument.js +1 -1
  21. package/js/PageElementWriter.js +17 -2
  22. package/js/Printer.js +133 -133
  23. package/js/Renderer.js +22 -14
  24. package/js/SVGMeasure.js +2 -2
  25. package/js/StyleContextStack.js +4 -0
  26. package/js/TableProcessor.js +40 -14
  27. package/js/TextBreaker.js +31 -4
  28. package/js/TextInlines.js +1 -1
  29. package/js/URLResolver.js +25 -55
  30. package/js/base.js +1 -1
  31. package/js/browser-extensions/OutputDocumentBrowser.js +35 -72
  32. package/js/browser-extensions/index.js +2 -2
  33. package/js/browser-extensions/pdfMake.js +0 -12
  34. package/js/browser-extensions/standard-fonts/Courier.js +4 -4
  35. package/js/browser-extensions/standard-fonts/Helvetica.js +4 -4
  36. package/js/browser-extensions/standard-fonts/Symbol.js +1 -1
  37. package/js/browser-extensions/standard-fonts/Times.js +4 -4
  38. package/js/browser-extensions/standard-fonts/ZapfDingbats.js +1 -1
  39. package/js/helpers/tools.js +6 -0
  40. package/js/index.js +1 -1
  41. package/package.json +25 -24
  42. package/src/DocMeasure.js +21 -1
  43. package/src/DocPreprocessor.js +25 -6
  44. package/src/DocumentContext.js +56 -20
  45. package/src/ElementWriter.js +30 -7
  46. package/src/LayoutBuilder.js +531 -144
  47. package/src/OutputDocument.js +23 -37
  48. package/src/OutputDocumentServer.js +6 -11
  49. package/src/PDFDocument.js +1 -1
  50. package/src/PageElementWriter.js +21 -2
  51. package/src/Printer.js +134 -131
  52. package/src/Renderer.js +13 -15
  53. package/src/SVGMeasure.js +2 -2
  54. package/src/StyleContextStack.js +4 -0
  55. package/src/TableProcessor.js +42 -18
  56. package/src/TextBreaker.js +24 -5
  57. package/src/TextInlines.js +1 -1
  58. package/src/URLResolver.js +24 -58
  59. package/src/base.js +1 -1
  60. package/src/browser-extensions/OutputDocumentBrowser.js +33 -71
  61. package/src/browser-extensions/index.js +3 -3
  62. package/src/browser-extensions/pdfMake.js +0 -14
  63. package/src/browser-extensions/standard-fonts/Courier.js +4 -4
  64. package/src/browser-extensions/standard-fonts/Helvetica.js +4 -4
  65. package/src/browser-extensions/standard-fonts/Symbol.js +1 -1
  66. package/src/browser-extensions/standard-fonts/Times.js +4 -4
  67. package/src/browser-extensions/standard-fonts/ZapfDingbats.js +1 -1
  68. package/src/columnCalculator.js +12 -12
  69. package/src/helpers/tools.js +5 -0
  70. package/src/helpers/variableType.js +1 -1
  71. package/src/index.js +1 -1
  72. package/standard-fonts/Helvetica.js +0 -1
  73. package/js/browser-extensions/URLBrowserResolver.js +0 -76
  74. package/src/browser-extensions/URLBrowserResolver.js +0 -84
@@ -7,7 +7,7 @@ import TableProcessor from './TableProcessor';
7
7
  import Line from './Line';
8
8
  import { isString, isValue, isNumber } from './helpers/variableType';
9
9
  import { stringifyNode, getNodeId } from './helpers/node';
10
- import { pack, offsetVector } from './helpers/tools';
10
+ import { pack, offsetVector, convertToDynamicContent } from './helpers/tools';
11
11
  import TextInlines from './TextInlines';
12
12
  import StyleContextStack from './StyleContextStack';
13
13
 
@@ -32,6 +32,7 @@ class LayoutBuilder {
32
32
  this.pageMargins = pageMargins;
33
33
  this.svgMeasure = svgMeasure;
34
34
  this.tableLayouts = {};
35
+ this.nestedLevel = 0;
35
36
  }
36
37
 
37
38
  registerTableLayouts(tableLayouts) {
@@ -71,7 +72,17 @@ class LayoutBuilder {
71
72
  return false;
72
73
  }
73
74
 
74
- linearNodeList = linearNodeList.filter(node => node.positions.length > 0);
75
+ const hasRenderableContent = node => {
76
+ if (!node || node.positions.length === 0) {
77
+ return false;
78
+ }
79
+ if (node.text === '' && !node.listMarker) {
80
+ return false;
81
+ }
82
+ return true;
83
+ };
84
+
85
+ linearNodeList = linearNodeList.filter(hasRenderableContent);
75
86
 
76
87
  linearNodeList.forEach(node => {
77
88
  let nodeInfo = {};
@@ -167,23 +178,42 @@ class LayoutBuilder {
167
178
  watermark
168
179
  ) {
169
180
 
181
+ const isNecessaryAddFirstPage = (docStructure) => {
182
+ if (docStructure.stack && docStructure.stack.length > 0 && docStructure.stack[0].section) {
183
+ return false;
184
+ } else if (docStructure.section) {
185
+ return false;
186
+ }
187
+
188
+ return true;
189
+ };
190
+
170
191
  this.linearNodeList = [];
171
192
  docStructure = this.docPreprocessor.preprocessDocument(docStructure);
172
193
  docStructure = this.docMeasure.measureDocument(docStructure);
173
194
 
174
- this.writer = new PageElementWriter(
175
- new DocumentContext(this.pageSize, this.pageMargins));
195
+ this.writer = new PageElementWriter(new DocumentContext());
176
196
 
177
- this.writer.context().addListener('pageAdded', () => {
178
- this.addBackground(background);
197
+ this.writer.context().addListener('pageAdded', (page) => {
198
+ let backgroundGetter = background;
199
+ if (page.customProperties['background'] || page.customProperties['background'] === null) {
200
+ backgroundGetter = page.customProperties['background'];
201
+ }
202
+
203
+ this.addBackground(backgroundGetter);
179
204
  });
180
205
 
181
- this.addBackground(background);
206
+ if (isNecessaryAddFirstPage(docStructure)) {
207
+ this.writer.addPage(
208
+ this.pageSize,
209
+ null,
210
+ this.pageMargins
211
+ );
212
+ }
213
+
182
214
  this.processNode(docStructure);
183
215
  this.addHeadersAndFooters(header, footer);
184
- if (watermark != null) {
185
- this.addWatermark(watermark, pdfDocument, defaultStyle);
186
- }
216
+ this.addWatermark(watermark, pdfDocument, defaultStyle);
187
217
 
188
218
  return { pages: this.writer.context().pages, linearNodeList: this.linearNodeList };
189
219
  }
@@ -198,31 +228,37 @@ class LayoutBuilder {
198
228
 
199
229
  if (pageBackground) {
200
230
  this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height);
201
- pageBackground = this.docPreprocessor.preprocessDocument(pageBackground);
202
- this.processNode(this.docMeasure.measureDocument(pageBackground));
231
+ pageBackground = this.docPreprocessor.preprocessBlock(pageBackground);
232
+ this.processNode(this.docMeasure.measureBlock(pageBackground));
203
233
  this.writer.commitUnbreakableBlock(0, 0);
204
234
  context.backgroundLength[context.page] += pageBackground.positions.length;
205
235
  }
206
236
  }
207
237
 
208
- addStaticRepeatable(headerOrFooter, sizeFunction) {
209
- this.addDynamicRepeatable(() => // copy to new object
210
- JSON.parse(JSON.stringify(headerOrFooter)), sizeFunction);
211
- }
212
-
213
- addDynamicRepeatable(nodeGetter, sizeFunction) {
238
+ addDynamicRepeatable(nodeGetter, sizeFunction, customPropertyName) {
214
239
  let pages = this.writer.context().pages;
215
240
 
216
241
  for (let pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) {
217
242
  this.writer.context().page = pageIndex;
218
243
 
219
- let node = nodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
244
+ let customProperties = this.writer.context().getCurrentPage().customProperties;
245
+
246
+ let pageNodeGetter = nodeGetter;
247
+ if (customProperties[customPropertyName] || customProperties[customPropertyName] === null) {
248
+ pageNodeGetter = customProperties[customPropertyName];
249
+ }
250
+
251
+ if ((typeof pageNodeGetter === 'undefined') || (pageNodeGetter === null)) {
252
+ continue;
253
+ }
254
+
255
+ let node = pageNodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
220
256
 
221
257
  if (node) {
222
- let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.pageMargins);
258
+ let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.writer.context().getCurrentPage().pageMargins);
223
259
  this.writer.beginUnbreakableBlock(sizes.width, sizes.height);
224
- node = this.docPreprocessor.preprocessDocument(node);
225
- this.processNode(this.docMeasure.measureDocument(node));
260
+ node = this.docPreprocessor.preprocessBlock(node);
261
+ this.processNode(this.docMeasure.measureBlock(node));
226
262
  this.writer.commitUnbreakableBlock(sizes.x, sizes.y);
227
263
  }
228
264
  }
@@ -243,58 +279,62 @@ class LayoutBuilder {
243
279
  height: pageMargins.bottom
244
280
  });
245
281
 
246
- if (typeof header === 'function') {
247
- this.addDynamicRepeatable(header, headerSizeFct);
248
- } else if (header) {
249
- this.addStaticRepeatable(header, headerSizeFct);
250
- }
251
-
252
- if (typeof footer === 'function') {
253
- this.addDynamicRepeatable(footer, footerSizeFct);
254
- } else if (footer) {
255
- this.addStaticRepeatable(footer, footerSizeFct);
256
- }
282
+ this.addDynamicRepeatable(header, headerSizeFct, 'header');
283
+ this.addDynamicRepeatable(footer, footerSizeFct, 'footer');
257
284
  }
258
285
 
259
286
  addWatermark(watermark, pdfDocument, defaultStyle) {
260
- if (isString(watermark)) {
261
- watermark = { 'text': watermark };
262
- }
287
+ let pages = this.writer.context().pages;
288
+ for (let i = 0, l = pages.length; i < l; i++) {
289
+ let pageWatermark = watermark;
290
+ if (pages[i].customProperties['watermark'] || pages[i].customProperties['watermark'] === null) {
291
+ pageWatermark = pages[i].customProperties['watermark'];
292
+ }
263
293
 
264
- if (!watermark.text) { // empty watermark text
265
- return;
266
- }
294
+ if (pageWatermark === undefined || pageWatermark === null) {
295
+ continue;
296
+ }
267
297
 
268
- watermark.font = watermark.font || defaultStyle.font || 'Roboto';
269
- watermark.fontSize = watermark.fontSize || 'auto';
270
- watermark.color = watermark.color || 'black';
271
- watermark.opacity = isNumber(watermark.opacity) ? watermark.opacity : 0.6;
272
- watermark.bold = watermark.bold || false;
273
- watermark.italics = watermark.italics || false;
274
- watermark.angle = isValue(watermark.angle) ? watermark.angle : null;
298
+ if (isString(pageWatermark)) {
299
+ pageWatermark = { 'text': pageWatermark };
300
+ }
275
301
 
276
- if (watermark.angle === null) {
277
- watermark.angle = Math.atan2(this.pageSize.height, this.pageSize.width) * -180 / Math.PI;
278
- }
302
+ if (!pageWatermark.text) { // empty watermark text
303
+ continue;
304
+ }
279
305
 
280
- if (watermark.fontSize === 'auto') {
281
- watermark.fontSize = getWatermarkFontSize(this.pageSize, watermark, pdfDocument);
306
+ pages[i].watermark = getWatermarkObject({ ...pageWatermark }, pages[i].pageSize, pdfDocument, defaultStyle);
282
307
  }
283
308
 
284
- let watermarkObject = {
285
- text: watermark.text,
286
- font: pdfDocument.provideFont(watermark.font, watermark.bold, watermark.italics),
287
- fontSize: watermark.fontSize,
288
- color: watermark.color,
289
- opacity: watermark.opacity,
290
- angle: watermark.angle
291
- };
309
+ function getWatermarkObject(watermark, pageSize, pdfDocument, defaultStyle) {
310
+ watermark.font = watermark.font || defaultStyle.font || 'Roboto';
311
+ watermark.fontSize = watermark.fontSize || 'auto';
312
+ watermark.color = watermark.color || 'black';
313
+ watermark.opacity = isNumber(watermark.opacity) ? watermark.opacity : 0.6;
314
+ watermark.bold = watermark.bold || false;
315
+ watermark.italics = watermark.italics || false;
316
+ watermark.angle = isValue(watermark.angle) ? watermark.angle : null;
317
+
318
+ if (watermark.angle === null) {
319
+ watermark.angle = Math.atan2(pageSize.height, pageSize.width) * -180 / Math.PI;
320
+ }
292
321
 
293
- watermarkObject._size = getWatermarkSize(watermark, pdfDocument);
322
+ if (watermark.fontSize === 'auto') {
323
+ watermark.fontSize = getWatermarkFontSize(pageSize, watermark, pdfDocument);
324
+ }
294
325
 
295
- let pages = this.writer.context().pages;
296
- for (let i = 0, l = pages.length; i < l; i++) {
297
- pages[i].watermark = watermarkObject;
326
+ let watermarkObject = {
327
+ text: watermark.text,
328
+ font: pdfDocument.provideFont(watermark.font, watermark.bold, watermark.italics),
329
+ fontSize: watermark.fontSize,
330
+ color: watermark.color,
331
+ opacity: watermark.opacity,
332
+ angle: watermark.angle
333
+ };
334
+
335
+ watermarkObject._size = getWatermarkSize(watermark, pdfDocument);
336
+
337
+ return watermarkObject;
298
338
  }
299
339
 
300
340
  function getWatermarkSize(watermark, pdfDocument) {
@@ -369,16 +409,50 @@ class LayoutBuilder {
369
409
  }
370
410
  }
371
411
 
372
- if (margin) {
373
- this.writer.context().moveDown(margin[1]);
412
+ const isDetachedBlock = node.relativePosition || node.absolutePosition;
413
+
414
+ // Detached nodes have no margins, their position is only determined by 'x' and 'y'
415
+ if (margin && !isDetachedBlock) {
416
+ const availableHeight = this.writer.context().availableHeight;
417
+ // If top margin is bigger than available space, move to next page
418
+ // Necessary for nodes inside tables
419
+ if (availableHeight - margin[1] < 0) {
420
+ // Consume the whole available space
421
+ this.writer.context().moveDown(availableHeight);
422
+ this.writer.moveToNextPage(node.pageOrientation);
423
+ /**
424
+ * TODO - Something to consider:
425
+ * Right now the node starts at the top of next page (after header)
426
+ * Another option would be to apply just the top margin that has not been consumed in the page before
427
+ * It would something like: this.write.context().moveDown(margin[1] - availableHeight)
428
+ */
429
+ } else {
430
+ this.writer.context().moveDown(margin[1]);
431
+ }
432
+ // Apply lateral margins
374
433
  this.writer.context().addMargin(margin[0], margin[2]);
375
434
  }
376
-
377
435
  callback();
378
436
 
379
- if (margin) {
437
+ // Detached nodes have no margins, their position is only determined by 'x' and 'y'
438
+ if (margin && !isDetachedBlock) {
439
+ const availableHeight = this.writer.context().availableHeight;
440
+ // If bottom margin is bigger than available space, move to next page
441
+ // Necessary for nodes inside tables
442
+ if (availableHeight - margin[3] < 0) {
443
+ this.writer.context().moveDown(availableHeight);
444
+ this.writer.moveToNextPage(node.pageOrientation);
445
+ /**
446
+ * TODO - Something to consider:
447
+ * Right now next node starts at the top of next page (after header)
448
+ * Another option would be to apply the bottom margin that has not been consumed in the next page?
449
+ * It would something like: this.write.context().moveDown(margin[3] - availableHeight)
450
+ */
451
+ } else {
452
+ this.writer.context().moveDown(margin[3]);
453
+ }
454
+ // Apply lateral margins
380
455
  this.writer.context().addMargin(-margin[0], -margin[2]);
381
- this.writer.context().moveDown(margin[3]);
382
456
  }
383
457
 
384
458
  if (node.pageBreak === 'after') {
@@ -419,6 +493,8 @@ class LayoutBuilder {
419
493
 
420
494
  if (node.stack) {
421
495
  this.processVerticalContainer(node);
496
+ } else if (node.section) {
497
+ this.processSection(node);
422
498
  } else if (node.columns) {
423
499
  this.processColumns(node);
424
500
  } else if (node.ul) {
@@ -465,8 +541,78 @@ class LayoutBuilder {
465
541
  }, this);
466
542
  }
467
543
 
544
+ // section
545
+ processSection(sectionNode) {
546
+ // TODO: properties
547
+
548
+ let page = this.writer.context().getCurrentPage();
549
+ if (!page || (page && page.items.length)) { // move to new empty page
550
+ // page definition inherit from current page
551
+ if (sectionNode.pageSize === 'inherit') {
552
+ sectionNode.pageSize = page ? { width: page.pageSize.width, height: page.pageSize.height } : undefined;
553
+ }
554
+ if (sectionNode.pageOrientation === 'inherit') {
555
+ sectionNode.pageOrientation = page ? page.pageSize.orientation : undefined;
556
+ }
557
+ if (sectionNode.pageMargins === 'inherit') {
558
+ sectionNode.pageMargins = page ? page.pageMargins : undefined;
559
+ }
560
+
561
+ if (sectionNode.header === 'inherit') {
562
+ sectionNode.header = page ? page.customProperties.header : undefined;
563
+ }
564
+
565
+ if (sectionNode.footer === 'inherit') {
566
+ sectionNode.footer = page ? page.customProperties.footer : undefined;
567
+ }
568
+
569
+ if (sectionNode.background === 'inherit') {
570
+ sectionNode.background = page ? page.customProperties.background : undefined;
571
+ }
572
+
573
+ if (sectionNode.watermark === 'inherit') {
574
+ sectionNode.watermark = page ? page.customProperties.watermark : undefined;
575
+ }
576
+
577
+ if (sectionNode.header && typeof sectionNode.header !== 'function' && sectionNode.header !== null) {
578
+ sectionNode.header = convertToDynamicContent(sectionNode.header);
579
+ }
580
+
581
+ if (sectionNode.footer && typeof sectionNode.footer !== 'function' && sectionNode.footer !== null) {
582
+ sectionNode.footer = convertToDynamicContent(sectionNode.footer);
583
+ }
584
+
585
+ let customProperties = {};
586
+ if (typeof sectionNode.header !== 'undefined') {
587
+ customProperties.header = sectionNode.header;
588
+ }
589
+
590
+ if (typeof sectionNode.footer !== 'undefined') {
591
+ customProperties.footer = sectionNode.footer;
592
+ }
593
+
594
+ if (typeof sectionNode.background !== 'undefined') {
595
+ customProperties.background = sectionNode.background;
596
+ }
597
+
598
+ if (typeof sectionNode.watermark !== 'undefined') {
599
+ customProperties.watermark = sectionNode.watermark;
600
+ }
601
+
602
+ this.writer.addPage(
603
+ sectionNode.pageSize || this.pageSize,
604
+ sectionNode.pageOrientation,
605
+ sectionNode.pageMargins || this.pageMargins,
606
+ customProperties
607
+ );
608
+ }
609
+
610
+ this.processNode(sectionNode.section);
611
+ }
612
+
468
613
  // columns
469
614
  processColumns(columnNode) {
615
+ this.nestedLevel++;
470
616
  let columns = columnNode.columns;
471
617
  let availableWidth = this.writer.context().availableWidth;
472
618
  let gaps = gapArray(columnNode._gap);
@@ -476,9 +622,17 @@ class LayoutBuilder {
476
622
  }
477
623
 
478
624
  ColumnCalculator.buildColumnWidths(columns, availableWidth);
479
- let result = this.processRow(columns, columns, gaps);
625
+ let result = this.processRow({
626
+ marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
627
+ cells: columns,
628
+ widths: columns,
629
+ gaps
630
+ });
480
631
  addAll(columnNode.positions, result.positions);
481
-
632
+ this.nestedLevel--;
633
+ if (this.nestedLevel === 0) {
634
+ this.writer.context().resetMarginXTopParent();
635
+ }
482
636
  function gapArray(gap) {
483
637
  if (!gap) {
484
638
  return null;
@@ -495,130 +649,306 @@ class LayoutBuilder {
495
649
  }
496
650
  }
497
651
 
498
- findStartingSpanCell(arr, i) {
652
+ /**
653
+ * Searches for a cell in the same row that starts a rowspan and is positioned immediately before the current cell.
654
+ * Alternatively, it finds a cell where the colspan initiating the rowspan extends to the cell just before the current one.
655
+ *
656
+ * @param {Array<object>} arr - An array representing cells in a row.
657
+ * @param {number} i - The index of the current cell to search backward from.
658
+ * @returns {object|null} The starting cell of the rowspan if found; otherwise, `null`.
659
+ */
660
+ _findStartingRowSpanCell(arr, i) {
499
661
  let requiredColspan = 1;
500
- for (let index = i - 1; index >= 0; index--) {
501
- if (!arr[index]._span) {
502
- if (arr[index].rowSpan > 1 && (arr[index].colSpan || 1) === requiredColspan) {
503
- return arr[index];
504
- } else {
505
- return null;
506
- }
507
- }
508
- requiredColspan++;
509
- }
510
- return null;
662
+ for (let index = i - 1; index >= 0; index--) {
663
+ if (!arr[index]._span) {
664
+ if (arr[index].rowSpan > 1 && (arr[index].colSpan || 1) === requiredColspan) {
665
+ return arr[index];
666
+ } else {
667
+ return null;
668
+ }
669
+ }
670
+ requiredColspan++;
671
+ }
672
+ return null;
673
+ }
674
+
675
+ /**
676
+ * Retrieves a page break description for a specified page from a list of page breaks.
677
+ *
678
+ * @param {Array<object>} pageBreaks - An array of page break descriptions, each containing `prevPage` properties.
679
+ * @param {number} page - The page number to find the associated page break for.
680
+ * @returns {object|undefined} The page break description object for the specified page if found; otherwise, `undefined`.
681
+ */
682
+ _getPageBreak(pageBreaks, page) {
683
+ return pageBreaks.find(desc => desc.prevPage === page);
684
+ }
685
+
686
+ _getPageBreakListBySpan(tableNode, page, rowIndex) {
687
+ if (!tableNode || !tableNode._breaksBySpan) {
688
+ return null;
689
+ }
690
+ const breaksList = tableNode._breaksBySpan.filter(desc => desc.prevPage === page && rowIndex <= desc.rowIndexOfSpanEnd);
691
+
692
+ let y = Number.MAX_VALUE,
693
+ prevY = Number.MIN_VALUE;
694
+
695
+ breaksList.forEach(b => {
696
+ prevY = Math.max(b.prevY, prevY);
697
+ y = Math.min(b.y, y);
698
+ });
699
+
700
+ return {
701
+ prevPage: page,
702
+ prevY: prevY,
703
+ y: y
704
+ };
705
+ }
706
+
707
+ _findSameRowPageBreakByRowSpanData(breaksBySpan, page, rowIndex) {
708
+ if (!breaksBySpan) {
709
+ return null;
710
+ }
711
+ return breaksBySpan.find(desc => desc.prevPage === page && rowIndex === desc.rowIndexOfSpanEnd);
511
712
  }
512
713
 
513
- processRow(columns, widths, gaps, tableBody, tableRow, height) {
514
- const storePageBreakData = data => {
515
- let pageDesc;
714
+ _updatePageBreaksData(pageBreaks, tableNode, rowIndex) {
715
+ Object.keys(tableNode._bottomByPage).forEach(p => {
716
+ const page = Number(p);
717
+ const pageBreak = this._getPageBreak(pageBreaks, page);
718
+ if (pageBreak) {
719
+ pageBreak.prevY = Math.max(pageBreak.prevY, tableNode._bottomByPage[page]);
720
+ }
721
+ if (tableNode._breaksBySpan && tableNode._breaksBySpan.length > 0) {
722
+ const breaksBySpanList = tableNode._breaksBySpan.filter(pb => pb.prevPage === page && rowIndex <= pb.rowIndexOfSpanEnd);
516
723
 
517
- for (let i = 0, l = pageBreaks.length; i < l; i++) {
518
- let desc = pageBreaks[i];
519
- if (desc.prevPage === data.prevPage) {
520
- pageDesc = desc;
521
- break;
724
+ if (breaksBySpanList && breaksBySpanList.length > 0) {
725
+ breaksBySpanList.forEach(b => {
726
+ b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
727
+ });
522
728
  }
523
729
  }
730
+ });
731
+ }
732
+
733
+ /**
734
+ * Resolves the Y-coordinates for a target object by comparing two break points.
735
+ *
736
+ * @param {object} break1 - The first break point with `prevY` and `y` properties.
737
+ * @param {object} break2 - The second break point with `prevY` and `y` properties.
738
+ * @param {object} target - The target object to be updated with resolved Y-coordinates.
739
+ * @property {number} target.prevY - Updated to the maximum `prevY` value between `break1` and `break2`.
740
+ * @property {number} target.y - Updated to the minimum `y` value between `break1` and `break2`.
741
+ */
742
+ _resolveBreakY(break1, break2, target) {
743
+ target.prevY = Math.max(break1.prevY, break2.prevY);
744
+ target.y = Math.min(break1.y, break2.y);
745
+ };
524
746
 
747
+ _storePageBreakData(data, startsRowSpan, pageBreaks, tableNode) {
748
+ if (!startsRowSpan) {
749
+ let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
750
+ let pageDescBySpan = this._getPageBreakListBySpan(tableNode, data.prevPage, data.rowIndex);
525
751
  if (!pageDesc) {
526
- pageDesc = data;
752
+ pageDesc = {
753
+ ...data
754
+ };
527
755
  pageBreaks.push(pageDesc);
528
756
  }
529
- pageDesc.prevY = Math.max(pageDesc.prevY, data.prevY);
530
- pageDesc.y = Math.min(pageDesc.y, data.y);
531
- };
757
+ if (pageDescBySpan) {
758
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
759
+ }
760
+ this._resolveBreakY(pageDesc, data, pageDesc);
761
+ } else {
762
+ const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
763
+ let pageDescBySpan = this._findSameRowPageBreakByRowSpanData(breaksBySpan, data.prevPage, data.rowIndex);
764
+ if (!pageDescBySpan) {
765
+ pageDescBySpan = {
766
+ ...data,
767
+ rowIndexOfSpanEnd: data.rowIndex + data.rowSpan - 1
768
+ };
769
+ if (!tableNode._breaksBySpan) {
770
+ tableNode._breaksBySpan = [];
771
+ }
772
+ tableNode._breaksBySpan.push(pageDescBySpan);
773
+ }
774
+ pageDescBySpan.prevY = Math.max(pageDescBySpan.prevY, data.prevY);
775
+ pageDescBySpan.y = Math.min(pageDescBySpan.y, data.y);
776
+ let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
777
+ if (pageDesc) {
778
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
779
+ }
780
+ }
781
+ };
782
+
783
+ /**
784
+ * Calculates the left offset for a column based on the specified gap values.
785
+ *
786
+ * @param {number} i - The index of the column for which the offset is being calculated.
787
+ * @param {Array<number>} gaps - An array of gap values for each column.
788
+ * @returns {number} The left offset for the column. Returns `gaps[i]` if it exists, otherwise `0`.
789
+ */
790
+ _colLeftOffset(i, gaps) {
791
+ if (gaps && gaps.length > i) {
792
+ return gaps[i];
793
+ }
794
+ return 0;
795
+ }
796
+
797
+ /**
798
+ * Retrieves the ending cell for a row span in case it exists in a specified table column.
799
+ *
800
+ * @param {Array<Array<object>>} tableBody - The table body, represented as a 2D array of cell objects.
801
+ * @param {number} rowIndex - The index of the starting row for the row span.
802
+ * @param {object} column - The column object containing row span information.
803
+ * @param {number} columnIndex - The index of the column within the row.
804
+ * @returns {object|null} The cell at the end of the row span if it exists; otherwise, `null`.
805
+ * @throws {Error} If the row span extends beyond the total row count.
806
+ */
807
+ _getRowSpanEndingCell(tableBody, rowIndex, column, columnIndex) {
808
+ if (column.rowSpan && column.rowSpan > 1) {
809
+ let endingRow = rowIndex + column.rowSpan - 1;
810
+ if (endingRow >= tableBody.length) {
811
+ throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
812
+ }
813
+ return tableBody[endingRow][columnIndex];
814
+ }
532
815
 
816
+ return null;
817
+ }
818
+
819
+ processRow({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height }) {
820
+ const isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
533
821
  let pageBreaks = [];
822
+ let pageBreaksByRowSpan = [];
534
823
  let positions = [];
824
+ let willBreakByHeight = false;
825
+ widths = widths || cells;
826
+
827
+ // Check if row should break by height
828
+ if (!isUnbreakableRow && height > this.writer.context().availableHeight) {
829
+ willBreakByHeight = true;
830
+ }
535
831
 
536
- this.writer.addListener('pageChanged', storePageBreakData);
832
+ // Use the marginX if we are in a top level table/column (not nested)
833
+ const marginXParent = this.nestedLevel === 1 ? marginX : null;
834
+ const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
835
+ this.writer.context().beginColumnGroup(marginXParent, _bottomByPage);
537
836
 
538
- widths = widths || columns;
837
+ for (let i = 0, l = cells.length; i < l; i++) {
838
+ let cell = cells[i];
539
839
 
540
- this.writer.context().beginColumnGroup();
840
+ // Page change handler
841
+ const storePageBreakClosure = data => {
842
+ const startsRowSpan = cell.rowSpan && cell.rowSpan > 1;
843
+ if (startsRowSpan) {
844
+ data.rowSpan = cell.rowSpan;
845
+ }
846
+ data.rowIndex = rowIndex;
847
+ this._storePageBreakData(data, startsRowSpan, pageBreaks, tableNode);
848
+ };
849
+
850
+ this.writer.addListener('pageChanged', storePageBreakClosure);
541
851
 
542
- for (let i = 0, l = columns.length; i < l; i++) {
543
- let column = columns[i];
544
852
  let width = widths[i]._calcWidth;
545
- let leftOffset = colLeftOffset(i);
853
+ let leftOffset = this._colLeftOffset(i, gaps);
854
+ // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
855
+ let startingSpanCell = this._findStartingRowSpanCell(cells, i);
546
856
 
547
- if (column.colSpan && column.colSpan > 1) {
548
- for (let j = 1; j < column.colSpan; j++) {
857
+ if (cell.colSpan && cell.colSpan > 1) {
858
+ for (let j = 1; j < cell.colSpan; j++) {
549
859
  width += widths[++i]._calcWidth + gaps[i];
550
860
  }
551
861
  }
552
862
 
553
863
  // if rowspan starts in this cell, we retrieve the last cell affected by the rowspan
554
- const endingCell = getEndingCell(column, i);
555
- if (endingCell) {
864
+ const rowSpanEndingCell = this._getRowSpanEndingCell(tableBody, rowIndex, cell, i);
865
+ if (rowSpanEndingCell) {
556
866
  // We store a reference of the ending cell in the first cell of the rowspan
557
- column._endingCell = endingCell;
867
+ cell._endingCell = rowSpanEndingCell;
868
+ cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
558
869
  }
559
870
 
560
- // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
561
- let startingSpanCell = this.findStartingSpanCell(columns, i);
562
- let endingSpanCell = null;
871
+ // If we are after a cell that started a rowspan
872
+ let endOfRowSpanCell = null;
563
873
  if (startingSpanCell && startingSpanCell._endingCell) {
564
874
  // Reference to the last cell of the rowspan
565
- endingSpanCell = startingSpanCell._endingCell;
875
+ endOfRowSpanCell = startingSpanCell._endingCell;
876
+ // Store if we are in an unbreakable block when we save the context and the originalX
877
+ if (this.writer.transactionLevel > 0) {
878
+ endOfRowSpanCell._isUnbreakableContext = true;
879
+ endOfRowSpanCell._originalXOffset = this.writer.originalX;
880
+ }
566
881
  }
567
882
 
568
883
  // We pass the endingSpanCell reference to store the context just after processing rowspan cell
569
- this.writer.context().beginColumn(width, leftOffset, endingSpanCell);
570
- if (!column._span) {
571
- this.processNode(column);
572
- addAll(positions, column.positions);
573
- } else if (column._columnEndingContext) {
884
+ this.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
885
+
886
+ if (!cell._span) {
887
+ this.processNode(cell);
888
+ this.writer.context().updateBottomByPage();
889
+ addAll(positions, cell.positions);
890
+ } else if (cell._columnEndingContext) {
891
+ let discountY = 0;
892
+ if (dontBreakRows) {
893
+ // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
894
+ const ctxBeforeRowSpanLastRow = this.writer.contextStack[this.writer.contextStack.length - 1];
895
+ discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
896
+ }
897
+ let originalXOffset = 0;
898
+ // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
899
+ // We have to sum the originalX (X before starting unbreakable block) to X
900
+ if (cell._isUnbreakableContext && !this.writer.transactionLevel) {
901
+ originalXOffset = cell._originalXOffset;
902
+ }
574
903
  // row-span ending
575
904
  // Recover the context after processing the rowspanned cell
576
- this.writer.context().markEnding(column);
905
+ this.writer.context().markEnding(cell, originalXOffset, discountY);
577
906
  }
907
+ this.writer.removeListener('pageChanged', storePageBreakClosure);
578
908
  }
579
909
 
580
910
  // Check if last cell is part of a span
581
911
  let endingSpanCell = null;
582
- const lastColumn = columns.length > 0 ? columns[columns.length - 1] : null;
912
+ const lastColumn = cells.length > 0 ? cells[cells.length - 1] : null;
583
913
  if (lastColumn) {
584
914
  // Previous column cell has a rowspan
585
915
  if (lastColumn._endingCell) {
586
916
  endingSpanCell = lastColumn._endingCell;
587
- // Previous column cell is part of a span
917
+ // Previous column cell is part of a span
588
918
  } else if (lastColumn._span === true) {
589
919
  // We get the cell that started the span where we set a reference to the ending cell
590
- const startingSpanCell = this.findStartingSpanCell(columns, columns.length);
920
+ const startingSpanCell = this._findStartingRowSpanCell(cells, cells.length);
591
921
  if (startingSpanCell) {
592
922
  // Context will be stored here (ending cell)
593
923
  endingSpanCell = startingSpanCell._endingCell;
924
+ // Store if we are in an unbreakable block when we save the context and the originalX
925
+ if (this.writer.transactionLevel > 0) {
926
+ endingSpanCell._isUnbreakableContext = true;
927
+ endingSpanCell._originalXOffset = this.writer.originalX;
928
+ }
594
929
  }
595
930
  }
596
931
  }
597
932
 
598
- this.writer.context().completeColumnGroup(height, endingSpanCell);
599
-
600
- this.writer.removeListener('pageChanged', storePageBreakData);
601
-
602
- return { pageBreaks: pageBreaks, positions: positions };
603
-
604
- function colLeftOffset(i) {
605
- if (gaps && gaps.length > i) {
606
- return gaps[i];
607
- }
608
- return 0;
933
+ // If content did not break page, check if we should break by height
934
+ if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
935
+ this.writer.context().moveDown(this.writer.context().availableHeight);
936
+ this.writer.moveToNextPage();
609
937
  }
610
938
 
611
- function getEndingCell(column, columnIndex) {
612
- if (column.rowSpan && column.rowSpan > 1) {
613
- let endingRow = tableRow + column.rowSpan - 1;
614
- if (endingRow >= tableBody.length) {
615
- throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
616
- }
617
- return tableBody[endingRow][columnIndex];
618
- }
939
+ const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
619
940
 
620
- return null;
941
+ if (tableNode) {
942
+ tableNode._bottomByPage = bottomByPage;
943
+ // If there are page breaks in this row, update data with prevY of last cell
944
+ this._updatePageBreaksData(pageBreaks, tableNode, rowIndex);
621
945
  }
946
+
947
+ return {
948
+ pageBreaksBySpan: pageBreaksByRowSpan,
949
+ pageBreaks: pageBreaks,
950
+ positions: positions
951
+ };
622
952
  }
623
953
 
624
954
  // lists
@@ -667,12 +997,23 @@ class LayoutBuilder {
667
997
 
668
998
  // tables
669
999
  processTable(tableNode) {
1000
+ this.nestedLevel++;
670
1001
  let processor = new TableProcessor(tableNode);
671
1002
 
672
1003
  processor.beginTable(this.writer);
673
1004
 
674
1005
  let rowHeights = tableNode.table.heights;
675
1006
  for (let i = 0, l = tableNode.table.body.length; i < l; i++) {
1007
+ // if dontBreakRows and row starts a rowspan
1008
+ // we store the 'y' of the beginning of each rowSpan
1009
+ if (processor.dontBreakRows) {
1010
+ tableNode.table.body[i].forEach(cell => {
1011
+ if (cell.rowSpan && cell.rowSpan > 1) {
1012
+ cell._startingRowSpanY = this.writer.context().y;
1013
+ }
1014
+ });
1015
+ }
1016
+
676
1017
  processor.beginRow(i, this.writer);
677
1018
 
678
1019
  let height;
@@ -688,13 +1029,40 @@ class LayoutBuilder {
688
1029
  height = undefined;
689
1030
  }
690
1031
 
691
- let result = this.processRow(tableNode.table.body[i], tableNode.table.widths, tableNode._offsets.offsets, tableNode.table.body, i, height);
1032
+ const pageBeforeProcessing = this.writer.context().page;
1033
+
1034
+ let result = this.processRow({
1035
+ marginX: tableNode._margin ? [tableNode._margin[0], tableNode._margin[2]] : [0, 0],
1036
+ dontBreakRows: processor.dontBreakRows,
1037
+ rowsWithoutPageBreak: processor.rowsWithoutPageBreak,
1038
+ cells: tableNode.table.body[i],
1039
+ widths: tableNode.table.widths,
1040
+ gaps: tableNode._offsets.offsets,
1041
+ tableBody: tableNode.table.body,
1042
+ tableNode,
1043
+ rowIndex: i,
1044
+ height
1045
+ });
1046
+
692
1047
  addAll(tableNode.positions, result.positions);
693
1048
 
1049
+ if (!result.pageBreaks || result.pageBreaks.length === 0) {
1050
+ const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
1051
+ const breakBySpanData = this._findSameRowPageBreakByRowSpanData(breaksBySpan, pageBeforeProcessing, i);
1052
+ if (breakBySpanData) {
1053
+ const finalBreakBySpanData = this._getPageBreakListBySpan(tableNode, breakBySpanData.prevPage, i);
1054
+ result.pageBreaks.push(finalBreakBySpanData);
1055
+ }
1056
+ }
1057
+
694
1058
  processor.endRow(i, this.writer, result.pageBreaks);
695
1059
  }
696
1060
 
697
1061
  processor.endTable(this.writer);
1062
+ this.nestedLevel--;
1063
+ if (this.nestedLevel === 0) {
1064
+ this.writer.context().resetMarginXTopParent();
1065
+ }
698
1066
  }
699
1067
 
700
1068
  // leafs (texts)
@@ -762,6 +1130,27 @@ class LayoutBuilder {
762
1130
  return newInline;
763
1131
  }
764
1132
 
1133
+ function findMaxFitLength(text, maxWidth, measureFn) {
1134
+ let low = 1;
1135
+ let high = text.length;
1136
+ let bestFit = 1;
1137
+
1138
+ while (low <= high) {
1139
+ const mid = Math.floor((low + high) / 2);
1140
+ const part = text.substring(0, mid);
1141
+ const width = measureFn(part);
1142
+
1143
+ if (width <= maxWidth) {
1144
+ bestFit = mid;
1145
+ low = mid + 1;
1146
+ } else {
1147
+ high = mid - 1;
1148
+ }
1149
+ }
1150
+
1151
+ return bestFit;
1152
+ }
1153
+
765
1154
  if (!textNode._inlines || textNode._inlines.length === 0) {
766
1155
  return null;
767
1156
  }
@@ -777,11 +1166,9 @@ class LayoutBuilder {
777
1166
  isForceContinue = false;
778
1167
 
779
1168
  if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
780
- let widthPerChar = inline.width / inline.text.length;
781
- let maxChars = Math.floor(line.getAvailableWidth() / widthPerChar);
782
- if (maxChars < 1) {
783
- maxChars = 1;
784
- }
1169
+ let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), (txt) =>
1170
+ textInlines.widthOfText(txt, inline)
1171
+ );
785
1172
  if (maxChars < inline.text.length) {
786
1173
  let newInline = cloneInline(inline);
787
1174