pdfmake 0.3.2 → 0.3.4

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 (94) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -21
  3. package/README.md +75 -78
  4. package/build/fonts/Roboto/Roboto-Italic.ttf +0 -0
  5. package/build/fonts/Roboto/Roboto-Medium.ttf +0 -0
  6. package/build/fonts/Roboto/Roboto-MediumItalic.ttf +0 -0
  7. package/build/fonts/Roboto/Roboto-Regular.ttf +0 -0
  8. package/build/fonts/Roboto.js +27 -0
  9. package/build/pdfmake.js +64813 -64584
  10. package/build/pdfmake.js.map +1 -1
  11. package/build/pdfmake.min.js +2 -2
  12. package/build/pdfmake.min.js.map +1 -1
  13. package/build/standard-fonts/Courier.js +27 -27
  14. package/build/standard-fonts/Helvetica.js +27 -27
  15. package/build/standard-fonts/Symbol.js +21 -21
  16. package/build/standard-fonts/Times.js +27 -27
  17. package/build/standard-fonts/ZapfDingbats.js +21 -21
  18. package/build/vfs_fonts.js +5 -5
  19. package/build-vfs.js +44 -44
  20. package/fonts/Roboto.js +8 -8
  21. package/js/3rd-party/svg-to-pdfkit/source.js +1 -1
  22. package/js/DocMeasure.js +11 -6
  23. package/js/DocumentContext.js +8 -3
  24. package/js/ElementWriter.js +42 -16
  25. package/js/LayoutBuilder.js +144 -78
  26. package/js/Line.js +16 -16
  27. package/js/OutputDocument.js +10 -10
  28. package/js/OutputDocumentServer.js +3 -3
  29. package/js/PDFDocument.js +3 -3
  30. package/js/PageElementWriter.js +15 -9
  31. package/js/Printer.js +28 -28
  32. package/js/Renderer.js +40 -8
  33. package/js/SVGMeasure.js +10 -10
  34. package/js/StyleContextStack.js +74 -51
  35. package/js/TableProcessor.js +14 -0
  36. package/js/TextBreaker.js +17 -17
  37. package/js/TextDecorator.js +12 -3
  38. package/js/TextInlines.js +34 -33
  39. package/js/base.js +4 -4
  40. package/js/browser-extensions/OutputDocumentBrowser.js +24 -24
  41. package/js/columnCalculator.js +2 -2
  42. package/js/helpers/node.js +47 -23
  43. package/js/helpers/variableType.js +18 -18
  44. package/js/qrEnc.js +38 -38
  45. package/js/virtual-fs.js +11 -11
  46. package/package.json +12 -12
  47. package/src/3rd-party/svg-to-pdfkit/LICENSE +9 -9
  48. package/src/3rd-party/svg-to-pdfkit/source.js +2745 -2745
  49. package/src/3rd-party/svg-to-pdfkit.js +3 -3
  50. package/src/DocMeasure.js +745 -738
  51. package/src/DocPreprocessor.js +283 -283
  52. package/src/DocumentContext.js +345 -338
  53. package/src/ElementWriter.js +441 -417
  54. package/src/LayoutBuilder.js +1336 -1258
  55. package/src/Line.js +114 -114
  56. package/src/OutputDocument.js +64 -64
  57. package/src/OutputDocumentServer.js +32 -32
  58. package/src/PDFDocument.js +174 -174
  59. package/src/PageElementWriter.js +187 -179
  60. package/src/PageSize.js +53 -53
  61. package/src/Printer.js +306 -306
  62. package/src/Renderer.js +445 -409
  63. package/src/SVGMeasure.js +109 -109
  64. package/src/StyleContextStack.js +208 -179
  65. package/src/TableProcessor.js +620 -602
  66. package/src/TextBreaker.js +168 -168
  67. package/src/TextDecorator.js +175 -161
  68. package/src/TextInlines.js +224 -223
  69. package/src/URLResolver.js +43 -43
  70. package/src/base.js +70 -70
  71. package/src/browser-extensions/OutputDocumentBrowser.js +80 -80
  72. package/src/browser-extensions/fonts/Roboto.js +27 -27
  73. package/src/browser-extensions/index.js +55 -55
  74. package/src/browser-extensions/pdfMake.js +1 -1
  75. package/src/browser-extensions/standard-fonts/Courier.js +27 -27
  76. package/src/browser-extensions/standard-fonts/Helvetica.js +27 -27
  77. package/src/browser-extensions/standard-fonts/Symbol.js +21 -21
  78. package/src/browser-extensions/standard-fonts/Times.js +27 -27
  79. package/src/browser-extensions/standard-fonts/ZapfDingbats.js +21 -21
  80. package/src/browser-extensions/virtual-fs-cjs.js +1 -1
  81. package/src/columnCalculator.js +154 -154
  82. package/src/helpers/node.js +134 -110
  83. package/src/helpers/tools.js +44 -44
  84. package/src/helpers/variableType.js +50 -50
  85. package/src/index.js +16 -16
  86. package/src/qrEnc.js +796 -796
  87. package/src/standardPageSizes.js +52 -52
  88. package/src/tableLayouts.js +100 -100
  89. package/src/virtual-fs.js +66 -66
  90. package/standard-fonts/Courier.js +8 -8
  91. package/standard-fonts/Helvetica.js +8 -8
  92. package/standard-fonts/Symbol.js +5 -5
  93. package/standard-fonts/Times.js +8 -8
  94. package/standard-fonts/ZapfDingbats.js +5 -5
@@ -1,1258 +1,1336 @@
1
- import DocPreprocessor from './DocPreprocessor';
2
- import DocMeasure from './DocMeasure';
3
- import DocumentContext from './DocumentContext';
4
- import PageElementWriter from './PageElementWriter';
5
- import ColumnCalculator from './columnCalculator';
6
- import TableProcessor from './TableProcessor';
7
- import Line from './Line';
8
- import { isString, isValue, isNumber } from './helpers/variableType';
9
- import { stringifyNode, getNodeId } from './helpers/node';
10
- import { pack, offsetVector, convertToDynamicContent } from './helpers/tools';
11
- import TextInlines from './TextInlines';
12
- import StyleContextStack from './StyleContextStack';
13
-
14
- function addAll(target, otherArray) {
15
- otherArray.forEach(item => {
16
- target.push(item);
17
- });
18
- }
19
-
20
- /**
21
- * Layout engine which turns document-definition-object into a set of pages, lines, inlines
22
- * and vectors ready to be rendered into a PDF
23
- */
24
- class LayoutBuilder {
25
- /**
26
- * @param {object} pageSize - an object defining page width and height
27
- * @param {object} pageMargins - an object defining top, left, right and bottom margins
28
- * @param {object} svgMeasure
29
- */
30
- constructor(pageSize, pageMargins, svgMeasure) {
31
- this.pageSize = pageSize;
32
- this.pageMargins = pageMargins;
33
- this.svgMeasure = svgMeasure;
34
- this.tableLayouts = {};
35
- this.nestedLevel = 0;
36
- }
37
-
38
- registerTableLayouts(tableLayouts) {
39
- this.tableLayouts = pack(this.tableLayouts, tableLayouts);
40
- }
41
-
42
- /**
43
- * Executes layout engine on document-definition-object and creates an array of pages
44
- * containing positioned Blocks, Lines and inlines
45
- *
46
- * @param {object} docStructure document-definition-object
47
- * @param {object} pdfDocument pdfkit document
48
- * @param {object} styleDictionary dictionary with style definitions
49
- * @param {object} defaultStyle default style definition
50
- * @param {object} background
51
- * @param {object} header
52
- * @param {object} footer
53
- * @param {object} watermark
54
- * @param {object} pageBreakBeforeFct
55
- * @returns {Array} an array of pages
56
- */
57
- layoutDocument(
58
- docStructure,
59
- pdfDocument,
60
- styleDictionary,
61
- defaultStyle,
62
- background,
63
- header,
64
- footer,
65
- watermark,
66
- pageBreakBeforeFct
67
- ) {
68
-
69
- function addPageBreaksIfNecessary(linearNodeList, pages) {
70
-
71
- if (typeof pageBreakBeforeFct !== 'function') {
72
- return false;
73
- }
74
-
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);
86
-
87
- linearNodeList.forEach(node => {
88
- let nodeInfo = {};
89
- [
90
- 'id', 'text', 'ul', 'ol', 'table', 'image', 'qr', 'canvas', 'svg', 'columns',
91
- 'headlineLevel', 'style', 'pageBreak', 'pageOrientation',
92
- 'width', 'height'
93
- ].forEach(key => {
94
- if (node[key] !== undefined) {
95
- nodeInfo[key] = node[key];
96
- }
97
- });
98
- nodeInfo.startPosition = node.positions[0];
99
- nodeInfo.pageNumbers = Array.from(new Set(node.positions.map(node => node.pageNumber)));
100
- nodeInfo.pages = pages.length;
101
- nodeInfo.stack = Array.isArray(node.stack);
102
-
103
- node.nodeInfo = nodeInfo;
104
- });
105
-
106
- for (let index = 0; index < linearNodeList.length; index++) {
107
- let node = linearNodeList[index];
108
- if (node.pageBreak !== 'before' && !node.pageBreakCalculated) {
109
- node.pageBreakCalculated = true;
110
- let pageNumber = node.nodeInfo.pageNumbers[0];
111
-
112
- if (
113
- pageBreakBeforeFct(node.nodeInfo, {
114
- getFollowingNodesOnPage: () => {
115
- let followingNodesOnPage = [];
116
- for (let ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
117
- if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
118
- followingNodesOnPage.push(linearNodeList[ii].nodeInfo);
119
- }
120
- }
121
- return followingNodesOnPage;
122
- },
123
- getNodesOnNextPage: () => {
124
- let nodesOnNextPage = [];
125
- for (let ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
126
- if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber + 1) > -1) {
127
- nodesOnNextPage.push(linearNodeList[ii].nodeInfo);
128
- }
129
- }
130
- return nodesOnNextPage;
131
- },
132
- getPreviousNodesOnPage: () => {
133
- let previousNodesOnPage = [];
134
- for (let ii = 0; ii < index; ii++) {
135
- if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
136
- previousNodesOnPage.push(linearNodeList[ii].nodeInfo);
137
- }
138
- }
139
- return previousNodesOnPage;
140
- },
141
- })
142
- ) {
143
- node.pageBreak = 'before';
144
- return true;
145
- }
146
- }
147
- }
148
-
149
- return false;
150
- }
151
-
152
- this.docPreprocessor = new DocPreprocessor();
153
- this.docMeasure = new DocMeasure(pdfDocument, styleDictionary, defaultStyle, this.svgMeasure, this.tableLayouts);
154
-
155
- function resetXYs(result) {
156
- result.linearNodeList.forEach(node => {
157
- node.resetXY();
158
- });
159
- }
160
-
161
- let result = this.tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark);
162
- while (addPageBreaksIfNecessary(result.linearNodeList, result.pages)) {
163
- resetXYs(result);
164
- result = this.tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark);
165
- }
166
-
167
- return result.pages;
168
- }
169
-
170
- tryLayoutDocument(
171
- docStructure,
172
- pdfDocument,
173
- styleDictionary,
174
- defaultStyle,
175
- background,
176
- header,
177
- footer,
178
- watermark
179
- ) {
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
-
191
- this.linearNodeList = [];
192
- docStructure = this.docPreprocessor.preprocessDocument(docStructure);
193
- docStructure = this.docMeasure.measureDocument(docStructure);
194
-
195
- this.writer = new PageElementWriter(new DocumentContext());
196
-
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);
204
- });
205
-
206
- if (isNecessaryAddFirstPage(docStructure)) {
207
- this.writer.addPage(
208
- this.pageSize,
209
- null,
210
- this.pageMargins
211
- );
212
- }
213
-
214
- this.processNode(docStructure);
215
- this.addHeadersAndFooters(header, footer);
216
- this.addWatermark(watermark, pdfDocument, defaultStyle);
217
-
218
- return { pages: this.writer.context().pages, linearNodeList: this.linearNodeList };
219
- }
220
-
221
- addBackground(background) {
222
- let backgroundGetter = typeof background === 'function' ? background : () => background;
223
-
224
- let context = this.writer.context();
225
- let pageSize = context.getCurrentPage().pageSize;
226
-
227
- let pageBackground = backgroundGetter(context.page + 1, pageSize);
228
-
229
- if (pageBackground) {
230
- this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height);
231
- pageBackground = this.docPreprocessor.preprocessBlock(pageBackground);
232
- this.processNode(this.docMeasure.measureBlock(pageBackground));
233
- this.writer.commitUnbreakableBlock(0, 0);
234
- context.backgroundLength[context.page] += pageBackground.positions.length;
235
- }
236
- }
237
-
238
- addDynamicRepeatable(nodeGetter, sizeFunction, customPropertyName) {
239
- let pages = this.writer.context().pages;
240
-
241
- for (let pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) {
242
- this.writer.context().page = pageIndex;
243
-
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);
256
-
257
- if (node) {
258
- let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.writer.context().getCurrentPage().pageMargins);
259
- this.writer.beginUnbreakableBlock(sizes.width, sizes.height);
260
- node = this.docPreprocessor.preprocessBlock(node);
261
- this.processNode(this.docMeasure.measureBlock(node));
262
- this.writer.commitUnbreakableBlock(sizes.x, sizes.y);
263
- }
264
- }
265
- }
266
-
267
- addHeadersAndFooters(header, footer) {
268
- const headerSizeFct = (pageSize, pageMargins) => ({
269
- x: 0,
270
- y: 0,
271
- width: pageSize.width,
272
- height: pageMargins.top
273
- });
274
-
275
- const footerSizeFct = (pageSize, pageMargins) => ({
276
- x: 0,
277
- y: pageSize.height - pageMargins.bottom,
278
- width: pageSize.width,
279
- height: pageMargins.bottom
280
- });
281
-
282
- this.addDynamicRepeatable(header, headerSizeFct, 'header');
283
- this.addDynamicRepeatable(footer, footerSizeFct, 'footer');
284
- }
285
-
286
- addWatermark(watermark, pdfDocument, defaultStyle) {
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
- }
293
-
294
- if (pageWatermark === undefined || pageWatermark === null) {
295
- continue;
296
- }
297
-
298
- if (isString(pageWatermark)) {
299
- pageWatermark = { 'text': pageWatermark };
300
- }
301
-
302
- if (!pageWatermark.text) { // empty watermark text
303
- continue;
304
- }
305
-
306
- pages[i].watermark = getWatermarkObject({ ...pageWatermark }, pages[i].pageSize, pdfDocument, defaultStyle);
307
- }
308
-
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
- }
321
-
322
- if (watermark.fontSize === 'auto') {
323
- watermark.fontSize = getWatermarkFontSize(pageSize, watermark, pdfDocument);
324
- }
325
-
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;
338
- }
339
-
340
- function getWatermarkSize(watermark, pdfDocument) {
341
- let textInlines = new TextInlines(pdfDocument);
342
- let styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
343
-
344
- styleContextStack.push({
345
- fontSize: watermark.fontSize
346
- });
347
-
348
- let size = textInlines.sizeOfText(watermark.text, styleContextStack);
349
- let rotatedSize = textInlines.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
350
-
351
- return { size: size, rotatedSize: rotatedSize };
352
- }
353
-
354
- function getWatermarkFontSize(pageSize, watermark, pdfDocument) {
355
- let textInlines = new TextInlines(pdfDocument);
356
- let styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
357
- let rotatedSize;
358
-
359
- /**
360
- * Binary search the best font size.
361
- * Initial bounds [0, 1000]
362
- * Break when range < 1
363
- */
364
- let a = 0;
365
- let b = 1000;
366
- let c = (a + b) / 2;
367
- while (Math.abs(a - b) > 1) {
368
- styleContextStack.push({
369
- fontSize: c
370
- });
371
- rotatedSize = textInlines.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
372
-
373
- if (rotatedSize.width > pageSize.width) {
374
- b = c;
375
- c = (a + b) / 2;
376
- } else if (rotatedSize.width < pageSize.width) {
377
- if (rotatedSize.height > pageSize.height) {
378
- b = c;
379
- c = (a + b) / 2;
380
- } else {
381
- a = c;
382
- c = (a + b) / 2;
383
- }
384
- }
385
- styleContextStack.pop();
386
- }
387
- /*
388
- End binary search
389
- */
390
- return c;
391
- }
392
- }
393
-
394
- processNode(node) {
395
- const applyMargins = callback => {
396
- let margin = node._margin;
397
-
398
- if (node.pageBreak === 'before') {
399
- this.writer.moveToNextPage(node.pageOrientation);
400
- } else if (node.pageBreak === 'beforeOdd') {
401
- this.writer.moveToNextPage(node.pageOrientation);
402
- if ((this.writer.context().page + 1) % 2 === 1) {
403
- this.writer.moveToNextPage(node.pageOrientation);
404
- }
405
- } else if (node.pageBreak === 'beforeEven') {
406
- this.writer.moveToNextPage(node.pageOrientation);
407
- if ((this.writer.context().page + 1) % 2 === 0) {
408
- this.writer.moveToNextPage(node.pageOrientation);
409
- }
410
- }
411
-
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
433
- this.writer.context().addMargin(margin[0], margin[2]);
434
- }
435
- callback();
436
-
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
455
- this.writer.context().addMargin(-margin[0], -margin[2]);
456
- }
457
-
458
- if (node.pageBreak === 'after') {
459
- this.writer.moveToNextPage(node.pageOrientation);
460
- } else if (node.pageBreak === 'afterOdd') {
461
- this.writer.moveToNextPage(node.pageOrientation);
462
- if ((this.writer.context().page + 1) % 2 === 1) {
463
- this.writer.moveToNextPage(node.pageOrientation);
464
- }
465
- } else if (node.pageBreak === 'afterEven') {
466
- this.writer.moveToNextPage(node.pageOrientation);
467
- if ((this.writer.context().page + 1) % 2 === 0) {
468
- this.writer.moveToNextPage(node.pageOrientation);
469
- }
470
- }
471
- };
472
-
473
- this.linearNodeList.push(node);
474
- decorateNode(node);
475
-
476
- applyMargins(() => {
477
- let unbreakable = node.unbreakable;
478
- if (unbreakable) {
479
- this.writer.beginUnbreakableBlock();
480
- }
481
-
482
- let absPosition = node.absolutePosition;
483
- if (absPosition) {
484
- this.writer.context().beginDetachedBlock();
485
- this.writer.context().moveTo(absPosition.x || 0, absPosition.y || 0);
486
- }
487
-
488
- let relPosition = node.relativePosition;
489
- if (relPosition) {
490
- this.writer.context().beginDetachedBlock();
491
- this.writer.context().moveToRelative(relPosition.x || 0, relPosition.y || 0);
492
- }
493
-
494
- if (node.stack) {
495
- this.processVerticalContainer(node);
496
- } else if (node.section) {
497
- this.processSection(node);
498
- } else if (node.columns) {
499
- this.processColumns(node);
500
- } else if (node.ul) {
501
- this.processList(false, node);
502
- } else if (node.ol) {
503
- this.processList(true, node);
504
- } else if (node.table) {
505
- this.processTable(node);
506
- } else if (node.text !== undefined) {
507
- this.processLeaf(node);
508
- } else if (node.toc) {
509
- this.processToc(node);
510
- } else if (node.image) {
511
- this.processImage(node);
512
- } else if (node.svg) {
513
- this.processSVG(node);
514
- } else if (node.canvas) {
515
- this.processCanvas(node);
516
- } else if (node.qr) {
517
- this.processQr(node);
518
- } else if (node.attachment) {
519
- this.processAttachment(node);
520
- } else if (!node._span) {
521
- throw new Error(`Unrecognized document structure: ${stringifyNode(node)}`);
522
- }
523
-
524
- if (absPosition || relPosition) {
525
- this.writer.context().endDetachedBlock();
526
- }
527
-
528
- if (unbreakable) {
529
- this.writer.commitUnbreakableBlock();
530
- }
531
- });
532
- }
533
-
534
- // vertical container
535
- processVerticalContainer(node) {
536
- node.stack.forEach(item => {
537
- this.processNode(item);
538
- addAll(node.positions, item.positions);
539
-
540
- //TODO: paragraph gap
541
- }, this);
542
- }
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
-
613
- // columns
614
- processColumns(columnNode) {
615
- this.nestedLevel++;
616
- let columns = columnNode.columns;
617
- let availableWidth = this.writer.context().availableWidth;
618
- let gaps = gapArray(columnNode._gap);
619
-
620
- if (gaps) {
621
- availableWidth -= (gaps.length - 1) * columnNode._gap;
622
- }
623
-
624
- ColumnCalculator.buildColumnWidths(columns, availableWidth);
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
- });
631
- addAll(columnNode.positions, result.positions);
632
- this.nestedLevel--;
633
- if (this.nestedLevel === 0) {
634
- this.writer.context().resetMarginXTopParent();
635
- }
636
- function gapArray(gap) {
637
- if (!gap) {
638
- return null;
639
- }
640
-
641
- let gaps = [];
642
- gaps.push(0);
643
-
644
- for (let i = columns.length - 1; i > 0; i--) {
645
- gaps.push(gap);
646
- }
647
-
648
- return gaps;
649
- }
650
- }
651
-
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);
723
-
724
- if (breaksBySpanList && breaksBySpanList.length > 0) {
725
- breaksBySpanList.forEach(b => {
726
- b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
727
- });
728
- }
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
- };
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);
751
- if (!pageDesc) {
752
- pageDesc = {
753
- ...data
754
- };
755
- pageBreaks.push(pageDesc);
756
- }
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
- }
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;
821
- let pageBreaks = [];
822
- let pageBreaksByRowSpan = [];
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);
836
-
837
- for (let i = 0, l = cells.length; i < l; i++) {
838
- let cell = cells[i];
839
-
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);
851
-
852
- let width = widths[i]._calcWidth;
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);
856
-
857
- if (cell.colSpan && cell.colSpan > 1) {
858
- for (let j = 1; j < cell.colSpan; j++) {
859
- width += widths[++i]._calcWidth + gaps[i];
860
- }
861
- }
862
-
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;
869
- }
870
-
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
- }
882
-
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);
906
- }
907
- this.writer.removeListener('pageChanged', storePageBreakClosure);
908
- }
909
-
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
- }
929
- }
930
- }
931
- }
932
-
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);
945
- }
946
-
947
- return {
948
- pageBreaksBySpan: pageBreaksByRowSpan,
949
- pageBreaks: pageBreaks,
950
- positions: positions
951
- };
952
- }
953
-
954
- // lists
955
- processList(orderedList, node) {
956
- const addMarkerToFirstLeaf = line => {
957
- // I'm not very happy with the way list processing is implemented
958
- // (both code and algorithm should be rethinked)
959
- if (nextMarker) {
960
- let marker = nextMarker;
961
- nextMarker = null;
962
-
963
- if (marker.canvas) {
964
- let vector = marker.canvas[0];
965
-
966
- offsetVector(vector, -marker._minWidth, 0);
967
- this.writer.addVector(vector);
968
- } else if (marker._inlines) {
969
- let markerLine = new Line(this.pageSize.width);
970
- markerLine.addInline(marker._inlines[0]);
971
- markerLine.x = -marker._minWidth;
972
- markerLine.y = line.getAscenderHeight() - markerLine.getAscenderHeight();
973
- this.writer.addLine(markerLine, true);
974
- }
975
- }
976
- };
977
-
978
- let items = orderedList ? node.ol : node.ul;
979
- let gapSize = node._gapSize;
980
-
981
- this.writer.context().addMargin(gapSize.width);
982
-
983
- let nextMarker;
984
-
985
- this.writer.addListener('lineAdded', addMarkerToFirstLeaf);
986
-
987
- items.forEach(item => {
988
- nextMarker = item.listMarker;
989
- this.processNode(item);
990
- addAll(node.positions, item.positions);
991
- });
992
-
993
- this.writer.removeListener('lineAdded', addMarkerToFirstLeaf);
994
-
995
- this.writer.context().addMargin(-gapSize.width);
996
- }
997
-
998
- // tables
999
- processTable(tableNode) {
1000
- this.nestedLevel++;
1001
- let processor = new TableProcessor(tableNode);
1002
-
1003
- processor.beginTable(this.writer);
1004
-
1005
- let rowHeights = tableNode.table.heights;
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
-
1017
- processor.beginRow(i, this.writer);
1018
-
1019
- let height;
1020
- if (typeof rowHeights === 'function') {
1021
- height = rowHeights(i);
1022
- } else if (Array.isArray(rowHeights)) {
1023
- height = rowHeights[i];
1024
- } else {
1025
- height = rowHeights;
1026
- }
1027
-
1028
- if (height === 'auto') {
1029
- height = undefined;
1030
- }
1031
-
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
-
1047
- addAll(tableNode.positions, result.positions);
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
-
1058
- processor.endRow(i, this.writer, result.pageBreaks);
1059
- }
1060
-
1061
- processor.endTable(this.writer);
1062
- this.nestedLevel--;
1063
- if (this.nestedLevel === 0) {
1064
- this.writer.context().resetMarginXTopParent();
1065
- }
1066
- }
1067
-
1068
- // leafs (texts)
1069
- processLeaf(node) {
1070
- let line = this.buildNextLine(node);
1071
- if (line && (node.tocItem || node.id)) {
1072
- line._node = node;
1073
- }
1074
- let currentHeight = (line) ? line.getHeight() : 0;
1075
- let maxHeight = node.maxHeight || -1;
1076
-
1077
- if (line) {
1078
- let nodeId = getNodeId(node);
1079
- if (nodeId) {
1080
- line.id = nodeId;
1081
- }
1082
- }
1083
-
1084
- if (node._tocItemRef) {
1085
- line._pageNodeRef = node._tocItemRef;
1086
- }
1087
-
1088
- if (node._pageRef) {
1089
- line._pageNodeRef = node._pageRef._nodeRef;
1090
- }
1091
-
1092
- if (line && line.inlines && Array.isArray(line.inlines)) {
1093
- for (let i = 0, l = line.inlines.length; i < l; i++) {
1094
- if (line.inlines[i]._tocItemRef) {
1095
- line.inlines[i]._pageNodeRef = line.inlines[i]._tocItemRef;
1096
- }
1097
-
1098
- if (line.inlines[i]._pageRef) {
1099
- line.inlines[i]._pageNodeRef = line.inlines[i]._pageRef._nodeRef;
1100
- }
1101
- }
1102
- }
1103
-
1104
- while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
1105
- let positions = this.writer.addLine(line);
1106
- node.positions.push(positions);
1107
- line = this.buildNextLine(node);
1108
- if (line) {
1109
- currentHeight += line.getHeight();
1110
- }
1111
- }
1112
- }
1113
-
1114
- processToc(node) {
1115
- if (node.toc.title) {
1116
- this.processNode(node.toc.title);
1117
- }
1118
- if (node.toc._table) {
1119
- this.processNode(node.toc._table);
1120
- }
1121
- }
1122
-
1123
- buildNextLine(textNode) {
1124
-
1125
- function cloneInline(inline) {
1126
- let newInline = inline.constructor();
1127
- for (let key in inline) {
1128
- newInline[key] = inline[key];
1129
- }
1130
- return newInline;
1131
- }
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
-
1154
- if (!textNode._inlines || textNode._inlines.length === 0) {
1155
- return null;
1156
- }
1157
-
1158
- let line = new Line(this.writer.context().availableWidth);
1159
- const textInlines = new TextInlines(null);
1160
-
1161
- let isForceContinue = false;
1162
- while (textNode._inlines && textNode._inlines.length > 0 &&
1163
- (line.hasEnoughSpaceForInline(textNode._inlines[0], textNode._inlines.slice(1)) || isForceContinue)) {
1164
- let isHardWrap = false;
1165
- let inline = textNode._inlines.shift();
1166
- isForceContinue = false;
1167
-
1168
- if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
1169
- let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), (txt) =>
1170
- textInlines.widthOfText(txt, inline)
1171
- );
1172
- if (maxChars < inline.text.length) {
1173
- let newInline = cloneInline(inline);
1174
-
1175
- newInline.text = inline.text.substr(maxChars);
1176
- inline.text = inline.text.substr(0, maxChars);
1177
-
1178
- newInline.width = textInlines.widthOfText(newInline.text, newInline);
1179
- inline.width = textInlines.widthOfText(inline.text, inline);
1180
-
1181
- textNode._inlines.unshift(newInline);
1182
- isHardWrap = true;
1183
- }
1184
- }
1185
-
1186
- line.addInline(inline);
1187
-
1188
- isForceContinue = inline.noNewLine && !isHardWrap;
1189
- }
1190
-
1191
- line.lastLineInParagraph = textNode._inlines.length === 0;
1192
-
1193
- return line;
1194
- }
1195
-
1196
- // images
1197
- processImage(node) {
1198
- let position = this.writer.addImage(node);
1199
- node.positions.push(position);
1200
- }
1201
-
1202
- processCanvas(node) {
1203
- let positions = this.writer.addCanvas(node);
1204
- addAll(node.positions, positions);
1205
- }
1206
-
1207
- processSVG(node) {
1208
- let position = this.writer.addSVG(node);
1209
- node.positions.push(position);
1210
- }
1211
-
1212
- processQr(node) {
1213
- let position = this.writer.addQr(node);
1214
- node.positions.push(position);
1215
- }
1216
-
1217
- processAttachment(node) {
1218
- let position = this.writer.addAttachment(node);
1219
- node.positions.push(position);
1220
- }
1221
- }
1222
-
1223
- function decorateNode(node) {
1224
- let x = node.x;
1225
- let y = node.y;
1226
- node.positions = [];
1227
-
1228
- if (Array.isArray(node.canvas)) {
1229
- node.canvas.forEach(vector => {
1230
- let x = vector.x;
1231
- let y = vector.y;
1232
- let x1 = vector.x1;
1233
- let y1 = vector.y1;
1234
- let x2 = vector.x2;
1235
- let y2 = vector.y2;
1236
- vector.resetXY = () => {
1237
- vector.x = x;
1238
- vector.y = y;
1239
- vector.x1 = x1;
1240
- vector.y1 = y1;
1241
- vector.x2 = x2;
1242
- vector.y2 = y2;
1243
- };
1244
- });
1245
- }
1246
-
1247
- node.resetXY = () => {
1248
- node.x = x;
1249
- node.y = y;
1250
- if (Array.isArray(node.canvas)) {
1251
- node.canvas.forEach(vector => {
1252
- vector.resetXY();
1253
- });
1254
- }
1255
- };
1256
- }
1257
-
1258
- export default LayoutBuilder;
1
+ import DocPreprocessor from './DocPreprocessor';
2
+ import DocMeasure from './DocMeasure';
3
+ import DocumentContext from './DocumentContext';
4
+ import PageElementWriter from './PageElementWriter';
5
+ import ColumnCalculator from './columnCalculator';
6
+ import TableProcessor from './TableProcessor';
7
+ import Line from './Line';
8
+ import { isString, isValue, isNumber } from './helpers/variableType';
9
+ import { stringifyNode, getNodeId } from './helpers/node';
10
+ import { pack, offsetVector, convertToDynamicContent } from './helpers/tools';
11
+ import TextInlines from './TextInlines';
12
+ import StyleContextStack from './StyleContextStack';
13
+
14
+ function addAll(target, otherArray) {
15
+ otherArray.forEach(item => {
16
+ target.push(item);
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Layout engine which turns document-definition-object into a set of pages, lines, inlines
22
+ * and vectors ready to be rendered into a PDF
23
+ */
24
+ class LayoutBuilder {
25
+ /**
26
+ * @param {object} pageSize - an object defining page width and height
27
+ * @param {object} pageMargins - an object defining top, left, right and bottom margins
28
+ * @param {object} svgMeasure
29
+ */
30
+ constructor(pageSize, pageMargins, svgMeasure) {
31
+ this.pageSize = pageSize;
32
+ this.pageMargins = pageMargins;
33
+ this.svgMeasure = svgMeasure;
34
+ this.tableLayouts = {};
35
+ this.nestedLevel = 0;
36
+ this.verticalAlignmentItemStack = [];
37
+ }
38
+
39
+ registerTableLayouts(tableLayouts) {
40
+ this.tableLayouts = pack(this.tableLayouts, tableLayouts);
41
+ }
42
+
43
+ /**
44
+ * Executes layout engine on document-definition-object and creates an array of pages
45
+ * containing positioned Blocks, Lines and inlines
46
+ *
47
+ * @param {object} docStructure document-definition-object
48
+ * @param {object} pdfDocument pdfkit document
49
+ * @param {object} styleDictionary dictionary with style definitions
50
+ * @param {object} defaultStyle default style definition
51
+ * @param {object} background
52
+ * @param {object} header
53
+ * @param {object} footer
54
+ * @param {object} watermark
55
+ * @param {object} pageBreakBeforeFct
56
+ * @returns {Array} an array of pages
57
+ */
58
+ layoutDocument(
59
+ docStructure,
60
+ pdfDocument,
61
+ styleDictionary,
62
+ defaultStyle,
63
+ background,
64
+ header,
65
+ footer,
66
+ watermark,
67
+ pageBreakBeforeFct
68
+ ) {
69
+
70
+ function addPageBreaksIfNecessary(linearNodeList, pages) {
71
+
72
+ if (typeof pageBreakBeforeFct !== 'function') {
73
+ return false;
74
+ }
75
+
76
+ const hasRenderableContent = node => {
77
+ if (!node || node.positions.length === 0) {
78
+ return false;
79
+ }
80
+ if (node.text === '' && !node.listMarker) {
81
+ return false;
82
+ }
83
+ return true;
84
+ };
85
+
86
+ linearNodeList = linearNodeList.filter(hasRenderableContent);
87
+
88
+ linearNodeList.forEach(node => {
89
+ let nodeInfo = {};
90
+ [
91
+ 'id', 'text', 'ul', 'ol', 'table', 'image', 'qr', 'canvas', 'svg', 'columns',
92
+ 'headlineLevel', 'style', 'pageBreak', 'pageOrientation',
93
+ 'width', 'height'
94
+ ].forEach(key => {
95
+ if (node[key] !== undefined) {
96
+ nodeInfo[key] = node[key];
97
+ }
98
+ });
99
+ nodeInfo.startPosition = node.positions[0];
100
+ nodeInfo.pageNumbers = Array.from(new Set(node.positions.map(node => node.pageNumber)));
101
+ nodeInfo.pages = pages.length;
102
+ nodeInfo.stack = Array.isArray(node.stack);
103
+
104
+ node.nodeInfo = nodeInfo;
105
+ });
106
+
107
+ for (let index = 0; index < linearNodeList.length; index++) {
108
+ let node = linearNodeList[index];
109
+ if (node.pageBreak !== 'before' && !node.pageBreakCalculated) {
110
+ node.pageBreakCalculated = true;
111
+ let pageNumber = node.nodeInfo.pageNumbers[0];
112
+
113
+ if (
114
+ pageBreakBeforeFct(node.nodeInfo, {
115
+ getFollowingNodesOnPage: () => {
116
+ let followingNodesOnPage = [];
117
+ for (let ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
118
+ if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
119
+ followingNodesOnPage.push(linearNodeList[ii].nodeInfo);
120
+ }
121
+ }
122
+ return followingNodesOnPage;
123
+ },
124
+ getNodesOnNextPage: () => {
125
+ let nodesOnNextPage = [];
126
+ for (let ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
127
+ if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber + 1) > -1) {
128
+ nodesOnNextPage.push(linearNodeList[ii].nodeInfo);
129
+ }
130
+ }
131
+ return nodesOnNextPage;
132
+ },
133
+ getPreviousNodesOnPage: () => {
134
+ let previousNodesOnPage = [];
135
+ for (let ii = 0; ii < index; ii++) {
136
+ if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
137
+ previousNodesOnPage.push(linearNodeList[ii].nodeInfo);
138
+ }
139
+ }
140
+ return previousNodesOnPage;
141
+ },
142
+ })
143
+ ) {
144
+ node.pageBreak = 'before';
145
+ return true;
146
+ }
147
+ }
148
+ }
149
+
150
+ return false;
151
+ }
152
+
153
+ this.docPreprocessor = new DocPreprocessor();
154
+ this.docMeasure = new DocMeasure(pdfDocument, styleDictionary, defaultStyle, this.svgMeasure, this.tableLayouts);
155
+
156
+ function resetXYs(result) {
157
+ result.linearNodeList.forEach(node => {
158
+ node.resetXY();
159
+ });
160
+ }
161
+
162
+ let result = this.tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark);
163
+ while (addPageBreaksIfNecessary(result.linearNodeList, result.pages)) {
164
+ resetXYs(result);
165
+ result = this.tryLayoutDocument(docStructure, pdfDocument, styleDictionary, defaultStyle, background, header, footer, watermark);
166
+ }
167
+
168
+ return result.pages;
169
+ }
170
+
171
+ tryLayoutDocument(
172
+ docStructure,
173
+ pdfDocument,
174
+ styleDictionary,
175
+ defaultStyle,
176
+ background,
177
+ header,
178
+ footer,
179
+ watermark
180
+ ) {
181
+
182
+ const isNecessaryAddFirstPage = (docStructure) => {
183
+ if (docStructure.stack && docStructure.stack.length > 0 && docStructure.stack[0].section) {
184
+ return false;
185
+ } else if (docStructure.section) {
186
+ return false;
187
+ }
188
+
189
+ return true;
190
+ };
191
+
192
+ this.linearNodeList = [];
193
+ docStructure = this.docPreprocessor.preprocessDocument(docStructure);
194
+ docStructure = this.docMeasure.measureDocument(docStructure);
195
+
196
+ this.writer = new PageElementWriter(new DocumentContext());
197
+
198
+ this.writer.context().addListener('pageAdded', (page) => {
199
+ let backgroundGetter = background;
200
+ if (page.customProperties['background'] || page.customProperties['background'] === null) {
201
+ backgroundGetter = page.customProperties['background'];
202
+ }
203
+
204
+ this.addBackground(backgroundGetter);
205
+ });
206
+
207
+ if (isNecessaryAddFirstPage(docStructure)) {
208
+ this.writer.addPage(
209
+ this.pageSize,
210
+ null,
211
+ this.pageMargins
212
+ );
213
+ }
214
+
215
+ this.processNode(docStructure);
216
+ this.addHeadersAndFooters(header, footer);
217
+ this.addWatermark(watermark, pdfDocument, defaultStyle);
218
+
219
+ return { pages: this.writer.context().pages, linearNodeList: this.linearNodeList };
220
+ }
221
+
222
+ addBackground(background) {
223
+ let backgroundGetter = typeof background === 'function' ? background : () => background;
224
+
225
+ let context = this.writer.context();
226
+ let pageSize = context.getCurrentPage().pageSize;
227
+
228
+ let pageBackground = backgroundGetter(context.page + 1, pageSize);
229
+
230
+ if (pageBackground) {
231
+ this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height);
232
+ pageBackground = this.docPreprocessor.preprocessBlock(pageBackground);
233
+ this.processNode(this.docMeasure.measureBlock(pageBackground));
234
+ this.writer.commitUnbreakableBlock(0, 0);
235
+ context.backgroundLength[context.page] += pageBackground.positions.length;
236
+ }
237
+ }
238
+
239
+ addDynamicRepeatable(nodeGetter, sizeFunction, customPropertyName) {
240
+ let pages = this.writer.context().pages;
241
+
242
+ for (let pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) {
243
+ this.writer.context().page = pageIndex;
244
+
245
+ let customProperties = this.writer.context().getCurrentPage().customProperties;
246
+
247
+ let pageNodeGetter = nodeGetter;
248
+ if (customProperties[customPropertyName] || customProperties[customPropertyName] === null) {
249
+ pageNodeGetter = customProperties[customPropertyName];
250
+ }
251
+
252
+ if ((typeof pageNodeGetter === 'undefined') || (pageNodeGetter === null)) {
253
+ continue;
254
+ }
255
+
256
+ let node = pageNodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
257
+
258
+ if (node) {
259
+ let sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.writer.context().getCurrentPage().pageMargins);
260
+ this.writer.beginUnbreakableBlock(sizes.width, sizes.height);
261
+ node = this.docPreprocessor.preprocessBlock(node);
262
+ this.processNode(this.docMeasure.measureBlock(node));
263
+ this.writer.commitUnbreakableBlock(sizes.x, sizes.y);
264
+ }
265
+ }
266
+ }
267
+
268
+ addHeadersAndFooters(header, footer) {
269
+ const headerSizeFct = (pageSize, pageMargins) => ({
270
+ x: 0,
271
+ y: 0,
272
+ width: pageSize.width,
273
+ height: pageMargins.top
274
+ });
275
+
276
+ const footerSizeFct = (pageSize, pageMargins) => ({
277
+ x: 0,
278
+ y: pageSize.height - pageMargins.bottom,
279
+ width: pageSize.width,
280
+ height: pageMargins.bottom
281
+ });
282
+
283
+ this.addDynamicRepeatable(header, headerSizeFct, 'header');
284
+ this.addDynamicRepeatable(footer, footerSizeFct, 'footer');
285
+ }
286
+
287
+ addWatermark(watermark, pdfDocument, defaultStyle) {
288
+ let pages = this.writer.context().pages;
289
+ for (let i = 0, l = pages.length; i < l; i++) {
290
+ let pageWatermark = watermark;
291
+ if (pages[i].customProperties['watermark'] || pages[i].customProperties['watermark'] === null) {
292
+ pageWatermark = pages[i].customProperties['watermark'];
293
+ }
294
+
295
+ if (pageWatermark === undefined || pageWatermark === null) {
296
+ continue;
297
+ }
298
+
299
+ if (isString(pageWatermark)) {
300
+ pageWatermark = { 'text': pageWatermark };
301
+ }
302
+
303
+ if (!pageWatermark.text) { // empty watermark text
304
+ continue;
305
+ }
306
+
307
+ pages[i].watermark = getWatermarkObject({ ...pageWatermark }, pages[i].pageSize, pdfDocument, defaultStyle);
308
+ }
309
+
310
+ function getWatermarkObject(watermark, pageSize, pdfDocument, defaultStyle) {
311
+ watermark.font = watermark.font || defaultStyle.font || 'Roboto';
312
+ watermark.fontSize = watermark.fontSize || 'auto';
313
+ watermark.color = watermark.color || 'black';
314
+ watermark.opacity = isNumber(watermark.opacity) ? watermark.opacity : 0.6;
315
+ watermark.bold = watermark.bold || false;
316
+ watermark.italics = watermark.italics || false;
317
+ watermark.angle = isValue(watermark.angle) ? watermark.angle : null;
318
+
319
+ if (watermark.angle === null) {
320
+ watermark.angle = Math.atan2(pageSize.height, pageSize.width) * -180 / Math.PI;
321
+ }
322
+
323
+ if (watermark.fontSize === 'auto') {
324
+ watermark.fontSize = getWatermarkFontSize(pageSize, watermark, pdfDocument);
325
+ }
326
+
327
+ let watermarkObject = {
328
+ text: watermark.text,
329
+ font: pdfDocument.provideFont(watermark.font, watermark.bold, watermark.italics),
330
+ fontSize: watermark.fontSize,
331
+ color: watermark.color,
332
+ opacity: watermark.opacity,
333
+ angle: watermark.angle
334
+ };
335
+
336
+ watermarkObject._size = getWatermarkSize(watermark, pdfDocument);
337
+
338
+ return watermarkObject;
339
+ }
340
+
341
+ function getWatermarkSize(watermark, pdfDocument) {
342
+ let textInlines = new TextInlines(pdfDocument);
343
+ let styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
344
+
345
+ styleContextStack.push({
346
+ fontSize: watermark.fontSize
347
+ });
348
+
349
+ let size = textInlines.sizeOfText(watermark.text, styleContextStack);
350
+ let rotatedSize = textInlines.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
351
+
352
+ return { size: size, rotatedSize: rotatedSize };
353
+ }
354
+
355
+ function getWatermarkFontSize(pageSize, watermark, pdfDocument) {
356
+ let textInlines = new TextInlines(pdfDocument);
357
+ let styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
358
+ let rotatedSize;
359
+
360
+ /**
361
+ * Binary search the best font size.
362
+ * Initial bounds [0, 1000]
363
+ * Break when range < 1
364
+ */
365
+ let a = 0;
366
+ let b = 1000;
367
+ let c = (a + b) / 2;
368
+ while (Math.abs(a - b) > 1) {
369
+ styleContextStack.push({
370
+ fontSize: c
371
+ });
372
+ rotatedSize = textInlines.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
373
+
374
+ if (rotatedSize.width > pageSize.width) {
375
+ b = c;
376
+ c = (a + b) / 2;
377
+ } else if (rotatedSize.width < pageSize.width) {
378
+ if (rotatedSize.height > pageSize.height) {
379
+ b = c;
380
+ c = (a + b) / 2;
381
+ } else {
382
+ a = c;
383
+ c = (a + b) / 2;
384
+ }
385
+ }
386
+ styleContextStack.pop();
387
+ }
388
+ /*
389
+ End binary search
390
+ */
391
+ return c;
392
+ }
393
+ }
394
+
395
+ processNode(node, isVerticalAlignmentAllowed = false) {
396
+ const applyMargins = callback => {
397
+ let margin = node._margin;
398
+
399
+ if (node.pageBreak === 'before') {
400
+ this.writer.moveToNextPage(node.pageOrientation);
401
+ } else if (node.pageBreak === 'beforeOdd') {
402
+ this.writer.moveToNextPage(node.pageOrientation);
403
+ if ((this.writer.context().page + 1) % 2 === 1) {
404
+ this.writer.moveToNextPage(node.pageOrientation);
405
+ }
406
+ } else if (node.pageBreak === 'beforeEven') {
407
+ this.writer.moveToNextPage(node.pageOrientation);
408
+ if ((this.writer.context().page + 1) % 2 === 0) {
409
+ this.writer.moveToNextPage(node.pageOrientation);
410
+ }
411
+ }
412
+
413
+ const isDetachedBlock = node.relativePosition || node.absolutePosition;
414
+
415
+ // Detached nodes have no margins, their position is only determined by 'x' and 'y'
416
+ if (margin && !isDetachedBlock) {
417
+ const availableHeight = this.writer.context().availableHeight;
418
+ // If top margin is bigger than available space, move to next page
419
+ // Necessary for nodes inside tables
420
+ if (availableHeight - margin[1] < 0) {
421
+ // Consume the whole available space
422
+ this.writer.context().moveDown(availableHeight);
423
+ this.writer.moveToNextPage(node.pageOrientation);
424
+ /**
425
+ * TODO - Something to consider:
426
+ * Right now the node starts at the top of next page (after header)
427
+ * Another option would be to apply just the top margin that has not been consumed in the page before
428
+ * It would something like: this.write.context().moveDown(margin[1] - availableHeight)
429
+ */
430
+ } else {
431
+ this.writer.context().moveDown(margin[1]);
432
+ }
433
+ // Apply lateral margins
434
+ this.writer.context().addMargin(margin[0], margin[2]);
435
+ }
436
+ callback();
437
+
438
+ // Detached nodes have no margins, their position is only determined by 'x' and 'y'
439
+ if (margin && !isDetachedBlock) {
440
+ const availableHeight = this.writer.context().availableHeight;
441
+ // If bottom margin is bigger than available space, move to next page
442
+ // Necessary for nodes inside tables
443
+ if (availableHeight - margin[3] < 0) {
444
+ this.writer.context().moveDown(availableHeight);
445
+ this.writer.moveToNextPage(node.pageOrientation);
446
+ /**
447
+ * TODO - Something to consider:
448
+ * Right now next node starts at the top of next page (after header)
449
+ * Another option would be to apply the bottom margin that has not been consumed in the next page?
450
+ * It would something like: this.write.context().moveDown(margin[3] - availableHeight)
451
+ */
452
+ } else {
453
+ this.writer.context().moveDown(margin[3]);
454
+ }
455
+ // Apply lateral margins
456
+ this.writer.context().addMargin(-margin[0], -margin[2]);
457
+ }
458
+
459
+ if (node.pageBreak === 'after') {
460
+ this.writer.moveToNextPage(node.pageOrientation);
461
+ } else if (node.pageBreak === 'afterOdd') {
462
+ this.writer.moveToNextPage(node.pageOrientation);
463
+ if ((this.writer.context().page + 1) % 2 === 1) {
464
+ this.writer.moveToNextPage(node.pageOrientation);
465
+ }
466
+ } else if (node.pageBreak === 'afterEven') {
467
+ this.writer.moveToNextPage(node.pageOrientation);
468
+ if ((this.writer.context().page + 1) % 2 === 0) {
469
+ this.writer.moveToNextPage(node.pageOrientation);
470
+ }
471
+ }
472
+ };
473
+
474
+ this.linearNodeList.push(node);
475
+ decorateNode(node);
476
+
477
+ if (this.writer.context().getCurrentPage() !== null) {
478
+ var prevTop = this.writer.context().getCurrentPosition().top;
479
+ }
480
+
481
+ applyMargins(() => {
482
+ let verticalAlignment = node.verticalAlignment;
483
+ if (isVerticalAlignmentAllowed && verticalAlignment) {
484
+ var verticalAlignmentBegin = this.writer.beginVerticalAlignment(verticalAlignment);
485
+ }
486
+
487
+ let unbreakable = node.unbreakable;
488
+ if (unbreakable) {
489
+ this.writer.beginUnbreakableBlock();
490
+ }
491
+
492
+ let absPosition = node.absolutePosition;
493
+ if (absPosition) {
494
+ this.writer.context().beginDetachedBlock();
495
+ this.writer.context().moveTo(absPosition.x || 0, absPosition.y || 0);
496
+ }
497
+
498
+ let relPosition = node.relativePosition;
499
+ if (relPosition) {
500
+ this.writer.context().beginDetachedBlock();
501
+ this.writer.context().moveToRelative(relPosition.x || 0, relPosition.y || 0);
502
+ }
503
+
504
+ if (node.stack) {
505
+ this.processVerticalContainer(node);
506
+ } else if (node.section) {
507
+ this.processSection(node);
508
+ } else if (node.columns) {
509
+ this.processColumns(node);
510
+ } else if (node.ul) {
511
+ this.processList(false, node);
512
+ } else if (node.ol) {
513
+ this.processList(true, node);
514
+ } else if (node.table) {
515
+ this.processTable(node);
516
+ } else if (node.text !== undefined) {
517
+ this.processLeaf(node);
518
+ } else if (node.toc) {
519
+ this.processToc(node);
520
+ } else if (node.image) {
521
+ this.processImage(node);
522
+ } else if (node.svg) {
523
+ this.processSVG(node);
524
+ } else if (node.canvas) {
525
+ this.processCanvas(node);
526
+ } else if (node.qr) {
527
+ this.processQr(node);
528
+ } else if (node.attachment) {
529
+ this.processAttachment(node);
530
+ } else if (!node._span) {
531
+ throw new Error(`Unrecognized document structure: ${stringifyNode(node)}`);
532
+ }
533
+
534
+ if (absPosition || relPosition) {
535
+ this.writer.context().endDetachedBlock();
536
+ }
537
+
538
+ if (unbreakable) {
539
+ this.writer.commitUnbreakableBlock();
540
+ }
541
+
542
+ if (isVerticalAlignmentAllowed && verticalAlignment) {
543
+ this.verticalAlignmentItemStack.push({
544
+ begin: verticalAlignmentBegin,
545
+ end: this.writer.endVerticalAlignment(verticalAlignment)
546
+ });
547
+ }
548
+ });
549
+
550
+ if (prevTop !== undefined) {
551
+ // TODO: for vertical alignment and does not work (at least) when page break in node
552
+ node.__height = this.writer.context().getCurrentPosition().top - prevTop;
553
+ }
554
+ }
555
+
556
+ // vertical container
557
+ processVerticalContainer(node) {
558
+ node.stack.forEach(item => {
559
+ this.processNode(item);
560
+ addAll(node.positions, item.positions);
561
+
562
+ //TODO: paragraph gap
563
+ }, this);
564
+ }
565
+
566
+ // section
567
+ processSection(sectionNode) {
568
+ // TODO: properties
569
+
570
+ let page = this.writer.context().getCurrentPage();
571
+ if (!page || (page && page.items.length)) { // move to new empty page
572
+ // page definition inherit from current page
573
+ if (sectionNode.pageSize === 'inherit') {
574
+ sectionNode.pageSize = page ? { width: page.pageSize.width, height: page.pageSize.height } : undefined;
575
+ }
576
+ if (sectionNode.pageOrientation === 'inherit') {
577
+ sectionNode.pageOrientation = page ? page.pageSize.orientation : undefined;
578
+ }
579
+ if (sectionNode.pageMargins === 'inherit') {
580
+ sectionNode.pageMargins = page ? page.pageMargins : undefined;
581
+ }
582
+
583
+ if (sectionNode.header === 'inherit') {
584
+ sectionNode.header = page ? page.customProperties.header : undefined;
585
+ }
586
+
587
+ if (sectionNode.footer === 'inherit') {
588
+ sectionNode.footer = page ? page.customProperties.footer : undefined;
589
+ }
590
+
591
+ if (sectionNode.background === 'inherit') {
592
+ sectionNode.background = page ? page.customProperties.background : undefined;
593
+ }
594
+
595
+ if (sectionNode.watermark === 'inherit') {
596
+ sectionNode.watermark = page ? page.customProperties.watermark : undefined;
597
+ }
598
+
599
+ if (sectionNode.header && typeof sectionNode.header !== 'function' && sectionNode.header !== null) {
600
+ sectionNode.header = convertToDynamicContent(sectionNode.header);
601
+ }
602
+
603
+ if (sectionNode.footer && typeof sectionNode.footer !== 'function' && sectionNode.footer !== null) {
604
+ sectionNode.footer = convertToDynamicContent(sectionNode.footer);
605
+ }
606
+
607
+ let customProperties = {};
608
+ if (typeof sectionNode.header !== 'undefined') {
609
+ customProperties.header = sectionNode.header;
610
+ }
611
+
612
+ if (typeof sectionNode.footer !== 'undefined') {
613
+ customProperties.footer = sectionNode.footer;
614
+ }
615
+
616
+ if (typeof sectionNode.background !== 'undefined') {
617
+ customProperties.background = sectionNode.background;
618
+ }
619
+
620
+ if (typeof sectionNode.watermark !== 'undefined') {
621
+ customProperties.watermark = sectionNode.watermark;
622
+ }
623
+
624
+ this.writer.addPage(
625
+ sectionNode.pageSize || this.pageSize,
626
+ sectionNode.pageOrientation,
627
+ sectionNode.pageMargins || this.pageMargins,
628
+ customProperties
629
+ );
630
+ }
631
+
632
+ this.processNode(sectionNode.section);
633
+ }
634
+
635
+ // columns
636
+ processColumns(columnNode) {
637
+ this.nestedLevel++;
638
+ let columns = columnNode.columns;
639
+ let availableWidth = this.writer.context().availableWidth;
640
+ let gaps = gapArray(columnNode._gap);
641
+
642
+ if (gaps) {
643
+ availableWidth -= (gaps.length - 1) * columnNode._gap;
644
+ }
645
+
646
+ ColumnCalculator.buildColumnWidths(columns, availableWidth);
647
+ let result = this.processRow({
648
+ marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
649
+ cells: columns,
650
+ widths: columns,
651
+ gaps
652
+ });
653
+ addAll(columnNode.positions, result.positions);
654
+ this.nestedLevel--;
655
+ if (this.nestedLevel === 0) {
656
+ this.writer.context().resetMarginXTopParent();
657
+ }
658
+ function gapArray(gap) {
659
+ if (!gap) {
660
+ return null;
661
+ }
662
+
663
+ let gaps = [];
664
+ gaps.push(0);
665
+
666
+ for (let i = columns.length - 1; i > 0; i--) {
667
+ gaps.push(gap);
668
+ }
669
+
670
+ return gaps;
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Searches for a cell in the same row that starts a rowspan and is positioned immediately before the current cell.
676
+ * Alternatively, it finds a cell where the colspan initiating the rowspan extends to the cell just before the current one.
677
+ *
678
+ * @param {Array<object>} arr - An array representing cells in a row.
679
+ * @param {number} i - The index of the current cell to search backward from.
680
+ * @returns {object|null} The starting cell of the rowspan if found; otherwise, `null`.
681
+ */
682
+ _findStartingRowSpanCell(arr, i) {
683
+ let requiredColspan = 1;
684
+ for (let index = i - 1; index >= 0; index--) {
685
+ if (!arr[index]._span) {
686
+ if (arr[index].rowSpan > 1 && (arr[index].colSpan || 1) === requiredColspan) {
687
+ return arr[index];
688
+ } else {
689
+ return null;
690
+ }
691
+ }
692
+ requiredColspan++;
693
+ }
694
+ return null;
695
+ }
696
+
697
+ /**
698
+ * Retrieves a page break description for a specified page from a list of page breaks.
699
+ *
700
+ * @param {Array<object>} pageBreaks - An array of page break descriptions, each containing `prevPage` properties.
701
+ * @param {number} page - The page number to find the associated page break for.
702
+ * @returns {object|undefined} The page break description object for the specified page if found; otherwise, `undefined`.
703
+ */
704
+ _getPageBreak(pageBreaks, page) {
705
+ return pageBreaks.find(desc => desc.prevPage === page);
706
+ }
707
+
708
+ _getPageBreakListBySpan(tableNode, page, rowIndex) {
709
+ if (!tableNode || !tableNode._breaksBySpan) {
710
+ return null;
711
+ }
712
+ const breaksList = tableNode._breaksBySpan.filter(desc => desc.prevPage === page && rowIndex <= desc.rowIndexOfSpanEnd);
713
+
714
+ let y = Number.MAX_VALUE,
715
+ prevY = Number.MIN_VALUE;
716
+
717
+ breaksList.forEach(b => {
718
+ prevY = Math.max(b.prevY, prevY);
719
+ y = Math.min(b.y, y);
720
+ });
721
+
722
+ return {
723
+ prevPage: page,
724
+ prevY: prevY,
725
+ y: y
726
+ };
727
+ }
728
+
729
+ _findSameRowPageBreakByRowSpanData(breaksBySpan, page, rowIndex) {
730
+ if (!breaksBySpan) {
731
+ return null;
732
+ }
733
+ return breaksBySpan.find(desc => desc.prevPage === page && rowIndex === desc.rowIndexOfSpanEnd);
734
+ }
735
+
736
+ _updatePageBreaksData(pageBreaks, tableNode, rowIndex) {
737
+ Object.keys(tableNode._bottomByPage).forEach(p => {
738
+ const page = Number(p);
739
+ const pageBreak = this._getPageBreak(pageBreaks, page);
740
+ if (pageBreak) {
741
+ pageBreak.prevY = Math.max(pageBreak.prevY, tableNode._bottomByPage[page]);
742
+ }
743
+ if (tableNode._breaksBySpan && tableNode._breaksBySpan.length > 0) {
744
+ const breaksBySpanList = tableNode._breaksBySpan.filter(pb => pb.prevPage === page && rowIndex <= pb.rowIndexOfSpanEnd);
745
+
746
+ if (breaksBySpanList && breaksBySpanList.length > 0) {
747
+ breaksBySpanList.forEach(b => {
748
+ b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
749
+ });
750
+ }
751
+ }
752
+ });
753
+ }
754
+
755
+ /**
756
+ * Resolves the Y-coordinates for a target object by comparing two break points.
757
+ *
758
+ * @param {object} break1 - The first break point with `prevY` and `y` properties.
759
+ * @param {object} break2 - The second break point with `prevY` and `y` properties.
760
+ * @param {object} target - The target object to be updated with resolved Y-coordinates.
761
+ * @property {number} target.prevY - Updated to the maximum `prevY` value between `break1` and `break2`.
762
+ * @property {number} target.y - Updated to the minimum `y` value between `break1` and `break2`.
763
+ */
764
+ _resolveBreakY(break1, break2, target) {
765
+ target.prevY = Math.max(break1.prevY, break2.prevY);
766
+ target.y = Math.min(break1.y, break2.y);
767
+ };
768
+
769
+ _storePageBreakData(data, startsRowSpan, pageBreaks, tableNode) {
770
+ if (!startsRowSpan) {
771
+ let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
772
+ let pageDescBySpan = this._getPageBreakListBySpan(tableNode, data.prevPage, data.rowIndex);
773
+ if (!pageDesc) {
774
+ pageDesc = {
775
+ ...data
776
+ };
777
+ pageBreaks.push(pageDesc);
778
+ }
779
+ if (pageDescBySpan) {
780
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
781
+ }
782
+ this._resolveBreakY(pageDesc, data, pageDesc);
783
+ } else {
784
+ const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
785
+ let pageDescBySpan = this._findSameRowPageBreakByRowSpanData(breaksBySpan, data.prevPage, data.rowIndex);
786
+ if (!pageDescBySpan) {
787
+ pageDescBySpan = {
788
+ ...data,
789
+ rowIndexOfSpanEnd: data.rowIndex + data.rowSpan - 1
790
+ };
791
+ if (!tableNode._breaksBySpan) {
792
+ tableNode._breaksBySpan = [];
793
+ }
794
+ tableNode._breaksBySpan.push(pageDescBySpan);
795
+ }
796
+ pageDescBySpan.prevY = Math.max(pageDescBySpan.prevY, data.prevY);
797
+ pageDescBySpan.y = Math.min(pageDescBySpan.y, data.y);
798
+ let pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
799
+ if (pageDesc) {
800
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
801
+ }
802
+ }
803
+ };
804
+
805
+ /**
806
+ * Calculates the left offset for a column based on the specified gap values.
807
+ *
808
+ * @param {number} i - The index of the column for which the offset is being calculated.
809
+ * @param {Array<number>} gaps - An array of gap values for each column.
810
+ * @returns {number} The left offset for the column. Returns `gaps[i]` if it exists, otherwise `0`.
811
+ */
812
+ _colLeftOffset(i, gaps) {
813
+ if (gaps && gaps.length > i) {
814
+ return gaps[i];
815
+ }
816
+ return 0;
817
+ }
818
+
819
+ /**
820
+ * Retrieves the ending cell for a row span in case it exists in a specified table column.
821
+ *
822
+ * @param {Array<Array<object>>} tableBody - The table body, represented as a 2D array of cell objects.
823
+ * @param {number} rowIndex - The index of the starting row for the row span.
824
+ * @param {object} column - The column object containing row span information.
825
+ * @param {number} columnIndex - The index of the column within the row.
826
+ * @returns {object|null} The cell at the end of the row span if it exists; otherwise, `null`.
827
+ * @throws {Error} If the row span extends beyond the total row count.
828
+ */
829
+ _getRowSpanEndingCell(tableBody, rowIndex, column, columnIndex) {
830
+ if (column.rowSpan && column.rowSpan > 1) {
831
+ let endingRow = rowIndex + column.rowSpan - 1;
832
+ if (endingRow >= tableBody.length) {
833
+ throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
834
+ }
835
+ return tableBody[endingRow][columnIndex];
836
+ }
837
+
838
+ return null;
839
+ }
840
+
841
+ processRow({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height }) {
842
+ const isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
843
+ let pageBreaks = [];
844
+ let pageBreaksByRowSpan = [];
845
+ let positions = [];
846
+ let willBreakByHeight = false;
847
+ let verticalAlignmentCells = {};
848
+ widths = widths || cells;
849
+
850
+ // Check if row should break by height
851
+ if (!isUnbreakableRow && height > this.writer.context().availableHeight) {
852
+ willBreakByHeight = true;
853
+ }
854
+
855
+ // Use the marginX if we are in a top level table/column (not nested)
856
+ const marginXParent = this.nestedLevel === 1 ? marginX : null;
857
+ const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
858
+ this.writer.context().beginColumnGroup(marginXParent, _bottomByPage);
859
+
860
+ for (let i = 0, l = cells.length; i < l; i++) {
861
+ let cell = cells[i];
862
+ let cellIndexBegin = i;
863
+
864
+ // Page change handler
865
+ const storePageBreakClosure = data => {
866
+ const startsRowSpan = cell.rowSpan && cell.rowSpan > 1;
867
+ if (startsRowSpan) {
868
+ data.rowSpan = cell.rowSpan;
869
+ }
870
+ data.rowIndex = rowIndex;
871
+ this._storePageBreakData(data, startsRowSpan, pageBreaks, tableNode);
872
+ };
873
+
874
+ this.writer.addListener('pageChanged', storePageBreakClosure);
875
+
876
+ let width = widths[i]._calcWidth;
877
+ let leftOffset = this._colLeftOffset(i, gaps);
878
+ // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
879
+ let startingSpanCell = this._findStartingRowSpanCell(cells, i);
880
+
881
+ if (cell.colSpan && cell.colSpan > 1) {
882
+ for (let j = 1; j < cell.colSpan; j++) {
883
+ width += widths[++i]._calcWidth + gaps[i];
884
+ }
885
+ }
886
+
887
+ // if rowspan starts in this cell, we retrieve the last cell affected by the rowspan
888
+ const rowSpanRightEndingCell = this._getRowSpanEndingCell(tableBody, rowIndex, cell, i);
889
+ const rowSpanLeftEndingCell = this._getRowSpanEndingCell(tableBody, rowIndex, cell, cellIndexBegin);
890
+ if (rowSpanRightEndingCell) {
891
+ // We store a reference of the ending cell in the first cell of the rowspan
892
+ cell._endingCell = rowSpanRightEndingCell;
893
+ cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
894
+ }
895
+ if (rowSpanLeftEndingCell) {
896
+ // We store a reference of the left ending cell in the first cell of the rowspan
897
+ cell._leftEndingCell = rowSpanLeftEndingCell;
898
+ cell._leftEndingCell._startingRowSpanY = cell._startingRowSpanY;
899
+ }
900
+
901
+ // If we are after a cell that started a rowspan
902
+ let endOfRowSpanCell = null;
903
+ if (startingSpanCell && startingSpanCell._endingCell) {
904
+ // Reference to the last cell of the rowspan
905
+ endOfRowSpanCell = startingSpanCell._endingCell;
906
+ // Store if we are in an unbreakable block when we save the context and the originalX
907
+ if (this.writer.transactionLevel > 0) {
908
+ endOfRowSpanCell._isUnbreakableContext = true;
909
+ endOfRowSpanCell._originalXOffset = this.writer.originalX;
910
+ }
911
+ }
912
+
913
+ // We pass the endingSpanCell reference to store the context just after processing rowspan cell
914
+ this.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell);
915
+
916
+ if (!cell._span) {
917
+ this.processNode(cell, true);
918
+ this.writer.context().updateBottomByPage();
919
+
920
+ if (cell.verticalAlignment) {
921
+ verticalAlignmentCells[cellIndexBegin] = this.verticalAlignmentItemStack.length - 1;
922
+ }
923
+
924
+ addAll(positions, cell.positions);
925
+ } else if (cell._columnEndingContext) {
926
+ let discountY = 0;
927
+ if (dontBreakRows) {
928
+ // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
929
+ const ctxBeforeRowSpanLastRow = this.writer.contextStack[this.writer.contextStack.length - 1];
930
+ discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
931
+ }
932
+ let originalXOffset = 0;
933
+ // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
934
+ // We have to sum the originalX (X before starting unbreakable block) to X
935
+ if (cell._isUnbreakableContext && !this.writer.transactionLevel) {
936
+ originalXOffset = cell._originalXOffset;
937
+ }
938
+ // row-span ending
939
+ // Recover the context after processing the rowspanned cell
940
+ this.writer.context().markEnding(cell, originalXOffset, discountY);
941
+ }
942
+ this.writer.removeListener('pageChanged', storePageBreakClosure);
943
+ }
944
+
945
+ // Check if last cell is part of a span
946
+ let endingSpanCell = null;
947
+ const lastColumn = cells.length > 0 ? cells[cells.length - 1] : null;
948
+ if (lastColumn) {
949
+ // Previous column cell has a rowspan
950
+ if (lastColumn._endingCell) {
951
+ endingSpanCell = lastColumn._endingCell;
952
+ // Previous column cell is part of a span
953
+ } else if (lastColumn._span === true) {
954
+ // We get the cell that started the span where we set a reference to the ending cell
955
+ const startingSpanCell = this._findStartingRowSpanCell(cells, cells.length);
956
+ if (startingSpanCell) {
957
+ // Context will be stored here (ending cell)
958
+ endingSpanCell = startingSpanCell._endingCell;
959
+ // Store if we are in an unbreakable block when we save the context and the originalX
960
+ if (this.writer.transactionLevel > 0) {
961
+ endingSpanCell._isUnbreakableContext = true;
962
+ endingSpanCell._originalXOffset = this.writer.originalX;
963
+ }
964
+ }
965
+ }
966
+ }
967
+
968
+ // If content did not break page, check if we should break by height
969
+ if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
970
+ this.writer.context().moveDown(this.writer.context().availableHeight);
971
+ this.writer.moveToNextPage();
972
+ }
973
+
974
+ const bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
975
+
976
+ if (tableNode) {
977
+ tableNode._bottomByPage = bottomByPage;
978
+ // If there are page breaks in this row, update data with prevY of last cell
979
+ this._updatePageBreaksData(pageBreaks, tableNode, rowIndex);
980
+ }
981
+
982
+ let rowHeight = this.writer.context().height;
983
+ for (let i = 0, l = cells.length; i < l; i++) {
984
+ let cell = cells[i];
985
+ if (!cell._span && cell.verticalAlignment) {
986
+ let itemBegin = this.verticalAlignmentItemStack[verticalAlignmentCells[i]].begin.item;
987
+ itemBegin.viewHeight = rowHeight;
988
+ itemBegin.nodeHeight = cell.__height;
989
+ itemBegin.cell = cell;
990
+ itemBegin.bottomY = this.writer.context().y;
991
+ itemBegin.isCellContentMultiPage = !itemBegin.cell.positions.every(item => item.pageNumber === itemBegin.cell.positions[0].pageNumber);
992
+ itemBegin.getViewHeight = function() {
993
+ if (this.cell._willBreak) {
994
+ return this.cell._bottomY - this.cell._rowTopPageY;
995
+ }
996
+
997
+ if (this.cell.rowSpan && this.cell.rowSpan > 1) {
998
+ if (dontBreakRows) {
999
+ let rowTopPageY = this.cell._leftEndingCell._startingRowSpanY + this.cell._leftEndingCell._rowTopPageYPadding;
1000
+ return this.cell._leftEndingCell._rowTopPageY - rowTopPageY + this.cell._leftEndingCell._bottomY;
1001
+ } else {
1002
+ if (this.cell.positions[0].pageNumber !== this.cell._leftEndingCell._lastPageNumber) {
1003
+ return this.bottomY - this.cell._leftEndingCell._bottomY;
1004
+ }
1005
+
1006
+ return this.viewHeight + this.cell._leftEndingCell._bottomY - this.bottomY;
1007
+ }
1008
+ }
1009
+
1010
+ return this.viewHeight;
1011
+ };
1012
+ itemBegin.getNodeHeight = function() {
1013
+ return this.nodeHeight;
1014
+ };
1015
+
1016
+ let itemEnd = this.verticalAlignmentItemStack[verticalAlignmentCells[i]].end.item;
1017
+ itemEnd.isCellContentMultiPage = itemBegin.isCellContentMultiPage;
1018
+ }
1019
+ }
1020
+
1021
+ return {
1022
+ pageBreaksBySpan: pageBreaksByRowSpan,
1023
+ pageBreaks: pageBreaks,
1024
+ positions: positions
1025
+ };
1026
+ }
1027
+
1028
+ // lists
1029
+ processList(orderedList, node) {
1030
+ const addMarkerToFirstLeaf = line => {
1031
+ // I'm not very happy with the way list processing is implemented
1032
+ // (both code and algorithm should be rethinked)
1033
+ if (nextMarker) {
1034
+ let marker = nextMarker;
1035
+ nextMarker = null;
1036
+
1037
+ if (marker.canvas) {
1038
+ let vector = marker.canvas[0];
1039
+
1040
+ offsetVector(vector, -marker._minWidth, 0);
1041
+ this.writer.addVector(vector);
1042
+ } else if (marker._inlines) {
1043
+ let markerLine = new Line(this.pageSize.width);
1044
+ markerLine.addInline(marker._inlines[0]);
1045
+ markerLine.x = -marker._minWidth;
1046
+ markerLine.y = line.getAscenderHeight() - markerLine.getAscenderHeight();
1047
+ this.writer.addLine(markerLine, true);
1048
+ }
1049
+ }
1050
+ };
1051
+
1052
+ let items = orderedList ? node.ol : node.ul;
1053
+ let gapSize = node._gapSize;
1054
+
1055
+ this.writer.context().addMargin(gapSize.width);
1056
+
1057
+ let nextMarker;
1058
+
1059
+ this.writer.addListener('lineAdded', addMarkerToFirstLeaf);
1060
+
1061
+ items.forEach(item => {
1062
+ nextMarker = item.listMarker;
1063
+ this.processNode(item);
1064
+ addAll(node.positions, item.positions);
1065
+ });
1066
+
1067
+ this.writer.removeListener('lineAdded', addMarkerToFirstLeaf);
1068
+
1069
+ this.writer.context().addMargin(-gapSize.width);
1070
+ }
1071
+
1072
+ // tables
1073
+ processTable(tableNode) {
1074
+ this.nestedLevel++;
1075
+ let processor = new TableProcessor(tableNode);
1076
+
1077
+ processor.beginTable(this.writer);
1078
+
1079
+ let rowHeights = tableNode.table.heights;
1080
+ for (let i = 0, l = tableNode.table.body.length; i < l; i++) {
1081
+ // if dontBreakRows and row starts a rowspan
1082
+ // we store the 'y' of the beginning of each rowSpan
1083
+ if (processor.dontBreakRows) {
1084
+ tableNode.table.body[i].forEach(cell => {
1085
+ if (cell.rowSpan && cell.rowSpan > 1) {
1086
+ cell._startingRowSpanY = this.writer.context().y;
1087
+ }
1088
+ });
1089
+ }
1090
+
1091
+ processor.beginRow(i, this.writer);
1092
+
1093
+ let height;
1094
+ if (typeof rowHeights === 'function') {
1095
+ height = rowHeights(i);
1096
+ } else if (Array.isArray(rowHeights)) {
1097
+ height = rowHeights[i];
1098
+ } else {
1099
+ height = rowHeights;
1100
+ }
1101
+
1102
+ if (height === 'auto') {
1103
+ height = undefined;
1104
+ }
1105
+
1106
+ const pageBeforeProcessing = this.writer.context().page;
1107
+
1108
+ let result = this.processRow({
1109
+ marginX: tableNode._margin ? [tableNode._margin[0], tableNode._margin[2]] : [0, 0],
1110
+ dontBreakRows: processor.dontBreakRows,
1111
+ rowsWithoutPageBreak: processor.rowsWithoutPageBreak,
1112
+ cells: tableNode.table.body[i],
1113
+ widths: tableNode.table.widths,
1114
+ gaps: tableNode._offsets.offsets,
1115
+ tableBody: tableNode.table.body,
1116
+ tableNode,
1117
+ rowIndex: i,
1118
+ height
1119
+ });
1120
+
1121
+ addAll(tableNode.positions, result.positions);
1122
+
1123
+ if (!result.pageBreaks || result.pageBreaks.length === 0) {
1124
+ const breaksBySpan = tableNode && tableNode._breaksBySpan || null;
1125
+ const breakBySpanData = this._findSameRowPageBreakByRowSpanData(breaksBySpan, pageBeforeProcessing, i);
1126
+ if (breakBySpanData) {
1127
+ const finalBreakBySpanData = this._getPageBreakListBySpan(tableNode, breakBySpanData.prevPage, i);
1128
+ result.pageBreaks.push(finalBreakBySpanData);
1129
+ }
1130
+ }
1131
+
1132
+ processor.endRow(i, this.writer, result.pageBreaks);
1133
+ }
1134
+
1135
+ processor.endTable(this.writer);
1136
+ this.nestedLevel--;
1137
+ if (this.nestedLevel === 0) {
1138
+ this.writer.context().resetMarginXTopParent();
1139
+ }
1140
+ }
1141
+
1142
+ // leafs (texts)
1143
+ processLeaf(node) {
1144
+ let line = this.buildNextLine(node);
1145
+ if (line && (node.tocItem || node.id)) {
1146
+ line._node = node;
1147
+ }
1148
+ let currentHeight = (line) ? line.getHeight() : 0;
1149
+ let maxHeight = node.maxHeight || -1;
1150
+
1151
+ if (line) {
1152
+ let nodeId = getNodeId(node);
1153
+ if (nodeId) {
1154
+ line.id = nodeId;
1155
+ }
1156
+ }
1157
+
1158
+ if (node._tocItemRef) {
1159
+ line._pageNodeRef = node._tocItemRef;
1160
+ }
1161
+
1162
+ if (node._pageRef) {
1163
+ line._pageNodeRef = node._pageRef._nodeRef;
1164
+ }
1165
+
1166
+ if (line && line.inlines && Array.isArray(line.inlines)) {
1167
+ for (let i = 0, l = line.inlines.length; i < l; i++) {
1168
+ if (line.inlines[i]._tocItemRef) {
1169
+ line.inlines[i]._pageNodeRef = line.inlines[i]._tocItemRef;
1170
+ }
1171
+
1172
+ if (line.inlines[i]._pageRef) {
1173
+ line.inlines[i]._pageNodeRef = line.inlines[i]._pageRef._nodeRef;
1174
+ }
1175
+ }
1176
+ }
1177
+
1178
+ while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
1179
+ let positions = this.writer.addLine(line);
1180
+ node.positions.push(positions);
1181
+ line = this.buildNextLine(node);
1182
+ if (line) {
1183
+ currentHeight += line.getHeight();
1184
+ }
1185
+ }
1186
+ }
1187
+
1188
+ processToc(node) {
1189
+ if (!node.toc._table && node.toc.hideEmpty === true) {
1190
+ return;
1191
+ }
1192
+
1193
+ if (node.toc.title) {
1194
+ this.processNode(node.toc.title);
1195
+ }
1196
+ if (node.toc._table) {
1197
+ this.processNode(node.toc._table);
1198
+ }
1199
+ }
1200
+
1201
+ buildNextLine(textNode) {
1202
+
1203
+ function cloneInline(inline) {
1204
+ let newInline = inline.constructor();
1205
+ for (let key in inline) {
1206
+ newInline[key] = inline[key];
1207
+ }
1208
+ return newInline;
1209
+ }
1210
+
1211
+ function findMaxFitLength(text, maxWidth, measureFn) {
1212
+ let low = 1;
1213
+ let high = text.length;
1214
+ let bestFit = 1;
1215
+
1216
+ while (low <= high) {
1217
+ const mid = Math.floor((low + high) / 2);
1218
+ const part = text.substring(0, mid);
1219
+ const width = measureFn(part);
1220
+
1221
+ if (width <= maxWidth) {
1222
+ bestFit = mid;
1223
+ low = mid + 1;
1224
+ } else {
1225
+ high = mid - 1;
1226
+ }
1227
+ }
1228
+
1229
+ return bestFit;
1230
+ }
1231
+
1232
+ if (!textNode._inlines || textNode._inlines.length === 0) {
1233
+ return null;
1234
+ }
1235
+
1236
+ let line = new Line(this.writer.context().availableWidth);
1237
+ const textInlines = new TextInlines(null);
1238
+
1239
+ let isForceContinue = false;
1240
+ while (textNode._inlines && textNode._inlines.length > 0 &&
1241
+ (line.hasEnoughSpaceForInline(textNode._inlines[0], textNode._inlines.slice(1)) || isForceContinue)) {
1242
+ let isHardWrap = false;
1243
+ let inline = textNode._inlines.shift();
1244
+ isForceContinue = false;
1245
+
1246
+ if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
1247
+ let maxChars = findMaxFitLength(inline.text, line.getAvailableWidth(), (txt) =>
1248
+ textInlines.widthOfText(txt, inline)
1249
+ );
1250
+ if (maxChars < inline.text.length) {
1251
+ let newInline = cloneInline(inline);
1252
+
1253
+ newInline.text = inline.text.substr(maxChars);
1254
+ inline.text = inline.text.substr(0, maxChars);
1255
+
1256
+ newInline.width = textInlines.widthOfText(newInline.text, newInline);
1257
+ inline.width = textInlines.widthOfText(inline.text, inline);
1258
+
1259
+ textNode._inlines.unshift(newInline);
1260
+ isHardWrap = true;
1261
+ }
1262
+ }
1263
+
1264
+ line.addInline(inline);
1265
+
1266
+ isForceContinue = inline.noNewLine && !isHardWrap;
1267
+ }
1268
+
1269
+ line.lastLineInParagraph = textNode._inlines.length === 0;
1270
+
1271
+ return line;
1272
+ }
1273
+
1274
+ // images
1275
+ processImage(node) {
1276
+ let position = this.writer.addImage(node);
1277
+ node.positions.push(position);
1278
+ }
1279
+
1280
+ processCanvas(node) {
1281
+ let positions = this.writer.addCanvas(node);
1282
+ addAll(node.positions, positions);
1283
+ }
1284
+
1285
+ processSVG(node) {
1286
+ let position = this.writer.addSVG(node);
1287
+ node.positions.push(position);
1288
+ }
1289
+
1290
+ processQr(node) {
1291
+ let position = this.writer.addQr(node);
1292
+ node.positions.push(position);
1293
+ }
1294
+
1295
+ processAttachment(node) {
1296
+ let position = this.writer.addAttachment(node);
1297
+ node.positions.push(position);
1298
+ }
1299
+ }
1300
+
1301
+ function decorateNode(node) {
1302
+ let x = node.x;
1303
+ let y = node.y;
1304
+ node.positions = [];
1305
+
1306
+ if (Array.isArray(node.canvas)) {
1307
+ node.canvas.forEach(vector => {
1308
+ let x = vector.x;
1309
+ let y = vector.y;
1310
+ let x1 = vector.x1;
1311
+ let y1 = vector.y1;
1312
+ let x2 = vector.x2;
1313
+ let y2 = vector.y2;
1314
+ vector.resetXY = () => {
1315
+ vector.x = x;
1316
+ vector.y = y;
1317
+ vector.x1 = x1;
1318
+ vector.y1 = y1;
1319
+ vector.x2 = x2;
1320
+ vector.y2 = y2;
1321
+ };
1322
+ });
1323
+ }
1324
+
1325
+ node.resetXY = () => {
1326
+ node.x = x;
1327
+ node.y = y;
1328
+ if (Array.isArray(node.canvas)) {
1329
+ node.canvas.forEach(vector => {
1330
+ vector.resetXY();
1331
+ });
1332
+ }
1333
+ };
1334
+ }
1335
+
1336
+ export default LayoutBuilder;