pdfmake 0.3.0-beta.8 → 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 (87) hide show
  1. package/CHANGELOG.md +7 -40
  2. package/LICENSE +1 -1
  3. package/README.md +78 -85
  4. package/build/pdfmake.js +60308 -68400
  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/3rd-party/svg-to-pdfkit/source.js +3626 -0
  14. package/js/3rd-party/svg-to-pdfkit.js +7 -0
  15. package/js/DocMeasure.js +645 -0
  16. package/js/DocPreprocessor.js +253 -0
  17. package/js/DocumentContext.js +305 -0
  18. package/js/ElementWriter.js +354 -0
  19. package/js/LayoutBuilder.js +1105 -0
  20. package/js/Line.js +105 -0
  21. package/js/OutputDocument.js +64 -0
  22. package/js/OutputDocumentServer.js +22 -0
  23. package/js/PDFDocument.js +144 -0
  24. package/js/PageElementWriter.js +155 -0
  25. package/js/PageSize.js +74 -0
  26. package/js/Printer.js +291 -0
  27. package/js/Renderer.js +383 -0
  28. package/js/SVGMeasure.js +69 -0
  29. package/js/StyleContextStack.js +168 -0
  30. package/js/TableProcessor.js +548 -0
  31. package/js/TextBreaker.js +166 -0
  32. package/js/TextDecorator.js +143 -0
  33. package/js/TextInlines.js +206 -0
  34. package/js/URLResolver.js +43 -0
  35. package/js/base.js +52 -0
  36. package/js/browser-extensions/OutputDocumentBrowser.js +81 -0
  37. package/js/browser-extensions/fonts/Roboto.js +38 -0
  38. package/js/browser-extensions/index.js +53 -0
  39. package/js/browser-extensions/pdfMake.js +3 -0
  40. package/js/browser-extensions/standard-fonts/Courier.js +38 -0
  41. package/js/browser-extensions/standard-fonts/Helvetica.js +38 -0
  42. package/js/browser-extensions/standard-fonts/Symbol.js +23 -0
  43. package/js/browser-extensions/standard-fonts/Times.js +38 -0
  44. package/js/browser-extensions/standard-fonts/ZapfDingbats.js +23 -0
  45. package/js/browser-extensions/virtual-fs-cjs.js +3 -0
  46. package/js/columnCalculator.js +148 -0
  47. package/js/helpers/node.js +98 -0
  48. package/js/helpers/tools.js +46 -0
  49. package/js/helpers/variableType.js +59 -0
  50. package/js/index.js +15 -0
  51. package/js/qrEnc.js +721 -0
  52. package/js/standardPageSizes.js +56 -0
  53. package/js/tableLayouts.js +98 -0
  54. package/js/virtual-fs.js +60 -0
  55. package/package.json +25 -24
  56. package/src/DocMeasure.js +28 -7
  57. package/src/DocPreprocessor.js +25 -6
  58. package/src/DocumentContext.js +62 -33
  59. package/src/ElementWriter.js +30 -7
  60. package/src/LayoutBuilder.js +557 -120
  61. package/src/OutputDocument.js +23 -37
  62. package/src/OutputDocumentServer.js +6 -11
  63. package/src/PDFDocument.js +1 -1
  64. package/src/PageElementWriter.js +21 -2
  65. package/src/Printer.js +134 -131
  66. package/src/Renderer.js +13 -15
  67. package/src/SVGMeasure.js +2 -2
  68. package/src/StyleContextStack.js +7 -44
  69. package/src/TableProcessor.js +62 -22
  70. package/src/TextBreaker.js +24 -5
  71. package/src/TextInlines.js +1 -1
  72. package/src/URLResolver.js +24 -58
  73. package/src/base.js +1 -1
  74. package/src/browser-extensions/OutputDocumentBrowser.js +33 -71
  75. package/src/browser-extensions/index.js +3 -3
  76. package/src/browser-extensions/pdfMake.js +0 -14
  77. package/src/browser-extensions/standard-fonts/Courier.js +4 -4
  78. package/src/browser-extensions/standard-fonts/Helvetica.js +4 -4
  79. package/src/browser-extensions/standard-fonts/Symbol.js +1 -1
  80. package/src/browser-extensions/standard-fonts/Times.js +4 -4
  81. package/src/browser-extensions/standard-fonts/ZapfDingbats.js +1 -1
  82. package/src/columnCalculator.js +24 -3
  83. package/src/helpers/tools.js +5 -0
  84. package/src/helpers/variableType.js +11 -0
  85. package/src/index.js +1 -1
  86. package/standard-fonts/Helvetica.js +0 -1
  87. 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,80 +649,306 @@ class LayoutBuilder {
495
649
  }
496
650
  }
497
651
 
498
- processRow(columns, widths, gaps, tableBody, tableRow, height) {
499
- const storePageBreakData = data => {
500
- let pageDesc;
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) {
661
+ let requiredColspan = 1;
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);
712
+ }
713
+
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);
501
723
 
502
- for (let i = 0, l = pageBreaks.length; i < l; i++) {
503
- let desc = pageBreaks[i];
504
- if (desc.prevPage === data.prevPage) {
505
- pageDesc = desc;
506
- break;
724
+ if (breaksBySpanList && breaksBySpanList.length > 0) {
725
+ breaksBySpanList.forEach(b => {
726
+ b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
727
+ });
507
728
  }
508
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
+ };
509
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);
510
751
  if (!pageDesc) {
511
- pageDesc = data;
752
+ pageDesc = {
753
+ ...data
754
+ };
512
755
  pageBreaks.push(pageDesc);
513
756
  }
514
- pageDesc.prevY = Math.max(pageDesc.prevY, data.prevY);
515
- pageDesc.y = Math.min(pageDesc.y, data.y);
516
- };
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
+ }
517
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;
518
821
  let pageBreaks = [];
822
+ let pageBreaksByRowSpan = [];
519
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
+ }
831
+
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);
520
836
 
521
- this.writer.addListener('pageChanged', storePageBreakData);
837
+ for (let i = 0, l = cells.length; i < l; i++) {
838
+ let cell = cells[i];
522
839
 
523
- widths = widths || columns;
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
+ };
524
849
 
525
- this.writer.context().beginColumnGroup();
850
+ this.writer.addListener('pageChanged', storePageBreakClosure);
526
851
 
527
- for (let i = 0, l = columns.length; i < l; i++) {
528
- let column = columns[i];
529
852
  let width = widths[i]._calcWidth;
530
- 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);
531
856
 
532
- if (column.colSpan && column.colSpan > 1) {
533
- for (let j = 1; j < column.colSpan; j++) {
857
+ if (cell.colSpan && cell.colSpan > 1) {
858
+ for (let j = 1; j < cell.colSpan; j++) {
534
859
  width += widths[++i]._calcWidth + gaps[i];
535
860
  }
536
861
  }
537
862
 
538
- this.writer.context().beginColumn(width, leftOffset, getEndingCell(column, i));
539
- if (!column._span) {
540
- this.processNode(column);
541
- addAll(positions, column.positions);
542
- } else if (column._columnEndingContext) {
543
- // row-span ending
544
- this.writer.context().markEnding(column);
863
+ // if rowspan starts in this cell, we retrieve the last cell affected by the rowspan
864
+ const rowSpanEndingCell = this._getRowSpanEndingCell(tableBody, rowIndex, cell, i);
865
+ if (rowSpanEndingCell) {
866
+ // We store a reference of the ending cell in the first cell of the rowspan
867
+ cell._endingCell = rowSpanEndingCell;
868
+ cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
545
869
  }
546
- }
547
-
548
- this.writer.context().completeColumnGroup(height);
549
870
 
550
- this.writer.removeListener('pageChanged', storePageBreakData);
551
-
552
- return { pageBreaks: pageBreaks, positions: positions };
871
+ // If we are after a cell that started a rowspan
872
+ let endOfRowSpanCell = null;
873
+ if (startingSpanCell && startingSpanCell._endingCell) {
874
+ // Reference to the last cell of the rowspan
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
+ }
881
+ }
553
882
 
554
- function colLeftOffset(i) {
555
- if (gaps && gaps.length > i) {
556
- return gaps[i];
883
+ // We pass the endingSpanCell reference to store the context just after processing rowspan cell
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
+ }
903
+ // row-span ending
904
+ // Recover the context after processing the rowspanned cell
905
+ this.writer.context().markEnding(cell, originalXOffset, discountY);
557
906
  }
558
- return 0;
907
+ this.writer.removeListener('pageChanged', storePageBreakClosure);
559
908
  }
560
909
 
561
- function getEndingCell(column, columnIndex) {
562
- if (column.rowSpan && column.rowSpan > 1) {
563
- let endingRow = tableRow + column.rowSpan - 1;
564
- if (endingRow >= tableBody.length) {
565
- throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
910
+ // Check if last cell is part of a span
911
+ let endingSpanCell = null;
912
+ const lastColumn = cells.length > 0 ? cells[cells.length - 1] : null;
913
+ if (lastColumn) {
914
+ // Previous column cell has a rowspan
915
+ if (lastColumn._endingCell) {
916
+ endingSpanCell = lastColumn._endingCell;
917
+ // Previous column cell is part of a span
918
+ } else if (lastColumn._span === true) {
919
+ // We get the cell that started the span where we set a reference to the ending cell
920
+ const startingSpanCell = this._findStartingRowSpanCell(cells, cells.length);
921
+ if (startingSpanCell) {
922
+ // Context will be stored here (ending cell)
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
+ }
566
929
  }
567
- return tableBody[endingRow][columnIndex];
568
930
  }
931
+ }
569
932
 
570
- return null;
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();
937
+ }
938
+
939
+ const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
940
+
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);
571
945
  }
946
+
947
+ return {
948
+ pageBreaksBySpan: pageBreaksByRowSpan,
949
+ pageBreaks: pageBreaks,
950
+ positions: positions
951
+ };
572
952
  }
573
953
 
574
954
  // lists
@@ -617,12 +997,23 @@ class LayoutBuilder {
617
997
 
618
998
  // tables
619
999
  processTable(tableNode) {
1000
+ this.nestedLevel++;
620
1001
  let processor = new TableProcessor(tableNode);
621
1002
 
622
1003
  processor.beginTable(this.writer);
623
1004
 
624
1005
  let rowHeights = tableNode.table.heights;
625
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
+
626
1017
  processor.beginRow(i, this.writer);
627
1018
 
628
1019
  let height;
@@ -638,13 +1029,40 @@ class LayoutBuilder {
638
1029
  height = undefined;
639
1030
  }
640
1031
 
641
- 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
+
642
1047
  addAll(tableNode.positions, result.positions);
643
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
+
644
1058
  processor.endRow(i, this.writer, result.pageBreaks);
645
1059
  }
646
1060
 
647
1061
  processor.endTable(this.writer);
1062
+ this.nestedLevel--;
1063
+ if (this.nestedLevel === 0) {
1064
+ this.writer.context().resetMarginXTopParent();
1065
+ }
648
1066
  }
649
1067
 
650
1068
  // leafs (texts)
@@ -712,6 +1130,27 @@ class LayoutBuilder {
712
1130
  return newInline;
713
1131
  }
714
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
+
715
1154
  if (!textNode._inlines || textNode._inlines.length === 0) {
716
1155
  return null;
717
1156
  }
@@ -727,11 +1166,9 @@ class LayoutBuilder {
727
1166
  isForceContinue = false;
728
1167
 
729
1168
  if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
730
- let widthPerChar = inline.width / inline.text.length;
731
- let maxChars = Math.floor(line.getAvailableWidth() / widthPerChar);
732
- if (maxChars < 1) {
733
- maxChars = 1;
734
- }
1169
+ let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), (txt) =>
1170
+ textInlines.widthOfText(txt, inline)
1171
+ );
735
1172
  if (maxChars < inline.text.length) {
736
1173
  let newInline = cloneInline(inline);
737
1174