@flowaccount/pdfmake 1.0.6-staging.3 → 1.0.6

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.
@@ -1,1993 +1,2012 @@
1
- 'use strict';
2
-
3
- var cloneDeep = require('lodash/cloneDeep');
4
- var TraversalTracker = require('./traversalTracker');
5
- var DocPreprocessor = require('./docPreprocessor');
6
- var DocMeasure = require('./docMeasure');
7
- var DocumentContext = require('./documentContext');
8
- var PageElementWriter = require('./pageElementWriter');
9
- var ColumnCalculator = require('./columnCalculator');
10
- var TableProcessor = require('./tableProcessor');
11
- var Line = require('./line');
12
- var isString = require('./helpers').isString;
13
- var isArray = require('./helpers').isArray;
14
- var isUndefined = require('./helpers').isUndefined;
15
- var isNull = require('./helpers').isNull;
16
- var pack = require('./helpers').pack;
17
- var offsetVector = require('./helpers').offsetVector;
18
- var fontStringify = require('./helpers').fontStringify;
19
- var getNodeId = require('./helpers').getNodeId;
20
- var isFunction = require('./helpers').isFunction;
21
- var TextTools = require('./textTools');
22
- var StyleContextStack = require('./styleContextStack');
23
- var isNumber = require('./helpers').isNumber;
24
-
25
- var footerBreak = false;
26
- var testTracker;
27
- var testWriter;
28
- var testVerticalAlignStack = [];
29
- var testResult = false;
30
- var currentLayoutBuilder;
31
-
32
- function addAll(target, otherArray) {
33
- if (!isArray(target) || !isArray(otherArray) || otherArray.length === 0) {
34
- return;
35
- }
36
-
37
- otherArray.forEach(function (item) {
38
- target.push(item);
39
- });
40
- }
41
-
42
- /**
43
- * Creates an instance of LayoutBuilder - layout engine which turns document-definition-object
44
- * into a set of pages, lines, inlines and vectors ready to be rendered into a PDF
45
- *
46
- * @param {Object} pageSize - an object defining page width and height
47
- * @param {Object} pageMargins - an object defining top, left, right and bottom margins
48
- */
49
- function LayoutBuilder(pageSize, pageMargins, imageMeasure, svgMeasure) {
50
- this.pageSize = pageSize;
51
- this.pageMargins = pageMargins;
52
- this.tracker = new TraversalTracker();
53
- this.imageMeasure = imageMeasure;
54
- this.svgMeasure = svgMeasure;
55
- this.tableLayouts = {};
56
- this.nestedLevel = 0;
57
- this.verticalAlignItemStack = [];
58
- this.heightHeaderAndFooter = {};
59
-
60
- this._footerColumnGuides = null;
61
- this._footerGapOption = null;
62
- }
63
-
64
- LayoutBuilder.prototype.registerTableLayouts = function (tableLayouts) {
65
- this.tableLayouts = pack(this.tableLayouts, tableLayouts);
66
- };
67
-
68
- /**
69
- * Executes layout engine on document-definition-object and creates an array of pages
70
- * containing positioned Blocks, Lines and inlines
71
- *
72
- * @param {Object} docStructure document-definition-object
73
- * @param {Object} fontProvider font provider
74
- * @param {Object} styleDictionary dictionary with style definitions
75
- * @param {Object} defaultStyle default style definition
76
- * @return {Array} an array of pages
77
- */
78
- LayoutBuilder.prototype.layoutDocument = function (docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark, pageBreakBeforeFct) {
79
-
80
- function addPageBreaksIfNecessary(linearNodeList, pages) {
81
-
82
- if (!isFunction(pageBreakBeforeFct)) {
83
- return false;
84
- }
85
-
86
- linearNodeList = linearNodeList.filter(function (node) {
87
- return node.positions.length > 0;
88
- });
89
-
90
- linearNodeList.forEach(function (node) {
91
- var nodeInfo = {};
92
- [
93
- 'id', 'text', 'ul', 'ol', 'table', 'image', 'qr', 'canvas', 'svg', 'columns', 'layers',
94
- 'headlineLevel', 'style', 'pageBreak', 'pageOrientation',
95
- 'width', 'height'
96
- ].forEach(function (key) {
97
- if (node[key] !== undefined) {
98
- nodeInfo[key] = node[key];
99
- }
100
- });
101
- nodeInfo.startPosition = node.positions[0];
102
- nodeInfo.pageNumbers = Array.from(new Set(node.positions.map(function (node) { return node.pageNumber; })));
103
- nodeInfo.pages = pages.length;
104
- nodeInfo.stack = isArray(node.stack);
105
- nodeInfo.layers = isArray(node.layers);
106
-
107
- node.nodeInfo = nodeInfo;
108
- });
109
-
110
- for (var index = 0; index < linearNodeList.length; index++) {
111
- var node = linearNodeList[index];
112
- if (node.pageBreak !== 'before' && !node.pageBreakCalculated) {
113
- node.pageBreakCalculated = true;
114
- var pageNumber = node.nodeInfo.pageNumbers[0];
115
- var followingNodesOnPage = [];
116
- var nodesOnNextPage = [];
117
- var previousNodesOnPage = [];
118
- if (pageBreakBeforeFct.length > 1) {
119
- for (var ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
120
- if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
121
- followingNodesOnPage.push(linearNodeList[ii].nodeInfo);
122
- }
123
- if (pageBreakBeforeFct.length > 2 && linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber + 1) > -1) {
124
- nodesOnNextPage.push(linearNodeList[ii].nodeInfo);
125
- }
126
- }
127
- }
128
- if (pageBreakBeforeFct.length > 3) {
129
- for (var jj = 0; jj < index; jj++) {
130
- if (linearNodeList[jj].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
131
- previousNodesOnPage.push(linearNodeList[jj].nodeInfo);
132
- }
133
- }
134
- }
135
- if (pageBreakBeforeFct(node.nodeInfo, followingNodesOnPage, nodesOnNextPage, previousNodesOnPage)) {
136
- node.pageBreak = 'before';
137
- return true;
138
- }
139
- }
140
- }
141
-
142
- return false;
143
- }
144
-
145
- this.docPreprocessor = new DocPreprocessor();
146
- this.docMeasure = new DocMeasure(fontProvider, styleDictionary, defaultStyle, this.imageMeasure, this.svgMeasure, this.tableLayouts, images);
147
-
148
-
149
- function resetXYs(result) {
150
- result.linearNodeList.forEach(function (node) {
151
- node.resetXY();
152
- });
153
- }
154
-
155
- var result = this.tryLayoutDocument(docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark);
156
- while (addPageBreaksIfNecessary(result.linearNodeList, result.pages)) {
157
- resetXYs(result);
158
- result = this.tryLayoutDocument(docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark);
159
- }
160
-
161
- return result.pages;
162
- };
163
-
164
- LayoutBuilder.prototype.tryLayoutDocument = function (docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark) {
165
- footerBreak = false;
166
-
167
- this.verticalAlignItemStack = this.verticalAlignItemStack || [];
168
- this.linearNodeList = [];
169
- this.writer = new PageElementWriter(
170
- new DocumentContext(this.pageSize, this.pageMargins, this._footerGapOption), this.tracker);
171
-
172
- this.heightHeaderAndFooter = this.addHeadersAndFooters(header, footer) || {};
173
- if (!isUndefined(this.heightHeaderAndFooter.header)) {
174
- this.pageMargins.top = this.heightHeaderAndFooter.header + 1;
175
- }
176
-
177
- if (isArray(docStructure) && docStructure[2] && isArray(docStructure[2]) && docStructure[2][0] && docStructure[2][0].remark) {
178
- var tableRemark = docStructure[2][0].remark;
179
- var remarkLabel = docStructure[2][0];
180
- var remarkDetail = docStructure[2][1] && docStructure[2][1].text;
181
-
182
- docStructure[2].splice(0, 1);
183
- if (docStructure[2].length > 0) {
184
- docStructure[2].splice(0, 1);
185
- }
186
-
187
- var labelRow = [];
188
- var detailRow = [];
189
-
190
- labelRow.push(remarkLabel);
191
- detailRow.push({ remarktest: true, text: remarkDetail });
192
-
193
- tableRemark.table.body.push(labelRow);
194
- tableRemark.table.body.push(detailRow);
195
-
196
- tableRemark.table.headerRows = 1;
197
-
198
- docStructure[2].push(tableRemark);
199
- }
200
-
201
- this.linearNodeList = [];
202
- docStructure = this.docPreprocessor.preprocessDocument(docStructure);
203
- docStructure = this.docMeasure.measureDocument(docStructure);
204
-
205
- this.verticalAlignItemStack = [];
206
- this.writer = new PageElementWriter(
207
- new DocumentContext(this.pageSize, this.pageMargins, this._footerGapOption), this.tracker);
208
-
209
- var _this = this;
210
- this.writer.context().tracker.startTracking('pageAdded', function () {
211
- _this.addBackground(background);
212
- });
213
-
214
- this.addBackground(background);
215
- this.processNode(docStructure);
216
- this.addHeadersAndFooters(header, footer,
217
- (this.heightHeaderAndFooter.header || 0) + 1,
218
- (this.heightHeaderAndFooter.footer || 0) + 1);
219
- if (watermark != null) {
220
- this.addWatermark(watermark, fontProvider, defaultStyle);
221
- }
222
-
223
- return { pages: this.writer.context().pages, linearNodeList: this.linearNodeList };
224
- };
225
-
226
- LayoutBuilder.prototype.applyFooterGapOption = function(opt) {
227
- if (opt === true) {
228
- opt = { enabled: true };
229
- }
230
-
231
- if (!opt) return;
232
-
233
- if (typeof opt !== 'object') {
234
- this._footerGapOption = { enabled: true, forcePageBreakForAllRows: false };
235
- return;
236
- }
237
-
238
- this._footerGapOption = {
239
- enabled: opt.enabled !== false,
240
- minRowHeight: typeof opt.minRowHeight === 'number' ? opt.minRowHeight : undefined, // Optional fallback - system auto-calculates from cell content if not provided
241
- forcePageBreakForAllRows: opt.forcePageBreakForAllRows === true, // Force page break for all rows (not just inline images)
242
- columns: opt.columns ? {
243
- widths: Array.isArray(opt.columns.widths) ? opt.columns.widths.slice() : undefined,
244
- widthLength: opt.columns.widths.length || 0,
245
- stops: Array.isArray(opt.columns.stops) ? opt.columns.stops.slice() : undefined,
246
- style: opt.columns.style ? Object.assign({}, opt.columns.style) : {},
247
- includeOuter: opt.columns.includeOuter !== false
248
- } : null
249
- };
250
- };
251
-
252
- LayoutBuilder.prototype.addBackground = function (background) {
253
- var backgroundGetter = isFunction(background) ? background : function () {
254
- return background;
255
- };
256
-
257
- var context = this.writer.context();
258
- var pageSize = context.getCurrentPage().pageSize;
259
-
260
- var pageBackground = backgroundGetter(context.page + 1, pageSize);
261
-
262
- if (pageBackground) {
263
- this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height);
264
- pageBackground = this.docPreprocessor.preprocessDocument(pageBackground);
265
- this.processNode(this.docMeasure.measureDocument(pageBackground));
266
- this.writer.commitUnbreakableBlock(0, 0);
267
- context.backgroundLength[context.page] += pageBackground.positions.length;
268
- }
269
- };
270
-
271
- LayoutBuilder.prototype.addStaticRepeatable = function (headerOrFooter, sizeFunction) {
272
- return this.addDynamicRepeatable(function () {
273
- return JSON.parse(JSON.stringify(headerOrFooter)); // copy to new object
274
- }, sizeFunction);
275
- };
276
-
277
- LayoutBuilder.prototype.addDynamicRepeatable = function (nodeGetter, sizeFunction) {
278
- var pages = this.writer.context().pages;
279
- var measuredHeight;
280
-
281
- for (var pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) {
282
- this.writer.context().page = pageIndex;
283
-
284
- var node = nodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
285
-
286
- if (node) {
287
- var sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.pageMargins);
288
- this.writer.beginUnbreakableBlock(sizes.width, sizes.height);
289
- node = this.docPreprocessor.preprocessDocument(node);
290
- this.processNode(this.docMeasure.measureDocument(node));
291
- this.writer.commitUnbreakableBlock(sizes.x, sizes.y);
292
- if (!isUndefined(node._height)) {
293
- measuredHeight = node._height;
294
- }
295
- }
296
- }
297
-
298
- return measuredHeight;
299
- };
300
-
301
- LayoutBuilder.prototype.addHeadersAndFooters = function (header, footer, headerHeight, footerHeight) {
302
- var measured = { header: undefined, footer: undefined };
303
-
304
- var headerSizeFct = function (pageSize) {
305
- var effectiveHeight = headerHeight;
306
- if (isUndefined(effectiveHeight)) {
307
- effectiveHeight = pageSize.height;
308
- }
309
- return {
310
- x: 0,
311
- y: 0,
312
- width: pageSize.width,
313
- height: effectiveHeight
314
- };
315
- };
316
-
317
- var footerSizeFct = function (pageSize) {
318
- var effectiveHeight = footerHeight;
319
- if (isUndefined(effectiveHeight)) {
320
- effectiveHeight = pageSize.height;
321
- }
322
- return {
323
- x: 0,
324
- y: pageSize.height - effectiveHeight,
325
- width: pageSize.width,
326
- height: effectiveHeight
327
- };
328
- };
329
-
330
- if (this._footerGapOption && !this.writer.context()._footerGapOption) {
331
- this.writer.context()._footerGapOption = this._footerGapOption;
332
- }
333
-
334
- if (isFunction(header)) {
335
- measured.header = this.addDynamicRepeatable(header, headerSizeFct);
336
- } else if (header) {
337
- measured.header = this.addStaticRepeatable(header, headerSizeFct);
338
- }
339
-
340
- if (isFunction(footer)) {
341
- measured.footer = this.addDynamicRepeatable(footer, footerSizeFct);
342
- } else if (footer) {
343
- measured.footer = this.addStaticRepeatable(footer, footerSizeFct);
344
- }
345
-
346
- return measured;
347
- };
348
-
349
- LayoutBuilder.prototype.addWatermark = function (watermark, fontProvider, defaultStyle) {
350
- if (isString(watermark)) {
351
- watermark = { 'text': watermark };
352
- }
353
-
354
- if (!watermark.text) { // empty watermark text
355
- return;
356
- }
357
-
358
- var pages = this.writer.context().pages;
359
- for (var i = 0, l = pages.length; i < l; i++) {
360
- pages[i].watermark = getWatermarkObject({ ...watermark }, pages[i].pageSize, fontProvider, defaultStyle);
361
- }
362
-
363
- function getWatermarkObject(watermark, pageSize, fontProvider, defaultStyle) {
364
- watermark.font = watermark.font || defaultStyle.font || 'Roboto';
365
- watermark.fontSize = watermark.fontSize || 'auto';
366
- watermark.color = watermark.color || 'black';
367
- watermark.opacity = isNumber(watermark.opacity) ? watermark.opacity : 0.6;
368
- watermark.bold = watermark.bold || false;
369
- watermark.italics = watermark.italics || false;
370
- watermark.angle = !isUndefined(watermark.angle) && !isNull(watermark.angle) ? watermark.angle : null;
371
-
372
- if (watermark.angle === null) {
373
- watermark.angle = Math.atan2(pageSize.height, pageSize.width) * -180 / Math.PI;
374
- }
375
-
376
- if (watermark.fontSize === 'auto') {
377
- watermark.fontSize = getWatermarkFontSize(pageSize, watermark, fontProvider);
378
- }
379
-
380
- var watermarkObject = {
381
- text: watermark.text,
382
- font: fontProvider.provideFont(watermark.font, watermark.bold, watermark.italics),
383
- fontSize: watermark.fontSize,
384
- color: watermark.color,
385
- opacity: watermark.opacity,
386
- angle: watermark.angle
387
- };
388
-
389
- watermarkObject._size = getWatermarkSize(watermark, fontProvider);
390
-
391
- return watermarkObject;
392
- }
393
-
394
- function getWatermarkSize(watermark, fontProvider) {
395
- var textTools = new TextTools(fontProvider);
396
- var styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
397
-
398
- styleContextStack.push({
399
- fontSize: watermark.fontSize
400
- });
401
-
402
- var size = textTools.sizeOfString(watermark.text, styleContextStack);
403
- var rotatedSize = textTools.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
404
-
405
- return { size: size, rotatedSize: rotatedSize };
406
- }
407
-
408
- function getWatermarkFontSize(pageSize, watermark, fontProvider) {
409
- var textTools = new TextTools(fontProvider);
410
- var styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
411
- var rotatedSize;
412
-
413
- /**
414
- * Binary search the best font size.
415
- * Initial bounds [0, 1000]
416
- * Break when range < 1
417
- */
418
- var a = 0;
419
- var b = 1000;
420
- var c = (a + b) / 2;
421
- while (Math.abs(a - b) > 1) {
422
- styleContextStack.push({
423
- fontSize: c
424
- });
425
- rotatedSize = textTools.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
426
- if (rotatedSize.width > pageSize.width) {
427
- b = c;
428
- c = (a + b) / 2;
429
- } else if (rotatedSize.width < pageSize.width) {
430
- if (rotatedSize.height > pageSize.height) {
431
- b = c;
432
- c = (a + b) / 2;
433
- } else {
434
- a = c;
435
- c = (a + b) / 2;
436
- }
437
- }
438
- styleContextStack.pop();
439
- }
440
- /*
441
- End binary search
442
- */
443
- return c;
444
- }
445
- };
446
-
447
- function decorateNode(node) {
448
- var x = node.x, y = node.y;
449
- node.positions = [];
450
-
451
- if (isArray(node.canvas)) {
452
- node.canvas.forEach(function (vector) {
453
- var x = vector.x, y = vector.y, x1 = vector.x1, y1 = vector.y1, x2 = vector.x2, y2 = vector.y2;
454
- vector.resetXY = function () {
455
- vector.x = x;
456
- vector.y = y;
457
- vector.x1 = x1;
458
- vector.y1 = y1;
459
- vector.x2 = x2;
460
- vector.y2 = y2;
461
- };
462
- });
463
- }
464
-
465
- node.resetXY = function () {
466
- node.x = x;
467
- node.y = y;
468
- if (isArray(node.canvas)) {
469
- node.canvas.forEach(function (vector) {
470
- vector.resetXY();
471
- });
472
- }
473
- };
474
- }
475
-
476
- LayoutBuilder.prototype.processNode = function (node) {
477
- var self = this;
478
-
479
- if (footerBreak && (node.footerBreak || node.footer)) {
480
- return;
481
- }
482
-
483
- if (node && node.unbreakable && node.summary && node.table && node.table.body &&
484
- node.table.body[0] && node.table.body[0][0] && node.table.body[0][0].summaryBreak) {
485
- testTracker = new TraversalTracker();
486
- testWriter = new PageElementWriter(self.writer.context(), testTracker);
487
- testVerticalAlignStack = self.verticalAlignItemStack.slice();
488
- currentLayoutBuilder = self;
489
- testResult = false;
490
- var nodeForTest = cloneDeep(node);
491
- if (nodeForTest.table.body[0]) {
492
- nodeForTest.table.body[0].splice(0, 1);
493
- }
494
- processNode_test(nodeForTest);
495
- currentLayoutBuilder = null;
496
- if (testResult && node.table.body[0]) {
497
- node.table.body[0].splice(0, 1);
498
- }
499
- }
500
-
501
- this.linearNodeList.push(node);
502
- decorateNode(node);
503
-
504
- var prevTop = self.writer.context().getCurrentPosition().top;
505
-
506
- applyMargins(function () {
507
- var unbreakable = node.unbreakable;
508
- if (unbreakable) {
509
- self.writer.beginUnbreakableBlock();
510
- }
511
-
512
- var absPosition = node.absolutePosition;
513
- if (absPosition) {
514
- self.writer.context().beginDetachedBlock();
515
- self.writer.context().moveTo(absPosition.x || 0, absPosition.y || 0);
516
- }
517
-
518
- var relPosition = node.relativePosition;
519
- if (relPosition) {
520
- self.writer.context().beginDetachedBlock();
521
- self.writer.context().moveToRelative(relPosition.x || 0, relPosition.y || 0);
522
- }
523
-
524
- var verticalAlignBegin;
525
- if (node.verticalAlign) {
526
- verticalAlignBegin = self.writer.beginVerticalAlign(node.verticalAlign);
527
- }
528
-
529
- if (node.stack) {
530
- // Handle _footerGapOption for breakable stacks
531
- var footerGapEnabled = !unbreakable && node.footer && node._footerGapOption && node._footerGapOption.enabled;
532
- var preRenderState = null;
533
-
534
- if (footerGapEnabled) {
535
- var ctx = self.writer.context();
536
- var currentPage = ctx.getCurrentPage();
537
- preRenderState = {
538
- startY: ctx.y,
539
- startPage: ctx.page,
540
- availableHeight: ctx.availableHeight,
541
- itemCount: currentPage ? currentPage.items.length : 0
542
- };
543
- }
544
-
545
- // Process the stack
546
- self.processVerticalContainer(node);
547
-
548
- // Post-render repositioning
549
- if (footerGapEnabled && preRenderState) {
550
- var ctx = self.writer.context();
551
-
552
- // Only reposition if on same page (single-page content)
553
- if (ctx.page === preRenderState.startPage) {
554
- var renderedHeight = ctx.y - preRenderState.startY;
555
- var gapHeight = preRenderState.availableHeight - renderedHeight;
556
-
557
- if (gapHeight > 0) {
558
- var currentPage = ctx.getCurrentPage();
559
-
560
- // SINGLE-PAGE: Draw guide lines in the gap area
561
- var gapTopY = preRenderState.startY;
562
- var colSpec = node._footerGapOption.columns || self._footerGapOption.columns || null;
563
- if (colSpec) {
564
- var rawWidths = colSpec.content.vLines || [];
565
- if (rawWidths && rawWidths.length > 1) {
566
- var style = (colSpec.style || {});
567
- var lw = style.lineWidth != null ? style.lineWidth : 0.5;
568
- var lc = style.color || '#000000';
569
- var dashCfg = style.dash;
570
- var includeOuter = colSpec.includeOuter !== false;
571
- var startIndex = includeOuter ? 0 : 1;
572
- var endIndex = includeOuter ? rawWidths.length : rawWidths.length - 1;
573
-
574
- for (var ci = startIndex; ci < endIndex; ci++) {
575
- var xGuide = ctx.x + rawWidths[ci] - 0.25;
576
- currentPage.items.push({
577
- type: 'vector',
578
- item: {
579
- type: 'line',
580
- x1: xGuide,
581
- y1: gapTopY,
582
- x2: xGuide,
583
- y2: gapTopY + gapHeight,
584
- lineWidth: lw,
585
- lineColor: lc,
586
- dash: dashCfg ? {
587
- length: dashCfg.length,
588
- space: dashCfg.space != null ? dashCfg.space : dashCfg.gap
589
- } : undefined,
590
- _footerGuideLine: true
591
- }
592
- });
593
- }
594
- }
595
- }
596
-
597
- // Reposition only items that belong to footer stack content
598
- // Iterate ALL items (backgrounds are inserted at lower indices, not appended)
599
- // Filter by Y position to exclude non-footer items
600
- for (var i = 0; i < currentPage.items.length; i++) {
601
- var item = currentPage.items[i];
602
-
603
- // Skip the guide lines we just added (they're already positioned correctly)
604
- if (item.item && item.item._footerGuideLine) {
605
- continue;
606
- }
607
-
608
- // Get the item's starting Y position
609
- var itemStartY = null;
610
- if (item.item && typeof item.item.y1 === 'number') {
611
- itemStartY = item.item.y1;
612
- } else if (item.item && typeof item.item.y === 'number') {
613
- itemStartY = item.item.y;
614
- }
615
-
616
- // Only reposition items that START at or after footer stack's Y position
617
- // This excludes outer table border lines that extend from earlier rows
618
- if (itemStartY !== null && itemStartY < preRenderState.startY) {
619
- continue;
620
- }
621
-
622
- // Reposition this item
623
- if (item.item && typeof item.item.y === 'number') {
624
- item.item.y += gapHeight;
625
- }
626
- if (item.item && typeof item.item.y1 === 'number') {
627
- item.item.y1 += gapHeight;
628
- }
629
- if (item.item && typeof item.item.y2 === 'number') {
630
- item.item.y2 += gapHeight;
631
- }
632
- }
633
- ctx.moveDown(gapHeight);
634
- }
635
- } else {
636
- // MULTI-PAGE: Process ALL pages from startPage+1 to current page
637
- // Only move FOOTER items to bottom, keep remark at top
638
- var pages = ctx.pages;
639
- var pageMargins = ctx.pageMargins;
640
-
641
- // Helper function to recursively find node with _isFooterTable
642
- function findFooterTableNode(n) {
643
- if (!n) return null;
644
- if (n._isFooterTable) return n;
645
- if (n.stack && Array.isArray(n.stack)) {
646
- for (var i = 0; i < n.stack.length; i++) {
647
- var found = findFooterTableNode(n.stack[i]);
648
- if (found) return found;
649
- }
650
- }
651
- if (Array.isArray(n)) {
652
- for (var i = 0; i < n.length; i++) {
653
- var found = findFooterTableNode(n[i]);
654
- if (found) return found;
655
- }
656
- }
657
- return null;
658
- }
659
-
660
- // Find footer table node to get its height
661
- var footerTableNode = null;
662
- if (node.stack && node.stack.length > 0) {
663
- for (var si = node.stack.length - 1; si >= 0; si--) {
664
- var found = findFooterTableNode(node.stack[si]);
665
- if (found) {
666
- footerTableNode = found;
667
- break;
668
- }
669
- }
670
- }
671
-
672
- var footerHeight = Math.abs((footerTableNode && footerTableNode._height) || 0);
673
-
674
- // Only process the LAST page where footer is located
675
- // Intermediate pages (remark only) should not be repositioned
676
- var pageIdx = ctx.page;
677
- if (pageIdx > preRenderState.startPage) {
678
- var page = pages[pageIdx];
679
- if (page && page.items && page.items.length > 0) {
680
- var pageHeight = page.pageSize.height;
681
- var bottomMargin = pageMargins.bottom;
682
- var pageBottom = pageHeight - bottomMargin;
683
-
684
- // Find footer start Y on this page (where footer items begin)
685
- // Use footerHeight to identify footer area from the bottom of content
686
- var contentBottom = 0;
687
- for (var i = 0; i < page.items.length; i++) {
688
- var item = page.items[i];
689
- var itemBottom = 0;
690
- if (item.item) {
691
- if (typeof item.item.y === 'number') {
692
- var h = item.item.h || item.item.height || 0;
693
- itemBottom = item.item.y + h;
694
- }
695
- if (typeof item.item.y2 === 'number' && item.item.y2 > itemBottom) {
696
- itemBottom = item.item.y2;
697
- }
698
- }
699
- if (itemBottom > contentBottom) {
700
- contentBottom = itemBottom;
701
- }
702
- }
703
-
704
- // Footer starts at contentBottom - footerHeight
705
- var footerStartY = contentBottom - footerHeight;
706
-
707
- // Calculate gap: how much to move footer down
708
- var gapHeight = pageBottom - contentBottom;
709
- if (gapHeight > 0) {
710
- // Only move items that are part of footer (Y >= footerStartY)
711
- for (var i = 0; i < page.items.length; i++) {
712
- var item = page.items[i];
713
- var itemY = null;
714
- if (item.item && typeof item.item.y1 === 'number') {
715
- itemY = item.item.y1;
716
- } else if (item.item && typeof item.item.y === 'number') {
717
- itemY = item.item.y;
718
- }
719
-
720
- // Skip items above footer
721
- if (itemY === null || itemY < footerStartY) continue;
722
-
723
- // Don't move items that are near the top margin (table headers)
724
- // Use actual header height from repeatables instead of hardcoded threshold
725
- var repeatableHeaderHeight = 0;
726
- for (var ri = 0; ri < self.writer.repeatables.length; ri++) {
727
- repeatableHeaderHeight += self.writer.repeatables[ri].height;
728
- }
729
- var headerThreshold = pageMargins.top + repeatableHeaderHeight;
730
- if (itemY < headerThreshold) continue;
731
-
732
- // Move footer item down
733
- if (item.item && typeof item.item.y === 'number') {
734
- item.item.y += gapHeight;
735
- }
736
- if (item.item && typeof item.item.y1 === 'number') {
737
- item.item.y1 += gapHeight;
738
- }
739
- if (item.item && typeof item.item.y2 === 'number') {
740
- item.item.y2 += gapHeight;
741
- }
742
- }
743
- }
744
- }
745
- }
746
-
747
- // Also update context for current page
748
- var lastPageGapHeight = ctx.availableHeight;
749
- if (lastPageGapHeight > 0) {
750
- ctx.moveDown(lastPageGapHeight);
751
- }
752
- }
753
- }
754
- } else if (node.layers) {
755
- self.processLayers(node);
756
- } else if (node.columns) {
757
- self.processColumns(node);
758
- } else if (node.ul) {
759
- self.processList(false, node);
760
- } else if (node.ol) {
761
- self.processList(true, node);
762
- } else if (node.table) {
763
- self.processTable(node);
764
- } else if (node.text !== undefined) {
765
- self.processLeaf(node);
766
- } else if (node.toc) {
767
- self.processToc(node);
768
- } else if (node.image) {
769
- self.processImage(node);
770
- } else if (node.svg) {
771
- self.processSVG(node);
772
- } else if (node.canvas) {
773
- self.processCanvas(node);
774
- } else if (node.qr) {
775
- self.processQr(node);
776
- } else if (!node._span) {
777
- throw new Error('Unrecognized document structure: ' + JSON.stringify(node, fontStringify));
778
- }
779
-
780
- if ((absPosition || relPosition) && !node.absoluteRepeatable) {
781
- self.writer.context().endDetachedBlock();
782
- }
783
-
784
- if (unbreakable) {
785
- if (node.footer) {
786
- footerBreak = self.writer.commitUnbreakableBlock(undefined, undefined, node.footer);
787
- } else {
788
- self.writer.commitUnbreakableBlock();
789
- }
790
- }
791
-
792
- if (node.verticalAlign) {
793
- var stackEntry = {
794
- begin: verticalAlignBegin,
795
- end: self.writer.endVerticalAlign(node.verticalAlign)
796
- };
797
- self.verticalAlignItemStack.push(stackEntry);
798
- node._verticalAlignIdx = self.verticalAlignItemStack.length - 1;
799
- }
800
- });
801
-
802
- node._height = self.writer.context().getCurrentPosition().top - prevTop;
803
-
804
- function applyMargins(callback) {
805
- var margin = node._margin;
806
-
807
- if (node.pageBreak === 'before') {
808
- self.writer.moveToNextPage(node.pageOrientation);
809
- }
810
-
811
- if (margin) {
812
- self.writer.context().moveDown(margin[1]);
813
- self.writer.context().addMargin(margin[0], margin[2]);
814
- }
815
-
816
- callback();
817
-
818
- if (margin) {
819
- self.writer.context().addMargin(-margin[0], -margin[2]);
820
- self.writer.context().moveDown(margin[3]);
821
- }
822
-
823
- if (node.pageBreak === 'after') {
824
- self.writer.moveToNextPage(node.pageOrientation);
825
- }
826
- }
827
- };
828
-
829
- // vertical container
830
- LayoutBuilder.prototype.processVerticalContainer = function (node) {
831
- var self = this;
832
- node.stack.forEach(function (item) {
833
- self.processNode(item);
834
- addAll(node.positions, item.positions);
835
-
836
- //TODO: paragraph gap
837
- });
838
- };
839
-
840
- // layers
841
- LayoutBuilder.prototype.processLayers = function(node) {
842
- var self = this;
843
- var ctxX = self.writer.context().x;
844
- var ctxY = self.writer.context().y;
845
- var maxX = ctxX;
846
- var maxY = ctxY;
847
- node.layers.forEach(function(item, i) {
848
- self.writer.context().x = ctxX;
849
- self.writer.context().y = ctxY;
850
- self.processNode(item);
851
- item._verticalAlignIdx = self.verticalAlignItemStack.length - 1;
852
- addAll(node.positions, item.positions);
853
- maxX = self.writer.context().x > maxX ? self.writer.context().x : maxX;
854
- maxY = self.writer.context().y > maxY ? self.writer.context().y : maxY;
855
- });
856
- self.writer.context().x = maxX;
857
- self.writer.context().y = maxY;
858
- };
859
-
860
- // columns
861
- LayoutBuilder.prototype.processColumns = function (columnNode) {
862
- this.nestedLevel++;
863
- var columns = columnNode.columns;
864
- var availableWidth = this.writer.context().availableWidth;
865
- var gaps = gapArray(columnNode._gap);
866
-
867
- if (gaps) {
868
- availableWidth -= (gaps.length - 1) * columnNode._gap;
869
- }
870
-
871
- ColumnCalculator.buildColumnWidths(columns, availableWidth);
872
- var result = this.processRow({
873
- marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
874
- cells: columns,
875
- widths: columns,
876
- gaps
877
- });
878
- addAll(columnNode.positions, result.positions);
879
-
880
- this.nestedLevel--;
881
- if (this.nestedLevel === 0) {
882
- this.writer.context().resetMarginXTopParent();
883
- }
884
-
885
- function gapArray(gap) {
886
- if (!gap) {
887
- return null;
888
- }
889
-
890
- var gaps = [];
891
- gaps.push(0);
892
-
893
- for (var i = columns.length - 1; i > 0; i--) {
894
- gaps.push(gap);
895
- }
896
-
897
- return gaps;
898
- }
899
- };
900
-
901
- /**
902
- * Searches for a cell in the same row that starts a rowspan and is positioned immediately before the current cell.
903
- * Alternatively, it finds a cell where the colspan initiating the rowspan extends to the cell just before the current one.
904
- *
905
- * @param {Array<object>} arr - An array representing cells in a row.
906
- * @param {number} i - The index of the current cell to search backward from.
907
- * @returns {object|null} The starting cell of the rowspan if found; otherwise, `null`.
908
- */
909
- LayoutBuilder.prototype._findStartingRowSpanCell = function (arr, i) {
910
- var requiredColspan = 1;
911
- for (var index = i - 1; index >= 0; index--) {
912
- if (!arr[index]._span) {
913
- if (arr[index].rowSpan > 1 && (arr[index].colSpan || 1) === requiredColspan) {
914
- return arr[index];
915
- } else {
916
- return null;
917
- }
918
- }
919
- requiredColspan++;
920
- }
921
- return null;
922
- };
923
-
924
- /**
925
- * Retrieves a page break description for a specified page from a list of page breaks.
926
- *
927
- * @param {Array<object>} pageBreaks - An array of page break descriptions, each containing `prevPage` properties.
928
- * @param {number} page - The page number to find the associated page break for.
929
- * @returns {object|undefined} The page break description object for the specified page if found; otherwise, `undefined`.
930
- */
931
- LayoutBuilder.prototype._getPageBreak = function (pageBreaks, page) {
932
- return pageBreaks.find(desc => desc.prevPage === page);
933
- };
934
-
935
- LayoutBuilder.prototype._getPageBreakListBySpan = function (tableNode, page, rowIndex) {
936
- if (!tableNode || !tableNode._breaksBySpan) {
937
- return null;
938
- }
939
- const breaksList = tableNode._breaksBySpan.filter(desc => desc.prevPage === page && rowIndex <= desc.rowIndexOfSpanEnd);
940
-
941
- var y = Number.MAX_VALUE,
942
- prevY = Number.MIN_VALUE;
943
-
944
- breaksList.forEach(b => {
945
- prevY = Math.max(b.prevY, prevY);
946
- y = Math.min(b.y, y);
947
- });
948
-
949
- return {
950
- prevPage: page,
951
- prevY: prevY,
952
- y: y
953
- };
954
- };
955
-
956
- LayoutBuilder.prototype._findSameRowPageBreakByRowSpanData = function (breaksBySpan, page, rowIndex) {
957
- if (!breaksBySpan) {
958
- return null;
959
- }
960
- return breaksBySpan.find(desc => desc.prevPage === page && rowIndex === desc.rowIndexOfSpanEnd);
961
- };
962
-
963
- LayoutBuilder.prototype._updatePageBreaksData = function (pageBreaks, tableNode, rowIndex) {
964
- Object.keys(tableNode._bottomByPage).forEach(p => {
965
- const page = Number(p);
966
- const pageBreak = this._getPageBreak(pageBreaks, page);
967
- if (pageBreak) {
968
- pageBreak.prevY = Math.max(pageBreak.prevY, tableNode._bottomByPage[page]);
969
- }
970
- if (tableNode._breaksBySpan && tableNode._breaksBySpan.length > 0) {
971
- const breaksBySpanList = tableNode._breaksBySpan.filter(pb => pb.prevPage === page && rowIndex <= pb.rowIndexOfSpanEnd);
972
- if (breaksBySpanList && breaksBySpanList.length > 0) {
973
- breaksBySpanList.forEach(b => {
974
- b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
975
- });
976
- }
977
- }
978
- });
979
- };
980
-
981
- /**
982
- * Resolves the Y-coordinates for a target object by comparing two break points.
983
- *
984
- * @param {object} break1 - The first break point with `prevY` and `y` properties.
985
- * @param {object} break2 - The second break point with `prevY` and `y` properties.
986
- * @param {object} target - The target object to be updated with resolved Y-coordinates.
987
- * @property {number} target.prevY - Updated to the maximum `prevY` value between `break1` and `break2`.
988
- * @property {number} target.y - Updated to the minimum `y` value between `break1` and `break2`.
989
- */
990
- LayoutBuilder.prototype._resolveBreakY = function (break1, break2, target) {
991
- target.prevY = Math.max(break1.prevY, break2.prevY);
992
- target.y = Math.min(break1.y, break2.y);
993
- };
994
-
995
- LayoutBuilder.prototype._storePageBreakData = function (data, startsRowSpan, pageBreaks, tableNode) {
996
- var pageDesc;
997
- var pageDescBySpan;
998
-
999
- if (!startsRowSpan) {
1000
- pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
1001
- pageDescBySpan = this._getPageBreakListBySpan(tableNode, data.prevPage, data.rowIndex);
1002
- if (!pageDesc) {
1003
- pageDesc = Object.assign({}, data);
1004
- pageBreaks.push(pageDesc);
1005
- }
1006
-
1007
- if (pageDescBySpan) {
1008
- this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
1009
- }
1010
- this._resolveBreakY(pageDesc, data, pageDesc);
1011
- } else {
1012
- var breaksBySpan = tableNode && tableNode._breaksBySpan || null;
1013
- pageDescBySpan = this._findSameRowPageBreakByRowSpanData(breaksBySpan, data.prevPage, data.rowIndex);
1014
- if (!pageDescBySpan) {
1015
- pageDescBySpan = Object.assign({}, data, {
1016
- rowIndexOfSpanEnd: data.rowIndex + data.rowSpan - 1
1017
- });
1018
- if (!tableNode._breaksBySpan) {
1019
- tableNode._breaksBySpan = [];
1020
- }
1021
- tableNode._breaksBySpan.push(pageDescBySpan);
1022
- }
1023
- pageDescBySpan.prevY = Math.max(pageDescBySpan.prevY, data.prevY);
1024
- pageDescBySpan.y = Math.min(pageDescBySpan.y, data.y);
1025
- pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
1026
- if (pageDesc) {
1027
- this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
1028
- }
1029
- }
1030
- };
1031
-
1032
- /**
1033
- * Calculates the left offset for a column based on the specified gap values.
1034
- *
1035
- * @param {number} i - The index of the column for which the offset is being calculated.
1036
- * @param {Array<number>} gaps - An array of gap values for each column.
1037
- * @returns {number} The left offset for the column. Returns `gaps[i]` if it exists, otherwise `0`.
1038
- */
1039
- LayoutBuilder.prototype._colLeftOffset = function (i, gaps) {
1040
- if (gaps && gaps.length > i) {
1041
- return gaps[i];
1042
- }
1043
- return 0;
1044
- };
1045
-
1046
- /**
1047
- * Checks if a cell or node contains an inline image.
1048
- *
1049
- * @param {object} node - The node to check for inline images.
1050
- * @returns {boolean} True if the node contains an inline image; otherwise, false.
1051
- */
1052
- LayoutBuilder.prototype._containsInlineImage = function (node) {
1053
- if (!node) {
1054
- return false;
1055
- }
1056
-
1057
- // Direct image node
1058
- if (node.image) {
1059
- return true;
1060
- }
1061
-
1062
-
1063
- // Check in table
1064
- if (node.table && isArray(node.table.body)) {
1065
- for (var r = 0; r < node.table.body.length; r++) {
1066
- if (isArray(node.table.body[r])) {
1067
- for (var c = 0; c < node.table.body[r].length; c++) {
1068
- if (this._containsInlineImage(node.table.body[r][c])) {
1069
- return true;
1070
- }
1071
- }
1072
- }
1073
- }
1074
- }
1075
-
1076
- return false;
1077
- };
1078
-
1079
-
1080
- /**
1081
- * Gets the maximum image height from cells in a row.
1082
- *
1083
- * @param {Array<object>} cells - Array of cell objects in a row.
1084
- * @returns {number} The maximum image height found in the cells.
1085
- */
1086
- LayoutBuilder.prototype._getMaxImageHeight = function (cells) {
1087
- var maxHeight = 0;
1088
-
1089
- for (var i = 0; i < cells.length; i++) {
1090
- var cellHeight = this._getImageHeightFromNode(cells[i]);
1091
- if (cellHeight > maxHeight) {
1092
- maxHeight = cellHeight;
1093
- }
1094
- }
1095
-
1096
- return maxHeight;
1097
- };
1098
-
1099
- /**
1100
- * Gets the maximum estimated height from cells in a row.
1101
- * Checks for measured heights (_height property) and content-based heights.
1102
- *
1103
- * @param {Array<object>} cells - Array of cell objects in a row.
1104
- * @returns {number} The maximum estimated height found in the cells, or 0 if cannot estimate.
1105
- */
1106
- LayoutBuilder.prototype._getMaxCellHeight = function (cells) {
1107
- var maxHeight = 0;
1108
-
1109
- for (var i = 0; i < cells.length; i++) {
1110
- var cell = cells[i];
1111
- if (!cell || cell._span) {
1112
- continue; // Skip null cells and span placeholders
1113
- }
1114
-
1115
- var cellHeight = 0;
1116
-
1117
- // Check if cell has measured height from docMeasure phase
1118
- if (cell._height) {
1119
- cellHeight = cell._height;
1120
- }
1121
- // Check for image content
1122
- else if (cell.image && cell._maxHeight) {
1123
- cellHeight = cell._maxHeight;
1124
- }
1125
- // Check for nested content with height
1126
- else {
1127
- cellHeight = this._getImageHeightFromNode(cell);
1128
- }
1129
-
1130
- if (cellHeight > maxHeight) {
1131
- maxHeight = cellHeight;
1132
- }
1133
- }
1134
-
1135
- return maxHeight;
1136
- };
1137
-
1138
- /**
1139
- * Recursively gets image height from a node.
1140
- *
1141
- * @param {object} node - The node to extract image height from.
1142
- * @returns {number} The image height if found; otherwise, 0.
1143
- */
1144
- LayoutBuilder.prototype._getImageHeightFromNode = function (node) {
1145
- if (!node) {
1146
- return 0;
1147
- }
1148
-
1149
- // Direct image node with height
1150
- if (node.image && node._height) {
1151
- return node._height;
1152
- }
1153
-
1154
- var maxHeight = 0;
1155
-
1156
- // Check in stack
1157
- if (isArray(node.stack)) {
1158
- for (var i = 0; i < node.stack.length; i++) {
1159
- var h = this._getImageHeightFromNode(node.stack[i]);
1160
- if (h > maxHeight) {
1161
- maxHeight = h;
1162
- }
1163
- }
1164
- }
1165
-
1166
- // Check in columns
1167
- if (isArray(node.columns)) {
1168
- for (var j = 0; j < node.columns.length; j++) {
1169
- var h2 = this._getImageHeightFromNode(node.columns[j]);
1170
- if (h2 > maxHeight) {
1171
- maxHeight = h2;
1172
- }
1173
- }
1174
- }
1175
-
1176
- // Check in table
1177
- if (node.table && isArray(node.table.body)) {
1178
- for (var r = 0; r < node.table.body.length; r++) {
1179
- if (isArray(node.table.body[r])) {
1180
- for (var c = 0; c < node.table.body[r].length; c++) {
1181
- var h3 = this._getImageHeightFromNode(node.table.body[r][c]);
1182
- if (h3 > maxHeight) {
1183
- maxHeight = h3;
1184
- }
1185
- }
1186
- }
1187
- }
1188
- }
1189
-
1190
- return maxHeight;
1191
- };
1192
-
1193
-
1194
- /**
1195
- * Retrieves the ending cell for a row span in case it exists in a specified table column.
1196
- *
1197
- * @param {Array<Array<object>>} tableBody - The table body, represented as a 2D array of cell objects.
1198
- * @param {number} rowIndex - The index of the starting row for the row span.
1199
- * @param {object} column - The column object containing row span information.
1200
- * @param {number} columnIndex - The index of the column within the row.
1201
- * @returns {object|null} The cell at the end of the row span if it exists; otherwise, `null`.
1202
- * @throws {Error} If the row span extends beyond the total row count.
1203
- */
1204
- LayoutBuilder.prototype._getRowSpanEndingCell = function (tableBody, rowIndex, column, columnIndex) {
1205
- if (column.rowSpan && column.rowSpan > 1) {
1206
- var endingRow = rowIndex + column.rowSpan - 1;
1207
- if (endingRow >= tableBody.length) {
1208
- throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
1209
- }
1210
- return tableBody[endingRow][columnIndex];
1211
- }
1212
-
1213
- return null;
1214
- };
1215
-
1216
- LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height, heightOffset = 0 }) {
1217
- var self = this;
1218
- var isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
1219
- var pageBreaks = [];
1220
- var pageBreaksByRowSpan = [];
1221
- var positions = [];
1222
- var willBreakByHeight = false;
1223
- var columnAlignIndexes = {};
1224
- var hasInlineImage = false;
1225
- widths = widths || cells;
1226
-
1227
- // Check if row contains inline images
1228
- if (!isUnbreakableRow) {
1229
- for (var cellIdx = 0; cellIdx < cells.length; cellIdx++) {
1230
- if (self._containsInlineImage(cells[cellIdx])) {
1231
- hasInlineImage = true;
1232
- break;
1233
- }
1234
- }
1235
- }
1236
-
1237
- // Check if row would cause page break and force move to next page first
1238
- // This keeps the entire row together on the new page
1239
- // Apply when: forcePageBreakForAllRows is enabled OR row has inline images
1240
-
1241
- // Priority for forcePageBreakForAllRows setting:
1242
- // 1. Table-specific layout.forcePageBreakForAllRows
1243
- // 2. Tables with footerGapCollect: 'product-items' (auto-enabled)
1244
- // 3. Global footerGapOption.forcePageBreakForAllRows
1245
- var tableLayout = tableNode && tableNode._layout;
1246
- var footerGapOpt = self.writer.context()._footerGapOption;
1247
- var shouldForcePageBreak = false;
1248
-
1249
- if (tableLayout && tableLayout.forcePageBreakForAllRows !== undefined) {
1250
- // Table-specific setting takes precedence
1251
- shouldForcePageBreak = tableLayout.forcePageBreakForAllRows === true;
1252
- }
1253
-
1254
- if (!isUnbreakableRow && (shouldForcePageBreak || hasInlineImage)) {
1255
- var availableHeight = self.writer.context().availableHeight;
1256
-
1257
- // Calculate estimated height from actual cell content
1258
- var estimatedHeight = height; // Use provided height if available
1259
-
1260
- if (!estimatedHeight) {
1261
- // Try to get maximum cell height from measured content
1262
- var maxCellHeight = self._getMaxCellHeight(cells);
1263
-
1264
- if (maxCellHeight > 0) {
1265
- // Add padding for table borders and cell padding (approximate)
1266
- // Using smaller padding to avoid overly conservative page break detection
1267
- var tablePadding = 10; // Account for row padding and borders
1268
- estimatedHeight = maxCellHeight + tablePadding;
1269
- } else {
1270
- // Fallback: use minRowHeight from table layout or global config if provided
1271
- // Priority: table-specific layout > global footerGapOption > default 80
1272
- // Using higher default (80px) to handle text rows with wrapping and multiple lines
1273
- // This is conservative but prevents text rows from being split across pages
1274
- var minRowHeight = (tableLayout && tableLayout.minRowHeight) || (footerGapOpt && footerGapOpt.minRowHeight) || 80;
1275
- estimatedHeight = minRowHeight;
1276
- }
1277
- }
1278
-
1279
- // Apply heightOffset from table definition to adjust page break calculation
1280
- // This allows fine-tuning of page break detection for specific tables
1281
- // heightOffset is passed as parameter from processTable
1282
- if (heightOffset) {
1283
- estimatedHeight = (estimatedHeight || 0) + heightOffset;
1284
- }
1285
-
1286
- // Check if row won't fit on current page
1287
- // Strategy: Force break if row won't fit AND we're not too close to page boundary
1288
- // "Too close" means availableHeight is very small (< 5px) - at that point forcing
1289
- // a break would create a nearly-blank page
1290
- var minSpaceThreshold = 5; // Only skip forced break if < 5px space left
1291
-
1292
- if (estimatedHeight > availableHeight && availableHeight > minSpaceThreshold) {
1293
- var currentPage = self.writer.context().page;
1294
- var currentY = self.writer.context().y;
1295
-
1296
- // Draw vertical lines to fill the gap from current position to page break
1297
- // This ensures vertical lines extend all the way to the bottom of the page
1298
- if (tableNode && tableNode._tableProcessor && rowIndex > 0) {
1299
- tableNode._tableProcessor.drawVerticalLinesForForcedPageBreak(
1300
- rowIndex,
1301
- self.writer,
1302
- currentY,
1303
- currentY + availableHeight
1304
- );
1305
- }
1306
-
1307
- // Move to next page before processing row
1308
- self.writer.context().moveDown(availableHeight);
1309
- self.writer.moveToNextPage();
1310
-
1311
- // Track this page break so tableProcessor can draw borders correctly
1312
- pageBreaks.push({
1313
- prevPage: currentPage,
1314
- prevY: currentY + availableHeight,
1315
- y: self.writer.context().y,
1316
- page: self.writer.context().page,
1317
- forced: true // Mark as forced page break
1318
- });
1319
-
1320
- // Mark that this row should not break anymore
1321
- isUnbreakableRow = true;
1322
- dontBreakRows = true;
1323
- }
1324
- }
1325
-
1326
- // Check if row should break by height
1327
- if (!isUnbreakableRow && height > self.writer.context().availableHeight) {
1328
- willBreakByHeight = true;
1329
- }
1330
-
1331
- // Use the marginX if we are in a top level table/column (not nested)
1332
- const marginXParent = self.nestedLevel === 1 ? marginX : null;
1333
- const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
1334
- this.writer.context().beginColumnGroup(marginXParent, _bottomByPage);
1335
-
1336
- for (var i = 0, l = cells.length; i < l; i++) {
1337
- var cell = cells[i];
1338
-
1339
- // Page change handler
1340
-
1341
- this.tracker.auto('pageChanged', storePageBreakClosure, function () {
1342
- var width = widths[i]._calcWidth;
1343
- var leftOffset = self._colLeftOffset(i, gaps);
1344
- // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
1345
- var startingSpanCell = self._findStartingRowSpanCell(cells, i);
1346
-
1347
- if (cell.colSpan && cell.colSpan > 1) {
1348
- for (var j = 1; j < cell.colSpan; j++) {
1349
- width += widths[++i]._calcWidth + gaps[i];
1350
- }
1351
- }
1352
-
1353
- // if rowspan starts in this cell, we retrieve the last cell affected by the rowspan
1354
- const rowSpanEndingCell = self._getRowSpanEndingCell(tableBody, rowIndex, cell, i);
1355
- if (rowSpanEndingCell) {
1356
- // We store a reference of the ending cell in the first cell of the rowspan
1357
- cell._endingCell = rowSpanEndingCell;
1358
- cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
1359
- }
1360
-
1361
- // If we are after a cell that started a rowspan
1362
- var endOfRowSpanCell = null;
1363
- if (startingSpanCell && startingSpanCell._endingCell) {
1364
- // Reference to the last cell of the rowspan
1365
- endOfRowSpanCell = startingSpanCell._endingCell;
1366
- // Store if we are in an unbreakable block when we save the context and the originalX
1367
- if (self.writer.transactionLevel > 0) {
1368
- endOfRowSpanCell._isUnbreakableContext = true;
1369
- endOfRowSpanCell._originalXOffset = self.writer.originalX;
1370
- }
1371
- }
1372
-
1373
- // We pass the endingSpanCell reference to store the context just after processing rowspan cell
1374
- self.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell, heightOffset);
1375
-
1376
- if (!cell._span) {
1377
- self.processNode(cell);
1378
- self.writer.context().updateBottomByPage();
1379
- addAll(positions, cell.positions);
1380
- if (cell.verticalAlign && cell._verticalAlignIdx !== undefined) {
1381
- columnAlignIndexes[i] = cell._verticalAlignIdx;
1382
- }
1383
- } else if (cell._columnEndingContext) {
1384
- var discountY = 0;
1385
- if (dontBreakRows) {
1386
- // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
1387
- const ctxBeforeRowSpanLastRow = self.writer.writer.contextStack[self.writer.writer.contextStack.length - 1];
1388
- discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
1389
- }
1390
- var originalXOffset = 0;
1391
- // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
1392
- // We have to sum the originalX (X before starting unbreakable block) to X
1393
- if (cell._isUnbreakableContext && !self.writer.transactionLevel) {
1394
- originalXOffset = cell._originalXOffset;
1395
- }
1396
- // row-span ending
1397
- // Recover the context after processing the rowspanned cell
1398
- self.writer.context().markEnding(cell, originalXOffset, discountY);
1399
- }
1400
- });
1401
- }
1402
-
1403
- // Check if last cell is part of a span
1404
- var endingSpanCell = null;
1405
- var lastColumn = cells.length > 0 ? cells[cells.length - 1] : null;
1406
- if (lastColumn) {
1407
- // Previous column cell has a rowspan
1408
- if (lastColumn._endingCell) {
1409
- endingSpanCell = lastColumn._endingCell;
1410
- // Previous column cell is part of a span
1411
- } else if (lastColumn._span === true) {
1412
- // We get the cell that started the span where we set a reference to the ending cell
1413
- const startingSpanCell = this._findStartingRowSpanCell(cells, cells.length);
1414
- if (startingSpanCell) {
1415
- // Context will be stored here (ending cell)
1416
- endingSpanCell = startingSpanCell._endingCell;
1417
- // Store if we are in an unbreakable block when we save the context and the originalX
1418
- if (this.writer.transactionLevel > 0) {
1419
- endingSpanCell._isUnbreakableContext = true;
1420
- endingSpanCell._originalXOffset = this.writer.originalX;
1421
- }
1422
- }
1423
- }
1424
- }
1425
-
1426
- // If content did not break page, check if we should break by height
1427
- if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
1428
- this.writer.context().moveDown(this.writer.context().availableHeight);
1429
- this.writer.moveToNextPage();
1430
- }
1431
-
1432
- var bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
1433
- var rowHeight = this.writer.context().height;
1434
- for (var colIndex = 0, columnsLength = cells.length; colIndex < columnsLength; colIndex++) {
1435
- var columnNode = cells[colIndex];
1436
- if (columnNode._span) {
1437
- continue;
1438
- }
1439
- if (columnNode.verticalAlign && columnAlignIndexes[colIndex] !== undefined) {
1440
- var alignEntry = self.verticalAlignItemStack[columnAlignIndexes[colIndex]];
1441
- if (alignEntry && alignEntry.begin && alignEntry.begin.item) {
1442
- alignEntry.begin.item.viewHeight = rowHeight;
1443
- alignEntry.begin.item.nodeHeight = columnNode._height;
1444
- }
1445
- }
1446
- if (columnNode.layers) {
1447
- columnNode.layers.forEach(function (layer) {
1448
- if (layer.verticalAlign && layer._verticalAlignIdx !== undefined) {
1449
- var layerEntry = self.verticalAlignItemStack[layer._verticalAlignIdx];
1450
- if (layerEntry && layerEntry.begin && layerEntry.begin.item) {
1451
- layerEntry.begin.item.viewHeight = rowHeight;
1452
- layerEntry.begin.item.nodeHeight = layer._height;
1453
- }
1454
- }
1455
- });
1456
- }
1457
- }
1458
-
1459
- if (tableNode) {
1460
- tableNode._bottomByPage = bottomByPage;
1461
- // If there are page breaks in this row, update data with prevY of last cell
1462
- this._updatePageBreaksData(pageBreaks, tableNode, rowIndex);
1463
- }
1464
-
1465
- return {
1466
- pageBreaksBySpan: pageBreaksByRowSpan,
1467
- pageBreaks: pageBreaks,
1468
- positions: positions
1469
- };
1470
-
1471
- function storePageBreakClosure(data) {
1472
- const startsRowSpan = cell.rowSpan && cell.rowSpan > 1;
1473
- if (startsRowSpan) {
1474
- data.rowSpan = cell.rowSpan;
1475
- }
1476
- data.rowIndex = rowIndex;
1477
- self._storePageBreakData(data, startsRowSpan, pageBreaks, tableNode);
1478
- }
1479
-
1480
- };
1481
-
1482
- // lists
1483
- LayoutBuilder.prototype.processList = function (orderedList, node) {
1484
- var self = this,
1485
- items = orderedList ? node.ol : node.ul,
1486
- gapSize = node._gapSize;
1487
-
1488
- this.writer.context().addMargin(gapSize.width);
1489
-
1490
- var nextMarker;
1491
- this.tracker.auto('lineAdded', addMarkerToFirstLeaf, function () {
1492
- items.forEach(function (item) {
1493
- nextMarker = item.listMarker;
1494
- self.processNode(item);
1495
- addAll(node.positions, item.positions);
1496
- });
1497
- });
1498
-
1499
- this.writer.context().addMargin(-gapSize.width);
1500
-
1501
- function addMarkerToFirstLeaf(line) {
1502
- // I'm not very happy with the way list processing is implemented
1503
- // (both code and algorithm should be rethinked)
1504
- if (nextMarker) {
1505
- var marker = nextMarker;
1506
- nextMarker = null;
1507
-
1508
- if (marker.canvas) {
1509
- var vector = marker.canvas[0];
1510
-
1511
- offsetVector(vector, -marker._minWidth, 0);
1512
- self.writer.addVector(vector);
1513
- } else if (marker._inlines) {
1514
- var markerLine = new Line(self.pageSize.width);
1515
- markerLine.addInline(marker._inlines[0]);
1516
- markerLine.x = -marker._minWidth;
1517
- markerLine.y = line.getAscenderHeight() - markerLine.getAscenderHeight();
1518
- self.writer.addLine(markerLine, true);
1519
- }
1520
- }
1521
- }
1522
- };
1523
-
1524
- // tables
1525
- LayoutBuilder.prototype.processTable = function (tableNode) {
1526
- this.nestedLevel++;
1527
- var processor = new TableProcessor(tableNode);
1528
-
1529
- // Store processor reference for forced page break vertical line drawing
1530
- tableNode._tableProcessor = processor;
1531
-
1532
- processor.beginTable(this.writer);
1533
-
1534
- var rowHeights = tableNode.table.heights;
1535
- for (var i = 0, l = tableNode.table.body.length; i < l; i++) {
1536
- // if dontBreakRows and row starts a rowspan
1537
- // we store the 'y' of the beginning of each rowSpan
1538
- if (processor.dontBreakRows) {
1539
- tableNode.table.body[i].forEach(cell => {
1540
- if (cell.rowSpan && cell.rowSpan > 1) {
1541
- cell._startingRowSpanY = this.writer.context().y;
1542
- }
1543
- });
1544
- }
1545
-
1546
- processor.beginRow(i, this.writer);
1547
-
1548
- var height;
1549
- if (isFunction(rowHeights)) {
1550
- height = rowHeights(i);
1551
- } else if (isArray(rowHeights)) {
1552
- height = rowHeights[i];
1553
- } else {
1554
- height = rowHeights;
1555
- }
1556
-
1557
- if (height === 'auto') {
1558
- height = undefined;
1559
- }
1560
-
1561
- var heightOffset = tableNode.heightOffset != undefined ? tableNode.heightOffset : 0;
1562
-
1563
- var pageBeforeProcessing = this.writer.context().page;
1564
-
1565
- var result = this.processRow({
1566
- marginX: tableNode._margin ? [tableNode._margin[0], tableNode._margin[2]] : [0, 0],
1567
- dontBreakRows: processor.dontBreakRows,
1568
- rowsWithoutPageBreak: processor.rowsWithoutPageBreak,
1569
- cells: tableNode.table.body[i],
1570
- widths: tableNode.table.widths,
1571
- gaps: tableNode._offsets.offsets,
1572
- tableBody: tableNode.table.body,
1573
- tableNode,
1574
- rowIndex: i,
1575
- height,
1576
- heightOffset
1577
- });
1578
- addAll(tableNode.positions, result.positions);
1579
-
1580
- if (!result.pageBreaks || result.pageBreaks.length === 0) {
1581
- var breaksBySpan = tableNode && tableNode._breaksBySpan || null;
1582
- var breakBySpanData = this._findSameRowPageBreakByRowSpanData(breaksBySpan, pageBeforeProcessing, i);
1583
- if (breakBySpanData) {
1584
- var finalBreakBySpanData = this._getPageBreakListBySpan(tableNode, breakBySpanData.prevPage, i);
1585
- result.pageBreaks.push(finalBreakBySpanData);
1586
- }
1587
- }
1588
-
1589
- // Get next row cells for look-ahead page break detection
1590
- var nextRowCells = (i + 1 < tableNode.table.body.length) ? tableNode.table.body[i + 1] : null;
1591
-
1592
- processor.endRow(i, this.writer, result.pageBreaks, nextRowCells, this);
1593
- }
1594
-
1595
- processor.endTable(this.writer);
1596
- this.nestedLevel--;
1597
- if (this.nestedLevel === 0) {
1598
- this.writer.context().resetMarginXTopParent();
1599
- }
1600
- };
1601
-
1602
- // leafs (texts)
1603
- LayoutBuilder.prototype.processLeaf = function (node) {
1604
- var line = this.buildNextLine(node);
1605
- if (line && (node.tocItem || node.id)) {
1606
- line._node = node;
1607
- }
1608
- var currentHeight = (line) ? line.getHeight() : 0;
1609
- var maxHeight = node.maxHeight || -1;
1610
-
1611
- if (line) {
1612
- var nodeId = getNodeId(node);
1613
- if (nodeId) {
1614
- line.id = nodeId;
1615
- }
1616
- }
1617
-
1618
- if (node._tocItemRef) {
1619
- line._pageNodeRef = node._tocItemRef;
1620
- }
1621
-
1622
- if (node._pageRef) {
1623
- line._pageNodeRef = node._pageRef._nodeRef;
1624
- }
1625
-
1626
- if (line && line.inlines && isArray(line.inlines)) {
1627
- for (var i = 0, l = line.inlines.length; i < l; i++) {
1628
- if (line.inlines[i]._tocItemRef) {
1629
- line.inlines[i]._pageNodeRef = line.inlines[i]._tocItemRef;
1630
- }
1631
-
1632
- if (line.inlines[i]._pageRef) {
1633
- line.inlines[i]._pageNodeRef = line.inlines[i]._pageRef._nodeRef;
1634
- }
1635
- }
1636
- }
1637
-
1638
- while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
1639
- var positions = this.writer.addLine(line);
1640
- node.positions.push(positions);
1641
- line = this.buildNextLine(node);
1642
- if (line) {
1643
- currentHeight += line.getHeight();
1644
- }
1645
- }
1646
- };
1647
-
1648
- LayoutBuilder.prototype.processToc = function (node) {
1649
- if (node.toc.title) {
1650
- this.processNode(node.toc.title);
1651
- }
1652
- if (node.toc._table) {
1653
- this.processNode(node.toc._table);
1654
- }
1655
- };
1656
-
1657
- LayoutBuilder.prototype.buildNextLine = function (textNode) {
1658
-
1659
- function cloneInline(inline) {
1660
- var newInline = inline.constructor();
1661
- for (var key in inline) {
1662
- newInline[key] = inline[key];
1663
- }
1664
- return newInline;
1665
- }
1666
-
1667
- if (!textNode._inlines || textNode._inlines.length === 0) {
1668
- return null;
1669
- }
1670
-
1671
- var line = new Line(this.writer.context().availableWidth);
1672
- var textTools = new TextTools(null);
1673
-
1674
- while (textNode._inlines && textNode._inlines.length > 0 && line.hasEnoughSpaceForInline(textNode._inlines[0])) {
1675
- var inline = textNode._inlines.shift();
1676
-
1677
- if (!inline.noWrap && inline.text.length > 1 && inline.width > line.maxWidth) {
1678
- var widthPerChar = inline.width / inline.text.length;
1679
- var maxChars = Math.floor(line.maxWidth / widthPerChar);
1680
- if (maxChars < 1) {
1681
- maxChars = 1;
1682
- }
1683
- if (maxChars < inline.text.length) {
1684
- var newInline = cloneInline(inline);
1685
-
1686
- newInline.text = inline.text.substr(maxChars);
1687
- inline.text = inline.text.substr(0, maxChars);
1688
-
1689
- newInline.width = textTools.widthOfString(newInline.text, newInline.font, newInline.fontSize, newInline.characterSpacing);
1690
- inline.width = textTools.widthOfString(inline.text, inline.font, inline.fontSize, inline.characterSpacing);
1691
-
1692
- textNode._inlines.unshift(newInline);
1693
- }
1694
- }
1695
-
1696
- line.addInline(inline);
1697
- }
1698
-
1699
- line.lastLineInParagraph = textNode._inlines.length === 0;
1700
-
1701
- return line;
1702
- };
1703
-
1704
- // images
1705
- LayoutBuilder.prototype.processImage = function (node) {
1706
- var position = this.writer.addImage(node);
1707
- node.positions.push(position);
1708
- };
1709
-
1710
- LayoutBuilder.prototype.processSVG = function (node) {
1711
- var position = this.writer.addSVG(node);
1712
- node.positions.push(position);
1713
- };
1714
-
1715
- LayoutBuilder.prototype.processCanvas = function (node) {
1716
- var height = node._minHeight;
1717
-
1718
- if (node.absolutePosition === undefined && this.writer.context().availableHeight < height) {
1719
- // TODO: support for canvas larger than a page
1720
- // TODO: support for other overflow methods
1721
-
1722
- this.writer.moveToNextPage();
1723
- }
1724
-
1725
- this.writer.alignCanvas(node);
1726
-
1727
- node.canvas.forEach(function (vector) {
1728
- var position = this.writer.addVector(vector);
1729
- node.positions.push(position);
1730
- }, this);
1731
-
1732
- this.writer.context().moveDown(height);
1733
- };
1734
-
1735
- LayoutBuilder.prototype.processQr = function (node) {
1736
- var position = this.writer.addQr(node);
1737
- node.positions.push(position);
1738
- };
1739
-
1740
- function processNode_test(node) {
1741
- decorateNode(node);
1742
-
1743
- var prevTop = testWriter.context().getCurrentPosition().top;
1744
-
1745
- applyMargins(function () {
1746
- var unbreakable = node.unbreakable;
1747
- if (unbreakable) {
1748
- testWriter.beginUnbreakableBlock();
1749
- }
1750
-
1751
- var absPosition = node.absolutePosition;
1752
- if (absPosition) {
1753
- testWriter.context().beginDetachedBlock();
1754
- testWriter.context().moveTo(absPosition.x || 0, absPosition.y || 0);
1755
- }
1756
-
1757
- var relPosition = node.relativePosition;
1758
- if (relPosition) {
1759
- testWriter.context().beginDetachedBlock();
1760
- if (typeof testWriter.context().moveToRelative === 'function') {
1761
- testWriter.context().moveToRelative(relPosition.x || 0, relPosition.y || 0);
1762
- } else if (currentLayoutBuilder && currentLayoutBuilder.writer) {
1763
- testWriter.context().moveTo(
1764
- (relPosition.x || 0) + currentLayoutBuilder.writer.context().x,
1765
- (relPosition.y || 0) + currentLayoutBuilder.writer.context().y
1766
- );
1767
- }
1768
- }
1769
-
1770
- var verticalAlignBegin;
1771
- if (node.verticalAlign) {
1772
- verticalAlignBegin = testWriter.beginVerticalAlign(node.verticalAlign);
1773
- }
1774
-
1775
- if (node.stack) {
1776
- processVerticalContainer_test(node);
1777
- } else if (node.table) {
1778
- processTable_test(node);
1779
- } else if (node.text !== undefined) {
1780
- processLeaf_test(node);
1781
- }
1782
-
1783
- if (absPosition || relPosition) {
1784
- testWriter.context().endDetachedBlock();
1785
- }
1786
-
1787
- if (unbreakable) {
1788
- testResult = testWriter.commitUnbreakableBlock_test();
1789
- }
1790
-
1791
- if (node.verticalAlign) {
1792
- testVerticalAlignStack.push({ begin: verticalAlignBegin, end: testWriter.endVerticalAlign(node.verticalAlign) });
1793
- }
1794
- });
1795
-
1796
- node._height = testWriter.context().getCurrentPosition().top - prevTop;
1797
-
1798
- function applyMargins(callback) {
1799
- var margin = node._margin;
1800
-
1801
- if (node.pageBreak === 'before') {
1802
- testWriter.moveToNextPage(node.pageOrientation);
1803
- }
1804
-
1805
- if (margin) {
1806
- testWriter.context().moveDown(margin[1]);
1807
- testWriter.context().addMargin(margin[0], margin[2]);
1808
- }
1809
-
1810
- callback();
1811
-
1812
- if (margin) {
1813
- testWriter.context().addMargin(-margin[0], -margin[2]);
1814
- testWriter.context().moveDown(margin[3]);
1815
- }
1816
-
1817
- if (node.pageBreak === 'after') {
1818
- testWriter.moveToNextPage(node.pageOrientation);
1819
- }
1820
- }
1821
- }
1822
-
1823
- function processVerticalContainer_test(node) {
1824
- node.stack.forEach(function (item) {
1825
- processNode_test(item);
1826
- addAll(node.positions, item.positions);
1827
- });
1828
- }
1829
-
1830
- function processTable_test(tableNode) {
1831
- var processor = new TableProcessor(tableNode);
1832
- processor.beginTable(testWriter);
1833
-
1834
- for (var i = 0, l = tableNode.table.body.length; i < l; i++) {
1835
- processor.beginRow(i, testWriter);
1836
- var result = processRow_test(tableNode.table.body[i], tableNode.table.widths, tableNode._offsets ? tableNode._offsets.offsets : null, tableNode.table.body, i);
1837
- addAll(tableNode.positions, result.positions);
1838
- processor.endRow(i, testWriter, result.pageBreaks);
1839
- }
1840
-
1841
- processor.endTable(testWriter);
1842
- }
1843
-
1844
- function processRow_test(columns, widths, gaps, tableBody, tableRow) {
1845
- var pageBreaks = [];
1846
- var positions = [];
1847
-
1848
- testTracker.auto('pageChanged', storePageBreakData, function () {
1849
- widths = widths || columns;
1850
-
1851
- testWriter.context().beginColumnGroup();
1852
-
1853
- var verticalAlignCols = {};
1854
-
1855
- for (var i = 0, l = columns.length; i < l; i++) {
1856
- var column = columns[i];
1857
- var width = widths[i]._calcWidth || widths[i];
1858
- var leftOffset = colLeftOffset(i);
1859
- var colIndex = i;
1860
- if (column.colSpan && column.colSpan > 1) {
1861
- for (var j = 1; j < column.colSpan; j++) {
1862
- width += (widths[++i]._calcWidth || widths[i]) + (gaps ? gaps[i] : 0);
1863
- }
1864
- }
1865
-
1866
- testWriter.context().beginColumn(width, leftOffset, getEndingCell(column, i));
1867
-
1868
- if (!column._span) {
1869
- processNode_test(column);
1870
- verticalAlignCols[colIndex] = testVerticalAlignStack.length - 1;
1871
- addAll(positions, column.positions);
1872
- } else if (column._columnEndingContext) {
1873
- testWriter.context().markEnding(column);
1874
- }
1875
- }
1876
-
1877
- testWriter.context().completeColumnGroup();
1878
-
1879
- var rowHeight = testWriter.context().height;
1880
- for (var c = 0, clen = columns.length; c < clen; c++) {
1881
- var col = columns[c];
1882
- if (col._span) {
1883
- continue;
1884
- }
1885
- if (col.verticalAlign && verticalAlignCols[c] !== undefined) {
1886
- var alignItem = testVerticalAlignStack[verticalAlignCols[c]].begin.item;
1887
- alignItem.viewHeight = rowHeight;
1888
- alignItem.nodeHeight = col._height;
1889
- }
1890
- }
1891
- });
1892
-
1893
- return { pageBreaks: pageBreaks, positions: positions };
1894
-
1895
- function storePageBreakData(data) {
1896
- var pageDesc;
1897
- for (var idx = 0, len = pageBreaks.length; idx < len; idx++) {
1898
- var desc = pageBreaks[idx];
1899
- if (desc.prevPage === data.prevPage) {
1900
- pageDesc = desc;
1901
- break;
1902
- }
1903
- }
1904
-
1905
- if (!pageDesc) {
1906
- pageDesc = data;
1907
- pageBreaks.push(pageDesc);
1908
- }
1909
- pageDesc.prevY = Math.max(pageDesc.prevY, data.prevY);
1910
- pageDesc.y = Math.min(pageDesc.y, data.y);
1911
- }
1912
-
1913
- function colLeftOffset(i) {
1914
- if (gaps && gaps.length > i) {
1915
- return gaps[i];
1916
- }
1917
- return 0;
1918
- }
1919
-
1920
- function getEndingCell(column, columnIndex) {
1921
- if (column.rowSpan && column.rowSpan > 1) {
1922
- var endingRow = tableRow + column.rowSpan - 1;
1923
- if (endingRow >= tableBody.length) {
1924
- throw new Error('Row span for column ' + columnIndex + ' (with indexes starting from 0) exceeded row count');
1925
- }
1926
- return tableBody[endingRow][columnIndex];
1927
- }
1928
- return null;
1929
- }
1930
- }
1931
-
1932
- function processLeaf_test(node) {
1933
- var line = buildNextLine_test(node);
1934
- var currentHeight = line ? line.getHeight() : 0;
1935
- var maxHeight = node.maxHeight || -1;
1936
-
1937
- while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
1938
- var positions = testWriter.addLine(line);
1939
- node.positions.push(positions);
1940
- line = buildNextLine_test(node);
1941
- if (line) {
1942
- currentHeight += line.getHeight();
1943
- }
1944
- }
1945
- }
1946
-
1947
- function buildNextLine_test(textNode) {
1948
- function cloneInline(inline) {
1949
- var newInline = inline.constructor();
1950
- for (var key in inline) {
1951
- newInline[key] = inline[key];
1952
- }
1953
- return newInline;
1954
- }
1955
-
1956
- if (!textNode._inlines || textNode._inlines.length === 0) {
1957
- return null;
1958
- }
1959
-
1960
- var line = new Line(testWriter.context().availableWidth);
1961
- var textTools = new TextTools(null);
1962
-
1963
- while (textNode._inlines && textNode._inlines.length > 0 && line.hasEnoughSpaceForInline(textNode._inlines[0])) {
1964
- var inline = textNode._inlines.shift();
1965
-
1966
- if (!inline.noWrap && inline.text.length > 1 && inline.width > line.maxWidth) {
1967
- var widthPerChar = inline.width / inline.text.length;
1968
- var maxChars = Math.floor(line.maxWidth / widthPerChar);
1969
- if (maxChars < 1) {
1970
- maxChars = 1;
1971
- }
1972
- if (maxChars < inline.text.length) {
1973
- var newInline = cloneInline(inline);
1974
-
1975
- newInline.text = inline.text.substr(maxChars);
1976
- inline.text = inline.text.substr(0, maxChars);
1977
-
1978
- newInline.width = textTools.widthOfString(newInline.text, newInline.font, newInline.fontSize, newInline.characterSpacing);
1979
- inline.width = textTools.widthOfString(inline.text, inline.font, inline.fontSize, inline.characterSpacing);
1980
-
1981
- textNode._inlines.unshift(newInline);
1982
- }
1983
- }
1984
-
1985
- line.addInline(inline);
1986
- }
1987
-
1988
- line.lastLineInParagraph = textNode._inlines.length === 0;
1989
-
1990
- return line;
1991
- }
1992
-
1993
- module.exports = LayoutBuilder;
1
+ 'use strict';
2
+
3
+ var cloneDeep = require('lodash/cloneDeep');
4
+ var TraversalTracker = require('./traversalTracker');
5
+ var DocPreprocessor = require('./docPreprocessor');
6
+ var DocMeasure = require('./docMeasure');
7
+ var DocumentContext = require('./documentContext');
8
+ var PageElementWriter = require('./pageElementWriter');
9
+ var ColumnCalculator = require('./columnCalculator');
10
+ var TableProcessor = require('./tableProcessor');
11
+ var Line = require('./line');
12
+ var isString = require('./helpers').isString;
13
+ var isArray = require('./helpers').isArray;
14
+ var isUndefined = require('./helpers').isUndefined;
15
+ var isNull = require('./helpers').isNull;
16
+ var pack = require('./helpers').pack;
17
+ var offsetVector = require('./helpers').offsetVector;
18
+ var fontStringify = require('./helpers').fontStringify;
19
+ var getNodeId = require('./helpers').getNodeId;
20
+ var isFunction = require('./helpers').isFunction;
21
+ var TextTools = require('./textTools');
22
+ var StyleContextStack = require('./styleContextStack');
23
+ var isNumber = require('./helpers').isNumber;
24
+
25
+ var footerBreak = false;
26
+ var testTracker;
27
+ var testWriter;
28
+ var testVerticalAlignStack = [];
29
+ var testResult = false;
30
+ var currentLayoutBuilder;
31
+
32
+ function addAll(target, otherArray) {
33
+ if (!isArray(target) || !isArray(otherArray) || otherArray.length === 0) {
34
+ return;
35
+ }
36
+
37
+ otherArray.forEach(function (item) {
38
+ target.push(item);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Creates an instance of LayoutBuilder - layout engine which turns document-definition-object
44
+ * into a set of pages, lines, inlines and vectors ready to be rendered into a PDF
45
+ *
46
+ * @param {Object} pageSize - an object defining page width and height
47
+ * @param {Object} pageMargins - an object defining top, left, right and bottom margins
48
+ */
49
+ function LayoutBuilder(pageSize, pageMargins, imageMeasure, svgMeasure) {
50
+ this.pageSize = pageSize;
51
+ this.pageMargins = pageMargins;
52
+ this.tracker = new TraversalTracker();
53
+ this.imageMeasure = imageMeasure;
54
+ this.svgMeasure = svgMeasure;
55
+ this.tableLayouts = {};
56
+ this.nestedLevel = 0;
57
+ this.verticalAlignItemStack = [];
58
+ this.heightHeaderAndFooter = {};
59
+
60
+ this._footerColumnGuides = null;
61
+ this._footerGapOption = null;
62
+ }
63
+
64
+ LayoutBuilder.prototype.registerTableLayouts = function (tableLayouts) {
65
+ this.tableLayouts = pack(this.tableLayouts, tableLayouts);
66
+ };
67
+
68
+ /**
69
+ * Executes layout engine on document-definition-object and creates an array of pages
70
+ * containing positioned Blocks, Lines and inlines
71
+ *
72
+ * @param {Object} docStructure document-definition-object
73
+ * @param {Object} fontProvider font provider
74
+ * @param {Object} styleDictionary dictionary with style definitions
75
+ * @param {Object} defaultStyle default style definition
76
+ * @return {Array} an array of pages
77
+ */
78
+ LayoutBuilder.prototype.layoutDocument = function (docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark, pageBreakBeforeFct) {
79
+
80
+ function addPageBreaksIfNecessary(linearNodeList, pages) {
81
+
82
+ if (!isFunction(pageBreakBeforeFct)) {
83
+ return false;
84
+ }
85
+
86
+ linearNodeList = linearNodeList.filter(function (node) {
87
+ return node.positions.length > 0;
88
+ });
89
+
90
+ linearNodeList.forEach(function (node) {
91
+ var nodeInfo = {};
92
+ [
93
+ 'id', 'text', 'ul', 'ol', 'table', 'image', 'qr', 'canvas', 'svg', 'columns', 'layers',
94
+ 'headlineLevel', 'style', 'pageBreak', 'pageOrientation',
95
+ 'width', 'height'
96
+ ].forEach(function (key) {
97
+ if (node[key] !== undefined) {
98
+ nodeInfo[key] = node[key];
99
+ }
100
+ });
101
+ nodeInfo.startPosition = node.positions[0];
102
+ nodeInfo.pageNumbers = Array.from(new Set(node.positions.map(function (node) { return node.pageNumber; })));
103
+ nodeInfo.pages = pages.length;
104
+ nodeInfo.stack = isArray(node.stack);
105
+ nodeInfo.layers = isArray(node.layers);
106
+
107
+ node.nodeInfo = nodeInfo;
108
+ });
109
+
110
+ for (var index = 0; index < linearNodeList.length; index++) {
111
+ var node = linearNodeList[index];
112
+ if (node.pageBreak !== 'before' && !node.pageBreakCalculated) {
113
+ node.pageBreakCalculated = true;
114
+ var pageNumber = node.nodeInfo.pageNumbers[0];
115
+ var followingNodesOnPage = [];
116
+ var nodesOnNextPage = [];
117
+ var previousNodesOnPage = [];
118
+ if (pageBreakBeforeFct.length > 1) {
119
+ for (var ii = index + 1, l = linearNodeList.length; ii < l; ii++) {
120
+ if (linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
121
+ followingNodesOnPage.push(linearNodeList[ii].nodeInfo);
122
+ }
123
+ if (pageBreakBeforeFct.length > 2 && linearNodeList[ii].nodeInfo.pageNumbers.indexOf(pageNumber + 1) > -1) {
124
+ nodesOnNextPage.push(linearNodeList[ii].nodeInfo);
125
+ }
126
+ }
127
+ }
128
+ if (pageBreakBeforeFct.length > 3) {
129
+ for (var jj = 0; jj < index; jj++) {
130
+ if (linearNodeList[jj].nodeInfo.pageNumbers.indexOf(pageNumber) > -1) {
131
+ previousNodesOnPage.push(linearNodeList[jj].nodeInfo);
132
+ }
133
+ }
134
+ }
135
+ if (pageBreakBeforeFct(node.nodeInfo, followingNodesOnPage, nodesOnNextPage, previousNodesOnPage)) {
136
+ node.pageBreak = 'before';
137
+ return true;
138
+ }
139
+ }
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ this.docPreprocessor = new DocPreprocessor();
146
+ this.docMeasure = new DocMeasure(fontProvider, styleDictionary, defaultStyle, this.imageMeasure, this.svgMeasure, this.tableLayouts, images);
147
+
148
+
149
+ function resetXYs(result) {
150
+ result.linearNodeList.forEach(function (node) {
151
+ node.resetXY();
152
+ });
153
+ }
154
+
155
+ var result = this.tryLayoutDocument(docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark);
156
+ while (addPageBreaksIfNecessary(result.linearNodeList, result.pages)) {
157
+ resetXYs(result);
158
+ result = this.tryLayoutDocument(docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark);
159
+ }
160
+
161
+ return result.pages;
162
+ };
163
+
164
+ LayoutBuilder.prototype.tryLayoutDocument = function (docStructure, fontProvider, styleDictionary, defaultStyle, background, header, footer, images, watermark) {
165
+ footerBreak = false;
166
+
167
+ this.verticalAlignItemStack = this.verticalAlignItemStack || [];
168
+ this.linearNodeList = [];
169
+ this.writer = new PageElementWriter(
170
+ new DocumentContext(this.pageSize, this.pageMargins, this._footerGapOption), this.tracker);
171
+
172
+ this.heightHeaderAndFooter = this.addHeadersAndFooters(header, footer) || {};
173
+ if (!isUndefined(this.heightHeaderAndFooter.header)) {
174
+ this.pageMargins.top = this.heightHeaderAndFooter.header + 1;
175
+ }
176
+
177
+ if (isArray(docStructure) && docStructure[2] && isArray(docStructure[2]) && docStructure[2][0] && docStructure[2][0].remark) {
178
+ var tableRemark = docStructure[2][0].remark;
179
+ var remarkLabel = docStructure[2][0];
180
+ var remarkDetail = docStructure[2][1] && docStructure[2][1].text;
181
+
182
+ docStructure[2].splice(0, 1);
183
+ if (docStructure[2].length > 0) {
184
+ docStructure[2].splice(0, 1);
185
+ }
186
+
187
+ var labelRow = [];
188
+ var detailRow = [];
189
+
190
+ labelRow.push(remarkLabel);
191
+ detailRow.push({ remarktest: true, text: remarkDetail });
192
+
193
+ tableRemark.table.body.push(labelRow);
194
+ tableRemark.table.body.push(detailRow);
195
+
196
+ tableRemark.table.headerRows = 1;
197
+
198
+ docStructure[2].push(tableRemark);
199
+ }
200
+
201
+ this.linearNodeList = [];
202
+ docStructure = this.docPreprocessor.preprocessDocument(docStructure);
203
+ docStructure = this.docMeasure.measureDocument(docStructure);
204
+
205
+ this.verticalAlignItemStack = [];
206
+ this.writer = new PageElementWriter(
207
+ new DocumentContext(this.pageSize, this.pageMargins, this._footerGapOption), this.tracker);
208
+
209
+ var _this = this;
210
+ this.writer.context().tracker.startTracking('pageAdded', function () {
211
+ _this.addBackground(background);
212
+ });
213
+
214
+ this.addBackground(background);
215
+ this.processNode(docStructure);
216
+ this.addHeadersAndFooters(header, footer,
217
+ (this.heightHeaderAndFooter.header || 0) + 1,
218
+ (this.heightHeaderAndFooter.footer || 0) + 1);
219
+ if (watermark != null) {
220
+ this.addWatermark(watermark, fontProvider, defaultStyle);
221
+ }
222
+
223
+ return { pages: this.writer.context().pages, linearNodeList: this.linearNodeList };
224
+ };
225
+
226
+ LayoutBuilder.prototype.applyFooterGapOption = function(opt) {
227
+ if (opt === true) {
228
+ opt = { enabled: true };
229
+ }
230
+
231
+ if (!opt) return;
232
+
233
+ if (typeof opt !== 'object') {
234
+ this._footerGapOption = { enabled: true, forcePageBreakForAllRows: false };
235
+ return;
236
+ }
237
+
238
+ this._footerGapOption = {
239
+ enabled: opt.enabled !== false,
240
+ minRowHeight: typeof opt.minRowHeight === 'number' ? opt.minRowHeight : undefined, // Optional fallback - system auto-calculates from cell content if not provided
241
+ forcePageBreakForAllRows: opt.forcePageBreakForAllRows === true, // Force page break for all rows (not just inline images)
242
+ columns: opt.columns ? {
243
+ widths: Array.isArray(opt.columns.widths) ? opt.columns.widths.slice() : undefined,
244
+ widthLength: opt.columns.widths.length || 0,
245
+ stops: Array.isArray(opt.columns.stops) ? opt.columns.stops.slice() : undefined,
246
+ style: opt.columns.style ? Object.assign({}, opt.columns.style) : {},
247
+ includeOuter: opt.columns.includeOuter !== false
248
+ } : null
249
+ };
250
+ };
251
+
252
+ LayoutBuilder.prototype.addBackground = function (background) {
253
+ var backgroundGetter = isFunction(background) ? background : function () {
254
+ return background;
255
+ };
256
+
257
+ var context = this.writer.context();
258
+ var pageSize = context.getCurrentPage().pageSize;
259
+
260
+ var pageBackground = backgroundGetter(context.page + 1, pageSize);
261
+
262
+ if (pageBackground) {
263
+ this.writer.beginUnbreakableBlock(pageSize.width, pageSize.height);
264
+ pageBackground = this.docPreprocessor.preprocessDocument(pageBackground);
265
+ this.processNode(this.docMeasure.measureDocument(pageBackground));
266
+ this.writer.commitUnbreakableBlock(0, 0);
267
+ context.backgroundLength[context.page] += pageBackground.positions.length;
268
+ }
269
+ };
270
+
271
+ LayoutBuilder.prototype.addStaticRepeatable = function (headerOrFooter, sizeFunction) {
272
+ return this.addDynamicRepeatable(function () {
273
+ return JSON.parse(JSON.stringify(headerOrFooter)); // copy to new object
274
+ }, sizeFunction);
275
+ };
276
+
277
+ LayoutBuilder.prototype.addDynamicRepeatable = function (nodeGetter, sizeFunction) {
278
+ var pages = this.writer.context().pages;
279
+ var measuredHeight;
280
+
281
+ for (var pageIndex = 0, l = pages.length; pageIndex < l; pageIndex++) {
282
+ this.writer.context().page = pageIndex;
283
+
284
+ var node = nodeGetter(pageIndex + 1, l, this.writer.context().pages[pageIndex].pageSize);
285
+
286
+ if (node) {
287
+ var sizes = sizeFunction(this.writer.context().getCurrentPage().pageSize, this.pageMargins);
288
+ this.writer.beginUnbreakableBlock(sizes.width, sizes.height);
289
+ node = this.docPreprocessor.preprocessDocument(node);
290
+ this.processNode(this.docMeasure.measureDocument(node));
291
+ this.writer.commitUnbreakableBlock(sizes.x, sizes.y);
292
+ if (!isUndefined(node._height)) {
293
+ measuredHeight = node._height;
294
+ }
295
+ }
296
+ }
297
+
298
+ return measuredHeight;
299
+ };
300
+
301
+ LayoutBuilder.prototype.addHeadersAndFooters = function (header, footer, headerHeight, footerHeight) {
302
+ var measured = { header: undefined, footer: undefined };
303
+
304
+ var headerSizeFct = function (pageSize) {
305
+ var effectiveHeight = headerHeight;
306
+ if (isUndefined(effectiveHeight)) {
307
+ effectiveHeight = pageSize.height;
308
+ }
309
+ return {
310
+ x: 0,
311
+ y: 0,
312
+ width: pageSize.width,
313
+ height: effectiveHeight
314
+ };
315
+ };
316
+
317
+ var footerSizeFct = function (pageSize) {
318
+ var effectiveHeight = footerHeight;
319
+ if (isUndefined(effectiveHeight)) {
320
+ effectiveHeight = pageSize.height;
321
+ }
322
+ return {
323
+ x: 0,
324
+ y: pageSize.height - effectiveHeight,
325
+ width: pageSize.width,
326
+ height: effectiveHeight
327
+ };
328
+ };
329
+
330
+ if (this._footerGapOption && !this.writer.context()._footerGapOption) {
331
+ this.writer.context()._footerGapOption = this._footerGapOption;
332
+ }
333
+
334
+ if (isFunction(header)) {
335
+ measured.header = this.addDynamicRepeatable(header, headerSizeFct);
336
+ } else if (header) {
337
+ measured.header = this.addStaticRepeatable(header, headerSizeFct);
338
+ }
339
+
340
+ if (isFunction(footer)) {
341
+ measured.footer = this.addDynamicRepeatable(footer, footerSizeFct);
342
+ } else if (footer) {
343
+ measured.footer = this.addStaticRepeatable(footer, footerSizeFct);
344
+ }
345
+
346
+ return measured;
347
+ };
348
+
349
+ LayoutBuilder.prototype.addWatermark = function (watermark, fontProvider, defaultStyle) {
350
+ if (isString(watermark)) {
351
+ watermark = { 'text': watermark };
352
+ }
353
+
354
+ if (!watermark.text) { // empty watermark text
355
+ return;
356
+ }
357
+
358
+ var pages = this.writer.context().pages;
359
+ for (var i = 0, l = pages.length; i < l; i++) {
360
+ pages[i].watermark = getWatermarkObject({ ...watermark }, pages[i].pageSize, fontProvider, defaultStyle);
361
+ }
362
+
363
+ function getWatermarkObject(watermark, pageSize, fontProvider, defaultStyle) {
364
+ watermark.font = watermark.font || defaultStyle.font || 'Roboto';
365
+ watermark.fontSize = watermark.fontSize || 'auto';
366
+ watermark.color = watermark.color || 'black';
367
+ watermark.opacity = isNumber(watermark.opacity) ? watermark.opacity : 0.6;
368
+ watermark.bold = watermark.bold || false;
369
+ watermark.italics = watermark.italics || false;
370
+ watermark.angle = !isUndefined(watermark.angle) && !isNull(watermark.angle) ? watermark.angle : null;
371
+
372
+ if (watermark.angle === null) {
373
+ watermark.angle = Math.atan2(pageSize.height, pageSize.width) * -180 / Math.PI;
374
+ }
375
+
376
+ if (watermark.fontSize === 'auto') {
377
+ watermark.fontSize = getWatermarkFontSize(pageSize, watermark, fontProvider);
378
+ }
379
+
380
+ var watermarkObject = {
381
+ text: watermark.text,
382
+ font: fontProvider.provideFont(watermark.font, watermark.bold, watermark.italics),
383
+ fontSize: watermark.fontSize,
384
+ color: watermark.color,
385
+ opacity: watermark.opacity,
386
+ angle: watermark.angle
387
+ };
388
+
389
+ watermarkObject._size = getWatermarkSize(watermark, fontProvider);
390
+
391
+ return watermarkObject;
392
+ }
393
+
394
+ function getWatermarkSize(watermark, fontProvider) {
395
+ var textTools = new TextTools(fontProvider);
396
+ var styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
397
+
398
+ styleContextStack.push({
399
+ fontSize: watermark.fontSize
400
+ });
401
+
402
+ var size = textTools.sizeOfString(watermark.text, styleContextStack);
403
+ var rotatedSize = textTools.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
404
+
405
+ return { size: size, rotatedSize: rotatedSize };
406
+ }
407
+
408
+ function getWatermarkFontSize(pageSize, watermark, fontProvider) {
409
+ var textTools = new TextTools(fontProvider);
410
+ var styleContextStack = new StyleContextStack(null, { font: watermark.font, bold: watermark.bold, italics: watermark.italics });
411
+ var rotatedSize;
412
+
413
+ /**
414
+ * Binary search the best font size.
415
+ * Initial bounds [0, 1000]
416
+ * Break when range < 1
417
+ */
418
+ var a = 0;
419
+ var b = 1000;
420
+ var c = (a + b) / 2;
421
+ while (Math.abs(a - b) > 1) {
422
+ styleContextStack.push({
423
+ fontSize: c
424
+ });
425
+ rotatedSize = textTools.sizeOfRotatedText(watermark.text, watermark.angle, styleContextStack);
426
+ if (rotatedSize.width > pageSize.width) {
427
+ b = c;
428
+ c = (a + b) / 2;
429
+ } else if (rotatedSize.width < pageSize.width) {
430
+ if (rotatedSize.height > pageSize.height) {
431
+ b = c;
432
+ c = (a + b) / 2;
433
+ } else {
434
+ a = c;
435
+ c = (a + b) / 2;
436
+ }
437
+ }
438
+ styleContextStack.pop();
439
+ }
440
+ /*
441
+ End binary search
442
+ */
443
+ return c;
444
+ }
445
+ };
446
+
447
+ function decorateNode(node) {
448
+ var x = node.x, y = node.y;
449
+ node.positions = [];
450
+
451
+ if (isArray(node.canvas)) {
452
+ node.canvas.forEach(function (vector) {
453
+ var x = vector.x, y = vector.y, x1 = vector.x1, y1 = vector.y1, x2 = vector.x2, y2 = vector.y2;
454
+ vector.resetXY = function () {
455
+ vector.x = x;
456
+ vector.y = y;
457
+ vector.x1 = x1;
458
+ vector.y1 = y1;
459
+ vector.x2 = x2;
460
+ vector.y2 = y2;
461
+ };
462
+ });
463
+ }
464
+
465
+ node.resetXY = function () {
466
+ node.x = x;
467
+ node.y = y;
468
+ if (isArray(node.canvas)) {
469
+ node.canvas.forEach(function (vector) {
470
+ vector.resetXY();
471
+ });
472
+ }
473
+ };
474
+ }
475
+
476
+ LayoutBuilder.prototype.processNode = function (node) {
477
+ var self = this;
478
+
479
+ if (footerBreak && (node.footerBreak || node.footer)) {
480
+ return;
481
+ }
482
+
483
+ if (node && node.unbreakable && node.summary && node.table && node.table.body &&
484
+ node.table.body[0] && node.table.body[0][0] && node.table.body[0][0].summaryBreak) {
485
+ testTracker = new TraversalTracker();
486
+ testWriter = new PageElementWriter(self.writer.context(), testTracker);
487
+ testVerticalAlignStack = self.verticalAlignItemStack.slice();
488
+ currentLayoutBuilder = self;
489
+ testResult = false;
490
+ var nodeForTest = cloneDeep(node);
491
+ if (nodeForTest.table.body[0]) {
492
+ nodeForTest.table.body[0].splice(0, 1);
493
+ }
494
+ processNode_test(nodeForTest);
495
+ currentLayoutBuilder = null;
496
+ if (testResult && node.table.body[0]) {
497
+ node.table.body[0].splice(0, 1);
498
+ }
499
+ }
500
+
501
+ this.linearNodeList.push(node);
502
+ decorateNode(node);
503
+
504
+ var prevTop = self.writer.context().getCurrentPosition().top;
505
+
506
+ applyMargins(function () {
507
+ var unbreakable = node.unbreakable;
508
+ if (unbreakable) {
509
+ self.writer.beginUnbreakableBlock();
510
+ }
511
+
512
+ var absPosition = node.absolutePosition;
513
+ if (absPosition) {
514
+ self.writer.context().beginDetachedBlock();
515
+ self.writer.context().moveTo(absPosition.x || 0, absPosition.y || 0);
516
+ }
517
+
518
+ var relPosition = node.relativePosition;
519
+ if (relPosition) {
520
+ self.writer.context().beginDetachedBlock();
521
+ self.writer.context().moveToRelative(relPosition.x || 0, relPosition.y || 0);
522
+ }
523
+
524
+ var verticalAlignBegin;
525
+ if (node.verticalAlign) {
526
+ verticalAlignBegin = self.writer.beginVerticalAlign(node.verticalAlign);
527
+ }
528
+
529
+ if (node.stack) {
530
+ // Handle _footerGapOption for breakable stacks
531
+ var footerGapEnabled = !unbreakable && node.footer && node._footerGapOption && node._footerGapOption.enabled;
532
+ var preRenderState = null;
533
+
534
+ if (footerGapEnabled) {
535
+ var ctx = self.writer.context();
536
+ var currentPage = ctx.getCurrentPage();
537
+ preRenderState = {
538
+ startY: ctx.y,
539
+ startPage: ctx.page,
540
+ availableHeight: ctx.availableHeight,
541
+ itemCount: currentPage ? currentPage.items.length : 0
542
+ };
543
+ }
544
+
545
+ // Process the stack
546
+ self.processVerticalContainer(node);
547
+
548
+ // Post-render repositioning
549
+ if (footerGapEnabled && preRenderState) {
550
+ var ctx = self.writer.context();
551
+
552
+ // Only reposition if on same page (single-page content)
553
+ if (ctx.page === preRenderState.startPage) {
554
+ var renderedHeight = ctx.y - preRenderState.startY;
555
+ var gapHeight = preRenderState.availableHeight - renderedHeight;
556
+
557
+ if (gapHeight > 0) {
558
+ var currentPage = ctx.getCurrentPage();
559
+
560
+ // SINGLE-PAGE: Draw guide lines in the gap area
561
+ var gapTopY = preRenderState.startY;
562
+ var colSpec = node._footerGapOption.columns || self._footerGapOption.columns || null;
563
+ if (colSpec) {
564
+ var rawWidths = colSpec.content.vLines || [];
565
+ if (rawWidths && rawWidths.length > 1) {
566
+ var style = (colSpec.style || {});
567
+ var lw = style.lineWidth != null ? style.lineWidth : 0.5;
568
+ var lc = style.color || '#000000';
569
+ var dashCfg = style.dash;
570
+ var includeOuter = colSpec.includeOuter !== false;
571
+ var startIndex = includeOuter ? 0 : 1;
572
+ var endIndex = includeOuter ? rawWidths.length : rawWidths.length - 1;
573
+
574
+ for (var ci = startIndex; ci < endIndex; ci++) {
575
+ var xGuide = ctx.x + rawWidths[ci] - 0.25;
576
+ currentPage.items.push({
577
+ type: 'vector',
578
+ item: {
579
+ type: 'line',
580
+ x1: xGuide,
581
+ y1: gapTopY,
582
+ x2: xGuide,
583
+ y2: gapTopY + gapHeight,
584
+ lineWidth: lw,
585
+ lineColor: lc,
586
+ dash: dashCfg ? {
587
+ length: dashCfg.length,
588
+ space: dashCfg.space != null ? dashCfg.space : dashCfg.gap
589
+ } : undefined,
590
+ _footerGuideLine: true
591
+ }
592
+ });
593
+ }
594
+ }
595
+ }
596
+
597
+ // Reposition only items that belong to footer stack content
598
+ // Iterate ALL items (backgrounds are inserted at lower indices, not appended)
599
+ // Filter by Y position to exclude non-footer items
600
+ for (var i = 0; i < currentPage.items.length; i++) {
601
+ var item = currentPage.items[i];
602
+
603
+ // Skip the guide lines we just added (they're already positioned correctly)
604
+ if (item.item && item.item._footerGuideLine) {
605
+ continue;
606
+ }
607
+
608
+ // Get the item's starting Y position
609
+ var itemStartY = null;
610
+ if (item.item && typeof item.item.y1 === 'number') {
611
+ itemStartY = item.item.y1;
612
+ } else if (item.item && typeof item.item.y === 'number') {
613
+ itemStartY = item.item.y;
614
+ }
615
+
616
+ // Only reposition items that START at or after footer stack's Y position
617
+ // This excludes outer table border lines that extend from earlier rows
618
+ if (itemStartY !== null && itemStartY < preRenderState.startY) {
619
+ continue;
620
+ }
621
+
622
+ // Reposition this item
623
+ if (item.item && typeof item.item.y === 'number') {
624
+ item.item.y += gapHeight;
625
+ }
626
+ if (item.item && typeof item.item.y1 === 'number') {
627
+ item.item.y1 += gapHeight;
628
+ }
629
+ if (item.item && typeof item.item.y2 === 'number') {
630
+ item.item.y2 += gapHeight;
631
+ }
632
+ // Handle polylines (points array)
633
+ if (item.item && item.item.points && Array.isArray(item.item.points)) {
634
+ for (var pi = 0; pi < item.item.points.length; pi++) {
635
+ if (typeof item.item.points[pi].y === 'number') {
636
+ item.item.points[pi].y += gapHeight;
637
+ }
638
+ }
639
+ }
640
+ }
641
+ ctx.moveDown(gapHeight);
642
+ }
643
+ } else {
644
+ // MULTI-PAGE: Process ALL pages from startPage+1 to current page
645
+ // Only move FOOTER items to bottom, keep remark at top
646
+ var pages = ctx.pages;
647
+ var pageMargins = ctx.pageMargins;
648
+
649
+ // Helper function to recursively find node with _isFooterTable
650
+ function findFooterTableNode(n) {
651
+ if (!n) return null;
652
+ if (n._isFooterTable) return n;
653
+ if (n.stack && Array.isArray(n.stack)) {
654
+ for (var i = 0; i < n.stack.length; i++) {
655
+ var found = findFooterTableNode(n.stack[i]);
656
+ if (found) return found;
657
+ }
658
+ }
659
+ if (Array.isArray(n)) {
660
+ for (var i = 0; i < n.length; i++) {
661
+ var found = findFooterTableNode(n[i]);
662
+ if (found) return found;
663
+ }
664
+ }
665
+ return null;
666
+ }
667
+
668
+ // Find footer table node to get its height
669
+ var footerStackItem = null;
670
+ if (node.stack && node.stack.length > 0) {
671
+ for (var si = node.stack.length - 1; si >= 0; si--) {
672
+ var found = findFooterTableNode(node.stack[si]);
673
+ if (found) {
674
+ footerStackItem = node.stack[si];
675
+ break;
676
+ }
677
+ }
678
+ }
679
+
680
+ var stackHeight = Math.abs((footerStackItem && footerStackItem._height) || 0);
681
+
682
+ // Only process the LAST page where footer is located
683
+ // Intermediate pages (remark only) should not be repositioned
684
+ var pageIdx = ctx.page;
685
+ if (pageIdx > preRenderState.startPage) {
686
+ var page = pages[pageIdx];
687
+ if (page && page.items && page.items.length > 0) {
688
+ var pageHeight = page.pageSize.height;
689
+ var bottomMargin = pageMargins.bottom;
690
+ var pageBottom = pageHeight - bottomMargin;
691
+
692
+ // Find footer start Y on this page (where footer items begin)
693
+ // Use footerHeight to identify footer area from the bottom of content
694
+ var contentBottom = 0;
695
+ for (var i = 0; i < page.items.length; i++) {
696
+ var item = page.items[i];
697
+ var itemBottom = 0;
698
+ if (item.item) {
699
+ if (typeof item.item.y === 'number') {
700
+ var h = item.item.h || item.item.height || 0;
701
+ itemBottom = item.item.y + h;
702
+ }
703
+ if (typeof item.item.y2 === 'number' && item.item.y2 > itemBottom) {
704
+ itemBottom = item.item.y2;
705
+ }
706
+ }
707
+ if (itemBottom > contentBottom) {
708
+ contentBottom = itemBottom;
709
+ }
710
+ }
711
+
712
+ // Footer starts at contentBottom - footerHeight
713
+ var stackStartY = contentBottom - stackHeight;
714
+
715
+ // Calculate gap: how much to move footer down
716
+ var gapHeight = pageBottom - contentBottom;
717
+ if (gapHeight > 0) {
718
+ // Only move items that are part of footer (Y >= footerStartY)
719
+ for (var i = 0; i < page.items.length; i++) {
720
+ var item = page.items[i];
721
+ var itemY = null;
722
+ if (item.item && typeof item.item.y1 === 'number') {
723
+ itemY = item.item.y1;
724
+ } else if (item.item && typeof item.item.y === 'number') {
725
+ itemY = item.item.y;
726
+ } else if (item.item && item.item.points && Array.isArray(item.item.points) && item.item.points.length > 0) {
727
+ // For polylines, use the first point's Y
728
+ itemY = item.item.points[0].y;
729
+ }
730
+
731
+ // Skip items above footer
732
+ if (itemY === null || itemY < stackStartY) continue;
733
+
734
+ // Don't move items that are near the top margin (table headers)
735
+ // Use actual header height from repeatables instead of hardcoded threshold
736
+ var repeatableHeaderHeight = 0;
737
+ for (var ri = 0; ri < self.writer.repeatables.length; ri++) {
738
+ repeatableHeaderHeight += self.writer.repeatables[ri].height;
739
+ }
740
+ var headerThreshold = pageMargins.top + repeatableHeaderHeight;
741
+ if (itemY < headerThreshold) continue;
742
+
743
+ // Move footer item down
744
+ if (item.item && typeof item.item.y === 'number') {
745
+ item.item.y += gapHeight;
746
+ }
747
+ if (item.item && typeof item.item.y1 === 'number') {
748
+ item.item.y1 += gapHeight;
749
+ }
750
+ if (item.item && typeof item.item.y2 === 'number') {
751
+ item.item.y2 += gapHeight;
752
+ }
753
+ // // Handle polylines (points array)
754
+ if (item.item && item.item.points && Array.isArray(item.item.points)) {
755
+ for (var pi = 0; pi < item.item.points.length; pi++) {
756
+ if (typeof item.item.points[pi].y === 'number') {
757
+ item.item.points[pi].y += gapHeight;
758
+ }
759
+ }
760
+ }
761
+ }
762
+ }
763
+ }
764
+ }
765
+
766
+ // Also update context for current page
767
+ var lastPageGapHeight = ctx.availableHeight;
768
+ if (lastPageGapHeight > 0) {
769
+ ctx.moveDown(lastPageGapHeight);
770
+ }
771
+ }
772
+ }
773
+ } else if (node.layers) {
774
+ self.processLayers(node);
775
+ } else if (node.columns) {
776
+ self.processColumns(node);
777
+ } else if (node.ul) {
778
+ self.processList(false, node);
779
+ } else if (node.ol) {
780
+ self.processList(true, node);
781
+ } else if (node.table) {
782
+ self.processTable(node);
783
+ } else if (node.text !== undefined) {
784
+ self.processLeaf(node);
785
+ } else if (node.toc) {
786
+ self.processToc(node);
787
+ } else if (node.image) {
788
+ self.processImage(node);
789
+ } else if (node.svg) {
790
+ self.processSVG(node);
791
+ } else if (node.canvas) {
792
+ self.processCanvas(node);
793
+ } else if (node.qr) {
794
+ self.processQr(node);
795
+ } else if (!node._span) {
796
+ throw new Error('Unrecognized document structure: ' + JSON.stringify(node, fontStringify));
797
+ }
798
+
799
+ if ((absPosition || relPosition) && !node.absoluteRepeatable) {
800
+ self.writer.context().endDetachedBlock();
801
+ }
802
+
803
+ if (unbreakable) {
804
+ if (node.footer) {
805
+ footerBreak = self.writer.commitUnbreakableBlock(undefined, undefined, node.footer);
806
+ } else {
807
+ self.writer.commitUnbreakableBlock();
808
+ }
809
+ }
810
+
811
+ if (node.verticalAlign) {
812
+ var stackEntry = {
813
+ begin: verticalAlignBegin,
814
+ end: self.writer.endVerticalAlign(node.verticalAlign)
815
+ };
816
+ self.verticalAlignItemStack.push(stackEntry);
817
+ node._verticalAlignIdx = self.verticalAlignItemStack.length - 1;
818
+ }
819
+ });
820
+
821
+ node._height = self.writer.context().getCurrentPosition().top - prevTop;
822
+
823
+ function applyMargins(callback) {
824
+ var margin = node._margin;
825
+
826
+ if (node.pageBreak === 'before') {
827
+ self.writer.moveToNextPage(node.pageOrientation);
828
+ }
829
+
830
+ if (margin) {
831
+ self.writer.context().moveDown(margin[1]);
832
+ self.writer.context().addMargin(margin[0], margin[2]);
833
+ }
834
+
835
+ callback();
836
+
837
+ if (margin) {
838
+ self.writer.context().addMargin(-margin[0], -margin[2]);
839
+ self.writer.context().moveDown(margin[3]);
840
+ }
841
+
842
+ if (node.pageBreak === 'after') {
843
+ self.writer.moveToNextPage(node.pageOrientation);
844
+ }
845
+ }
846
+ };
847
+
848
+ // vertical container
849
+ LayoutBuilder.prototype.processVerticalContainer = function (node) {
850
+ var self = this;
851
+ node.stack.forEach(function (item) {
852
+ self.processNode(item);
853
+ addAll(node.positions, item.positions);
854
+
855
+ //TODO: paragraph gap
856
+ });
857
+ };
858
+
859
+ // layers
860
+ LayoutBuilder.prototype.processLayers = function(node) {
861
+ var self = this;
862
+ var ctxX = self.writer.context().x;
863
+ var ctxY = self.writer.context().y;
864
+ var maxX = ctxX;
865
+ var maxY = ctxY;
866
+ node.layers.forEach(function(item, i) {
867
+ self.writer.context().x = ctxX;
868
+ self.writer.context().y = ctxY;
869
+ self.processNode(item);
870
+ item._verticalAlignIdx = self.verticalAlignItemStack.length - 1;
871
+ addAll(node.positions, item.positions);
872
+ maxX = self.writer.context().x > maxX ? self.writer.context().x : maxX;
873
+ maxY = self.writer.context().y > maxY ? self.writer.context().y : maxY;
874
+ });
875
+ self.writer.context().x = maxX;
876
+ self.writer.context().y = maxY;
877
+ };
878
+
879
+ // columns
880
+ LayoutBuilder.prototype.processColumns = function (columnNode) {
881
+ this.nestedLevel++;
882
+ var columns = columnNode.columns;
883
+ var availableWidth = this.writer.context().availableWidth;
884
+ var gaps = gapArray(columnNode._gap);
885
+
886
+ if (gaps) {
887
+ availableWidth -= (gaps.length - 1) * columnNode._gap;
888
+ }
889
+
890
+ ColumnCalculator.buildColumnWidths(columns, availableWidth);
891
+ var result = this.processRow({
892
+ marginX: columnNode._margin ? [columnNode._margin[0], columnNode._margin[2]] : [0, 0],
893
+ cells: columns,
894
+ widths: columns,
895
+ gaps
896
+ });
897
+ addAll(columnNode.positions, result.positions);
898
+
899
+ this.nestedLevel--;
900
+ if (this.nestedLevel === 0) {
901
+ this.writer.context().resetMarginXTopParent();
902
+ }
903
+
904
+ function gapArray(gap) {
905
+ if (!gap) {
906
+ return null;
907
+ }
908
+
909
+ var gaps = [];
910
+ gaps.push(0);
911
+
912
+ for (var i = columns.length - 1; i > 0; i--) {
913
+ gaps.push(gap);
914
+ }
915
+
916
+ return gaps;
917
+ }
918
+ };
919
+
920
+ /**
921
+ * Searches for a cell in the same row that starts a rowspan and is positioned immediately before the current cell.
922
+ * Alternatively, it finds a cell where the colspan initiating the rowspan extends to the cell just before the current one.
923
+ *
924
+ * @param {Array<object>} arr - An array representing cells in a row.
925
+ * @param {number} i - The index of the current cell to search backward from.
926
+ * @returns {object|null} The starting cell of the rowspan if found; otherwise, `null`.
927
+ */
928
+ LayoutBuilder.prototype._findStartingRowSpanCell = function (arr, i) {
929
+ var requiredColspan = 1;
930
+ for (var index = i - 1; index >= 0; index--) {
931
+ if (!arr[index]._span) {
932
+ if (arr[index].rowSpan > 1 && (arr[index].colSpan || 1) === requiredColspan) {
933
+ return arr[index];
934
+ } else {
935
+ return null;
936
+ }
937
+ }
938
+ requiredColspan++;
939
+ }
940
+ return null;
941
+ };
942
+
943
+ /**
944
+ * Retrieves a page break description for a specified page from a list of page breaks.
945
+ *
946
+ * @param {Array<object>} pageBreaks - An array of page break descriptions, each containing `prevPage` properties.
947
+ * @param {number} page - The page number to find the associated page break for.
948
+ * @returns {object|undefined} The page break description object for the specified page if found; otherwise, `undefined`.
949
+ */
950
+ LayoutBuilder.prototype._getPageBreak = function (pageBreaks, page) {
951
+ return pageBreaks.find(desc => desc.prevPage === page);
952
+ };
953
+
954
+ LayoutBuilder.prototype._getPageBreakListBySpan = function (tableNode, page, rowIndex) {
955
+ if (!tableNode || !tableNode._breaksBySpan) {
956
+ return null;
957
+ }
958
+ const breaksList = tableNode._breaksBySpan.filter(desc => desc.prevPage === page && rowIndex <= desc.rowIndexOfSpanEnd);
959
+
960
+ var y = Number.MAX_VALUE,
961
+ prevY = Number.MIN_VALUE;
962
+
963
+ breaksList.forEach(b => {
964
+ prevY = Math.max(b.prevY, prevY);
965
+ y = Math.min(b.y, y);
966
+ });
967
+
968
+ return {
969
+ prevPage: page,
970
+ prevY: prevY,
971
+ y: y
972
+ };
973
+ };
974
+
975
+ LayoutBuilder.prototype._findSameRowPageBreakByRowSpanData = function (breaksBySpan, page, rowIndex) {
976
+ if (!breaksBySpan) {
977
+ return null;
978
+ }
979
+ return breaksBySpan.find(desc => desc.prevPage === page && rowIndex === desc.rowIndexOfSpanEnd);
980
+ };
981
+
982
+ LayoutBuilder.prototype._updatePageBreaksData = function (pageBreaks, tableNode, rowIndex) {
983
+ Object.keys(tableNode._bottomByPage).forEach(p => {
984
+ const page = Number(p);
985
+ const pageBreak = this._getPageBreak(pageBreaks, page);
986
+ if (pageBreak) {
987
+ pageBreak.prevY = Math.max(pageBreak.prevY, tableNode._bottomByPage[page]);
988
+ }
989
+ if (tableNode._breaksBySpan && tableNode._breaksBySpan.length > 0) {
990
+ const breaksBySpanList = tableNode._breaksBySpan.filter(pb => pb.prevPage === page && rowIndex <= pb.rowIndexOfSpanEnd);
991
+ if (breaksBySpanList && breaksBySpanList.length > 0) {
992
+ breaksBySpanList.forEach(b => {
993
+ b.prevY = Math.max(b.prevY, tableNode._bottomByPage[page]);
994
+ });
995
+ }
996
+ }
997
+ });
998
+ };
999
+
1000
+ /**
1001
+ * Resolves the Y-coordinates for a target object by comparing two break points.
1002
+ *
1003
+ * @param {object} break1 - The first break point with `prevY` and `y` properties.
1004
+ * @param {object} break2 - The second break point with `prevY` and `y` properties.
1005
+ * @param {object} target - The target object to be updated with resolved Y-coordinates.
1006
+ * @property {number} target.prevY - Updated to the maximum `prevY` value between `break1` and `break2`.
1007
+ * @property {number} target.y - Updated to the minimum `y` value between `break1` and `break2`.
1008
+ */
1009
+ LayoutBuilder.prototype._resolveBreakY = function (break1, break2, target) {
1010
+ target.prevY = Math.max(break1.prevY, break2.prevY);
1011
+ target.y = Math.min(break1.y, break2.y);
1012
+ };
1013
+
1014
+ LayoutBuilder.prototype._storePageBreakData = function (data, startsRowSpan, pageBreaks, tableNode) {
1015
+ var pageDesc;
1016
+ var pageDescBySpan;
1017
+
1018
+ if (!startsRowSpan) {
1019
+ pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
1020
+ pageDescBySpan = this._getPageBreakListBySpan(tableNode, data.prevPage, data.rowIndex);
1021
+ if (!pageDesc) {
1022
+ pageDesc = Object.assign({}, data);
1023
+ pageBreaks.push(pageDesc);
1024
+ }
1025
+
1026
+ if (pageDescBySpan) {
1027
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
1028
+ }
1029
+ this._resolveBreakY(pageDesc, data, pageDesc);
1030
+ } else {
1031
+ var breaksBySpan = tableNode && tableNode._breaksBySpan || null;
1032
+ pageDescBySpan = this._findSameRowPageBreakByRowSpanData(breaksBySpan, data.prevPage, data.rowIndex);
1033
+ if (!pageDescBySpan) {
1034
+ pageDescBySpan = Object.assign({}, data, {
1035
+ rowIndexOfSpanEnd: data.rowIndex + data.rowSpan - 1
1036
+ });
1037
+ if (!tableNode._breaksBySpan) {
1038
+ tableNode._breaksBySpan = [];
1039
+ }
1040
+ tableNode._breaksBySpan.push(pageDescBySpan);
1041
+ }
1042
+ pageDescBySpan.prevY = Math.max(pageDescBySpan.prevY, data.prevY);
1043
+ pageDescBySpan.y = Math.min(pageDescBySpan.y, data.y);
1044
+ pageDesc = this._getPageBreak(pageBreaks, data.prevPage);
1045
+ if (pageDesc) {
1046
+ this._resolveBreakY(pageDesc, pageDescBySpan, pageDesc);
1047
+ }
1048
+ }
1049
+ };
1050
+
1051
+ /**
1052
+ * Calculates the left offset for a column based on the specified gap values.
1053
+ *
1054
+ * @param {number} i - The index of the column for which the offset is being calculated.
1055
+ * @param {Array<number>} gaps - An array of gap values for each column.
1056
+ * @returns {number} The left offset for the column. Returns `gaps[i]` if it exists, otherwise `0`.
1057
+ */
1058
+ LayoutBuilder.prototype._colLeftOffset = function (i, gaps) {
1059
+ if (gaps && gaps.length > i) {
1060
+ return gaps[i];
1061
+ }
1062
+ return 0;
1063
+ };
1064
+
1065
+ /**
1066
+ * Checks if a cell or node contains an inline image.
1067
+ *
1068
+ * @param {object} node - The node to check for inline images.
1069
+ * @returns {boolean} True if the node contains an inline image; otherwise, false.
1070
+ */
1071
+ LayoutBuilder.prototype._containsInlineImage = function (node) {
1072
+ if (!node) {
1073
+ return false;
1074
+ }
1075
+
1076
+ // Direct image node
1077
+ if (node.image) {
1078
+ return true;
1079
+ }
1080
+
1081
+
1082
+ // Check in table
1083
+ if (node.table && isArray(node.table.body)) {
1084
+ for (var r = 0; r < node.table.body.length; r++) {
1085
+ if (isArray(node.table.body[r])) {
1086
+ for (var c = 0; c < node.table.body[r].length; c++) {
1087
+ if (this._containsInlineImage(node.table.body[r][c])) {
1088
+ return true;
1089
+ }
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+
1095
+ return false;
1096
+ };
1097
+
1098
+
1099
+ /**
1100
+ * Gets the maximum image height from cells in a row.
1101
+ *
1102
+ * @param {Array<object>} cells - Array of cell objects in a row.
1103
+ * @returns {number} The maximum image height found in the cells.
1104
+ */
1105
+ LayoutBuilder.prototype._getMaxImageHeight = function (cells) {
1106
+ var maxHeight = 0;
1107
+
1108
+ for (var i = 0; i < cells.length; i++) {
1109
+ var cellHeight = this._getImageHeightFromNode(cells[i]);
1110
+ if (cellHeight > maxHeight) {
1111
+ maxHeight = cellHeight;
1112
+ }
1113
+ }
1114
+
1115
+ return maxHeight;
1116
+ };
1117
+
1118
+ /**
1119
+ * Gets the maximum estimated height from cells in a row.
1120
+ * Checks for measured heights (_height property) and content-based heights.
1121
+ *
1122
+ * @param {Array<object>} cells - Array of cell objects in a row.
1123
+ * @returns {number} The maximum estimated height found in the cells, or 0 if cannot estimate.
1124
+ */
1125
+ LayoutBuilder.prototype._getMaxCellHeight = function (cells) {
1126
+ var maxHeight = 0;
1127
+
1128
+ for (var i = 0; i < cells.length; i++) {
1129
+ var cell = cells[i];
1130
+ if (!cell || cell._span) {
1131
+ continue; // Skip null cells and span placeholders
1132
+ }
1133
+
1134
+ var cellHeight = 0;
1135
+
1136
+ // Check if cell has measured height from docMeasure phase
1137
+ if (cell._height) {
1138
+ cellHeight = cell._height;
1139
+ }
1140
+ // Check for image content
1141
+ else if (cell.image && cell._maxHeight) {
1142
+ cellHeight = cell._maxHeight;
1143
+ }
1144
+ // Check for nested content with height
1145
+ else {
1146
+ cellHeight = this._getImageHeightFromNode(cell);
1147
+ }
1148
+
1149
+ if (cellHeight > maxHeight) {
1150
+ maxHeight = cellHeight;
1151
+ }
1152
+ }
1153
+
1154
+ return maxHeight;
1155
+ };
1156
+
1157
+ /**
1158
+ * Recursively gets image height from a node.
1159
+ *
1160
+ * @param {object} node - The node to extract image height from.
1161
+ * @returns {number} The image height if found; otherwise, 0.
1162
+ */
1163
+ LayoutBuilder.prototype._getImageHeightFromNode = function (node) {
1164
+ if (!node) {
1165
+ return 0;
1166
+ }
1167
+
1168
+ // Direct image node with height
1169
+ if (node.image && node._height) {
1170
+ return node._height;
1171
+ }
1172
+
1173
+ var maxHeight = 0;
1174
+
1175
+ // Check in stack
1176
+ if (isArray(node.stack)) {
1177
+ for (var i = 0; i < node.stack.length; i++) {
1178
+ var h = this._getImageHeightFromNode(node.stack[i]);
1179
+ if (h > maxHeight) {
1180
+ maxHeight = h;
1181
+ }
1182
+ }
1183
+ }
1184
+
1185
+ // Check in columns
1186
+ if (isArray(node.columns)) {
1187
+ for (var j = 0; j < node.columns.length; j++) {
1188
+ var h2 = this._getImageHeightFromNode(node.columns[j]);
1189
+ if (h2 > maxHeight) {
1190
+ maxHeight = h2;
1191
+ }
1192
+ }
1193
+ }
1194
+
1195
+ // Check in table
1196
+ if (node.table && isArray(node.table.body)) {
1197
+ for (var r = 0; r < node.table.body.length; r++) {
1198
+ if (isArray(node.table.body[r])) {
1199
+ for (var c = 0; c < node.table.body[r].length; c++) {
1200
+ var h3 = this._getImageHeightFromNode(node.table.body[r][c]);
1201
+ if (h3 > maxHeight) {
1202
+ maxHeight = h3;
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+ }
1208
+
1209
+ return maxHeight;
1210
+ };
1211
+
1212
+
1213
+ /**
1214
+ * Retrieves the ending cell for a row span in case it exists in a specified table column.
1215
+ *
1216
+ * @param {Array<Array<object>>} tableBody - The table body, represented as a 2D array of cell objects.
1217
+ * @param {number} rowIndex - The index of the starting row for the row span.
1218
+ * @param {object} column - The column object containing row span information.
1219
+ * @param {number} columnIndex - The index of the column within the row.
1220
+ * @returns {object|null} The cell at the end of the row span if it exists; otherwise, `null`.
1221
+ * @throws {Error} If the row span extends beyond the total row count.
1222
+ */
1223
+ LayoutBuilder.prototype._getRowSpanEndingCell = function (tableBody, rowIndex, column, columnIndex) {
1224
+ if (column.rowSpan && column.rowSpan > 1) {
1225
+ var endingRow = rowIndex + column.rowSpan - 1;
1226
+ if (endingRow >= tableBody.length) {
1227
+ throw new Error(`Row span for column ${columnIndex} (with indexes starting from 0) exceeded row count`);
1228
+ }
1229
+ return tableBody[endingRow][columnIndex];
1230
+ }
1231
+
1232
+ return null;
1233
+ };
1234
+
1235
+ LayoutBuilder.prototype.processRow = function ({ marginX = [0, 0], dontBreakRows = false, rowsWithoutPageBreak = 0, cells, widths, gaps, tableNode, tableBody, rowIndex, height, heightOffset = 0 }) {
1236
+ var self = this;
1237
+ var isUnbreakableRow = dontBreakRows || rowIndex <= rowsWithoutPageBreak - 1;
1238
+ var pageBreaks = [];
1239
+ var pageBreaksByRowSpan = [];
1240
+ var positions = [];
1241
+ var willBreakByHeight = false;
1242
+ var columnAlignIndexes = {};
1243
+ var hasInlineImage = false;
1244
+ widths = widths || cells;
1245
+
1246
+ // Check if row contains inline images
1247
+ if (!isUnbreakableRow) {
1248
+ for (var cellIdx = 0; cellIdx < cells.length; cellIdx++) {
1249
+ if (self._containsInlineImage(cells[cellIdx])) {
1250
+ hasInlineImage = true;
1251
+ break;
1252
+ }
1253
+ }
1254
+ }
1255
+
1256
+ // Check if row would cause page break and force move to next page first
1257
+ // This keeps the entire row together on the new page
1258
+ // Apply when: forcePageBreakForAllRows is enabled OR row has inline images
1259
+
1260
+ // Priority for forcePageBreakForAllRows setting:
1261
+ // 1. Table-specific layout.forcePageBreakForAllRows
1262
+ // 2. Tables with footerGapCollect: 'product-items' (auto-enabled)
1263
+ // 3. Global footerGapOption.forcePageBreakForAllRows
1264
+ var tableLayout = tableNode && tableNode._layout;
1265
+ var footerGapOpt = self.writer.context()._footerGapOption;
1266
+ var shouldForcePageBreak = false;
1267
+
1268
+ if (tableLayout && tableLayout.forcePageBreakForAllRows !== undefined) {
1269
+ // Table-specific setting takes precedence
1270
+ shouldForcePageBreak = tableLayout.forcePageBreakForAllRows === true;
1271
+ }
1272
+
1273
+ if (!isUnbreakableRow && (shouldForcePageBreak || hasInlineImage)) {
1274
+ var availableHeight = self.writer.context().availableHeight;
1275
+
1276
+ // Calculate estimated height from actual cell content
1277
+ var estimatedHeight = height; // Use provided height if available
1278
+
1279
+ if (!estimatedHeight) {
1280
+ // Try to get maximum cell height from measured content
1281
+ var maxCellHeight = self._getMaxCellHeight(cells);
1282
+
1283
+ if (maxCellHeight > 0) {
1284
+ // Add padding for table borders and cell padding (approximate)
1285
+ // Using smaller padding to avoid overly conservative page break detection
1286
+ var tablePadding = 10; // Account for row padding and borders
1287
+ estimatedHeight = maxCellHeight + tablePadding;
1288
+ } else {
1289
+ // Fallback: use minRowHeight from table layout or global config if provided
1290
+ // Priority: table-specific layout > global footerGapOption > default 80
1291
+ // Using higher default (80px) to handle text rows with wrapping and multiple lines
1292
+ // This is conservative but prevents text rows from being split across pages
1293
+ var minRowHeight = (tableLayout && tableLayout.minRowHeight) || (footerGapOpt && footerGapOpt.minRowHeight) || 80;
1294
+ estimatedHeight = minRowHeight;
1295
+ }
1296
+ }
1297
+
1298
+ // Apply heightOffset from table definition to adjust page break calculation
1299
+ // This allows fine-tuning of page break detection for specific tables
1300
+ // heightOffset is passed as parameter from processTable
1301
+ if (heightOffset) {
1302
+ estimatedHeight = (estimatedHeight || 0) + heightOffset;
1303
+ }
1304
+
1305
+ // Check if row won't fit on current page
1306
+ // Strategy: Force break if row won't fit AND we're not too close to page boundary
1307
+ // "Too close" means availableHeight is very small (< 5px) - at that point forcing
1308
+ // a break would create a nearly-blank page
1309
+ var minSpaceThreshold = 5; // Only skip forced break if < 5px space left
1310
+
1311
+ if (estimatedHeight > availableHeight && availableHeight > minSpaceThreshold) {
1312
+ var currentPage = self.writer.context().page;
1313
+ var currentY = self.writer.context().y;
1314
+
1315
+ // Draw vertical lines to fill the gap from current position to page break
1316
+ // This ensures vertical lines extend all the way to the bottom of the page
1317
+ if (tableNode && tableNode._tableProcessor && rowIndex > 0) {
1318
+ tableNode._tableProcessor.drawVerticalLinesForForcedPageBreak(
1319
+ rowIndex,
1320
+ self.writer,
1321
+ currentY,
1322
+ currentY + availableHeight
1323
+ );
1324
+ }
1325
+
1326
+ // Move to next page before processing row
1327
+ self.writer.context().moveDown(availableHeight);
1328
+ self.writer.moveToNextPage();
1329
+
1330
+ // Track this page break so tableProcessor can draw borders correctly
1331
+ pageBreaks.push({
1332
+ prevPage: currentPage,
1333
+ prevY: currentY + availableHeight,
1334
+ y: self.writer.context().y,
1335
+ page: self.writer.context().page,
1336
+ forced: true // Mark as forced page break
1337
+ });
1338
+
1339
+ // Mark that this row should not break anymore
1340
+ isUnbreakableRow = true;
1341
+ dontBreakRows = true;
1342
+ }
1343
+ }
1344
+
1345
+ // Check if row should break by height
1346
+ if (!isUnbreakableRow && height > self.writer.context().availableHeight) {
1347
+ willBreakByHeight = true;
1348
+ }
1349
+
1350
+ // Use the marginX if we are in a top level table/column (not nested)
1351
+ const marginXParent = self.nestedLevel === 1 ? marginX : null;
1352
+ const _bottomByPage = tableNode ? tableNode._bottomByPage : null;
1353
+ this.writer.context().beginColumnGroup(marginXParent, _bottomByPage);
1354
+
1355
+ for (var i = 0, l = cells.length; i < l; i++) {
1356
+ var cell = cells[i];
1357
+
1358
+ // Page change handler
1359
+
1360
+ this.tracker.auto('pageChanged', storePageBreakClosure, function () {
1361
+ var width = widths[i]._calcWidth;
1362
+ var leftOffset = self._colLeftOffset(i, gaps);
1363
+ // Check if exists and retrieve the cell that started the rowspan in case we are in the cell just after
1364
+ var startingSpanCell = self._findStartingRowSpanCell(cells, i);
1365
+
1366
+ if (cell.colSpan && cell.colSpan > 1) {
1367
+ for (var j = 1; j < cell.colSpan; j++) {
1368
+ width += widths[++i]._calcWidth + gaps[i];
1369
+ }
1370
+ }
1371
+
1372
+ // if rowspan starts in this cell, we retrieve the last cell affected by the rowspan
1373
+ const rowSpanEndingCell = self._getRowSpanEndingCell(tableBody, rowIndex, cell, i);
1374
+ if (rowSpanEndingCell) {
1375
+ // We store a reference of the ending cell in the first cell of the rowspan
1376
+ cell._endingCell = rowSpanEndingCell;
1377
+ cell._endingCell._startingRowSpanY = cell._startingRowSpanY;
1378
+ }
1379
+
1380
+ // If we are after a cell that started a rowspan
1381
+ var endOfRowSpanCell = null;
1382
+ if (startingSpanCell && startingSpanCell._endingCell) {
1383
+ // Reference to the last cell of the rowspan
1384
+ endOfRowSpanCell = startingSpanCell._endingCell;
1385
+ // Store if we are in an unbreakable block when we save the context and the originalX
1386
+ if (self.writer.transactionLevel > 0) {
1387
+ endOfRowSpanCell._isUnbreakableContext = true;
1388
+ endOfRowSpanCell._originalXOffset = self.writer.originalX;
1389
+ }
1390
+ }
1391
+
1392
+ // We pass the endingSpanCell reference to store the context just after processing rowspan cell
1393
+ self.writer.context().beginColumn(width, leftOffset, endOfRowSpanCell, heightOffset);
1394
+
1395
+ if (!cell._span) {
1396
+ self.processNode(cell);
1397
+ self.writer.context().updateBottomByPage();
1398
+ addAll(positions, cell.positions);
1399
+ if (cell.verticalAlign && cell._verticalAlignIdx !== undefined) {
1400
+ columnAlignIndexes[i] = cell._verticalAlignIdx;
1401
+ }
1402
+ } else if (cell._columnEndingContext) {
1403
+ var discountY = 0;
1404
+ if (dontBreakRows) {
1405
+ // Calculate how many points we have to discount to Y when dontBreakRows and rowSpan are combined
1406
+ const ctxBeforeRowSpanLastRow = self.writer.writer.contextStack[self.writer.writer.contextStack.length - 1];
1407
+ discountY = ctxBeforeRowSpanLastRow.y - cell._startingRowSpanY;
1408
+ }
1409
+ var originalXOffset = 0;
1410
+ // If context was saved from an unbreakable block and we are not in an unbreakable block anymore
1411
+ // We have to sum the originalX (X before starting unbreakable block) to X
1412
+ if (cell._isUnbreakableContext && !self.writer.transactionLevel) {
1413
+ originalXOffset = cell._originalXOffset;
1414
+ }
1415
+ // row-span ending
1416
+ // Recover the context after processing the rowspanned cell
1417
+ self.writer.context().markEnding(cell, originalXOffset, discountY);
1418
+ }
1419
+ });
1420
+ }
1421
+
1422
+ // Check if last cell is part of a span
1423
+ var endingSpanCell = null;
1424
+ var lastColumn = cells.length > 0 ? cells[cells.length - 1] : null;
1425
+ if (lastColumn) {
1426
+ // Previous column cell has a rowspan
1427
+ if (lastColumn._endingCell) {
1428
+ endingSpanCell = lastColumn._endingCell;
1429
+ // Previous column cell is part of a span
1430
+ } else if (lastColumn._span === true) {
1431
+ // We get the cell that started the span where we set a reference to the ending cell
1432
+ const startingSpanCell = this._findStartingRowSpanCell(cells, cells.length);
1433
+ if (startingSpanCell) {
1434
+ // Context will be stored here (ending cell)
1435
+ endingSpanCell = startingSpanCell._endingCell;
1436
+ // Store if we are in an unbreakable block when we save the context and the originalX
1437
+ if (this.writer.transactionLevel > 0) {
1438
+ endingSpanCell._isUnbreakableContext = true;
1439
+ endingSpanCell._originalXOffset = this.writer.originalX;
1440
+ }
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ // If content did not break page, check if we should break by height
1446
+ if (willBreakByHeight && !isUnbreakableRow && pageBreaks.length === 0) {
1447
+ this.writer.context().moveDown(this.writer.context().availableHeight);
1448
+ this.writer.moveToNextPage();
1449
+ }
1450
+
1451
+ var bottomByPage = this.writer.context().completeColumnGroup(height, endingSpanCell);
1452
+ var rowHeight = this.writer.context().height;
1453
+ for (var colIndex = 0, columnsLength = cells.length; colIndex < columnsLength; colIndex++) {
1454
+ var columnNode = cells[colIndex];
1455
+ if (columnNode._span) {
1456
+ continue;
1457
+ }
1458
+ if (columnNode.verticalAlign && columnAlignIndexes[colIndex] !== undefined) {
1459
+ var alignEntry = self.verticalAlignItemStack[columnAlignIndexes[colIndex]];
1460
+ if (alignEntry && alignEntry.begin && alignEntry.begin.item) {
1461
+ alignEntry.begin.item.viewHeight = rowHeight;
1462
+ alignEntry.begin.item.nodeHeight = columnNode._height;
1463
+ }
1464
+ }
1465
+ if (columnNode.layers) {
1466
+ columnNode.layers.forEach(function (layer) {
1467
+ if (layer.verticalAlign && layer._verticalAlignIdx !== undefined) {
1468
+ var layerEntry = self.verticalAlignItemStack[layer._verticalAlignIdx];
1469
+ if (layerEntry && layerEntry.begin && layerEntry.begin.item) {
1470
+ layerEntry.begin.item.viewHeight = rowHeight;
1471
+ layerEntry.begin.item.nodeHeight = layer._height;
1472
+ }
1473
+ }
1474
+ });
1475
+ }
1476
+ }
1477
+
1478
+ if (tableNode) {
1479
+ tableNode._bottomByPage = bottomByPage;
1480
+ // If there are page breaks in this row, update data with prevY of last cell
1481
+ this._updatePageBreaksData(pageBreaks, tableNode, rowIndex);
1482
+ }
1483
+
1484
+ return {
1485
+ pageBreaksBySpan: pageBreaksByRowSpan,
1486
+ pageBreaks: pageBreaks,
1487
+ positions: positions
1488
+ };
1489
+
1490
+ function storePageBreakClosure(data) {
1491
+ const startsRowSpan = cell.rowSpan && cell.rowSpan > 1;
1492
+ if (startsRowSpan) {
1493
+ data.rowSpan = cell.rowSpan;
1494
+ }
1495
+ data.rowIndex = rowIndex;
1496
+ self._storePageBreakData(data, startsRowSpan, pageBreaks, tableNode);
1497
+ }
1498
+
1499
+ };
1500
+
1501
+ // lists
1502
+ LayoutBuilder.prototype.processList = function (orderedList, node) {
1503
+ var self = this,
1504
+ items = orderedList ? node.ol : node.ul,
1505
+ gapSize = node._gapSize;
1506
+
1507
+ this.writer.context().addMargin(gapSize.width);
1508
+
1509
+ var nextMarker;
1510
+ this.tracker.auto('lineAdded', addMarkerToFirstLeaf, function () {
1511
+ items.forEach(function (item) {
1512
+ nextMarker = item.listMarker;
1513
+ self.processNode(item);
1514
+ addAll(node.positions, item.positions);
1515
+ });
1516
+ });
1517
+
1518
+ this.writer.context().addMargin(-gapSize.width);
1519
+
1520
+ function addMarkerToFirstLeaf(line) {
1521
+ // I'm not very happy with the way list processing is implemented
1522
+ // (both code and algorithm should be rethinked)
1523
+ if (nextMarker) {
1524
+ var marker = nextMarker;
1525
+ nextMarker = null;
1526
+
1527
+ if (marker.canvas) {
1528
+ var vector = marker.canvas[0];
1529
+
1530
+ offsetVector(vector, -marker._minWidth, 0);
1531
+ self.writer.addVector(vector);
1532
+ } else if (marker._inlines) {
1533
+ var markerLine = new Line(self.pageSize.width);
1534
+ markerLine.addInline(marker._inlines[0]);
1535
+ markerLine.x = -marker._minWidth;
1536
+ markerLine.y = line.getAscenderHeight() - markerLine.getAscenderHeight();
1537
+ self.writer.addLine(markerLine, true);
1538
+ }
1539
+ }
1540
+ }
1541
+ };
1542
+
1543
+ // tables
1544
+ LayoutBuilder.prototype.processTable = function (tableNode) {
1545
+ this.nestedLevel++;
1546
+ var processor = new TableProcessor(tableNode);
1547
+
1548
+ // Store processor reference for forced page break vertical line drawing
1549
+ tableNode._tableProcessor = processor;
1550
+
1551
+ processor.beginTable(this.writer);
1552
+
1553
+ var rowHeights = tableNode.table.heights;
1554
+ for (var i = 0, l = tableNode.table.body.length; i < l; i++) {
1555
+ // if dontBreakRows and row starts a rowspan
1556
+ // we store the 'y' of the beginning of each rowSpan
1557
+ if (processor.dontBreakRows) {
1558
+ tableNode.table.body[i].forEach(cell => {
1559
+ if (cell.rowSpan && cell.rowSpan > 1) {
1560
+ cell._startingRowSpanY = this.writer.context().y;
1561
+ }
1562
+ });
1563
+ }
1564
+
1565
+ processor.beginRow(i, this.writer);
1566
+
1567
+ var height;
1568
+ if (isFunction(rowHeights)) {
1569
+ height = rowHeights(i);
1570
+ } else if (isArray(rowHeights)) {
1571
+ height = rowHeights[i];
1572
+ } else {
1573
+ height = rowHeights;
1574
+ }
1575
+
1576
+ if (height === 'auto') {
1577
+ height = undefined;
1578
+ }
1579
+
1580
+ var heightOffset = tableNode.heightOffset != undefined ? tableNode.heightOffset : 0;
1581
+
1582
+ var pageBeforeProcessing = this.writer.context().page;
1583
+
1584
+ var result = this.processRow({
1585
+ marginX: tableNode._margin ? [tableNode._margin[0], tableNode._margin[2]] : [0, 0],
1586
+ dontBreakRows: processor.dontBreakRows,
1587
+ rowsWithoutPageBreak: processor.rowsWithoutPageBreak,
1588
+ cells: tableNode.table.body[i],
1589
+ widths: tableNode.table.widths,
1590
+ gaps: tableNode._offsets.offsets,
1591
+ tableBody: tableNode.table.body,
1592
+ tableNode,
1593
+ rowIndex: i,
1594
+ height,
1595
+ heightOffset
1596
+ });
1597
+ addAll(tableNode.positions, result.positions);
1598
+
1599
+ if (!result.pageBreaks || result.pageBreaks.length === 0) {
1600
+ var breaksBySpan = tableNode && tableNode._breaksBySpan || null;
1601
+ var breakBySpanData = this._findSameRowPageBreakByRowSpanData(breaksBySpan, pageBeforeProcessing, i);
1602
+ if (breakBySpanData) {
1603
+ var finalBreakBySpanData = this._getPageBreakListBySpan(tableNode, breakBySpanData.prevPage, i);
1604
+ result.pageBreaks.push(finalBreakBySpanData);
1605
+ }
1606
+ }
1607
+
1608
+ // Get next row cells for look-ahead page break detection
1609
+ var nextRowCells = (i + 1 < tableNode.table.body.length) ? tableNode.table.body[i + 1] : null;
1610
+
1611
+ processor.endRow(i, this.writer, result.pageBreaks, nextRowCells, this);
1612
+ }
1613
+
1614
+ processor.endTable(this.writer);
1615
+ this.nestedLevel--;
1616
+ if (this.nestedLevel === 0) {
1617
+ this.writer.context().resetMarginXTopParent();
1618
+ }
1619
+ };
1620
+
1621
+ // leafs (texts)
1622
+ LayoutBuilder.prototype.processLeaf = function (node) {
1623
+ var line = this.buildNextLine(node);
1624
+ if (line && (node.tocItem || node.id)) {
1625
+ line._node = node;
1626
+ }
1627
+ var currentHeight = (line) ? line.getHeight() : 0;
1628
+ var maxHeight = node.maxHeight || -1;
1629
+
1630
+ if (line) {
1631
+ var nodeId = getNodeId(node);
1632
+ if (nodeId) {
1633
+ line.id = nodeId;
1634
+ }
1635
+ }
1636
+
1637
+ if (node._tocItemRef) {
1638
+ line._pageNodeRef = node._tocItemRef;
1639
+ }
1640
+
1641
+ if (node._pageRef) {
1642
+ line._pageNodeRef = node._pageRef._nodeRef;
1643
+ }
1644
+
1645
+ if (line && line.inlines && isArray(line.inlines)) {
1646
+ for (var i = 0, l = line.inlines.length; i < l; i++) {
1647
+ if (line.inlines[i]._tocItemRef) {
1648
+ line.inlines[i]._pageNodeRef = line.inlines[i]._tocItemRef;
1649
+ }
1650
+
1651
+ if (line.inlines[i]._pageRef) {
1652
+ line.inlines[i]._pageNodeRef = line.inlines[i]._pageRef._nodeRef;
1653
+ }
1654
+ }
1655
+ }
1656
+
1657
+ while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
1658
+ var positions = this.writer.addLine(line);
1659
+ node.positions.push(positions);
1660
+ line = this.buildNextLine(node);
1661
+ if (line) {
1662
+ currentHeight += line.getHeight();
1663
+ }
1664
+ }
1665
+ };
1666
+
1667
+ LayoutBuilder.prototype.processToc = function (node) {
1668
+ if (node.toc.title) {
1669
+ this.processNode(node.toc.title);
1670
+ }
1671
+ if (node.toc._table) {
1672
+ this.processNode(node.toc._table);
1673
+ }
1674
+ };
1675
+
1676
+ LayoutBuilder.prototype.buildNextLine = function (textNode) {
1677
+
1678
+ function cloneInline(inline) {
1679
+ var newInline = inline.constructor();
1680
+ for (var key in inline) {
1681
+ newInline[key] = inline[key];
1682
+ }
1683
+ return newInline;
1684
+ }
1685
+
1686
+ if (!textNode._inlines || textNode._inlines.length === 0) {
1687
+ return null;
1688
+ }
1689
+
1690
+ var line = new Line(this.writer.context().availableWidth);
1691
+ var textTools = new TextTools(null);
1692
+
1693
+ while (textNode._inlines && textNode._inlines.length > 0 && line.hasEnoughSpaceForInline(textNode._inlines[0])) {
1694
+ var inline = textNode._inlines.shift();
1695
+
1696
+ if (!inline.noWrap && inline.text.length > 1 && inline.width > line.maxWidth) {
1697
+ var widthPerChar = inline.width / inline.text.length;
1698
+ var maxChars = Math.floor(line.maxWidth / widthPerChar);
1699
+ if (maxChars < 1) {
1700
+ maxChars = 1;
1701
+ }
1702
+ if (maxChars < inline.text.length) {
1703
+ var newInline = cloneInline(inline);
1704
+
1705
+ newInline.text = inline.text.substr(maxChars);
1706
+ inline.text = inline.text.substr(0, maxChars);
1707
+
1708
+ newInline.width = textTools.widthOfString(newInline.text, newInline.font, newInline.fontSize, newInline.characterSpacing);
1709
+ inline.width = textTools.widthOfString(inline.text, inline.font, inline.fontSize, inline.characterSpacing);
1710
+
1711
+ textNode._inlines.unshift(newInline);
1712
+ }
1713
+ }
1714
+
1715
+ line.addInline(inline);
1716
+ }
1717
+
1718
+ line.lastLineInParagraph = textNode._inlines.length === 0;
1719
+
1720
+ return line;
1721
+ };
1722
+
1723
+ // images
1724
+ LayoutBuilder.prototype.processImage = function (node) {
1725
+ var position = this.writer.addImage(node);
1726
+ node.positions.push(position);
1727
+ };
1728
+
1729
+ LayoutBuilder.prototype.processSVG = function (node) {
1730
+ var position = this.writer.addSVG(node);
1731
+ node.positions.push(position);
1732
+ };
1733
+
1734
+ LayoutBuilder.prototype.processCanvas = function (node) {
1735
+ var height = node._minHeight;
1736
+
1737
+ if (node.absolutePosition === undefined && this.writer.context().availableHeight < height) {
1738
+ // TODO: support for canvas larger than a page
1739
+ // TODO: support for other overflow methods
1740
+
1741
+ this.writer.moveToNextPage();
1742
+ }
1743
+
1744
+ this.writer.alignCanvas(node);
1745
+
1746
+ node.canvas.forEach(function (vector) {
1747
+ var position = this.writer.addVector(vector);
1748
+ node.positions.push(position);
1749
+ }, this);
1750
+
1751
+ this.writer.context().moveDown(height);
1752
+ };
1753
+
1754
+ LayoutBuilder.prototype.processQr = function (node) {
1755
+ var position = this.writer.addQr(node);
1756
+ node.positions.push(position);
1757
+ };
1758
+
1759
+ function processNode_test(node) {
1760
+ decorateNode(node);
1761
+
1762
+ var prevTop = testWriter.context().getCurrentPosition().top;
1763
+
1764
+ applyMargins(function () {
1765
+ var unbreakable = node.unbreakable;
1766
+ if (unbreakable) {
1767
+ testWriter.beginUnbreakableBlock();
1768
+ }
1769
+
1770
+ var absPosition = node.absolutePosition;
1771
+ if (absPosition) {
1772
+ testWriter.context().beginDetachedBlock();
1773
+ testWriter.context().moveTo(absPosition.x || 0, absPosition.y || 0);
1774
+ }
1775
+
1776
+ var relPosition = node.relativePosition;
1777
+ if (relPosition) {
1778
+ testWriter.context().beginDetachedBlock();
1779
+ if (typeof testWriter.context().moveToRelative === 'function') {
1780
+ testWriter.context().moveToRelative(relPosition.x || 0, relPosition.y || 0);
1781
+ } else if (currentLayoutBuilder && currentLayoutBuilder.writer) {
1782
+ testWriter.context().moveTo(
1783
+ (relPosition.x || 0) + currentLayoutBuilder.writer.context().x,
1784
+ (relPosition.y || 0) + currentLayoutBuilder.writer.context().y
1785
+ );
1786
+ }
1787
+ }
1788
+
1789
+ var verticalAlignBegin;
1790
+ if (node.verticalAlign) {
1791
+ verticalAlignBegin = testWriter.beginVerticalAlign(node.verticalAlign);
1792
+ }
1793
+
1794
+ if (node.stack) {
1795
+ processVerticalContainer_test(node);
1796
+ } else if (node.table) {
1797
+ processTable_test(node);
1798
+ } else if (node.text !== undefined) {
1799
+ processLeaf_test(node);
1800
+ }
1801
+
1802
+ if (absPosition || relPosition) {
1803
+ testWriter.context().endDetachedBlock();
1804
+ }
1805
+
1806
+ if (unbreakable) {
1807
+ testResult = testWriter.commitUnbreakableBlock_test();
1808
+ }
1809
+
1810
+ if (node.verticalAlign) {
1811
+ testVerticalAlignStack.push({ begin: verticalAlignBegin, end: testWriter.endVerticalAlign(node.verticalAlign) });
1812
+ }
1813
+ });
1814
+
1815
+ node._height = testWriter.context().getCurrentPosition().top - prevTop;
1816
+
1817
+ function applyMargins(callback) {
1818
+ var margin = node._margin;
1819
+
1820
+ if (node.pageBreak === 'before') {
1821
+ testWriter.moveToNextPage(node.pageOrientation);
1822
+ }
1823
+
1824
+ if (margin) {
1825
+ testWriter.context().moveDown(margin[1]);
1826
+ testWriter.context().addMargin(margin[0], margin[2]);
1827
+ }
1828
+
1829
+ callback();
1830
+
1831
+ if (margin) {
1832
+ testWriter.context().addMargin(-margin[0], -margin[2]);
1833
+ testWriter.context().moveDown(margin[3]);
1834
+ }
1835
+
1836
+ if (node.pageBreak === 'after') {
1837
+ testWriter.moveToNextPage(node.pageOrientation);
1838
+ }
1839
+ }
1840
+ }
1841
+
1842
+ function processVerticalContainer_test(node) {
1843
+ node.stack.forEach(function (item) {
1844
+ processNode_test(item);
1845
+ addAll(node.positions, item.positions);
1846
+ });
1847
+ }
1848
+
1849
+ function processTable_test(tableNode) {
1850
+ var processor = new TableProcessor(tableNode);
1851
+ processor.beginTable(testWriter);
1852
+
1853
+ for (var i = 0, l = tableNode.table.body.length; i < l; i++) {
1854
+ processor.beginRow(i, testWriter);
1855
+ var result = processRow_test(tableNode.table.body[i], tableNode.table.widths, tableNode._offsets ? tableNode._offsets.offsets : null, tableNode.table.body, i);
1856
+ addAll(tableNode.positions, result.positions);
1857
+ processor.endRow(i, testWriter, result.pageBreaks);
1858
+ }
1859
+
1860
+ processor.endTable(testWriter);
1861
+ }
1862
+
1863
+ function processRow_test(columns, widths, gaps, tableBody, tableRow) {
1864
+ var pageBreaks = [];
1865
+ var positions = [];
1866
+
1867
+ testTracker.auto('pageChanged', storePageBreakData, function () {
1868
+ widths = widths || columns;
1869
+
1870
+ testWriter.context().beginColumnGroup();
1871
+
1872
+ var verticalAlignCols = {};
1873
+
1874
+ for (var i = 0, l = columns.length; i < l; i++) {
1875
+ var column = columns[i];
1876
+ var width = widths[i]._calcWidth || widths[i];
1877
+ var leftOffset = colLeftOffset(i);
1878
+ var colIndex = i;
1879
+ if (column.colSpan && column.colSpan > 1) {
1880
+ for (var j = 1; j < column.colSpan; j++) {
1881
+ width += (widths[++i]._calcWidth || widths[i]) + (gaps ? gaps[i] : 0);
1882
+ }
1883
+ }
1884
+
1885
+ testWriter.context().beginColumn(width, leftOffset, getEndingCell(column, i));
1886
+
1887
+ if (!column._span) {
1888
+ processNode_test(column);
1889
+ verticalAlignCols[colIndex] = testVerticalAlignStack.length - 1;
1890
+ addAll(positions, column.positions);
1891
+ } else if (column._columnEndingContext) {
1892
+ testWriter.context().markEnding(column);
1893
+ }
1894
+ }
1895
+
1896
+ testWriter.context().completeColumnGroup();
1897
+
1898
+ var rowHeight = testWriter.context().height;
1899
+ for (var c = 0, clen = columns.length; c < clen; c++) {
1900
+ var col = columns[c];
1901
+ if (col._span) {
1902
+ continue;
1903
+ }
1904
+ if (col.verticalAlign && verticalAlignCols[c] !== undefined) {
1905
+ var alignItem = testVerticalAlignStack[verticalAlignCols[c]].begin.item;
1906
+ alignItem.viewHeight = rowHeight;
1907
+ alignItem.nodeHeight = col._height;
1908
+ }
1909
+ }
1910
+ });
1911
+
1912
+ return { pageBreaks: pageBreaks, positions: positions };
1913
+
1914
+ function storePageBreakData(data) {
1915
+ var pageDesc;
1916
+ for (var idx = 0, len = pageBreaks.length; idx < len; idx++) {
1917
+ var desc = pageBreaks[idx];
1918
+ if (desc.prevPage === data.prevPage) {
1919
+ pageDesc = desc;
1920
+ break;
1921
+ }
1922
+ }
1923
+
1924
+ if (!pageDesc) {
1925
+ pageDesc = data;
1926
+ pageBreaks.push(pageDesc);
1927
+ }
1928
+ pageDesc.prevY = Math.max(pageDesc.prevY, data.prevY);
1929
+ pageDesc.y = Math.min(pageDesc.y, data.y);
1930
+ }
1931
+
1932
+ function colLeftOffset(i) {
1933
+ if (gaps && gaps.length > i) {
1934
+ return gaps[i];
1935
+ }
1936
+ return 0;
1937
+ }
1938
+
1939
+ function getEndingCell(column, columnIndex) {
1940
+ if (column.rowSpan && column.rowSpan > 1) {
1941
+ var endingRow = tableRow + column.rowSpan - 1;
1942
+ if (endingRow >= tableBody.length) {
1943
+ throw new Error('Row span for column ' + columnIndex + ' (with indexes starting from 0) exceeded row count');
1944
+ }
1945
+ return tableBody[endingRow][columnIndex];
1946
+ }
1947
+ return null;
1948
+ }
1949
+ }
1950
+
1951
+ function processLeaf_test(node) {
1952
+ var line = buildNextLine_test(node);
1953
+ var currentHeight = line ? line.getHeight() : 0;
1954
+ var maxHeight = node.maxHeight || -1;
1955
+
1956
+ while (line && (maxHeight === -1 || currentHeight < maxHeight)) {
1957
+ var positions = testWriter.addLine(line);
1958
+ node.positions.push(positions);
1959
+ line = buildNextLine_test(node);
1960
+ if (line) {
1961
+ currentHeight += line.getHeight();
1962
+ }
1963
+ }
1964
+ }
1965
+
1966
+ function buildNextLine_test(textNode) {
1967
+ function cloneInline(inline) {
1968
+ var newInline = inline.constructor();
1969
+ for (var key in inline) {
1970
+ newInline[key] = inline[key];
1971
+ }
1972
+ return newInline;
1973
+ }
1974
+
1975
+ if (!textNode._inlines || textNode._inlines.length === 0) {
1976
+ return null;
1977
+ }
1978
+
1979
+ var line = new Line(testWriter.context().availableWidth);
1980
+ var textTools = new TextTools(null);
1981
+
1982
+ while (textNode._inlines && textNode._inlines.length > 0 && line.hasEnoughSpaceForInline(textNode._inlines[0])) {
1983
+ var inline = textNode._inlines.shift();
1984
+
1985
+ if (!inline.noWrap && inline.text.length > 1 && inline.width > line.maxWidth) {
1986
+ var widthPerChar = inline.width / inline.text.length;
1987
+ var maxChars = Math.floor(line.maxWidth / widthPerChar);
1988
+ if (maxChars < 1) {
1989
+ maxChars = 1;
1990
+ }
1991
+ if (maxChars < inline.text.length) {
1992
+ var newInline = cloneInline(inline);
1993
+
1994
+ newInline.text = inline.text.substr(maxChars);
1995
+ inline.text = inline.text.substr(0, maxChars);
1996
+
1997
+ newInline.width = textTools.widthOfString(newInline.text, newInline.font, newInline.fontSize, newInline.characterSpacing);
1998
+ inline.width = textTools.widthOfString(inline.text, inline.font, inline.fontSize, inline.characterSpacing);
1999
+
2000
+ textNode._inlines.unshift(newInline);
2001
+ }
2002
+ }
2003
+
2004
+ line.addInline(inline);
2005
+ }
2006
+
2007
+ line.lastLineInParagraph = textNode._inlines.length === 0;
2008
+
2009
+ return line;
2010
+ }
2011
+
2012
+ module.exports = LayoutBuilder;