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
@@ -36,6 +36,7 @@ class LayoutBuilder {
36
36
  this.pageMargins = pageMargins;
37
37
  this.svgMeasure = svgMeasure;
38
38
  this.tableLayouts = {};
39
+ this.nestedLevel = 0;
39
40
  }
40
41
  registerTableLayouts(tableLayouts) {
41
42
  this.tableLayouts = (0, _tools.pack)(this.tableLayouts, tableLayouts);
@@ -61,7 +62,16 @@ class LayoutBuilder {
61
62
  if (typeof pageBreakBeforeFct !== 'function') {
62
63
  return false;
63
64
  }
64
- linearNodeList = linearNodeList.filter(node => node.positions.length > 0);
65
+ const hasRenderableContent = node => {
66
+ if (!node || node.positions.length === 0) {
67
+ return false;
68
+ }
69
+ if (node.text === '' && !node.listMarker) {
70
+ return false;
71
+ }
72
+ return true;
73
+ };
74
+ linearNodeList = linearNodeList.filter(hasRenderableContent);
65
75
  linearNodeList.forEach(node => {
66
76
  let nodeInfo = {};
67
77
  ['id', 'text', 'ul', 'ol', 'table', 'image', 'qr', 'canvas', 'svg', 'columns', 'headlineLevel', 'style', 'pageBreak', 'pageOrientation', 'width', 'height'].forEach(key => {
@@ -131,19 +141,31 @@ class LayoutBuilder {
131
141
  return result.pages;
132
142
  }
133
143
  tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark) {
144
+ const isNecessaryAddFirstPage = docStructure => {
145
+ if (docStructure.stack && docStructure.stack.length > 0 && docStructure.stack[0].section) {
146
+ return false;
147
+ } else if (docStructure.section) {
148
+ return false;
149
+ }
150
+ return true;
151
+ };
134
152
  this.linearNodeList = [];
135
153
  docStructure = this.docPreprocessor.preprocessDocument(docStructure);
136
154
  docStructure = this.docMeasure.measureDocument(docStructure);
137
- this.writer = new _PageElementWriter.default(new _DocumentContext.default(this.pageSize, this.pageMargins));
138
- this.writer.context().addListener('pageAdded', () => {
139
- this.addBackground(background);
155
+ this.writer = new _PageElementWriter.default(new _DocumentContext.default());
156
+ this.writer.context().addListener('pageAdded', page => {
157
+ let backgroundGetter = background;
158
+ if (page.customProperties['background'] || page.customProperties['background'] === null) {
159
+ backgroundGetter = page.customProperties['background'];
160
+ }
161
+ this.addBackground(backgroundGetter);
140
162
  });
141
- this.addBackground(background);
163
+ if (isNecessaryAddFirstPage(docStructure)) {
164
+ this.writer.addPage(this.pageSize, null, this.pageMargins);
165
+ }
142
166
  this.processNode(docStructure);
143
167
  this.addHeadersAndFooters(header, footer);
144
- if (watermark != null) {
145
- this.addWatermark(watermark, pdfDocument, defaultStyle);
146
- }
168
+ this.addWatermark(watermark, pdfDocument, defaultStyle);
147
169
  return {
148
170
  pages: this.writer.context().pages,
149
171
  linearNodeList: this.linearNodeList
@@ -156,27 +178,30 @@ class LayoutBuilder {
156
178
  let pageBackground = backgroundGetter(context.page + 1, pageSize);
157
179
  if (pageBackground) {
158
180
  this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height);
159
- pageBackground = this.docPreprocessor.preprocessDocument(pageBackground);
160
- this.processNode(this.docMeasure.measureDocument(pageBackground));
181
+ pageBackground = this.docPreprocessor.preprocessBlock(pageBackground);
182
+ this.processNode(this.docMeasure.measureBlock(pageBackground));
161
183
  this.writer.commitUnbreakableBlock(0, 0);
162
184
  context.backgroundLength[context.page] += pageBackground.positions.length;
163
185
  }
164
186
  }
165
- addStaticRepeatable(headerOrFooter, sizeFunction) {
166
- this.addDynamicRepeatable(() =>
167
- // copy to new object
168
- JSON.parse(JSON.stringify(headerOrFooter)), sizeFunction);
169
- }
170
- addDynamicRepeatable(nodeGetter, sizeFunction) {
187
+ addDynamicRepeatable(nodeGetter, sizeFunction, customPropertyName) {
171
188
  let pages = this.writer.context().pages;
172
189
  for (let pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) {
173
190
  this.writer.context().page = pageIndex;
174
- let node = nodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
191
+ let customProperties = this.writer.context().getCurrentPage().customProperties;
192
+ let pageNodeGetter = nodeGetter;
193
+ if (customProperties[customPropertyName] || customProperties[customPropertyName] === null) {
194
+ pageNodeGetter = customProperties[customPropertyName];
195
+ }
196
+ if (typeof pageNodeGetter === 'undefined' || pageNodeGetter === null) {
197
+ continue;
198
+ }
199
+ let node = pageNodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
175
200
  if (node) {
176
- let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.pageMargins);
201
+ let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.writer.context().getCurrentPage().pageMargins);
177
202
  this.writer.beginUnbreakableBlock(sizes.width, sizes.height);
178
- node = this.docPreprocessor.preprocessDocument(node);
179
- this.processNode(this.docMeasure.measureDocument(node));
203
+ node = this.docPreprocessor.preprocessBlock(node);
204
+ this.processNode(this.docMeasure.measureBlock(node));
180
205
  this.writer.commitUnbreakableBlock(sizes.x, sizes.y);
181
206
  }
182
207
  }
@@ -194,52 +219,56 @@ class LayoutBuilder {
194
219
  width: pageSize.width,
195
220
  height: pageMargins.bottom
196
221
  });
197
- if (typeof header === 'function') {
198
- this.addDynamicRepeatable(header, headerSizeFct);
199
- } else if (header) {
200
- this.addStaticRepeatable(header, headerSizeFct);
201
- }
202
- if (typeof footer === 'function') {
203
- this.addDynamicRepeatable(footer, footerSizeFct);
204
- } else if (footer) {
205
- this.addStaticRepeatable(footer, footerSizeFct);
206
- }
222
+ this.addDynamicRepeatable(header, headerSizeFct, 'header');
223
+ this.addDynamicRepeatable(footer, footerSizeFct, 'footer');
207
224
  }
208
225
  addWatermark(watermark, pdfDocument, defaultStyle) {
209
- if ((0, _variableType.isString)(watermark)) {
210
- watermark = {
211
- 'text': watermark
212
- };
213
- }
214
- if (!watermark.text) {
215
- // empty watermark text
216
- return;
217
- }
218
- watermark.font = watermark.font || defaultStyle.font || 'Roboto';
219
- watermark.fontSize = watermark.fontSize || 'auto';
220
- watermark.color = watermark.color || 'black';
221
- watermark.opacity = (0, _variableType.isNumber)(watermark.opacity) ? watermark.opacity : 0.6;
222
- watermark.bold = watermark.bold || false;
223
- watermark.italics = watermark.italics || false;
224
- watermark.angle = (0, _variableType.isValue)(watermark.angle) ? watermark.angle : null;
225
- if (watermark.angle === null) {
226
- watermark.angle = Math.atan2(this.pageSize.height, this.pageSize.width) * -180 / Math.PI;
227
- }
228
- if (watermark.fontSize === 'auto') {
229
- watermark.fontSize = getWatermarkFontSize(this.pageSize, watermark, pdfDocument);
230
- }
231
- let watermarkObject = {
232
- text: watermark.text,
233
- font: pdfDocument.provideFont(watermark.font, watermark.bold, watermark.italics),
234
- fontSize: watermark.fontSize,
235
- color: watermark.color,
236
- opacity: watermark.opacity,
237
- angle: watermark.angle
238
- };
239
- watermarkObject._size = getWatermarkSize(watermark, pdfDocument);
240
226
  let pages = this.writer.context().pages;
241
227
  for (let i = 0, l = pages.length; i < l; i++) {
242
- pages[i].watermark = watermarkObject;
228
+ let pageWatermark = watermark;
229
+ if (pages[i].customProperties['watermark'] || pages[i].customProperties['watermark'] === null) {
230
+ pageWatermark = pages[i].customProperties['watermark'];
231
+ }
232
+ if (pageWatermark === undefined || pageWatermark === null) {
233
+ continue;
234
+ }
235
+ if ((0, _variableType.isString)(pageWatermark)) {
236
+ pageWatermark = {
237
+ 'text': pageWatermark
238
+ };
239
+ }
240
+ if (!pageWatermark.text) {
241
+ // empty watermark text
242
+ continue;
243
+ }
244
+ pages[i].watermark = getWatermarkObject({
245
+ ...pageWatermark
246
+ }, pages[i].pageSize, pdfDocument, defaultStyle);
247
+ }
248
+ function getWatermarkObject(watermark, pageSize, pdfDocument, defaultStyle) {
249
+ watermark.font = watermark.font || defaultStyle.font || 'Roboto';
250
+ watermark.fontSize = watermark.fontSize || 'auto';
251
+ watermark.color = watermark.color || 'black';
252
+ watermark.opacity = (0, _variableType.isNumber)(watermark.opacity) ? watermark.opacity : 0.6;
253
+ watermark.bold = watermark.bold || false;
254
+ watermark.italics = watermark.italics || false;
255
+ watermark.angle = (0, _variableType.isValue)(watermark.angle) ? watermark.angle : null;
256
+ if (watermark.angle === null) {
257
+ watermark.angle = Math.atan2(pageSize.height, pageSize.width) * -180 / Math.PI;
258
+ }
259
+ if (watermark.fontSize === 'auto') {
260
+ watermark.fontSize = getWatermarkFontSize(pageSize, watermark, pdfDocument);
261
+ }
262
+ let watermarkObject = {
263
+ text: watermark.text,
264
+ font: pdfDocument.provideFont(watermark.font, watermark.bold, watermark.italics),
265
+ fontSize: watermark.fontSize,
266
+ color: watermark.color,
267
+ opacity: watermark.opacity,
268
+ angle: watermark.angle
269
+ };
270
+ watermarkObject._size = getWatermarkSize(watermark, pdfDocument);
271
+ return watermarkObject;
243
272
  }
244
273
  function getWatermarkSize(watermark, pdfDocument) {
245
274
  let textInlines = new _TextInlines.default(pdfDocument);
@@ -316,14 +345,50 @@ class LayoutBuilder {
316
345
  this.writer.moveToNextPage(node.pageOrientation);
317
346
  }
318
347
  }
319
- if (margin) {
320
- this.writer.context().moveDown(margin[1]);
348
+ const isDetachedBlock = node.relativePosition || node.absolutePosition;
349
+
350
+ // Detached nodes have no margins, their position is only determined by 'x' and 'y'
351
+ if (margin && !isDetachedBlock) {
352
+ const availableHeight = this.writer.context().availableHeight;
353
+ // If top margin is bigger than available space, move to next page
354
+ // Necessary for nodes inside tables
355
+ if (availableHeight - margin[1] < 0) {
356
+ // Consume the whole available space
357
+ this.writer.context().moveDown(availableHeight);
358
+ this.writer.moveToNextPage(node.pageOrientation);
359
+ /**
360
+ * TODO - Something to consider:
361
+ * Right now the node starts at the top of next page (after header)
362
+ * Another option would be to apply just the top margin that has not been consumed in the page before
363
+ * It would something like: this.write.context().moveDown(margin[1] - availableHeight)
364
+ */
365
+ } else {
366
+ this.writer.context().moveDown(margin[1]);
367
+ }
368
+ // Apply lateral margins
321
369
  this.writer.context().addMargin(margin[0], margin[2]);
322
370
  }
323
371
  callback();
324
- if (margin) {
372
+
373
+ // Detached nodes have no margins, their position is only determined by 'x' and 'y'
374
+ if (margin && !isDetachedBlock) {
375
+ const availableHeight = this.writer.context().availableHeight;
376
+ // If bottom margin is bigger than available space, move to next page
377
+ // Necessary for nodes inside tables
378
+ if (availableHeight - margin[3] < 0) {
379
+ this.writer.context().moveDown(availableHeight);
380
+ this.writer.moveToNextPage(node.pageOrientation);
381
+ /**
382
+ * TODO - Something to consider:
383
+ * Right now next node starts at the top of next page (after header)
384
+ * Another option would be to apply the bottom margin that has not been consumed in the next page?
385
+ * It would something like: this.write.context().moveDown(margin[3] - availableHeight)
386
+ */
387
+ } else {
388
+ this.writer.context().moveDown(margin[3]);
389
+ }
390
+ // Apply lateral margins
325
391
  this.writer.context().addMargin(-margin[0], -margin[2]);
326
- this.writer.context().moveDown(margin[3]);
327
392
  }
328
393
  if (node.pageBreak === 'after') {
329
394
  this.writer.moveToNextPage(node.pageOrientation);
@@ -358,6 +423,8 @@ class LayoutBuilder {
358
423
  }
359
424
  if (node.stack) {
360
425
  this.processVerticalContainer(node);
426
+ } else if (node.section) {
427
+ this.processSection(node);
361
428
  } else if (node.columns) {
362
429
  this.processColumns(node);
363
430
  } else if (node.ul) {
@@ -402,8 +469,65 @@ class LayoutBuilder {
402
469
  }, this);
403
470
  }
404
471
 
472
+ // section
473
+ processSection(sectionNode) {
474
+ // TODO: properties
475
+
476
+ let page = this.writer.context().getCurrentPage();
477
+ if (!page || page && page.items.length) {
478
+ // move to new empty page
479
+ // page definition inherit from current page
480
+ if (sectionNode.pageSize === 'inherit') {
481
+ sectionNode.pageSize = page ? {
482
+ width: page.pageSize.width,
483
+ height: page.pageSize.height
484
+ } : undefined;
485
+ }
486
+ if (sectionNode.pageOrientation === 'inherit') {
487
+ sectionNode.pageOrientation = page ? page.pageSize.orientation : undefined;
488
+ }
489
+ if (sectionNode.pageMargins === 'inherit') {
490
+ sectionNode.pageMargins = page ? page.pageMargins : undefined;
491
+ }
492
+ if (sectionNode.header === 'inherit') {
493
+ sectionNode.header = page ? page.customProperties.header : undefined;
494
+ }
495
+ if (sectionNode.footer === 'inherit') {
496
+ sectionNode.footer = page ? page.customProperties.footer : undefined;
497
+ }
498
+ if (sectionNode.background === 'inherit') {
499
+ sectionNode.background = page ? page.customProperties.background : undefined;
500
+ }
501
+ if (sectionNode.watermark === 'inherit') {
502
+ sectionNode.watermark = page ? page.customProperties.watermark : undefined;
503
+ }
504
+ if (sectionNode.header && typeof sectionNode.header !== 'function' && sectionNode.header !== null) {
505
+ sectionNode.header = (0, _tools.convertToDynamicContent)(sectionNode.header);
506
+ }
507
+ if (sectionNode.footer && typeof sectionNode.footer !== 'function' && sectionNode.footer !== null) {
508
+ sectionNode.footer = (0, _tools.convertToDynamicContent)(sectionNode.footer);
509
+ }
510
+ let customProperties = {};
511
+ if (typeof sectionNode.header !== 'undefined') {
512
+ customProperties.header = sectionNode.header;
513
+ }
514
+ if (typeof sectionNode.footer !== 'undefined') {
515
+ customProperties.footer = sectionNode.footer;
516
+ }
517
+ if (typeof sectionNode.background !== 'undefined') {
518
+ customProperties.background = sectionNode.background;
519
+ }
520
+ if (typeof sectionNode.watermark !== 'undefined') {
521
+ customProperties.watermark = sectionNode.watermark;
522
+ }
523
+ this.writer.addPage(sectionNode.pageSize || this.pageSize, sectionNode.pageOrientation, sectionNode.pageMargins || this.pageMargins, customProperties);
524
+ }
525
+ this.processNode(sectionNode.section);
526
+ }
527
+
405
528
  // columns
406
529
  processColumns(columnNode) {
530
+ this.nestedLevel++;
407
531
  let columns = columnNode.columns;
408
532
  let availableWidth = this.writer.context().availableWidth;
409
533
  let gaps = gapArray(columnNode._gap);
@@ -411,8 +535,17 @@ class LayoutBuilder {
411
535
  availableWidth -= (gaps.length - 1) * columnNode._gap;
412
536
  }
413
537
  _columnCalculator.default.buildColumnWidths(columns, availableWidth);
414
- let result = this.processRow(columns, columns, gaps);
538
+ let result = this.processRow({
539
+ marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
540
+ cells: columns,
541
+ widths: columns,
542
+ gaps
543
+ });
415
544
  addAll(columnNode.positions, result.positions);
545
+ this.nestedLevel--;
546
+ if (this.nestedLevel === 0) {
547
+ this.writer.context().resetMarginXTopParent();
548
+ }
416
549
  function gapArray(gap) {
417
550
  if (!gap) {
418
551
  return null;
@@ -425,7 +558,16 @@ class LayoutBuilder {
425
558
  return gaps;
426
559
  }
427
560
  }
428
- findStartingSpanCell(arr, i) {
561
+
562
+ /**
563
+ * Searches for a cell in the same row that starts a rowspan and is positioned immediately before the current cell.
564
+ * Alternatively, it finds a cell where the colspan initiating the rowspan extends to the cell just before the current one.
565
+ *
566
+ * @param {Array<object>} arr - An array representing cells in a row.
567
+ * @param {number} i - The index of the current cell to search backward from.
568
+ * @returns {object|null} The starting cell of the rowspan if found; otherwise, `null`.
569
+ */
570
+ _findStartingRowSpanCell(arr, i) {
429
571
  let requiredColspan = 1;
430
572
  for (let index = i - 1; index >= 0; index--) {
431
573
  if (!arr[index]._span) {
@@ -439,68 +581,240 @@ class LayoutBuilder {
439
581
  }
440
582
  return null;
441
583
  }
442
- processRow(columns, widths, gaps, tableBody, tableRow, height) {
443
- const storePageBreakData = data => {
444
- let pageDesc;
445
- for (let i = 0, l = pageBreaks.length; i < l; i++) {
446
- let desc = pageBreaks[i];
447
- if (desc.prevPage === data.prevPage) {
448
- pageDesc = desc;
449
- break;
584
+
585
+ /**
586
+ * Retrieves a page break description for a specified page from a list of page breaks.
587
+ *
588
+ * @param {Array<object>} pageBreaks - An array of page break descriptions, each containing `prevPage` properties.
589
+ * @param {number} page - The page number to find the associated page break for.
590
+ * @returns {object|undefined} The page break description object for the specified page if found; otherwise, `undefined`.
591
+ */
592
+ _getPageBreak(pageBreaks, page) {
593
+ return pageBreaks.find(desc => desc.prevPage === page);
594
+ }
595
+ _getPageBreakListBySpan(tableNode, page, rowIndex) {
596
+ if (!tableNode || !tableNode._breaksBySpan) {
597
+ return null;
598
+ }
599
+ const breaksList = tableNode._breaksBySpan.filter(desc => desc.prevPage === page && rowIndex <= desc.rowIndexOfSpanEnd);
600
+ let y = Number.MAX_VALUE,
601
+ prevY = Number.MIN_VALUE;
602
+ breaksList.forEach(b => {
603
+ prevY = Math.max(b.prevY, prevY);
604
+ y = Math.min(b.y, y);
605
+ });
606
+ return {
607
+ prevPage: page,
608
+ prevY: prevY,
609
+ y: y
610
+ };
611
+ }
612
+ _findSameRowPageBreakByRowSpanData(breaksBySpan, page, rowIndex) {
613
+ if (!breaksBySpan) {
614
+ return null;
615
+ }
616
+ return breaksBySpan.find(desc => desc.prevPage === page && rowIndex === desc.rowIndexOfSpanEnd);
617
+ }
618
+ _updatePageBreaksData(pageBreaks, tableNode, rowIndex) {
619
+ Object.keys(tableNode._bottomByPage).forEach(p => {
620
+ const page = Number(p);
621
+ const pageBreak = this._getPageBreak(pageBreaks, page);
622
+ if (pageBreak) {
623
+ pageBreak.prevY = Math.max(pageBreak.prevY, tableNode._bottomByPage[page]);
624
+ }
625
+ if (tableNode._breaksBySpan && tableNode._breaksBySpan.length > 0) {
626
+ const breaksBySpanList = tableNode._breaksBySpan.filter(pb => pb.prevPage === page && rowIndex <= pb.rowIndexOfSpanEnd);
627
+ if (breaksBySpanList && breaksBySpanList.length > 0) {
628
+ breaksBySpanList.forEach(b => {
629
+ b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
630
+ });
450
631
  }
451
632
  }
633
+ });
634
+ }
635
+
636
+ /**
637
+ * Resolves the Y-coordinates for a target object by comparing two break points.
638
+ *
639
+ * @param {object} break1 - The first break point with `prevY` and `y` properties.
640
+ * @param {object} break2 - The second break point with `prevY` and `y` properties.
641
+ * @param {object} target - The target object to be updated with resolved Y-coordinates.
642
+ * @property {number} target.prevY - Updated to the maximum `prevY` value between `break1` and `break2`.
643
+ * @property {number} target.y - Updated to the minimum `y` value between `break1` and `break2`.
644
+ */
645
+ _resolveBreakY(break1, break2, target) {
646
+ target.prevY = Math.max(break1.prevY, break2.prevY);
647
+ target.y = Math.min(break1.y, break2.y);
648
+ }
649
+ _storePageBreakData(data, startsRowSpan, pageBreaks, tableNode) {
650
+ if (!startsRowSpan) {
651
+ let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
652
+ let pageDescBySpan = this._getPageBreakListBySpan(tableNode, data.prevPage, data.rowIndex);
452
653
  if (!pageDesc) {
453
- pageDesc = data;
654
+ pageDesc = {
655
+ ...data
656
+ };
454
657
  pageBreaks.push(pageDesc);
455
658
  }
456
- pageDesc.prevY = Math.max(pageDesc.prevY, data.prevY);
457
- pageDesc.y = Math.min(pageDesc.y, data.y);
458
- };
659
+ if (pageDescBySpan) {
660
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
661
+ }
662
+ this._resolveBreakY(pageDesc, data, pageDesc);
663
+ } else {
664
+ const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
665
+ let pageDescBySpan = this._findSameRowPageBreakByRowSpanData(breaksBySpan, data.prevPage, data.rowIndex);
666
+ if (!pageDescBySpan) {
667
+ pageDescBySpan = {
668
+ ...data,
669
+ rowIndexOfSpanEnd: data.rowIndex + data.rowSpan - 1
670
+ };
671
+ if (!tableNode._breaksBySpan) {
672
+ tableNode._breaksBySpan = [];
673
+ }
674
+ tableNode._breaksBySpan.push(pageDescBySpan);
675
+ }
676
+ pageDescBySpan.prevY = Math.max(pageDescBySpan.prevY, data.prevY);
677
+ pageDescBySpan.y = Math.min(pageDescBySpan.y, data.y);
678
+ let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
679
+ if (pageDesc) {
680
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
681
+ }
682
+ }
683
+ }
684
+ /**
685
+ * Calculates the left offset for a column based on the specified gap values.
686
+ *
687
+ * @param {number} i - The index of the column for which the offset is being calculated.
688
+ * @param {Array<number>} gaps - An array of gap values for each column.
689
+ * @returns {number} The left offset for the column. Returns `gaps[i]` if it exists, otherwise `0`.
690
+ */
691
+ _colLeftOffset(i, gaps) {
692
+ if (gaps && gaps.length > i) {
693
+ return gaps[i];
694
+ }
695
+ return 0;
696
+ }
697
+
698
+ /**
699
+ * Retrieves the ending cell for a row span in case it exists in a specified table column.
700
+ *
701
+ * @param {Array<Array<object>>} tableBody - The table body, represented as a 2D array of cell objects.
702
+ * @param {number} rowIndex - The index of the starting row for the row span.
703
+ * @param {object} column - The column object containing row span information.
704
+ * @param {number} columnIndex - The index of the column within the row.
705
+ * @returns {object|null} The cell at the end of the row span if it exists; otherwise, `null`.
706
+ * @throws {Error} If the row span extends beyond the total row count.
707
+ */
708
+ _getRowSpanEndingCell(tableBody, rowIndex, column, columnIndex) {
709
+ if (column.rowSpan && column.rowSpan > 1) {
710
+ let endingRow = rowIndex + column.rowSpan - 1;
711
+ if (endingRow >= tableBody.length) {
712
+ throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
713
+ }
714
+ return tableBody[endingRow][columnIndex];
715
+ }
716
+ return null;
717
+ }
718
+ processRow({
719
+ marginX = [0, 0],
720
+ dontBreakRows = false,
721
+ rowsWithoutPageBreak = 0,
722
+ cells,
723
+ widths,
724
+ gaps,
725
+ tableNode,
726
+ tableBody,
727
+ rowIndex,
728
+ height
729
+ }) {
730
+ const isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
459
731
  let pageBreaks = [];
732
+ let pageBreaksByRowSpan = [];
460
733
  let positions = [];
461
- this.writer.addListener('pageChanged', storePageBreakData);
462
- widths = widths || columns;
463
- this.writer.context().beginColumnGroup();
464
- for (let i = 0, l = columns.length; i < l; i++) {
465
- let column = columns[i];
734
+ let willBreakByHeight = false;
735
+ widths = widths || cells;
736
+
737
+ // Check if row should break by height
738
+ if (!isUnbreakableRow && height > this.writer.context().availableHeight) {
739
+ willBreakByHeight = true;
740
+ }
741
+
742
+ // Use the marginX if we are in a top level table/column (not nested)
743
+ const marginXParent = this.nestedLevel === 1 ? marginX : null;
744
+ const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
745
+ this.writer.context().beginColumnGroup(marginXParent, _bottomByPage);
746
+ for (let i = 0, l = cells.length; i < l; i++) {
747
+ let cell = cells[i];
748
+
749
+ // Page change handler
750
+ const storePageBreakClosure = data => {
751
+ const startsRowSpan = cell.rowSpan && cell.rowSpan > 1;
752
+ if (startsRowSpan) {
753
+ data.rowSpan = cell.rowSpan;
754
+ }
755
+ data.rowIndex = rowIndex;
756
+ this._storePageBreakData(data, startsRowSpan, pageBreaks, tableNode);
757
+ };
758
+ this.writer.addListener('pageChanged', storePageBreakClosure);
466
759
  let width = widths[i]._calcWidth;
467
- let leftOffset = colLeftOffset(i);
468
- if (column.colSpan && column.colSpan > 1) {
469
- for (let j = 1; j < column.colSpan; j++) {
760
+ let leftOffset = this._colLeftOffset(i, gaps);
761
+ // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
762
+ let startingSpanCell = this._findStartingRowSpanCell(cells, i);
763
+ if (cell.colSpan && cell.colSpan > 1) {
764
+ for (let j = 1; j < cell.colSpan; j++) {
470
765
  width += widths[++i]._calcWidth + gaps[i];
471
766
  }
472
767
  }
473
768
 
474
769
  // if rowspan starts in this cell, we retrieve the last cell affected by the rowspan
475
- const endingCell = getEndingCell(column, i);
476
- if (endingCell) {
770
+ const rowSpanEndingCell = this._getRowSpanEndingCell(tableBody, rowIndex, cell, i);
771
+ if (rowSpanEndingCell) {
477
772
  // We store a reference of the ending cell in the first cell of the rowspan
478
- column._endingCell = endingCell;
773
+ cell._endingCell = rowSpanEndingCell;
774
+ cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
479
775
  }
480
776
 
481
- // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
482
- let startingSpanCell = this.findStartingSpanCell(columns, i);
483
- let endingSpanCell = null;
777
+ // If we are after a cell that started a rowspan
778
+ let endOfRowSpanCell = null;
484
779
  if (startingSpanCell && startingSpanCell._endingCell) {
485
780
  // Reference to the last cell of the rowspan
486
- endingSpanCell = startingSpanCell._endingCell;
781
+ endOfRowSpanCell = startingSpanCell._endingCell;
782
+ // Store if we are in an unbreakable block when we save the context and the originalX
783
+ if (this.writer.transactionLevel > 0) {
784
+ endOfRowSpanCell._isUnbreakableContext = true;
785
+ endOfRowSpanCell._originalXOffset = this.writer.originalX;
786
+ }
487
787
  }
488
788
 
489
789
  // We pass the endingSpanCell reference to store the context just after processing rowspan cell
490
- this.writer.context().beginColumn(width, leftOffset, endingSpanCell);
491
- if (!column._span) {
492
- this.processNode(column);
493
- addAll(positions, column.positions);
494
- } else if (column._columnEndingContext) {
790
+ this.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
791
+ if (!cell._span) {
792
+ this.processNode(cell);
793
+ this.writer.context().updateBottomByPage();
794
+ addAll(positions, cell.positions);
795
+ } else if (cell._columnEndingContext) {
796
+ let discountY = 0;
797
+ if (dontBreakRows) {
798
+ // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
799
+ const ctxBeforeRowSpanLastRow = this.writer.contextStack[this.writer.contextStack.length - 1];
800
+ discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
801
+ }
802
+ let originalXOffset = 0;
803
+ // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
804
+ // We have to sum the originalX (X before starting unbreakable block) to X
805
+ if (cell._isUnbreakableContext && !this.writer.transactionLevel) {
806
+ originalXOffset = cell._originalXOffset;
807
+ }
495
808
  // row-span ending
496
809
  // Recover the context after processing the rowspanned cell
497
- this.writer.context().markEnding(column);
810
+ this.writer.context().markEnding(cell, originalXOffset, discountY);
498
811
  }
812
+ this.writer.removeListener('pageChanged', storePageBreakClosure);
499
813
  }
500
814
 
501
815
  // Check if last cell is part of a span
502
816
  let endingSpanCell = null;
503
- const lastColumn = columns.length > 0 ? columns[columns.length - 1] : null;
817
+ const lastColumn = cells.length > 0 ? cells[cells.length - 1] : null;
504
818
  if (lastColumn) {
505
819
  // Previous column cell has a rowspan
506
820
  if (lastColumn._endingCell) {
@@ -508,35 +822,35 @@ class LayoutBuilder {
508
822
  // Previous column cell is part of a span
509
823
  } else if (lastColumn._span === true) {
510
824
  // We get the cell that started the span where we set a reference to the ending cell
511
- const startingSpanCell = this.findStartingSpanCell(columns, columns.length);
825
+ const startingSpanCell = this._findStartingRowSpanCell(cells, cells.length);
512
826
  if (startingSpanCell) {
513
827
  // Context will be stored here (ending cell)
514
828
  endingSpanCell = startingSpanCell._endingCell;
829
+ // Store if we are in an unbreakable block when we save the context and the originalX
830
+ if (this.writer.transactionLevel > 0) {
831
+ endingSpanCell._isUnbreakableContext = true;
832
+ endingSpanCell._originalXOffset = this.writer.originalX;
833
+ }
515
834
  }
516
835
  }
517
836
  }
518
- this.writer.context().completeColumnGroup(height, endingSpanCell);
519
- this.writer.removeListener('pageChanged', storePageBreakData);
837
+
838
+ // If content did not break page, check if we should break by height
839
+ if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
840
+ this.writer.context().moveDown(this.writer.context().availableHeight);
841
+ this.writer.moveToNextPage();
842
+ }
843
+ const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
844
+ if (tableNode) {
845
+ tableNode._bottomByPage = bottomByPage;
846
+ // If there are page breaks in this row, update data with prevY of last cell
847
+ this._updatePageBreaksData(pageBreaks, tableNode, rowIndex);
848
+ }
520
849
  return {
850
+ pageBreaksBySpan: pageBreaksByRowSpan,
521
851
  pageBreaks: pageBreaks,
522
852
  positions: positions
523
853
  };
524
- function colLeftOffset(i) {
525
- if (gaps && gaps.length > i) {
526
- return gaps[i];
527
- }
528
- return 0;
529
- }
530
- function getEndingCell(column, columnIndex) {
531
- if (column.rowSpan && column.rowSpan > 1) {
532
- let endingRow = tableRow + column.rowSpan - 1;
533
- if (endingRow >= tableBody.length) {
534
- throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
535
- }
536
- return tableBody[endingRow][columnIndex];
537
- }
538
- return null;
539
- }
540
854
  }
541
855
 
542
856
  // lists
@@ -576,10 +890,20 @@ class LayoutBuilder {
576
890
 
577
891
  // tables
578
892
  processTable(tableNode) {
893
+ this.nestedLevel++;
579
894
  let processor = new _TableProcessor.default(tableNode);
580
895
  processor.beginTable(this.writer);
581
896
  let rowHeights = tableNode.table.heights;
582
897
  for (let i = 0, l = tableNode.table.body.length; i < l; i++) {
898
+ // if dontBreakRows and row starts a rowspan
899
+ // we store the 'y' of the beginning of each rowSpan
900
+ if (processor.dontBreakRows) {
901
+ tableNode.table.body[i].forEach(cell => {
902
+ if (cell.rowSpan && cell.rowSpan > 1) {
903
+ cell._startingRowSpanY = this.writer.context().y;
904
+ }
905
+ });
906
+ }
583
907
  processor.beginRow(i, this.writer);
584
908
  let height;
585
909
  if (typeof rowHeights === 'function') {
@@ -592,11 +916,35 @@ class LayoutBuilder {
592
916
  if (height === 'auto') {
593
917
  height = undefined;
594
918
  }
595
- let result = this.processRow(tableNode.table.body[i], tableNode.table.widths, tableNode._offsets.offsets, tableNode.table.body, i, height);
919
+ const pageBeforeProcessing = this.writer.context().page;
920
+ let result = this.processRow({
921
+ marginX: tableNode._margin ? [tableNode._margin[0], tableNode._margin[2]] : [0, 0],
922
+ dontBreakRows: processor.dontBreakRows,
923
+ rowsWithoutPageBreak: processor.rowsWithoutPageBreak,
924
+ cells: tableNode.table.body[i],
925
+ widths: tableNode.table.widths,
926
+ gaps: tableNode._offsets.offsets,
927
+ tableBody: tableNode.table.body,
928
+ tableNode,
929
+ rowIndex: i,
930
+ height
931
+ });
596
932
  addAll(tableNode.positions, result.positions);
933
+ if (!result.pageBreaks || result.pageBreaks.length === 0) {
934
+ const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
935
+ const breakBySpanData = this._findSameRowPageBreakByRowSpanData(breaksBySpan, pageBeforeProcessing, i);
936
+ if (breakBySpanData) {
937
+ const finalBreakBySpanData = this._getPageBreakListBySpan(tableNode, breakBySpanData.prevPage, i);
938
+ result.pageBreaks.push(finalBreakBySpanData);
939
+ }
940
+ }
597
941
  processor.endRow(i, this.writer, result.pageBreaks);
598
942
  }
599
943
  processor.endTable(this.writer);
944
+ this.nestedLevel--;
945
+ if (this.nestedLevel === 0) {
946
+ this.writer.context().resetMarginXTopParent();
947
+ }
600
948
  }
601
949
 
602
950
  // leafs (texts)
@@ -654,6 +1002,23 @@ class LayoutBuilder {
654
1002
  }
655
1003
  return newInline;
656
1004
  }
1005
+ function findMaxFitLength(text, maxWidth, measureFn) {
1006
+ let low = 1;
1007
+ let high = text.length;
1008
+ let bestFit = 1;
1009
+ while (low <= high) {
1010
+ const mid = Math.floor((low + high) / 2);
1011
+ const part = text.substring(0, mid);
1012
+ const width = measureFn(part);
1013
+ if (width <= maxWidth) {
1014
+ bestFit = mid;
1015
+ low = mid + 1;
1016
+ } else {
1017
+ high = mid - 1;
1018
+ }
1019
+ }
1020
+ return bestFit;
1021
+ }
657
1022
  if (!textNode._inlines || textNode._inlines.length === 0) {
658
1023
  return null;
659
1024
  }
@@ -665,11 +1030,7 @@ class LayoutBuilder {
665
1030
  let inline = textNode._inlines.shift();
666
1031
  isForceContinue = false;
667
1032
  if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
668
- let widthPerChar = inline.width / inline.text.length;
669
- let maxChars = Math.floor(line.getAvailableWidth() / widthPerChar);
670
- if (maxChars < 1) {
671
- maxChars = 1;
672
- }
1033
+ let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), txt => textInlines.widthOfText(txt, inline));
673
1034
  if (maxChars < inline.text.length) {
674
1035
  let newInline = cloneInline(inline);
675
1036
  newInline.text = inline.text.substr(maxChars);