@softwear/latestcollectioncore 1.0.176 → 1.0.178

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/dist/pdf.d.ts CHANGED
@@ -54,6 +54,8 @@ 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). */
58
+ printOnlyAtStart?: boolean;
57
59
  }
58
60
  export type LayoutObject = TextLayoutObject | FieldLayoutObject | RectangleLayoutObject | ImageLayoutObject | ContainerLayoutObject;
59
61
  export interface Layout {
package/dist/reports.js CHANGED
@@ -43,6 +43,12 @@ function getProperty(propertyName, object) {
43
43
  }
44
44
  return property;
45
45
  }
46
+ /** Repeat once over the current buffer when the container is only a visual group (no `source` binding). */
47
+ function containerLoopSource(container, printBuffer) {
48
+ if (!container.source || container.source === '')
49
+ return [printBuffer];
50
+ return getProperty(container.source, printBuffer);
51
+ }
46
52
  /** jsPDF font style strings. fontStyle bitmask: 0=normal, 1=bold, 2=italic, 3=bold+italic */
47
53
  const FONT_STYLES = ['normal', 'bold', 'italic', 'bolditalic'];
48
54
  /** Recursively collect used custom fonts from layout objects (text/field with fontFamily) and layout default. */
@@ -160,7 +166,7 @@ function collectImageUrlsFromLayout(layout, printBuffer) {
160
166
  }
161
167
  if (object.type !== 'container')
162
168
  return;
163
- const source = object.printOnlyAtEnd === true && (!object.source || object.source === '') ? [currentPrintBuffer] : getProperty(object.source, currentPrintBuffer);
169
+ const source = containerLoopSource(object, currentPrintBuffer);
164
170
  if (!Array.isArray(source))
165
171
  return;
166
172
  source.forEach((containerPrintBuffer) => walk(object.children, containerPrintBuffer));
@@ -450,8 +456,7 @@ addObjectToPDF = function (originX, originY, doc, object, printBuffer, paperSize
450
456
  // Recursively draw all child objects from a container object
451
457
  if (object.type == 'container') {
452
458
  let originX = x * 10, originY = y * 10;
453
- const isPrintOnlyAtEnd = object.printOnlyAtEnd === true;
454
- const source = isPrintOnlyAtEnd && (!object.source || object.source === '') ? [printBuffer] : getProperty(object.source, printBuffer);
459
+ const source = containerLoopSource(object, printBuffer);
455
460
  if (source === undefined) {
456
461
  (_a = options.onAlert) === null || _a === void 0 ? void 0 : _a.call(options, { header: 'containernotfound', body: object.source, type: 'warning', timeout: 10000 });
457
462
  return absoluteLowerRightHandSide;
@@ -657,16 +662,27 @@ function genPDF(layout, printBuffer, options = {}) {
657
662
  yield embedFontsInDoc(measurementDoc, usedFonts, options);
658
663
  }
659
664
  drawStaticPartOfPage(measurementDoc, layout, printBuffer, paperSize, options, measurementContext);
660
- // Draw containers in measurement mode (same order as render: normal first, then printOnlyAtEnd on fresh page)
665
+ // Draw containers in measurement mode (same order as render)
666
+ let measurementTopPrintedStartBottomTenths = 0;
667
+ layout.objects
668
+ .filter((object) => object.type == 'container' && object.printOnlyAtStart)
669
+ .forEach((object) => {
670
+ const lh = addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
671
+ measurementTopPrintedStartBottomTenths = Math.max(measurementTopPrintedStartBottomTenths, lh.y * 10);
672
+ });
661
673
  layout.objects
662
- .filter((object) => object.type == 'container' && !object.printOnlyAtEnd)
674
+ .filter((object) => object.type == 'container' &&
675
+ !object.printOnlyAtEnd &&
676
+ !object.printOnlyAtStart)
663
677
  .forEach((object) => {
664
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
678
+ const bump = Math.max(0, measurementTopPrintedStartBottomTenths - object.y);
679
+ addObjectToPDF(0, bump, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
665
680
  });
666
681
  layout.objects
667
682
  .filter((object) => object.type == 'container' && object.printOnlyAtEnd)
668
683
  .forEach((object) => {
669
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
684
+ const bump = Math.max(0, measurementTopPrintedStartBottomTenths - object.y);
685
+ addObjectToPDF(0, bump, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext);
670
686
  });
671
687
  // Get total page count from measurement pass
672
688
  const totalPageCount = measurementContext.currentPageCount || 1;
@@ -685,16 +701,27 @@ function genPDF(layout, printBuffer, options = {}) {
685
701
  defaultFontFamily: layout.defaultFontFamily,
686
702
  };
687
703
  drawStaticPartOfPage(doc, layout, printBuffer, paperSize, options, renderContext);
688
- // Draw containers in rendering mode (normal containers first)
704
+ let renderTopPrintedStartBottomTenths = 0;
705
+ layout.objects
706
+ .filter((object) => object.type == 'container' && object.printOnlyAtStart)
707
+ .forEach((object) => {
708
+ const lh = addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
709
+ renderTopPrintedStartBottomTenths = Math.max(renderTopPrintedStartBottomTenths, lh.y * 10);
710
+ });
711
+ // Draw containers in rendering mode (body containers after printOnlyAtStart)
689
712
  layout.objects
690
- .filter((object) => object.type == 'container' && !object.printOnlyAtEnd)
713
+ .filter((object) => object.type == 'container' &&
714
+ !object.printOnlyAtEnd &&
715
+ !object.printOnlyAtStart)
691
716
  .forEach((object) => {
692
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
717
+ const bump = Math.max(0, renderTopPrintedStartBottomTenths - object.y);
718
+ addObjectToPDF(0, bump, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
693
719
  });
694
720
  // Draw printOnlyAtEnd containers once at the end (at their layout coordinates on current page)
695
721
  const printOnlyAtEndContainers = layout.objects.filter((object) => object.type == 'container' && object.printOnlyAtEnd);
696
722
  printOnlyAtEndContainers.forEach((object) => {
697
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
723
+ const bump = Math.max(0, renderTopPrintedStartBottomTenths - object.y);
724
+ addObjectToPDF(0, bump, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext);
698
725
  });
699
726
  return doc;
700
727
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softwear/latestcollectioncore",
3
- "version": "1.0.176",
3
+ "version": "1.0.178",
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,6 +61,8 @@ 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). */
65
+ printOnlyAtStart?: boolean
64
66
  }
65
67
 
66
68
  export type LayoutObject = TextLayoutObject | FieldLayoutObject | RectangleLayoutObject | ImageLayoutObject | ContainerLayoutObject
package/src/reports.ts CHANGED
@@ -92,6 +92,12 @@ function getProperty(propertyName: string, object: any): any {
92
92
  return property
93
93
  }
94
94
 
95
+ /** Repeat once over the current buffer when the container is only a visual group (no `source` binding). */
96
+ function containerLoopSource(container: ContainerLayoutObject, printBuffer: any): any {
97
+ if (!container.source || container.source === '') return [printBuffer]
98
+ return getProperty(container.source, printBuffer)
99
+ }
100
+
95
101
  /** jsPDF font style strings. fontStyle bitmask: 0=normal, 1=bold, 2=italic, 3=bold+italic */
96
102
  const FONT_STYLES = ['normal', 'bold', 'italic', 'bolditalic'] as const
97
103
 
@@ -222,7 +228,7 @@ function collectImageUrlsFromLayout(layout: Layout, printBuffer: any): string[]
222
228
 
223
229
  if (object.type !== 'container') return
224
230
 
225
- const source = (object as any).printOnlyAtEnd === true && (!object.source || object.source === '') ? [currentPrintBuffer] : getProperty(object.source, currentPrintBuffer)
231
+ const source = containerLoopSource(object as ContainerLayoutObject, currentPrintBuffer)
226
232
  if (!Array.isArray(source)) return
227
233
  source.forEach((containerPrintBuffer) => walk(object.children, containerPrintBuffer))
228
234
  })
@@ -539,8 +545,7 @@ addObjectToPDF = function (
539
545
  let originX = x * 10,
540
546
  originY = y * 10
541
547
 
542
- const isPrintOnlyAtEnd = (object as any).printOnlyAtEnd === true
543
- const source = isPrintOnlyAtEnd && (!object.source || object.source === '') ? [printBuffer] : getProperty(object.source, printBuffer)
548
+ const source = containerLoopSource(object as ContainerLayoutObject, printBuffer)
544
549
  if (source === undefined) {
545
550
  options.onAlert?.({ header: 'containernotfound', body: object.source, type: 'warning', timeout: 10000 })
546
551
  return absoluteLowerRightHandSide
@@ -780,16 +785,30 @@ export async function genPDF(layout: Layout, printBuffer: any, options: GenPdfOp
780
785
  }
781
786
  drawStaticPartOfPage(measurementDoc, layout, printBuffer, paperSize, options, measurementContext)
782
787
 
783
- // Draw containers in measurement mode (same order as render: normal first, then printOnlyAtEnd on fresh page)
788
+ // Draw containers in measurement mode (same order as render)
789
+ let measurementTopPrintedStartBottomTenths = 0
790
+ layout.objects
791
+ .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtStart)
792
+ .forEach((object) => {
793
+ const lh = addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
794
+ measurementTopPrintedStartBottomTenths = Math.max(measurementTopPrintedStartBottomTenths, lh.y * 10)
795
+ })
784
796
  layout.objects
785
- .filter((object) => object.type == 'container' && !(object as any).printOnlyAtEnd)
797
+ .filter(
798
+ (object) =>
799
+ object.type == 'container' &&
800
+ !(object as ContainerLayoutObject).printOnlyAtEnd &&
801
+ !(object as ContainerLayoutObject).printOnlyAtStart
802
+ )
786
803
  .forEach((object) => {
787
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
804
+ const bump = Math.max(0, measurementTopPrintedStartBottomTenths - object.y)
805
+ addObjectToPDF(0, bump, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
788
806
  })
789
807
  layout.objects
790
- .filter((object) => object.type == 'container' && (object as any).printOnlyAtEnd)
808
+ .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtEnd)
791
809
  .forEach((object) => {
792
- addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
810
+ const bump = Math.max(0, measurementTopPrintedStartBottomTenths - object.y)
811
+ addObjectToPDF(0, bump, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
793
812
  })
794
813
 
795
814
  // Get total page count from measurement pass
@@ -811,17 +830,34 @@ export async function genPDF(layout: Layout, printBuffer: any, options: GenPdfOp
811
830
  }
812
831
  drawStaticPartOfPage(doc, layout, printBuffer, paperSize, options, renderContext)
813
832
 
814
- // Draw containers in rendering mode (normal containers first)
833
+ let renderTopPrintedStartBottomTenths = 0
815
834
  layout.objects
816
- .filter((object) => object.type == 'container' && !(object as any).printOnlyAtEnd)
835
+ .filter((object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtStart)
817
836
  .forEach((object) => {
818
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
837
+ const lh = addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
838
+ renderTopPrintedStartBottomTenths = Math.max(renderTopPrintedStartBottomTenths, lh.y * 10)
839
+ })
840
+
841
+ // Draw containers in rendering mode (body containers after printOnlyAtStart)
842
+ layout.objects
843
+ .filter(
844
+ (object) =>
845
+ object.type == 'container' &&
846
+ !(object as ContainerLayoutObject).printOnlyAtEnd &&
847
+ !(object as ContainerLayoutObject).printOnlyAtStart
848
+ )
849
+ .forEach((object) => {
850
+ const bump = Math.max(0, renderTopPrintedStartBottomTenths - object.y)
851
+ addObjectToPDF(0, bump, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
819
852
  })
820
853
 
821
854
  // Draw printOnlyAtEnd containers once at the end (at their layout coordinates on current page)
822
- const printOnlyAtEndContainers = layout.objects.filter((object) => object.type == 'container' && (object as any).printOnlyAtEnd)
855
+ const printOnlyAtEndContainers = layout.objects.filter(
856
+ (object) => object.type == 'container' && (object as ContainerLayoutObject).printOnlyAtEnd
857
+ )
823
858
  printOnlyAtEndContainers.forEach((object) => {
824
- addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
859
+ const bump = Math.max(0, renderTopPrintedStartBottomTenths - object.y)
860
+ addObjectToPDF(0, bump, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
825
861
  })
826
862
  return doc
827
863
  }
@@ -1,8 +1,15 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { genPDF } from '../src/reports'
3
3
  import type { Layout } from '../src/pdf'
4
- // import fs from 'fs'
5
4
 
5
+ /** Tiny valid JPEG as data URL — avoids flaky network-dependent image fetch in CI/sandbox. */
6
+ function offlineJpegFixture(): string {
7
+ const b64 =
8
+ '/9j/4AAQSkZJRgABAQIAASABIAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAKAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHhAP/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAQkCf//EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8B/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgMBPwF//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQYPAj//xAAVEAEAAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQBPCQ//Z'
9
+ return `data:image/jpeg;base64,${b64}`
10
+ }
11
+
12
+ // URL still used so collectImageUrlsFromLayout resolves the layout object
6
13
  const imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/7/79/Phillips_PM5538.jpg/500px-Phillips_PM5538.jpg'
7
14
 
8
15
  describe('genPDF', () => {
@@ -59,7 +66,14 @@ describe('genPDF', () => {
59
66
  title: 'Minimal PDF',
60
67
  }
61
68
 
62
- const doc = await genPDF(layout, printBuffer)
69
+ const doc = await genPDF(layout, printBuffer, {
70
+ loadImage: async () => ({
71
+ dataUrl: offlineJpegFixture(),
72
+ format: 'JPEG',
73
+ width: 500,
74
+ height: 375,
75
+ }),
76
+ })
63
77
  const pdf = doc.output('arraybuffer')
64
78
  const pdfText = Buffer.from(pdf).toString('latin1')
65
79
 
@@ -71,4 +85,128 @@ describe('genPDF', () => {
71
85
  expect(pdfText).toContain(printBuffer.title)
72
86
  // fs.writeFileSync('test.pdf', Buffer.from(pdf))
73
87
  })
88
+
89
+ it('draws printOnlyAtStart before body containers and printOnlyAtEnd after', async () => {
90
+ const textChild = (name: string, y: number, label: string) =>
91
+ ({
92
+ type: 'text' as const,
93
+ name,
94
+ x: 20,
95
+ y,
96
+ width: 400,
97
+ height: 40,
98
+ active: true,
99
+ text: label,
100
+ textAlign: 1 as const,
101
+ fontFamily: 'Helvetica',
102
+ fontSize: 24,
103
+ fontStyle: 0,
104
+ }) as const
105
+
106
+ const layout: Layout = {
107
+ name: 'order-test',
108
+ paperSize: { width: 210, height: 297, footerHeight: 20 },
109
+ objects: [
110
+ {
111
+ type: 'container',
112
+ x: 0,
113
+ y: 40,
114
+ width: 500,
115
+ height: 80,
116
+ active: true,
117
+ source: '',
118
+ printOnlyAtStart: true,
119
+ children: [textChild('start', 0, 'START_ONLY')],
120
+ },
121
+ {
122
+ type: 'container',
123
+ x: 0,
124
+ y: 120,
125
+ width: 500,
126
+ height: 80,
127
+ active: true,
128
+ source: '',
129
+ children: [textChild('mid', 0, 'BODY_MIDDLE')],
130
+ },
131
+ {
132
+ type: 'container',
133
+ x: 0,
134
+ y: 200,
135
+ width: 500,
136
+ height: 80,
137
+ active: true,
138
+ source: '',
139
+ printOnlyAtEnd: true,
140
+ children: [textChild('end', 0, 'END_ONLY')],
141
+ },
142
+ ],
143
+ }
144
+ const doc = await genPDF(layout, {})
145
+ const pdfText = Buffer.from(doc.output('arraybuffer')).toString('latin1')
146
+ const iStart = pdfText.indexOf('(START_ONLY)')
147
+ const iMid = pdfText.indexOf('(BODY_MIDDLE)')
148
+ const iEnd = pdfText.indexOf('(END_ONLY)')
149
+ expect(iStart).toBeGreaterThanOrEqual(0)
150
+ expect(iMid).toBeGreaterThanOrEqual(0)
151
+ expect(iEnd).toBeGreaterThanOrEqual(0)
152
+ expect(iStart).toBeLessThan(iMid)
153
+ expect(iMid).toBeLessThan(iEnd)
154
+ })
155
+
156
+ it('stacks top-level body containers below printOnlyAtStart when they share the same layout y', async () => {
157
+ const text = (name: string, label: string, y = 0, fontSize = 80) =>
158
+ ({
159
+ type: 'text' as const,
160
+ name,
161
+ x: 20,
162
+ y,
163
+ width: 900,
164
+ height: 400,
165
+ active: true,
166
+ text: label,
167
+ textAlign: 1 as const,
168
+ fontFamily: 'Helvetica',
169
+ fontSize,
170
+ fontStyle: 0,
171
+ }) as const
172
+
173
+ const sharedY = 400
174
+ const layout: Layout = {
175
+ name: 'same-y-stack',
176
+ paperSize: { width: 210, height: 297, footerHeight: 20 },
177
+ objects: [
178
+ {
179
+ type: 'container',
180
+ x: 0,
181
+ y: sharedY,
182
+ width: 1000,
183
+ height: 900,
184
+ active: true,
185
+ source: '',
186
+ printOnlyAtStart: true,
187
+ children: [text('h', 'PAGE1_HEADER_BLOCK')],
188
+ },
189
+ {
190
+ type: 'container',
191
+ x: 0,
192
+ y: sharedY,
193
+ width: 1000,
194
+ height: 500,
195
+ active: true,
196
+ source: '',
197
+ children: [text('b', 'BODY_AFTER_STACK', 0, 32)],
198
+ },
199
+ ],
200
+ }
201
+
202
+ const doc = await genPDF(layout, {})
203
+ const pdfText = Buffer.from(doc.output('arraybuffer')).toString('latin1')
204
+ expect(pdfText).toContain('(PAGE1_HEADER_BLOCK)')
205
+ expect(pdfText).toContain('(BODY_AFTER_STACK)')
206
+ const iH = pdfText.indexOf('(PAGE1_HEADER_BLOCK)')
207
+ const iB = pdfText.indexOf('(BODY_AFTER_STACK)')
208
+ expect(iH).toBeGreaterThanOrEqual(0)
209
+ expect(iB).toBeGreaterThanOrEqual(0)
210
+ expect(iH).toBeLessThan(iB)
211
+ })
74
212
  })