@softwear/latestcollectioncore 1.0.183 → 1.0.185

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.
package/.prettierrc.json CHANGED
@@ -4,5 +4,5 @@
4
4
  "htmlWhitespaceSensitivity": "ignore",
5
5
  "semi": false,
6
6
  "arrowParens": "always",
7
- "printWidth": 180
8
- }
7
+ "printWidth": 280
8
+ }
package/dist/pdf.d.ts CHANGED
@@ -54,7 +54,10 @@ export interface ContainerLayoutObject extends BaseLayoutObject {
54
54
  minHeightBeforeBreak?: number;
55
55
  /** When true, container and children are drawn only once at the end of the report, not on every page */
56
56
  printOnlyAtEnd?: boolean;
57
- /** Only effective on top-level `layout.objects` (PDF engine ignores this flag on nested containers). */
57
+ /**
58
+ * Only on top-level `layout.objects`. Same intent as onlyRenderOnFirstPage: drawn once on the first printable page,
59
+ * not repeated after overflow page breaks.
60
+ */
58
61
  printOnlyAtStart?: boolean;
59
62
  }
60
63
  export type LayoutObject = TextLayoutObject | FieldLayoutObject | RectangleLayoutObject | ImageLayoutObject | ContainerLayoutObject;
package/dist/reports.js CHANGED
@@ -16,6 +16,14 @@ exports.genPDF = void 0;
16
16
  const jspdf_1 = require("jspdf");
17
17
  const date_fns_1 = require("date-fns");
18
18
  const deepCopy_1 = __importDefault(require("./deepCopy"));
19
+ /** Measurement never advances jsPDF sheet count; rendering uses the active canvas page (1-based). */
20
+ function getCurrentPageNumber(doc, context) {
21
+ var _a;
22
+ if (context.mode === 'measurement') {
23
+ return (_a = context.currentPageNumber) !== null && _a !== void 0 ? _a : 1;
24
+ }
25
+ return doc.getCurrentPageInfo().pageNumber;
26
+ }
19
27
  // We have to declare some functions that will be called by other functions but also have to call those other functions
20
28
  // We overwrite the function definition as soon as those other functions have been declared
21
29
  /* eslint-disable @typescript-eslint/no-unused-vars */
@@ -24,7 +32,7 @@ let drawStaticPartOfPage = function (doc, layout, printBuffer, paperSize, option
24
32
  };
25
33
  // Forward declaration - parameters intentionally unused, function is overwritten below
26
34
  /* eslint-disable @typescript-eslint/no-unused-vars */
27
- let addObjectToPDF = function (_originX, _originY, _doc, _object, _printBuffer, _paperSize, _layout, _options, _rootPrintBuffer, _containerChain, _context) {
35
+ let addObjectToPDF = function (_originX, _originY, _doc, _object, _printBuffer, _paperSize, _layout, _options, _rootPrintBuffer, _containerChain, _context, _printOnlyAtStartContainersOffset) {
28
36
  // to be assigned later
29
37
  return { x: 0, y: 0 };
30
38
  };
@@ -61,6 +69,26 @@ function containerLoopSource(container, printBuffer) {
61
69
  return [printBuffer];
62
70
  return getProperty(container.source, printBuffer);
63
71
  }
72
+ /**
73
+ * Top-level empty-source containers (invoice “billTo” bands, wrappers) sit beside overflowing repeats.
74
+ * They are not on the nested {@link containerChain}; redraw them whenever we append a continuation page.
75
+ * Omit the overflowing root when it is chain[0] — redrawing it would recurse (e.g. wrapper around lines).
76
+ */
77
+ function redrawRootDummyBodyContainersOnNewPage(doc, layout, rootPrintBuffer, paperSize, options, context, containerChain) {
78
+ const overflowingRootContainer = containerChain.length > 0 ? containerChain[0].object : undefined;
79
+ layout.objects.forEach((object) => {
80
+ if (object.type !== 'container')
81
+ return;
82
+ if (overflowingRootContainer !== undefined && object === overflowingRootContainer)
83
+ return;
84
+ const c = object;
85
+ if (c.printOnlyAtStart === true || c.printOnlyAtEnd === true)
86
+ return;
87
+ if (!dummyLayoutSource(c.source))
88
+ return;
89
+ addObjectToPDF(0, 0, doc, object, rootPrintBuffer, paperSize, layout, options, rootPrintBuffer, [], context);
90
+ });
91
+ }
64
92
  /** jsPDF font style strings. fontStyle bitmask: 0=normal, 1=bold, 2=italic, 3=bold+italic */
65
93
  const FONT_STYLES = ['normal', 'bold', 'italic', 'bolditalic'];
66
94
  /** Recursively collect used custom fonts from layout objects (text/field with fontFamily) and layout default. */
@@ -380,18 +408,12 @@ function drawSimpleObject(x, y, doc, object, printBuffer, width, height, options
380
408
  }
381
409
  }
382
410
  function addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, options, containerChain, context) {
411
+ var _a;
383
412
  /** Absolute tenths where the overflowing repeat resumes; callers already computed this correctly. */
384
413
  const continuationOxTenths = originX;
385
414
  const continuationOyTenths = originY;
386
- context.pendingRootStartBumpTenths = 0;
387
- context.applyRootStartBumpToThisCall = false;
388
- context.remainingRootStartBumpTenths = 0;
389
415
  if (context.mode === 'measurement') {
390
- // Track page count in measurement mode
391
- if (context.currentPageCount === undefined) {
392
- context.currentPageCount = 1;
393
- }
394
- context.currentPageCount++;
416
+ context.currentPageNumber = ((_a = context.currentPageNumber) !== null && _a !== void 0 ? _a : 1) + 1;
395
417
  }
396
418
  else if (!context.measureOnly) {
397
419
  // Actually add page in rendering mode (skip when measuring for fillContainer)
@@ -399,6 +421,7 @@ function addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, opti
399
421
  }
400
422
  if (!context.measureOnly) {
401
423
  drawStaticPartOfPage(doc, layout, rootPrintBuffer, paperSize, options, context);
424
+ redrawRootDummyBodyContainersOnNewPage(doc, layout, rootPrintBuffer, paperSize, options, context, containerChain);
402
425
  }
403
426
  // Bounding box tracked for snap-to-bottom; continuation cursor is ALWAYS the passed-in coords.
404
427
  const absoluteLowerRightHandSide = { x: continuationOxTenths / 10, y: continuationOyTenths / 10 };
@@ -417,10 +440,7 @@ function addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, opti
417
440
  if (child.snapToBottom)
418
441
  return;
419
442
  // Skip inner repeat container on chain: advancing origin by x,y is enough; the live loop draws rows.
420
- if (child.type === 'container' &&
421
- deeperLinkChild &&
422
- child === deeperLinkChild &&
423
- !containerRedrawsAfterContinuationPage(child)) {
443
+ if (child.type === 'container' && deeperLinkChild && child === deeperLinkChild && !containerRedrawsAfterContinuationPage(child)) {
424
444
  const c = child;
425
445
  originX += c.x;
426
446
  originY += c.y;
@@ -482,49 +502,35 @@ function drawBottomDwellers(object, originX, originY, absoluteLowerRightHandSide
482
502
  *
483
503
  * Use originX,originY as origin to displace the object's own x,y coordinates
484
504
  */
485
- addObjectToPDF = function (originX, originY, doc, object, printBuffer, paperSize, layout, options, rootPrintBuffer, containerChain, context) {
486
- var _a, _b, _c, _d, _f;
487
- if (context.applyRootStartBumpToThisCall) {
488
- context.remainingRootStartBumpTenths = (_a = context.pendingRootStartBumpTenths) !== null && _a !== void 0 ? _a : 0;
489
- context.applyRootStartBumpToThisCall = false;
490
- context.pendingRootStartBumpTenths = 0;
491
- }
505
+ addObjectToPDF = function (originX, originY, doc, object, printBuffer, paperSize, layout, options, rootPrintBuffer, containerChain, context, printOnlyAtStartContainersOffset) {
506
+ var _a;
492
507
  const layoutXtenths = originX + object.x;
493
508
  const layoutYtenths = originY + object.y;
494
- /** printOnlyAtStart offset only for outermost recursion and only until first positioning or addPage clears it */
495
- const bumpForDrawNonContainer = containerChain.length === 0 ? (_b = context.remainingRootStartBumpTenths) !== null && _b !== void 0 ? _b : 0 : 0;
509
+ const rawPrintOnlyAtStartOffset = printOnlyAtStartContainersOffset !== null && printOnlyAtStartContainersOffset !== void 0 ? printOnlyAtStartContainersOffset : 0;
510
+ const effectivePrintOnlyAtStartContainersOffset = containerChain.length === 0 && rawPrintOnlyAtStartOffset > 0 && getCurrentPageNumber(doc, context) === 1 ? rawPrintOnlyAtStartOffset : 0;
496
511
  const x = layoutXtenths / 10;
497
- const y = (layoutYtenths + bumpForDrawNonContainer) / 10;
512
+ const y = (layoutYtenths + effectivePrintOnlyAtStartContainersOffset) / 10;
498
513
  const width = object.width / 10;
499
514
  const height = object.height / 10;
500
515
  // Keep track of the lower righthandside of all objects in this container
501
516
  // We need to return this object so whatever calls addObjectToPDF knows how big our drawing was
502
517
  let absoluteLowerRightHandSide = { x: x + width, y: y + height };
503
518
  if (!object.active) {
504
- if (bumpForDrawNonContainer !== 0)
505
- context.remainingRootStartBumpTenths = 0;
506
519
  return absoluteLowerRightHandSide;
507
520
  }
508
521
  if (object.type != 'container') {
509
- if (bumpForDrawNonContainer !== 0)
510
- context.remainingRootStartBumpTenths = 0;
511
522
  drawSimpleObject(x, y, doc, object, printBuffer, width, height, options, context);
512
523
  }
513
524
  // Recursively draw all child objects from a container object
514
525
  if (object.type == 'container') {
515
526
  const source = containerLoopSource(object, printBuffer);
516
527
  if (source === undefined) {
517
- (_c = options.onAlert) === null || _c === void 0 ? void 0 : _c.call(options, { header: 'containernotfound', body: object.source, type: 'warning', timeout: 10000 });
518
- if (containerChain.length === 0)
519
- context.remainingRootStartBumpTenths = 0;
528
+ (_a = options.onAlert) === null || _a === void 0 ? void 0 : _a.call(options, { header: 'containernotfound', body: object.source, type: 'warning', timeout: 10000 });
520
529
  return absoluteLowerRightHandSide;
521
530
  }
522
531
  const nrContainers = source.length;
523
- let bumpOnce = containerChain.length === 0 ? (_d = context.remainingRootStartBumpTenths) !== null && _d !== void 0 ? _d : 0 : 0;
524
- if (bumpOnce !== 0 && containerChain.length === 0)
525
- context.remainingRootStartBumpTenths = 0;
526
532
  let originX = layoutXtenths;
527
- let originY = layoutYtenths + bumpOnce;
533
+ let originY = layoutYtenths + effectivePrintOnlyAtStartContainersOffset;
528
534
  for (let containerIndex = 0; containerIndex < nrContainers; containerIndex++) {
529
535
  const newContainerChain = containerChain.filter(function () {
530
536
  return true;
@@ -536,7 +542,7 @@ addObjectToPDF = function (originX, originY, doc, object, printBuffer, paperSize
536
542
  if (useOrphanCheck && (object.pageBreak || originY + minHeightMm > paperSize.height - paperSize.footerHeight)) {
537
543
  // We ran out of paper
538
544
  originX = layoutXtenths;
539
- originY = layoutYtenths + (containerChain.length === 0 ? (_f = context.remainingRootStartBumpTenths) !== null && _f !== void 0 ? _f : 0 : 0);
545
+ originY = layoutYtenths + effectivePrintOnlyAtStartContainersOffset;
540
546
  const newOrigin = addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, options, newContainerChain, context);
541
547
  originX = newOrigin.originX;
542
548
  originY = newOrigin.originY;
@@ -710,10 +716,12 @@ function genPDF(layout, printBuffer, options = {}) {
710
716
  paperSize.width *= 10;
711
717
  paperSize.height *= 10;
712
718
  paperSize.footerHeight *= 10;
713
- // First pass: Measurement mode - count pages without rendering
719
+ // First pass: Measurement mode page count without visible output; overlaps must match render pass inputs.
714
720
  const measurementContext = {
715
721
  mode: 'measurement',
716
- currentPageCount: 1, // Start with page 1
722
+ currentPageNumber: 1,
723
+ imageAssets,
724
+ defaultFontFamily: layout.defaultFontFamily,
717
725
  };
718
726
  // Create a minimal jsPDF instance for measurement (needed for some calculations)
719
727
  const measurementDoc = new jspdf_1.jsPDF({
@@ -726,37 +734,26 @@ function genPDF(layout, printBuffer, options = {}) {
726
734
  }
727
735
  drawStaticPartOfPage(measurementDoc, layout, printBuffer, paperSize, options, measurementContext);
728
736
  // Draw containers in measurement mode (same order as render)
729
- let measurementTopPrintedStartBottomTenths = 0;
737
+ /** Max lower edge (layout units ×10 = tenths-mm) of print-only-at-start roots; deterministic for fixed layout+buffer once measure uses same context as render. */
738
+ let measuredPrintOnlyAtStartOffset = 0;
730
739
  layout.objects
731
740
  .filter((object) => object.type == 'container' && object.printOnlyAtStart === true)
732
741
  .forEach((object) => {
733
742
  const lh = addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
734
- measurementTopPrintedStartBottomTenths = Math.max(measurementTopPrintedStartBottomTenths, lh.y * 10);
743
+ measuredPrintOnlyAtStartOffset = Math.max(measuredPrintOnlyAtStartOffset, lh.y * 10);
735
744
  });
736
745
  layout.objects
737
- .filter((object) => object.type == 'container' &&
738
- object.printOnlyAtEnd !== true &&
739
- object.printOnlyAtStart !== true)
746
+ .filter((object) => object.type == 'container' && object.printOnlyAtEnd !== true && object.printOnlyAtStart !== true)
740
747
  .forEach((object) => {
741
- const bumpTenths = Math.max(0, measurementTopPrintedStartBottomTenths - object.y);
742
- if (bumpTenths > 0) {
743
- measurementContext.applyRootStartBumpToThisCall = true;
744
- measurementContext.pendingRootStartBumpTenths = bumpTenths;
745
- }
746
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
748
+ addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y));
747
749
  });
748
750
  layout.objects
749
751
  .filter((object) => object.type == 'container' && object.printOnlyAtEnd === true)
750
752
  .forEach((object) => {
751
- const bumpTenths = Math.max(0, measurementTopPrintedStartBottomTenths - object.y);
752
- if (bumpTenths > 0) {
753
- measurementContext.applyRootStartBumpToThisCall = true;
754
- measurementContext.pendingRootStartBumpTenths = bumpTenths;
755
- }
756
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
753
+ addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y));
757
754
  });
758
755
  // Get total page count from measurement pass
759
- const totalPageCount = measurementContext.currentPageCount || 1;
756
+ const totalPageCount = measurementContext.currentPageNumber || 1;
760
757
  // Second pass: Rendering mode - actual PDF generation with pageCount available
761
758
  const doc = new jspdf_1.jsPDF({
762
759
  orientation: paperSizeCopy.width > paperSizeCopy.height ? 'landscape' : 'portrait',
@@ -772,35 +769,21 @@ function genPDF(layout, printBuffer, options = {}) {
772
769
  defaultFontFamily: layout.defaultFontFamily,
773
770
  };
774
771
  drawStaticPartOfPage(doc, layout, printBuffer, paperSize, options, renderContext);
775
- let renderTopPrintedStartBottomTenths = 0;
776
772
  layout.objects
777
773
  .filter((object) => object.type == 'container' && object.printOnlyAtStart === true)
778
774
  .forEach((object) => {
779
- const lh = addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
780
- renderTopPrintedStartBottomTenths = Math.max(renderTopPrintedStartBottomTenths, lh.y * 10);
775
+ addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
781
776
  });
782
777
  // Draw containers in rendering mode (body containers after printOnlyAtStart)
783
778
  layout.objects
784
- .filter((object) => object.type == 'container' &&
785
- object.printOnlyAtEnd !== true &&
786
- object.printOnlyAtStart !== true)
779
+ .filter((object) => object.type == 'container' && object.printOnlyAtEnd !== true && object.printOnlyAtStart !== true)
787
780
  .forEach((object) => {
788
- const bumpTenths = Math.max(0, renderTopPrintedStartBottomTenths - object.y);
789
- if (bumpTenths > 0) {
790
- renderContext.applyRootStartBumpToThisCall = true;
791
- renderContext.pendingRootStartBumpTenths = bumpTenths;
792
- }
793
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
781
+ addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y));
794
782
  });
795
783
  // Draw printOnlyAtEnd containers once at the end (at their layout coordinates on current page)
796
784
  const printOnlyAtEndContainers = layout.objects.filter((object) => object.type == 'container' && object.printOnlyAtEnd === true);
797
785
  printOnlyAtEndContainers.forEach((object) => {
798
- const bumpTenths = Math.max(0, renderTopPrintedStartBottomTenths - object.y);
799
- if (bumpTenths > 0) {
800
- renderContext.applyRootStartBumpToThisCall = true;
801
- renderContext.pendingRootStartBumpTenths = bumpTenths;
802
- }
803
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
786
+ addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y));
804
787
  });
805
788
  return doc;
806
789
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softwear/latestcollectioncore",
3
- "version": "1.0.183",
3
+ "version": "1.0.185",
4
4
  "description": "Core functions for LatestCollections applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/pdf.ts CHANGED
@@ -61,7 +61,10 @@ export interface ContainerLayoutObject extends BaseLayoutObject {
61
61
  minHeightBeforeBreak?: number
62
62
  /** When true, container and children are drawn only once at the end of the report, not on every page */
63
63
  printOnlyAtEnd?: boolean
64
- /** Only effective on top-level `layout.objects` (PDF engine ignores this flag on nested containers). */
64
+ /**
65
+ * Only on top-level `layout.objects`. Same intent as onlyRenderOnFirstPage: drawn once on the first printable page,
66
+ * not repeated after overflow page breaks.
67
+ */
65
68
  printOnlyAtStart?: boolean
66
69
  }
67
70
 
package/src/reports.ts CHANGED
@@ -40,20 +40,22 @@ export interface GenPdfOptions {
40
40
 
41
41
  interface RenderContext {
42
42
  mode: RenderMode
43
+ /** Total pages from measurement pass; used for field sources like `pageCount` / `page`. */
43
44
  pageCount?: number
44
- currentPageCount?: number // For measurement mode
45
+ /** During measurement, logical printable page (starts at 1); incremented when {@link addPage} runs. In rendering use {@link getCurrentPageNumber} (jsPDF). */
46
+ currentPageNumber?: number
45
47
  measureOnly?: boolean // When true, skip drawing to measure bounds (for fillContainer rectangles)
46
48
  imageAssets?: Map<string, PdfImageAsset>
47
49
  /** Layout default font used when object.fontFamily is undefined */
48
50
  defaultFontFamily?: string
49
- /** Set with {@link pendingRootStartBumpTenths} only for top-level genPDF container draws; moved to remaining on entry. */
50
- applyRootStartBumpToThisCall?: boolean
51
- /** Vertical pad (tenths-mm) so body roots sit below rendered printOnlyAtStart siblings; use with applyRootStartBumpToThisCall only. */
52
- pendingRootStartBumpTenths?: number
53
- /**
54
- * First-fragment bump for top-level draws from genPDF — applied with layout coords, then cleared after first positioning or addPage().
55
- */
56
- remainingRootStartBumpTenths?: number
51
+ }
52
+
53
+ /** Measurement never advances jsPDF sheet count; rendering uses the active canvas page (1-based). */
54
+ function getCurrentPageNumber(doc: jsPDF, context: RenderContext): number {
55
+ if (context.mode === 'measurement') {
56
+ return context.currentPageNumber ?? 1
57
+ }
58
+ return doc.getCurrentPageInfo().pageNumber
57
59
  }
58
60
 
59
61
  // We have to declare some functions that will be called by other functions but also have to call those other functions
@@ -76,7 +78,8 @@ let addObjectToPDF = function (
76
78
  _options: GenPdfOptions,
77
79
  _rootPrintBuffer: any,
78
80
  _containerChain: { object: ContainerLayoutObject; printBuffer: any }[],
79
- _context: RenderContext
81
+ _context: RenderContext,
82
+ _printOnlyAtStartContainersOffset?: number
80
83
  ): { x: number; y: number } {
81
84
  // to be assigned later
82
85
  return { x: 0, y: 0 }
@@ -118,6 +121,24 @@ function containerLoopSource(container: ContainerLayoutObject, printBuffer: any)
118
121
  return getProperty(container.source as string, printBuffer)
119
122
  }
120
123
 
124
+ /**
125
+ * Top-level empty-source containers (invoice “billTo” bands, wrappers) sit beside overflowing repeats.
126
+ * They are not on the nested {@link containerChain}; redraw them whenever we append a continuation page.
127
+ * Omit the overflowing root when it is chain[0] — redrawing it would recurse (e.g. wrapper around lines).
128
+ */
129
+ function redrawRootDummyBodyContainersOnNewPage(doc: jsPDF, layout: Layout, rootPrintBuffer: any, paperSize: PaperSize, options: GenPdfOptions, context: RenderContext, containerChain: { object: ContainerLayoutObject; printBuffer: any }[]): void {
130
+ const overflowingRootContainer = containerChain.length > 0 ? (containerChain[0].object as ContainerLayoutObject) : undefined
131
+
132
+ layout.objects.forEach((object) => {
133
+ if (object.type !== 'container') return
134
+ if (overflowingRootContainer !== undefined && object === overflowingRootContainer) return
135
+ const c = object as ContainerLayoutObject
136
+ if (c.printOnlyAtStart === true || c.printOnlyAtEnd === true) return
137
+ if (!dummyLayoutSource(c.source)) return
138
+ addObjectToPDF(0, 0, doc, object, rootPrintBuffer, paperSize, layout, options, rootPrintBuffer, [], context)
139
+ })
140
+ }
141
+
121
142
  /** jsPDF font style strings. fontStyle bitmask: 0=normal, 1=bold, 2=italic, 3=bold+italic */
122
143
  const FONT_STYLES = ['normal', 'bold', 'italic', 'bolditalic'] as const
123
144
 
@@ -306,17 +327,7 @@ function truncateTextToFitWidth(doc: jsPDF, text: string, maxWidth: number): str
306
327
  return truncatedText + ELLIPSIS
307
328
  }
308
329
 
309
- function drawSimpleObject(
310
- x: number,
311
- y: number,
312
- doc: jsPDF,
313
- object: LayoutObject,
314
- printBuffer: any,
315
- width: number,
316
- height: number,
317
- options: GenPdfOptions,
318
- context: RenderContext
319
- ): void {
330
+ function drawSimpleObject(x: number, y: number, doc: jsPDF, object: LayoutObject, printBuffer: any, width: number, height: number, options: GenPdfOptions, context: RenderContext): void {
320
331
  let text = '' // Will hold text to display for either 'text' or 'field'
321
332
 
322
333
  // Get text from object for text objects
@@ -457,21 +468,15 @@ function addPage(
457
468
  const continuationOxTenths = originX
458
469
  const continuationOyTenths = originY
459
470
 
460
- context.pendingRootStartBumpTenths = 0
461
- context.applyRootStartBumpToThisCall = false
462
- context.remainingRootStartBumpTenths = 0
463
471
  if (context.mode === 'measurement') {
464
- // Track page count in measurement mode
465
- if (context.currentPageCount === undefined) {
466
- context.currentPageCount = 1
467
- }
468
- context.currentPageCount++
472
+ context.currentPageNumber = (context.currentPageNumber ?? 1) + 1
469
473
  } else if (!context.measureOnly) {
470
474
  // Actually add page in rendering mode (skip when measuring for fillContainer)
471
475
  doc.addPage()
472
476
  }
473
477
  if (!context.measureOnly) {
474
478
  drawStaticPartOfPage(doc, layout, rootPrintBuffer, paperSize, options, context)
479
+ redrawRootDummyBodyContainersOnNewPage(doc, layout, rootPrintBuffer, paperSize, options, context, containerChain)
475
480
  }
476
481
  // Bounding box tracked for snap-to-bottom; continuation cursor is ALWAYS the passed-in coords.
477
482
  const absoluteLowerRightHandSide = { x: continuationOxTenths / 10, y: continuationOyTenths / 10 }
@@ -489,12 +494,7 @@ function addPage(
489
494
  link.object.children.forEach((child) => {
490
495
  if (child.snapToBottom) return
491
496
  // Skip inner repeat container on chain: advancing origin by x,y is enough; the live loop draws rows.
492
- if (
493
- child.type === 'container' &&
494
- deeperLinkChild &&
495
- child === deeperLinkChild &&
496
- !containerRedrawsAfterContinuationPage(child)
497
- ) {
497
+ if (child.type === 'container' && deeperLinkChild && child === deeperLinkChild && !containerRedrawsAfterContinuationPage(child)) {
498
498
  const c = child as ContainerLayoutObject
499
499
  originX += c.x
500
500
  originY += c.y
@@ -579,21 +579,17 @@ addObjectToPDF = function (
579
579
  options: GenPdfOptions,
580
580
  rootPrintBuffer: any,
581
581
  containerChain: { object: ContainerLayoutObject; printBuffer: any }[],
582
- context: RenderContext
582
+ context: RenderContext,
583
+ printOnlyAtStartContainersOffset?: number
583
584
  ): { x: number; y: number } {
584
- if (context.applyRootStartBumpToThisCall) {
585
- context.remainingRootStartBumpTenths = context.pendingRootStartBumpTenths ?? 0
586
- context.applyRootStartBumpToThisCall = false
587
- context.pendingRootStartBumpTenths = 0
588
- }
589
585
  const layoutXtenths = originX + object.x
590
586
  const layoutYtenths = originY + object.y
591
- /** printOnlyAtStart offset only for outermost recursion and only until first positioning or addPage clears it */
592
- const bumpForDrawNonContainer =
593
- containerChain.length === 0 ? context.remainingRootStartBumpTenths ?? 0 : 0
587
+ const rawPrintOnlyAtStartOffset = printOnlyAtStartContainersOffset ?? 0
588
+ const effectivePrintOnlyAtStartContainersOffset =
589
+ containerChain.length === 0 && rawPrintOnlyAtStartOffset > 0 && getCurrentPageNumber(doc, context) === 1 ? rawPrintOnlyAtStartOffset : 0
594
590
 
595
591
  const x = layoutXtenths / 10
596
- const y = (layoutYtenths + bumpForDrawNonContainer) / 10
592
+ const y = (layoutYtenths + effectivePrintOnlyAtStartContainersOffset) / 10
597
593
  const width = object.width / 10
598
594
  const height = object.height / 10
599
595
  // Keep track of the lower righthandside of all objects in this container
@@ -601,11 +597,9 @@ addObjectToPDF = function (
601
597
  let absoluteLowerRightHandSide = { x: x + width, y: y + height }
602
598
 
603
599
  if (!object.active) {
604
- if (bumpForDrawNonContainer !== 0) context.remainingRootStartBumpTenths = 0
605
600
  return absoluteLowerRightHandSide
606
601
  }
607
602
  if (object.type != 'container') {
608
- if (bumpForDrawNonContainer !== 0) context.remainingRootStartBumpTenths = 0
609
603
  drawSimpleObject(x, y, doc, object, printBuffer, width, height, options, context)
610
604
  }
611
605
 
@@ -614,15 +608,12 @@ addObjectToPDF = function (
614
608
  const source = containerLoopSource(object as ContainerLayoutObject, printBuffer)
615
609
  if (source === undefined) {
616
610
  options.onAlert?.({ header: 'containernotfound', body: object.source, type: 'warning', timeout: 10000 })
617
- if (containerChain.length === 0) context.remainingRootStartBumpTenths = 0
618
611
  return absoluteLowerRightHandSide
619
612
  }
620
613
  const nrContainers = source.length
621
614
 
622
- let bumpOnce = containerChain.length === 0 ? context.remainingRootStartBumpTenths ?? 0 : 0
623
- if (bumpOnce !== 0 && containerChain.length === 0) context.remainingRootStartBumpTenths = 0
624
615
  let originX = layoutXtenths
625
- let originY = layoutYtenths + bumpOnce
616
+ let originY = layoutYtenths + effectivePrintOnlyAtStartContainersOffset
626
617
 
627
618
  for (let containerIndex = 0; containerIndex < nrContainers; containerIndex++) {
628
619
  const newContainerChain = containerChain.filter(function () {
@@ -635,7 +626,7 @@ addObjectToPDF = function (
635
626
  if (useOrphanCheck && (object.pageBreak || originY + minHeightMm > paperSize.height - paperSize.footerHeight)) {
636
627
  // We ran out of paper
637
628
  originX = layoutXtenths
638
- originY = layoutYtenths + (containerChain.length === 0 ? context.remainingRootStartBumpTenths ?? 0 : 0)
629
+ originY = layoutYtenths + effectivePrintOnlyAtStartContainersOffset
639
630
  const newOrigin = addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, options, newContainerChain, context)
640
631
  originX = newOrigin.originX
641
632
  originY = newOrigin.originY
@@ -666,21 +657,7 @@ addObjectToPDF = function (
666
657
  measureBounds.y = Math.max(measureBounds.y, r.y)
667
658
  }
668
659
  })
669
- const measureBottom = drawBottomDwellers(
670
- object as ContainerLayoutObject,
671
- measureOriginX,
672
- measureOriginY,
673
- { x: measureBounds.x, y: measureBounds.y },
674
- measureBottomChildY,
675
- paperSize,
676
- doc,
677
- layout,
678
- rootPrintBuffer,
679
- options,
680
- measureChain,
681
- container,
682
- measureContext
683
- )
660
+ const measureBottom = drawBottomDwellers(object as ContainerLayoutObject, measureOriginX, measureOriginY, { x: measureBounds.x, y: measureBounds.y }, measureBottomChildY, paperSize, doc, layout, rootPrintBuffer, options, measureChain, container, measureContext)
684
661
  measureBounds.y = Math.max(measureBounds.y, measureBottom.y)
685
662
  // Draw fillContainer rectangles as background
686
663
  fillContainerRects.forEach(function (rect: any) {
@@ -748,42 +725,14 @@ addObjectToPDF = function (
748
725
  originX = newOrigin.originX
749
726
  originY = newOrigin.originY
750
727
  absoluteLowerRightHandSide = { x: newOrigin.x, y: newOrigin.y }
751
- const newestOrigin = drawBottomDwellers(
752
- object as ContainerLayoutObject,
753
- originX,
754
- originY,
755
- absoluteLowerRightHandSide,
756
- bottomChildY,
757
- paperSize,
758
- doc,
759
- layout,
760
- rootPrintBuffer,
761
- options,
762
- newContainerChain,
763
- container,
764
- context
765
- )
728
+ const newestOrigin = drawBottomDwellers(object as ContainerLayoutObject, originX, originY, absoluteLowerRightHandSide, bottomChildY, paperSize, doc, layout, rootPrintBuffer, options, newContainerChain, container, context)
766
729
  originX = newestOrigin.originX
767
730
  originY = newestOrigin.originY
768
731
  absoluteLowerRightHandSide = { x: newestOrigin.x, y: newestOrigin.y }
769
732
  continue
770
733
  }
771
734
  }
772
- const newestOrigin = drawBottomDwellers(
773
- object as ContainerLayoutObject,
774
- originX,
775
- originY,
776
- absoluteLowerRightHandSide,
777
- bottomChildY,
778
- paperSize,
779
- doc,
780
- layout,
781
- rootPrintBuffer,
782
- options,
783
- newContainerChain,
784
- container,
785
- context
786
- )
735
+ const newestOrigin = drawBottomDwellers(object as ContainerLayoutObject, originX, originY, absoluteLowerRightHandSide, bottomChildY, paperSize, doc, layout, rootPrintBuffer, options, newContainerChain, container, context)
787
736
  originX = newestOrigin.originX
788
737
  originY = newestOrigin.originY
789
738
  absoluteLowerRightHandSide = { x: newestOrigin.x, y: newestOrigin.y }
@@ -842,10 +791,12 @@ export async function genPDF(layout: Layout, printBuffer: any, options: GenPdfOp
842
791
  paperSize.height *= 10
843
792
  paperSize.footerHeight *= 10
844
793
 
845
- // First pass: Measurement mode - count pages without rendering
794
+ // First pass: Measurement mode page count without visible output; overlaps must match render pass inputs.
846
795
  const measurementContext: RenderContext = {
847
796
  mode: 'measurement',
848
- currentPageCount: 1, // Start with page 1
797
+ currentPageNumber: 1,
798
+ imageAssets,
799
+ defaultFontFamily: layout.defaultFontFamily,
849
800
  }
850
801
  // Create a minimal jsPDF instance for measurement (needed for some calculations)
851
802
  const measurementDoc = new jsPDF({
@@ -859,41 +810,27 @@ export async function genPDF(layout: Layout, printBuffer: any, options: GenPdfOp
859
810
  drawStaticPartOfPage(measurementDoc, layout, printBuffer, paperSize, options, measurementContext)
860
811
 
861
812
  // Draw containers in measurement mode (same order as render)
862
- let measurementTopPrintedStartBottomTenths = 0
813
+ /** Max lower edge (layout units ×10 = tenths-mm) of print-only-at-start roots; deterministic for fixed layout+buffer once measure uses same context as render. */
814
+ let measuredPrintOnlyAtStartOffset = 0
863
815
  layout.objects
864
816
  .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtStart === true)
865
817
  .forEach((object) => {
866
818
  const lh = addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
867
- measurementTopPrintedStartBottomTenths = Math.max(measurementTopPrintedStartBottomTenths, lh.y * 10)
819
+ measuredPrintOnlyAtStartOffset = Math.max(measuredPrintOnlyAtStartOffset, lh.y * 10)
868
820
  })
869
821
  layout.objects
870
- .filter(
871
- (object) =>
872
- object.type == 'container' &&
873
- (object as ContainerLayoutObject).printOnlyAtEnd !== true &&
874
- (object as ContainerLayoutObject).printOnlyAtStart !== true
875
- )
822
+ .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtEnd !== true && (object as ContainerLayoutObject).printOnlyAtStart !== true)
876
823
  .forEach((object) => {
877
- const bumpTenths = Math.max(0, measurementTopPrintedStartBottomTenths - object.y)
878
- if (bumpTenths > 0) {
879
- measurementContext.applyRootStartBumpToThisCall = true
880
- measurementContext.pendingRootStartBumpTenths = bumpTenths
881
- }
882
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
824
+ addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y))
883
825
  })
884
826
  layout.objects
885
827
  .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtEnd === true)
886
828
  .forEach((object) => {
887
- const bumpTenths = Math.max(0, measurementTopPrintedStartBottomTenths - object.y)
888
- if (bumpTenths > 0) {
889
- measurementContext.applyRootStartBumpToThisCall = true
890
- measurementContext.pendingRootStartBumpTenths = bumpTenths
891
- }
892
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
829
+ addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y))
893
830
  })
894
831
 
895
832
  // Get total page count from measurement pass
896
- const totalPageCount = measurementContext.currentPageCount || 1
833
+ const totalPageCount = measurementContext.currentPageNumber || 1
897
834
 
898
835
  // Second pass: Rendering mode - actual PDF generation with pageCount available
899
836
  const doc = new jsPDF({
@@ -911,42 +848,23 @@ export async function genPDF(layout: Layout, printBuffer: any, options: GenPdfOp
911
848
  }
912
849
  drawStaticPartOfPage(doc, layout, printBuffer, paperSize, options, renderContext)
913
850
 
914
- let renderTopPrintedStartBottomTenths = 0
915
851
  layout.objects
916
852
  .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtStart === true)
917
853
  .forEach((object) => {
918
- const lh = addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
919
- renderTopPrintedStartBottomTenths = Math.max(renderTopPrintedStartBottomTenths, lh.y * 10)
854
+ addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
920
855
  })
921
856
 
922
857
  // Draw containers in rendering mode (body containers after printOnlyAtStart)
923
858
  layout.objects
924
- .filter(
925
- (object) =>
926
- object.type == 'container' &&
927
- (object as ContainerLayoutObject).printOnlyAtEnd !== true &&
928
- (object as ContainerLayoutObject).printOnlyAtStart !== true
929
- )
859
+ .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtEnd !== true && (object as ContainerLayoutObject).printOnlyAtStart !== true)
930
860
  .forEach((object) => {
931
- const bumpTenths = Math.max(0, renderTopPrintedStartBottomTenths - object.y)
932
- if (bumpTenths > 0) {
933
- renderContext.applyRootStartBumpToThisCall = true
934
- renderContext.pendingRootStartBumpTenths = bumpTenths
935
- }
936
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
861
+ addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y))
937
862
  })
938
863
 
939
864
  // Draw printOnlyAtEnd containers once at the end (at their layout coordinates on current page)
940
- const printOnlyAtEndContainers = layout.objects.filter(
941
- (object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtEnd === true
942
- )
865
+ const printOnlyAtEndContainers = layout.objects.filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtEnd === true)
943
866
  printOnlyAtEndContainers.forEach((object) => {
944
- const bumpTenths = Math.max(0, renderTopPrintedStartBottomTenths - object.y)
945
- if (bumpTenths > 0) {
946
- renderContext.applyRootStartBumpToThisCall = true
947
- renderContext.pendingRootStartBumpTenths = bumpTenths
948
- }
949
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
867
+ addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext, Math.max(0, measuredPrintOnlyAtStartOffset - object.y))
950
868
  })
951
869
  return doc
952
870
  }
@@ -263,4 +263,72 @@ describe('genPDF', () => {
263
263
  expect(pdfText).toContain('ROW_0_MARK')
264
264
  expect(pdfText).toContain('ROW_35_MARK')
265
265
  })
266
+
267
+ it('redraws top-level empty-source body containers on every continuation page beside an overflowing sibling', async () => {
268
+ const chromeText = {
269
+ type: 'text' as const,
270
+ name: 'chrome-label',
271
+ x: 0,
272
+ y: 0,
273
+ width: 300,
274
+ height: 48,
275
+ active: true,
276
+ text: 'CHROME_EACH_PAGE',
277
+ textAlign: 1 as const,
278
+ fontFamily: 'Helvetica',
279
+ fontSize: 28,
280
+ fontStyle: 0,
281
+ }
282
+ const lineField = {
283
+ type: 'field' as const,
284
+ name: 'ln',
285
+ x: 0,
286
+ y: 0,
287
+ width: 800,
288
+ height: 96,
289
+ active: true,
290
+ source: 'line',
291
+ textAlign: 1 as const,
292
+ fontFamily: 'Helvetica',
293
+ fontSize: 36,
294
+ fontStyle: 0,
295
+ }
296
+ const ITEMS = Array.from({ length: 48 }, (_, i) => ({ line: `SKU_${i}` }))
297
+ const layout: Layout = {
298
+ name: 'sibling-chrome',
299
+ paperSize: { width: 210, height: 297, footerHeight: 20 },
300
+ objects: [
301
+ {
302
+ type: 'container',
303
+ name: 'addressBand',
304
+ x: 60,
305
+ y: 440,
306
+ width: 400,
307
+ height: 280,
308
+ active: true,
309
+ source: '',
310
+ children: [chromeText],
311
+ },
312
+ {
313
+ type: 'container',
314
+ name: 'lines',
315
+ x: 500,
316
+ y: 440,
317
+ width: 1500,
318
+ height: 200,
319
+ active: true,
320
+ source: 'ITEMS',
321
+ repeatContainer: 'vertical' as const,
322
+ children: [lineField],
323
+ },
324
+ ],
325
+ }
326
+
327
+ const doc = await genPDF(layout, { ITEMS })
328
+ expect(doc.getNumberOfPages()).toBeGreaterThanOrEqual(2)
329
+ const pdfText = Buffer.from(doc.output('arraybuffer')).toString('latin1')
330
+ const chromeMatches = pdfText.match(/\(CHROME_EACH_PAGE\)/g)
331
+ expect(chromeMatches?.length ?? 0).toBeGreaterThanOrEqual(doc.getNumberOfPages())
332
+ expect(pdfText).toContain('SKU_20')
333
+ })
266
334
  })