@softwear/latestcollectioncore 1.0.174 → 1.0.176
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/imageBinder.js +18 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/pdf.d.ts +67 -0
- package/dist/pdf.js +2 -0
- package/dist/reports.d.ts +35 -0
- package/dist/reports.js +705 -0
- package/package.json +3 -2
- package/src/imageBinder.ts +18 -16
- package/src/index.ts +3 -0
- package/src/pdf.ts +76 -0
- package/src/reports.ts +831 -0
- package/test/imageBinder.spec.js +8 -8
- package/test/imageBinderTestData/brandDataMatched/dreamstar.json +6 -709
- package/test/imageBinderTestData/brandDataMatched/lowa.json +6 -1456
- package/test/imageBinderTestData/brandDataMatched/miraclesuit.json +838 -1721
- package/test/imageBinderTestData/brandDataMatched/vacanzeitaliane.json +1215 -2185
- package/test/imageBinderTestData/brandSkus/dreamstar.json +68 -34
- package/test/imageBinderTestData/brandSkus/lowa.json +168 -84
- package/test/imageBinderTestData/brandSkus/miraclesuit.json +78 -39
- package/test/imageBinderTestData/brandSkus/vacanzeitaliane.json +106 -53
- package/test/reports.spec.ts +74 -0
- package/test.pdf +0 -0
package/src/reports.ts
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
import { jsPDF } from 'jspdf'
|
|
2
|
+
import { format } from 'date-fns'
|
|
3
|
+
import deepCopy from './deepCopy'
|
|
4
|
+
import { PaperSize, Layout, LayoutObject, ContainerLayoutObject } from './pdf'
|
|
5
|
+
|
|
6
|
+
type RenderMode = 'measurement' | 'rendering'
|
|
7
|
+
|
|
8
|
+
export interface GenPdfAlert {
|
|
9
|
+
header: string
|
|
10
|
+
body?: string
|
|
11
|
+
type?: 'warning' | 'error' | 'success' | 'info'
|
|
12
|
+
timeout?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PdfFontVariant {
|
|
16
|
+
style: 'normal' | 'bold' | 'italic' | 'bolditalic'
|
|
17
|
+
url: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PdfFontDefinition {
|
|
21
|
+
family: string
|
|
22
|
+
variants: PdfFontVariant[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PdfImageAsset {
|
|
26
|
+
dataUrl: string
|
|
27
|
+
format: 'PNG' | 'JPEG'
|
|
28
|
+
width?: number
|
|
29
|
+
height?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface GenPdfOptions {
|
|
33
|
+
onAlert?: (alert: GenPdfAlert) => void
|
|
34
|
+
translate?: (key: string) => string
|
|
35
|
+
resolveLogoUrl?: () => string | undefined
|
|
36
|
+
isCustomPdfFont?: (fontFamily: string) => boolean
|
|
37
|
+
getPdfFontDefinition?: (fontFamily: string) => PdfFontDefinition | undefined
|
|
38
|
+
loadImage?: (url: string) => Promise<PdfImageAsset | undefined>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface RenderContext {
|
|
42
|
+
mode: RenderMode
|
|
43
|
+
pageCount?: number
|
|
44
|
+
currentPageCount?: number // For measurement mode
|
|
45
|
+
measureOnly?: boolean // When true, skip drawing to measure bounds (for fillContainer rectangles)
|
|
46
|
+
imageAssets?: Map<string, PdfImageAsset>
|
|
47
|
+
/** Layout default font used when object.fontFamily is undefined */
|
|
48
|
+
defaultFontFamily?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// We have to declare some functions that will be called by other functions but also have to call those other functions
|
|
52
|
+
// We overwrite the function definition as soon as those other functions have been declared
|
|
53
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
54
|
+
let drawStaticPartOfPage = function (doc: jsPDF, layout: Layout, printBuffer: any, paperSize: PaperSize, options: GenPdfOptions, context: RenderContext): void {
|
|
55
|
+
// to be assigned later
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Forward declaration - parameters intentionally unused, function is overwritten below
|
|
59
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
60
|
+
let addObjectToPDF = function (
|
|
61
|
+
_originX: number,
|
|
62
|
+
_originY: number,
|
|
63
|
+
_doc: jsPDF,
|
|
64
|
+
_object: LayoutObject,
|
|
65
|
+
_printBuffer: any,
|
|
66
|
+
_paperSize: PaperSize,
|
|
67
|
+
_layout: Layout,
|
|
68
|
+
_options: GenPdfOptions,
|
|
69
|
+
_rootPrintBuffer: any,
|
|
70
|
+
_containerChain: { object: ContainerLayoutObject; printBuffer: any }[],
|
|
71
|
+
_context: RenderContext
|
|
72
|
+
): { x: number; y: number } {
|
|
73
|
+
// to be assigned later
|
|
74
|
+
return { x: 0, y: 0 }
|
|
75
|
+
}
|
|
76
|
+
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
77
|
+
|
|
78
|
+
const formatDate = function (date: number | string): string {
|
|
79
|
+
return format(new Date(date), 'MM-dd-yy HH:mm:ss')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getProperty(propertyName: string, object: any): any {
|
|
83
|
+
const parts = propertyName.split('.')
|
|
84
|
+
const length = parts.length
|
|
85
|
+
let property = object
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < length; i++) {
|
|
88
|
+
if (property) {
|
|
89
|
+
property = property[parts[i]]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return property
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** jsPDF font style strings. fontStyle bitmask: 0=normal, 1=bold, 2=italic, 3=bold+italic */
|
|
96
|
+
const FONT_STYLES = ['normal', 'bold', 'italic', 'bolditalic'] as const
|
|
97
|
+
|
|
98
|
+
/** Recursively collect used custom fonts from layout objects (text/field with fontFamily) and layout default. */
|
|
99
|
+
function collectUsedFontsFromLayout(layout: Layout, options: GenPdfOptions): Set<string> {
|
|
100
|
+
const used = new Set<string>()
|
|
101
|
+
const isCustomPdfFont = options.isCustomPdfFont || (() => false)
|
|
102
|
+
function walk(obj: LayoutObject) {
|
|
103
|
+
if ((obj.type === 'text' || obj.type === 'field') && obj.fontFamily && isCustomPdfFont(obj.fontFamily)) {
|
|
104
|
+
used.add(obj.fontFamily)
|
|
105
|
+
}
|
|
106
|
+
if (obj.type === 'container' && obj.children) {
|
|
107
|
+
obj.children.forEach(walk)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
layout.objects.forEach(walk)
|
|
111
|
+
if (layout.defaultFontFamily && isCustomPdfFont(layout.defaultFontFamily)) {
|
|
112
|
+
used.add(layout.defaultFontFamily)
|
|
113
|
+
}
|
|
114
|
+
return used
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function binaryToBase64(binary: string): string {
|
|
118
|
+
if (typeof btoa === 'function') return btoa(binary)
|
|
119
|
+
|
|
120
|
+
const buffer = (globalThis as any).Buffer
|
|
121
|
+
if (buffer) return buffer.from(binary, 'binary').toString('base64')
|
|
122
|
+
|
|
123
|
+
throw new Error('No base64 encoder available')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
127
|
+
const buffer = (globalThis as any).Buffer
|
|
128
|
+
if (buffer) return buffer.from(bytes).toString('base64')
|
|
129
|
+
|
|
130
|
+
const chunkSize = 8192
|
|
131
|
+
let binary = ''
|
|
132
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
133
|
+
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length))
|
|
134
|
+
binary += String.fromCharCode.apply(null, Array.from(chunk))
|
|
135
|
+
}
|
|
136
|
+
return binaryToBase64(binary)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readUint16BE(bytes: Uint8Array, offset: number): number {
|
|
140
|
+
return (bytes[offset] << 8) | bytes[offset + 1]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readUint32BE(bytes: Uint8Array, offset: number): number {
|
|
144
|
+
return bytes[offset] * 0x1000000 + ((bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3])
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isPng(bytes: Uint8Array): boolean {
|
|
148
|
+
return bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isJpeg(bytes: Uint8Array): boolean {
|
|
152
|
+
return bytes[0] === 0xff && bytes[1] === 0xd8
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getImageFormat(url: string, contentType: string | null, bytes: Uint8Array): 'PNG' | 'JPEG' {
|
|
156
|
+
const lowerContentType = contentType?.toLowerCase() || ''
|
|
157
|
+
const lowerUrl = url.toLowerCase()
|
|
158
|
+
if (lowerContentType.includes('png') || isPng(bytes) || lowerUrl.endsWith('.png')) return 'PNG'
|
|
159
|
+
return 'JPEG'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getImageMimeType(format: 'PNG' | 'JPEG'): string {
|
|
163
|
+
return format === 'PNG' ? 'image/png' : 'image/jpeg'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getImageDimensions(bytes: Uint8Array, format: 'PNG' | 'JPEG'): { width?: number; height?: number } {
|
|
167
|
+
if (format === 'PNG' && bytes.length >= 24 && isPng(bytes)) {
|
|
168
|
+
return { width: readUint32BE(bytes, 16), height: readUint32BE(bytes, 20) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (format === 'JPEG' && isJpeg(bytes)) {
|
|
172
|
+
let offset = 2
|
|
173
|
+
while (offset + 9 < bytes.length) {
|
|
174
|
+
if (bytes[offset] !== 0xff) {
|
|
175
|
+
offset++
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const marker = bytes[offset + 1]
|
|
180
|
+
const length = readUint16BE(bytes, offset + 2)
|
|
181
|
+
const isStartOfFrameMarker = (marker >= 0xc0 && marker <= 0xc3) || (marker >= 0xc5 && marker <= 0xc7) || (marker >= 0xc9 && marker <= 0xcb) || (marker >= 0xcd && marker <= 0xcf)
|
|
182
|
+
if (isStartOfFrameMarker) {
|
|
183
|
+
return { height: readUint16BE(bytes, offset + 5), width: readUint16BE(bytes, offset + 7) }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (length < 2) break
|
|
187
|
+
offset += 2 + length
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function loadImageForPdf(url: string): Promise<PdfImageAsset> {
|
|
195
|
+
const res = await fetch(url, { mode: 'cors' })
|
|
196
|
+
if (!res.ok) throw new Error(`Image fetch failed: ${url} ${res.status}`)
|
|
197
|
+
|
|
198
|
+
const arrayBuffer = await res.arrayBuffer()
|
|
199
|
+
const bytes = new Uint8Array(arrayBuffer)
|
|
200
|
+
const format = getImageFormat(url, res.headers.get('content-type'), bytes)
|
|
201
|
+
const dimensions = getImageDimensions(bytes, format)
|
|
202
|
+
const dataUrl = `data:${getImageMimeType(format)};base64,${bytesToBase64(bytes)}`
|
|
203
|
+
return { dataUrl, format, ...dimensions }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function resolveImageUrl(object: LayoutObject, printBuffer: any): string | undefined {
|
|
207
|
+
if (object.type !== 'image') return undefined
|
|
208
|
+
const imgObj = object as any
|
|
209
|
+
return imgObj.source && printBuffer ? getProperty(imgObj.source, printBuffer) : imgObj.url
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function collectImageUrlsFromLayout(layout: Layout, printBuffer: any): string[] {
|
|
213
|
+
const urls = new Set<string>()
|
|
214
|
+
|
|
215
|
+
function walk(objects: LayoutObject[], currentPrintBuffer: any) {
|
|
216
|
+
objects.forEach((object) => {
|
|
217
|
+
if (object.type === 'image') {
|
|
218
|
+
const imageUrl = resolveImageUrl(object, currentPrintBuffer)
|
|
219
|
+
if (imageUrl) urls.add(String(imageUrl))
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (object.type !== 'container') return
|
|
224
|
+
|
|
225
|
+
const source = (object as any).printOnlyAtEnd === true && (!object.source || object.source === '') ? [currentPrintBuffer] : getProperty(object.source, currentPrintBuffer)
|
|
226
|
+
if (!Array.isArray(source)) return
|
|
227
|
+
source.forEach((containerPrintBuffer) => walk(object.children, containerPrintBuffer))
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
walk(layout.objects, printBuffer)
|
|
232
|
+
return [...urls]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Fetch TTF from URL and return as base64. */
|
|
236
|
+
async function fetchFontAsBase64(url: string): Promise<string> {
|
|
237
|
+
const res = await fetch(url, { mode: 'cors' })
|
|
238
|
+
if (!res.ok) throw new Error(`Font fetch failed: ${url} ${res.status}`)
|
|
239
|
+
const arrayBuffer = await res.arrayBuffer()
|
|
240
|
+
const bytes = new Uint8Array(arrayBuffer)
|
|
241
|
+
const chunkSize = 8192
|
|
242
|
+
let binary = ''
|
|
243
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
244
|
+
const chunk = bytes.subarray(i, Math.min(i + chunkSize, bytes.length))
|
|
245
|
+
binary += String.fromCharCode.apply(null, Array.from(chunk))
|
|
246
|
+
}
|
|
247
|
+
const base64 = binaryToBase64(binary)
|
|
248
|
+
return base64
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Embed custom fonts into jsPDF doc. Only embeds fonts that are actually used. */
|
|
252
|
+
async function embedFontsInDoc(doc: jsPDF, usedFontFamilies: Set<string>, options: GenPdfOptions): Promise<void> {
|
|
253
|
+
for (const family of usedFontFamilies) {
|
|
254
|
+
const def = options.getPdfFontDefinition?.(family)
|
|
255
|
+
if (!def) {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
if (def.variants.length === 0) {
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
for (const variant of def.variants) {
|
|
262
|
+
const vfsName = `${family}-${variant.style}.ttf`
|
|
263
|
+
const base64 = await fetchFontAsBase64(variant.url)
|
|
264
|
+
doc.addFileToVFS(vfsName, base64)
|
|
265
|
+
doc.addFont(vfsName, family, variant.style, 'Identity-H')
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function truncateTextToFitWidth(doc: jsPDF, text: string, maxWidth: number): string {
|
|
271
|
+
if (doc.getTextWidth(text) <= maxWidth) return text
|
|
272
|
+
const ELLIPSIS = '...'
|
|
273
|
+
|
|
274
|
+
let truncatedText = ''
|
|
275
|
+
for (let i = 0; i < text.length; i++) {
|
|
276
|
+
const tempText = truncatedText + text[i]
|
|
277
|
+
if (doc.getTextWidth(tempText + ELLIPSIS) > maxWidth) break
|
|
278
|
+
truncatedText = tempText
|
|
279
|
+
}
|
|
280
|
+
return truncatedText + ELLIPSIS
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function drawSimpleObject(
|
|
284
|
+
x: number,
|
|
285
|
+
y: number,
|
|
286
|
+
doc: jsPDF,
|
|
287
|
+
object: LayoutObject,
|
|
288
|
+
printBuffer: any,
|
|
289
|
+
width: number,
|
|
290
|
+
height: number,
|
|
291
|
+
options: GenPdfOptions,
|
|
292
|
+
context: RenderContext
|
|
293
|
+
): void {
|
|
294
|
+
let text = '' // Will hold text to display for either 'text' or 'field'
|
|
295
|
+
|
|
296
|
+
// Get text from object for text objects
|
|
297
|
+
if (object.type == 'text') {
|
|
298
|
+
text = options.translate ? options.translate(object.text) : object.text
|
|
299
|
+
}
|
|
300
|
+
// Get text from printBuffer for field objects
|
|
301
|
+
if (object.type == 'field') {
|
|
302
|
+
if (object.source === 'pageNumber') {
|
|
303
|
+
text = String(doc.getNumberOfPages())
|
|
304
|
+
} else if (object.source === 'pageCount') {
|
|
305
|
+
text = String(context.pageCount || 0)
|
|
306
|
+
} else if (object.source === 'page') {
|
|
307
|
+
text = String(doc.getNumberOfPages()) + ' / ' + String(context.pageCount || 0)
|
|
308
|
+
} else if (object.source && printBuffer) {
|
|
309
|
+
text = getProperty(object.source, printBuffer)
|
|
310
|
+
} else {
|
|
311
|
+
text = printBuffer
|
|
312
|
+
}
|
|
313
|
+
if (typeof text == 'number') text = '' + text
|
|
314
|
+
if (object.format == 'date') text = formatDate(parseInt(text))
|
|
315
|
+
if (text === undefined || typeof text != 'string') {
|
|
316
|
+
options.onAlert?.({ header: 'fieldnotfound', body: object.source, type: 'warning', timeout: 10000 })
|
|
317
|
+
text = ''
|
|
318
|
+
}
|
|
319
|
+
if (object.format == 'currency') text = String.fromCharCode(128) + ' ' + parseFloat(text).toFixed(2)
|
|
320
|
+
}
|
|
321
|
+
// Manipulate case as specified in object
|
|
322
|
+
if ((object.type === 'text' || object.type === 'field') && object.case === 1) text = text.toUpperCase()
|
|
323
|
+
if ((object.type === 'text' || object.type === 'field') && object.case === 2) text = text.toLowerCase()
|
|
324
|
+
|
|
325
|
+
// Skip actual drawing in measurement mode or when measuring for fillContainer
|
|
326
|
+
if (context.mode === 'measurement' || context.measureOnly) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Draw rectangle for rectangle objects (supports backgroundColor, borderRadius, strokeWidth, strokeColor)
|
|
331
|
+
// fillContainer rectangles are drawn separately as background - skip here to avoid duplicate 1px strip
|
|
332
|
+
if (object.type == 'rectangle') {
|
|
333
|
+
const rectObj = object as any
|
|
334
|
+
if (rectObj.fillContainer) return
|
|
335
|
+
const hasFill = rectObj.backgroundColor
|
|
336
|
+
const radius = (rectObj.borderRadius || 0) / 10 // layout units to mm
|
|
337
|
+
const hasCustomStroke = rectObj.strokeColor != null || rectObj.strokeWidth != null
|
|
338
|
+
if (hasCustomStroke) {
|
|
339
|
+
if (rectObj.strokeColor) {
|
|
340
|
+
let hex = String(rectObj.strokeColor).replace(/^#/, '')
|
|
341
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
342
|
+
doc.setDrawColor(parseInt(hex.slice(0, 2), 16) || 0, parseInt(hex.slice(2, 4), 16) || 0, parseInt(hex.slice(4, 6), 16) || 0)
|
|
343
|
+
}
|
|
344
|
+
if (rectObj.strokeWidth != null) doc.setLineWidth(rectObj.strokeWidth)
|
|
345
|
+
}
|
|
346
|
+
if (radius > 0 || hasFill) {
|
|
347
|
+
if (hasFill) {
|
|
348
|
+
let hex = String(rectObj.backgroundColor).replace(/^#/, '')
|
|
349
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
350
|
+
doc.setFillColor(parseInt(hex.slice(0, 2), 16) || 0, parseInt(hex.slice(2, 4), 16) || 0, parseInt(hex.slice(4, 6), 16) || 0)
|
|
351
|
+
}
|
|
352
|
+
if (typeof doc.roundedRect === 'function') {
|
|
353
|
+
const mode = hasFill ? 'FD' : 'S' // FD = fill + stroke, S = stroke only
|
|
354
|
+
doc.roundedRect(x, y, width, height, radius, radius, mode)
|
|
355
|
+
} else {
|
|
356
|
+
if (hasFill) doc.rect(x, y, width, height, 'F')
|
|
357
|
+
else doc.rect(x, y, width, height)
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
doc.rect(x, y, width, height)
|
|
361
|
+
}
|
|
362
|
+
if (hasCustomStroke) {
|
|
363
|
+
doc.setDrawColor(0, 0, 0)
|
|
364
|
+
doc.setLineWidth(0.35)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Draw image for image objects (static url or dynamic source)
|
|
369
|
+
if (object.type == 'image') {
|
|
370
|
+
const imgObj = object as any
|
|
371
|
+
const imgUrl = resolveImageUrl(object, printBuffer)
|
|
372
|
+
if (imgUrl) {
|
|
373
|
+
const urlLower = String(imgUrl).toLowerCase()
|
|
374
|
+
const imageAsset = context.imageAssets?.get(String(imgUrl))
|
|
375
|
+
const format = imageAsset?.format || (urlLower.endsWith('.png') ? 'PNG' : urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg') ? 'JPEG' : 'JPEG')
|
|
376
|
+
let drawX = x
|
|
377
|
+
let drawY = y
|
|
378
|
+
let drawWidth = width
|
|
379
|
+
let drawHeight = height
|
|
380
|
+
// Preserve aspect ratio when objectFit is 'contain' and we have dimensions
|
|
381
|
+
if (imgObj.objectFit === 'contain' && imageAsset?.width && imageAsset?.height) {
|
|
382
|
+
const scale = Math.min(width / imageAsset.width, height / imageAsset.height)
|
|
383
|
+
drawWidth = imageAsset.width * scale
|
|
384
|
+
drawHeight = imageAsset.height * scale
|
|
385
|
+
drawX = x + (width - drawWidth) / 2
|
|
386
|
+
drawY = y + (height - drawHeight) / 2
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
doc.addImage(imageAsset?.dataUrl || imgUrl, format, drawX, drawY, drawWidth, drawHeight)
|
|
390
|
+
} catch (_e) {
|
|
391
|
+
// Skip if image fails to load (e.g. CORS, invalid URL)
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Draw text for text and field objets
|
|
397
|
+
if ((object.type == 'text' || object.type == 'field') && text) {
|
|
398
|
+
let textAlign: 'left' | 'center' | 'right' | undefined
|
|
399
|
+
if (object.textAlign == 1) textAlign = 'left'
|
|
400
|
+
if (object.textAlign == 2) {
|
|
401
|
+
textAlign = 'center'
|
|
402
|
+
x = x + width / 2
|
|
403
|
+
}
|
|
404
|
+
if (object.textAlign == 3) {
|
|
405
|
+
textAlign = 'right'
|
|
406
|
+
x = x + width
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (object.name == 'qty' && text == '0') return
|
|
410
|
+
const fontStyleStr = FONT_STYLES[object.fontStyle & 3] || 'normal'
|
|
411
|
+
// Use object font if it's a custom font; otherwise layout default overrides standard fonts (Helvetica etc.)
|
|
412
|
+
const fontFamily = object.fontFamily && options.isCustomPdfFont?.(object.fontFamily) ? object.fontFamily : context.defaultFontFamily || object.fontFamily || 'Helvetica'
|
|
413
|
+
doc.setFont(fontFamily, fontStyleStr)
|
|
414
|
+
doc.setFontSize(object.fontSize / 4)
|
|
415
|
+
doc.text(truncateTextToFitWidth(doc, text, width), x, y, { align: textAlign, baseline: 'hanging' })
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function addPage(
|
|
420
|
+
originX: number,
|
|
421
|
+
originY: number,
|
|
422
|
+
doc: jsPDF,
|
|
423
|
+
layout: Layout,
|
|
424
|
+
rootPrintBuffer: any,
|
|
425
|
+
paperSize: PaperSize,
|
|
426
|
+
options: GenPdfOptions,
|
|
427
|
+
containerChain: { object: ContainerLayoutObject; printBuffer: any }[],
|
|
428
|
+
context: RenderContext
|
|
429
|
+
): { originX: number; originY: number; x: number; y: number } {
|
|
430
|
+
if (context.mode === 'measurement') {
|
|
431
|
+
// Track page count in measurement mode
|
|
432
|
+
if (context.currentPageCount === undefined) {
|
|
433
|
+
context.currentPageCount = 1
|
|
434
|
+
}
|
|
435
|
+
context.currentPageCount++
|
|
436
|
+
} else if (!context.measureOnly) {
|
|
437
|
+
// Actually add page in rendering mode (skip when measuring for fillContainer)
|
|
438
|
+
doc.addPage()
|
|
439
|
+
}
|
|
440
|
+
if (!context.measureOnly) {
|
|
441
|
+
drawStaticPartOfPage(doc, layout, rootPrintBuffer, paperSize, options, context)
|
|
442
|
+
}
|
|
443
|
+
const absoluteLowerRightHandSide = { x: originX / 10, y: originY / 10 }
|
|
444
|
+
// Now draw all the parent containers of the current container
|
|
445
|
+
if (containerChain.length > 0) {
|
|
446
|
+
// Let origin start at (x,y) of the outermost container
|
|
447
|
+
originX = containerChain[0].object.x
|
|
448
|
+
originY = containerChain[0].object.y
|
|
449
|
+
absoluteLowerRightHandSide.x = originX / 10
|
|
450
|
+
absoluteLowerRightHandSide.y = originY / 10
|
|
451
|
+
containerChain.forEach((link) => {
|
|
452
|
+
link.object.children.forEach((child) => {
|
|
453
|
+
if (!child.snapToBottom && (child.type != 'container' || child.repeatOnOverflow)) {
|
|
454
|
+
const childLowerRightHandSide = addObjectToPDF(originX, originY, doc, child, link.printBuffer, paperSize, layout, options, rootPrintBuffer, [], context)
|
|
455
|
+
absoluteLowerRightHandSide.x = Math.max(absoluteLowerRightHandSide.x, childLowerRightHandSide.x)
|
|
456
|
+
absoluteLowerRightHandSide.y = Math.max(absoluteLowerRightHandSide.y, childLowerRightHandSide.y)
|
|
457
|
+
}
|
|
458
|
+
})
|
|
459
|
+
if (link.object.repeatContainer == 'horizontal') originX = absoluteLowerRightHandSide.x * 10
|
|
460
|
+
if (link.object.repeatContainer == 'vertical') originY = absoluteLowerRightHandSide.y * 10
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
return { originX: originX, originY: originY, x: absoluteLowerRightHandSide.x, y: absoluteLowerRightHandSide.y }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function drawBottomDwellers(
|
|
467
|
+
object: ContainerLayoutObject,
|
|
468
|
+
originX: number,
|
|
469
|
+
originY: number,
|
|
470
|
+
absoluteLowerRightHandSide: { x: number; y: number },
|
|
471
|
+
bottomChildY: number,
|
|
472
|
+
paperSize: PaperSize,
|
|
473
|
+
doc: jsPDF,
|
|
474
|
+
layout: Layout,
|
|
475
|
+
rootPrintBuffer: any,
|
|
476
|
+
options: GenPdfOptions,
|
|
477
|
+
newContainerChain: { object: ContainerLayoutObject; printBuffer: any }[],
|
|
478
|
+
container: any,
|
|
479
|
+
context: RenderContext
|
|
480
|
+
): { originX: number; originY: number; x: number; y: number } {
|
|
481
|
+
let childRelativeToBottomDrawn = false
|
|
482
|
+
let firstBottomRelativeChildY = Math.min(...object.children.filter((child) => child.snapToBottom).map((child) => child.y))
|
|
483
|
+
object.children.forEach(function (child) {
|
|
484
|
+
if (child.snapToBottom) {
|
|
485
|
+
// Objects relative to bottom/right
|
|
486
|
+
if (!childRelativeToBottomDrawn) originY = absoluteLowerRightHandSide.y * 10
|
|
487
|
+
childRelativeToBottomDrawn = true
|
|
488
|
+
// child.text = '======'+originX+':'+originY+'======'
|
|
489
|
+
if (object.pageBreak || originY + object.height - bottomChildY > paperSize.height - paperSize.footerHeight) {
|
|
490
|
+
// We ran out of paper
|
|
491
|
+
const newOrigin = addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, options, newContainerChain, context)
|
|
492
|
+
originX = newOrigin.originX
|
|
493
|
+
originY = newOrigin.originY
|
|
494
|
+
absoluteLowerRightHandSide = { x: newOrigin.x, y: newOrigin.y }
|
|
495
|
+
firstBottomRelativeChildY = 0
|
|
496
|
+
}
|
|
497
|
+
// originY = absoluteLowerRightHandSide.y * 10
|
|
498
|
+
const childLowerRightHandSide = addObjectToPDF(originX, originY - firstBottomRelativeChildY, doc, child, container, paperSize, layout, options, rootPrintBuffer, [], context)
|
|
499
|
+
absoluteLowerRightHandSide.x = Math.max(absoluteLowerRightHandSide.x, childLowerRightHandSide.x)
|
|
500
|
+
absoluteLowerRightHandSide.y = Math.max(absoluteLowerRightHandSide.y, childLowerRightHandSide.y)
|
|
501
|
+
absoluteLowerRightHandSide.y = Math.max(absoluteLowerRightHandSide.y, (originY + object.height - firstBottomRelativeChildY) / 10)
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
if (childRelativeToBottomDrawn) originY = absoluteLowerRightHandSide.y * 10
|
|
505
|
+
return { originX: originX, originY: originY, x: absoluteLowerRightHandSide.x, y: absoluteLowerRightHandSide.y }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Add an object to the PDF document.
|
|
510
|
+
*
|
|
511
|
+
* Use originX,originY as origin to displace the object's own x,y coordinates
|
|
512
|
+
*/
|
|
513
|
+
addObjectToPDF = function (
|
|
514
|
+
originX: number,
|
|
515
|
+
originY: number,
|
|
516
|
+
doc: jsPDF,
|
|
517
|
+
object: LayoutObject,
|
|
518
|
+
printBuffer: any,
|
|
519
|
+
paperSize: PaperSize,
|
|
520
|
+
layout: Layout,
|
|
521
|
+
options: GenPdfOptions,
|
|
522
|
+
rootPrintBuffer: any,
|
|
523
|
+
containerChain: { object: ContainerLayoutObject; printBuffer: any }[],
|
|
524
|
+
context: RenderContext
|
|
525
|
+
): { x: number; y: number } {
|
|
526
|
+
const x = (originX + object.x) / 10
|
|
527
|
+
const y = (originY + object.y) / 10
|
|
528
|
+
const width = object.width / 10
|
|
529
|
+
const height = object.height / 10
|
|
530
|
+
// Keep track of the lower righthandside of all objects in this container
|
|
531
|
+
// We need to return this object so whatever calls addObjectToPDF knows how big our drawing was
|
|
532
|
+
let absoluteLowerRightHandSide = { x: x + width, y: y + height }
|
|
533
|
+
|
|
534
|
+
if (!object.active) return absoluteLowerRightHandSide
|
|
535
|
+
if (object.type != 'container') drawSimpleObject(x, y, doc, object, printBuffer, width, height, options, context)
|
|
536
|
+
|
|
537
|
+
// Recursively draw all child objects from a container object
|
|
538
|
+
if (object.type == 'container') {
|
|
539
|
+
let originX = x * 10,
|
|
540
|
+
originY = y * 10
|
|
541
|
+
|
|
542
|
+
const isPrintOnlyAtEnd = (object as any).printOnlyAtEnd === true
|
|
543
|
+
const source = isPrintOnlyAtEnd && (!object.source || object.source === '') ? [printBuffer] : getProperty(object.source, printBuffer)
|
|
544
|
+
if (source === undefined) {
|
|
545
|
+
options.onAlert?.({ header: 'containernotfound', body: object.source, type: 'warning', timeout: 10000 })
|
|
546
|
+
return absoluteLowerRightHandSide
|
|
547
|
+
}
|
|
548
|
+
const nrContainers = source.length
|
|
549
|
+
for (let containerIndex = 0; containerIndex < nrContainers; containerIndex++) {
|
|
550
|
+
const newContainerChain = containerChain.filter(function () {
|
|
551
|
+
return true
|
|
552
|
+
})
|
|
553
|
+
const container = source[containerIndex]
|
|
554
|
+
absoluteLowerRightHandSide = { x: originX / 10, y: originY / 10 }
|
|
555
|
+
const minHeightMm = (object as any).minHeightBeforeBreak
|
|
556
|
+
const useOrphanCheck = minHeightMm != null && minHeightMm > 0
|
|
557
|
+
if (useOrphanCheck && (object.pageBreak || originY + minHeightMm > paperSize.height - paperSize.footerHeight)) {
|
|
558
|
+
// We ran out of paper
|
|
559
|
+
originX = x * 10
|
|
560
|
+
originY = y * 10
|
|
561
|
+
const newOrigin = addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, options, newContainerChain, context)
|
|
562
|
+
originX = newOrigin.originX
|
|
563
|
+
originY = newOrigin.originY
|
|
564
|
+
absoluteLowerRightHandSide = { x: newOrigin.x, y: newOrigin.y }
|
|
565
|
+
}
|
|
566
|
+
// If container has fillContainer rectangles, measure bounds first then draw them as background
|
|
567
|
+
const fillContainerRects = object.children.filter((c: any) => c.type === 'rectangle' && c.fillContainer)
|
|
568
|
+
if (fillContainerRects.length > 0 && !context.measureOnly) {
|
|
569
|
+
const measureContext = { ...context, measureOnly: true }
|
|
570
|
+
const measureOriginX = originX
|
|
571
|
+
const measureOriginY = originY
|
|
572
|
+
const measureBounds = { x: originX / 10, y: originY / 10 }
|
|
573
|
+
let measureBottomChildY = 0
|
|
574
|
+
object.children.forEach(function (child: any) {
|
|
575
|
+
if (child.type != 'container' && !child.snapToBottom) {
|
|
576
|
+
measureBottomChildY = Math.max(measureBottomChildY, child.y + child.height)
|
|
577
|
+
const r = addObjectToPDF(measureOriginX, measureOriginY, doc, child, container, paperSize, layout, options, rootPrintBuffer, [], measureContext)
|
|
578
|
+
measureBounds.x = Math.max(measureBounds.x, r.x)
|
|
579
|
+
measureBounds.y = Math.max(measureBounds.y, r.y)
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
const measureChain = [...newContainerChain, { object: object as ContainerLayoutObject, printBuffer: container }]
|
|
583
|
+
object.children.forEach(function (child: any) {
|
|
584
|
+
if (child.type == 'container' && !child.snapToBottom) {
|
|
585
|
+
measureBottomChildY = Math.max(measureBottomChildY, child.y + child.height)
|
|
586
|
+
const r = addObjectToPDF(measureOriginX, measureOriginY, doc, child, container, paperSize, layout, options, rootPrintBuffer, measureChain, measureContext)
|
|
587
|
+
measureBounds.x = Math.max(measureBounds.x, r.x)
|
|
588
|
+
measureBounds.y = Math.max(measureBounds.y, r.y)
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
const measureBottom = drawBottomDwellers(
|
|
592
|
+
object as ContainerLayoutObject,
|
|
593
|
+
measureOriginX,
|
|
594
|
+
measureOriginY,
|
|
595
|
+
{ x: measureBounds.x, y: measureBounds.y },
|
|
596
|
+
measureBottomChildY,
|
|
597
|
+
paperSize,
|
|
598
|
+
doc,
|
|
599
|
+
layout,
|
|
600
|
+
rootPrintBuffer,
|
|
601
|
+
options,
|
|
602
|
+
measureChain,
|
|
603
|
+
container,
|
|
604
|
+
measureContext
|
|
605
|
+
)
|
|
606
|
+
measureBounds.y = Math.max(measureBounds.y, measureBottom.y)
|
|
607
|
+
// Draw fillContainer rectangles as background
|
|
608
|
+
fillContainerRects.forEach(function (rect: any) {
|
|
609
|
+
const rx = (measureOriginX + rect.x) / 10
|
|
610
|
+
const ry = (measureOriginY + rect.y) / 10
|
|
611
|
+
const rw = rect.width / 10
|
|
612
|
+
const rh = measureBounds.y - ry
|
|
613
|
+
if (rh > 0) {
|
|
614
|
+
const rectObj = rect as any
|
|
615
|
+
const hasFill = rectObj.backgroundColor
|
|
616
|
+
const radius = (rectObj.borderRadius || 0) / 10
|
|
617
|
+
const hasCustomStroke = rectObj.strokeColor != null || rectObj.strokeWidth != null
|
|
618
|
+
if (hasCustomStroke) {
|
|
619
|
+
if (rectObj.strokeColor) {
|
|
620
|
+
let hex = String(rectObj.strokeColor).replace(/^#/, '')
|
|
621
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
622
|
+
doc.setDrawColor(parseInt(hex.slice(0, 2), 16) || 0, parseInt(hex.slice(2, 4), 16) || 0, parseInt(hex.slice(4, 6), 16) || 0)
|
|
623
|
+
}
|
|
624
|
+
if (rectObj.strokeWidth != null) doc.setLineWidth(rectObj.strokeWidth)
|
|
625
|
+
}
|
|
626
|
+
if (hasFill) {
|
|
627
|
+
let hex = String(rectObj.backgroundColor).replace(/^#/, '')
|
|
628
|
+
if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]
|
|
629
|
+
doc.setFillColor(parseInt(hex.slice(0, 2), 16) || 0, parseInt(hex.slice(2, 4), 16) || 0, parseInt(hex.slice(4, 6), 16) || 0)
|
|
630
|
+
}
|
|
631
|
+
if (typeof doc.roundedRect === 'function') {
|
|
632
|
+
doc.roundedRect(rx, ry, rw, rh, radius, radius, hasFill ? 'FD' : 'S')
|
|
633
|
+
} else {
|
|
634
|
+
if (hasFill) doc.rect(rx, ry, rw, rh, 'F')
|
|
635
|
+
else doc.rect(rx, ry, rw, rh)
|
|
636
|
+
}
|
|
637
|
+
if (hasCustomStroke) {
|
|
638
|
+
doc.setDrawColor(0, 0, 0)
|
|
639
|
+
doc.setLineWidth(0.35)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
// normal objects first, then containers
|
|
645
|
+
let bottomChildY = 0
|
|
646
|
+
object.children.forEach(function (child) {
|
|
647
|
+
if (child.type != 'container' && !child.snapToBottom) {
|
|
648
|
+
// Objects relative to top/left
|
|
649
|
+
bottomChildY = Math.max(bottomChildY, child.y + child.height)
|
|
650
|
+
const childLowerRightHandSide = addObjectToPDF(originX, originY, doc, child, container, paperSize, layout, options, rootPrintBuffer, [], context)
|
|
651
|
+
absoluteLowerRightHandSide.x = Math.max(absoluteLowerRightHandSide.x, childLowerRightHandSide.x)
|
|
652
|
+
absoluteLowerRightHandSide.y = Math.max(absoluteLowerRightHandSide.y, childLowerRightHandSide.y)
|
|
653
|
+
}
|
|
654
|
+
})
|
|
655
|
+
newContainerChain.push({ object: object as ContainerLayoutObject, printBuffer: container })
|
|
656
|
+
object.children.forEach(function (child) {
|
|
657
|
+
if (child.type == 'container' && !child.snapToBottom) {
|
|
658
|
+
bottomChildY = Math.max(bottomChildY, child.y + child.height)
|
|
659
|
+
const childLowerRightHandSide = addObjectToPDF(originX, originY, doc, child, container, paperSize, layout, options, rootPrintBuffer, newContainerChain, context)
|
|
660
|
+
absoluteLowerRightHandSide.x = Math.max(absoluteLowerRightHandSide.x, childLowerRightHandSide.x)
|
|
661
|
+
absoluteLowerRightHandSide.y = Math.max(absoluteLowerRightHandSide.y, childLowerRightHandSide.y)
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
if (containerIndex < nrContainers - 1) {
|
|
665
|
+
if (object.repeatContainer == 'horizontal') originX = absoluteLowerRightHandSide.x * 10
|
|
666
|
+
if (object.repeatContainer == 'vertical') originY = absoluteLowerRightHandSide.y * 10
|
|
667
|
+
if (object.pageBreak || originY + object.height - bottomChildY > paperSize.height - paperSize.footerHeight) {
|
|
668
|
+
// We ran out of paper
|
|
669
|
+
const newOrigin = addPage(originX, originY, doc, layout, rootPrintBuffer, paperSize, options, newContainerChain, context)
|
|
670
|
+
originX = newOrigin.originX
|
|
671
|
+
originY = newOrigin.originY
|
|
672
|
+
absoluteLowerRightHandSide = { x: newOrigin.x, y: newOrigin.y }
|
|
673
|
+
const newestOrigin = drawBottomDwellers(
|
|
674
|
+
object as ContainerLayoutObject,
|
|
675
|
+
originX,
|
|
676
|
+
originY,
|
|
677
|
+
absoluteLowerRightHandSide,
|
|
678
|
+
bottomChildY,
|
|
679
|
+
paperSize,
|
|
680
|
+
doc,
|
|
681
|
+
layout,
|
|
682
|
+
rootPrintBuffer,
|
|
683
|
+
options,
|
|
684
|
+
newContainerChain,
|
|
685
|
+
container,
|
|
686
|
+
context
|
|
687
|
+
)
|
|
688
|
+
originX = newestOrigin.originX
|
|
689
|
+
originY = newestOrigin.originY
|
|
690
|
+
absoluteLowerRightHandSide = { x: newestOrigin.x, y: newestOrigin.y }
|
|
691
|
+
continue
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const newestOrigin = drawBottomDwellers(
|
|
695
|
+
object as ContainerLayoutObject,
|
|
696
|
+
originX,
|
|
697
|
+
originY,
|
|
698
|
+
absoluteLowerRightHandSide,
|
|
699
|
+
bottomChildY,
|
|
700
|
+
paperSize,
|
|
701
|
+
doc,
|
|
702
|
+
layout,
|
|
703
|
+
rootPrintBuffer,
|
|
704
|
+
options,
|
|
705
|
+
newContainerChain,
|
|
706
|
+
container,
|
|
707
|
+
context
|
|
708
|
+
)
|
|
709
|
+
originX = newestOrigin.originX
|
|
710
|
+
originY = newestOrigin.originY
|
|
711
|
+
absoluteLowerRightHandSide = { x: newestOrigin.x, y: newestOrigin.y }
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return absoluteLowerRightHandSide
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Draw all non-container objects on the page
|
|
719
|
+
*/
|
|
720
|
+
drawStaticPartOfPage = function (doc: jsPDF, layout: Layout, printBuffer: any, paperSize: PaperSize, options: GenPdfOptions, context: RenderContext): void {
|
|
721
|
+
layout.objects.forEach(function (object) {
|
|
722
|
+
if (object.type != 'container') {
|
|
723
|
+
addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], context)
|
|
724
|
+
}
|
|
725
|
+
})
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function sortVertical(objects: LayoutObject[]): void {
|
|
729
|
+
objects.sort((a, b) => a.y - b.y)
|
|
730
|
+
objects.forEach((element) => {
|
|
731
|
+
if ((element as ContainerLayoutObject).children) sortVertical((element as ContainerLayoutObject).children)
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export async function genPDF(layout: Layout, printBuffer: any, options: GenPdfOptions = {}): Promise<jsPDF> {
|
|
736
|
+
// Make a deep copy of the layout so we can add temporary properties
|
|
737
|
+
layout = deepCopy(layout)
|
|
738
|
+
sortVertical(layout.objects)
|
|
739
|
+
// Load images for static images
|
|
740
|
+
layout.objects.forEach((object) => {
|
|
741
|
+
if (object.type != 'image') return
|
|
742
|
+
if (object.name == 'Logo') {
|
|
743
|
+
const logoUrl = options.resolveLogoUrl?.()
|
|
744
|
+
if (logoUrl) object.url = logoUrl
|
|
745
|
+
}
|
|
746
|
+
})
|
|
747
|
+
// Preload image bytes for Node.js and dimensions for objectFit:contain
|
|
748
|
+
const imageAssets = new Map<string, PdfImageAsset>()
|
|
749
|
+
const loadImage = options.loadImage || loadImageForPdf
|
|
750
|
+
const urls = collectImageUrlsFromLayout(layout, printBuffer)
|
|
751
|
+
await Promise.all(
|
|
752
|
+
urls.map(async (url) => {
|
|
753
|
+
try {
|
|
754
|
+
const asset = await loadImage(url)
|
|
755
|
+
if (asset) imageAssets.set(url, asset)
|
|
756
|
+
} catch {
|
|
757
|
+
// Ignore load failures; jsPDF will still get the original URL as fallback
|
|
758
|
+
}
|
|
759
|
+
})
|
|
760
|
+
)
|
|
761
|
+
const paperSize = layout.paperSize
|
|
762
|
+
const paperSizeCopy = { ...paperSize }
|
|
763
|
+
paperSize.width *= 10
|
|
764
|
+
paperSize.height *= 10
|
|
765
|
+
paperSize.footerHeight *= 10
|
|
766
|
+
|
|
767
|
+
// First pass: Measurement mode - count pages without rendering
|
|
768
|
+
const measurementContext: RenderContext = {
|
|
769
|
+
mode: 'measurement',
|
|
770
|
+
currentPageCount: 1, // Start with page 1
|
|
771
|
+
}
|
|
772
|
+
// Create a minimal jsPDF instance for measurement (needed for some calculations)
|
|
773
|
+
const measurementDoc = new jsPDF({
|
|
774
|
+
orientation: paperSizeCopy.width > paperSizeCopy.height ? 'landscape' : 'portrait',
|
|
775
|
+
format: [paperSizeCopy.width, paperSizeCopy.height],
|
|
776
|
+
})
|
|
777
|
+
const usedFonts = collectUsedFontsFromLayout(layout, options)
|
|
778
|
+
if (usedFonts.size > 0) {
|
|
779
|
+
await embedFontsInDoc(measurementDoc, usedFonts, options)
|
|
780
|
+
}
|
|
781
|
+
drawStaticPartOfPage(measurementDoc, layout, printBuffer, paperSize, options, measurementContext)
|
|
782
|
+
|
|
783
|
+
// Draw containers in measurement mode (same order as render: normal first, then printOnlyAtEnd on fresh page)
|
|
784
|
+
layout.objects
|
|
785
|
+
.filter((object) => object.type == 'container' && !(object as any).printOnlyAtEnd)
|
|
786
|
+
.forEach((object) => {
|
|
787
|
+
addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
|
|
788
|
+
})
|
|
789
|
+
layout.objects
|
|
790
|
+
.filter((object) => object.type == 'container' && (object as any).printOnlyAtEnd)
|
|
791
|
+
.forEach((object) => {
|
|
792
|
+
addObjectToPDF(0, 0, measurementDoc, object, printBuffer, paperSize, layout, options, printBuffer, [], measurementContext)
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
// Get total page count from measurement pass
|
|
796
|
+
const totalPageCount = measurementContext.currentPageCount || 1
|
|
797
|
+
|
|
798
|
+
// Second pass: Rendering mode - actual PDF generation with pageCount available
|
|
799
|
+
const doc = new jsPDF({
|
|
800
|
+
orientation: paperSizeCopy.width > paperSizeCopy.height ? 'landscape' : 'portrait',
|
|
801
|
+
format: [paperSizeCopy.width, paperSizeCopy.height],
|
|
802
|
+
})
|
|
803
|
+
if (usedFonts.size > 0) {
|
|
804
|
+
await embedFontsInDoc(doc, usedFonts, options)
|
|
805
|
+
}
|
|
806
|
+
const renderContext: RenderContext = {
|
|
807
|
+
mode: 'rendering',
|
|
808
|
+
pageCount: totalPageCount,
|
|
809
|
+
imageAssets,
|
|
810
|
+
defaultFontFamily: layout.defaultFontFamily,
|
|
811
|
+
}
|
|
812
|
+
drawStaticPartOfPage(doc, layout, printBuffer, paperSize, options, renderContext)
|
|
813
|
+
|
|
814
|
+
// Draw containers in rendering mode (normal containers first)
|
|
815
|
+
layout.objects
|
|
816
|
+
.filter((object) => object.type == 'container' && !(object as any).printOnlyAtEnd)
|
|
817
|
+
.forEach((object) => {
|
|
818
|
+
addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
// 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)
|
|
823
|
+
printOnlyAtEndContainers.forEach((object) => {
|
|
824
|
+
addObjectToPDF(0, 0, doc, object, printBuffer, paperSize, layout, options, printBuffer, [], renderContext)
|
|
825
|
+
})
|
|
826
|
+
return doc
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export default {
|
|
830
|
+
genPDF,
|
|
831
|
+
}
|