@softwear/latestcollectioncore 1.0.192 → 1.0.195
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/reports.d.ts +9 -0
- package/dist/reports.js +24 -17
- package/package.json +3 -3
- package/src/reports.ts +39 -14
- package/test/reports.spec.ts +13 -1
package/dist/reports.d.ts
CHANGED
|
@@ -32,8 +32,17 @@ export interface GenPdfOptions {
|
|
|
32
32
|
resolveLogoUrl?: () => string | undefined;
|
|
33
33
|
isCustomPdfFont?: (fontFamily: string) => boolean;
|
|
34
34
|
getPdfFontDefinition?: (fontFamily: string) => PdfFontDefinition | undefined;
|
|
35
|
+
/** Load TTF bytes as base64 (e.g. from disk in Node). Default: fetch variant.url. */
|
|
36
|
+
loadFont?: (url: string) => Promise<string>;
|
|
35
37
|
loadImage?: (url: string) => Promise<PdfImageAsset | undefined>;
|
|
36
38
|
}
|
|
39
|
+
/** Fit image in layout box preserving aspect ratio (like CSS object-fit: contain). Stretch only when objectFit is fill/stretch. */
|
|
40
|
+
export declare function computeImageDrawRect(x: number, y: number, boxWidth: number, boxHeight: number, imageWidth: number, imageHeight: number, objectFit?: string): {
|
|
41
|
+
x: number;
|
|
42
|
+
y: number;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
37
46
|
export declare function genPDF(layout: Layout, printBuffer: any, options?: GenPdfOptions): Promise<jsPDF>;
|
|
38
47
|
declare const _default: {
|
|
39
48
|
genPDF: typeof genPDF;
|
package/dist/reports.js
CHANGED
|
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.genPDF = void 0;
|
|
15
|
+
exports.genPDF = exports.computeImageDrawRect = 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"));
|
|
@@ -152,6 +152,23 @@ function getImageFormat(url, contentType, bytes) {
|
|
|
152
152
|
function getImageMimeType(format) {
|
|
153
153
|
return format === 'PNG' ? 'image/png' : 'image/jpeg';
|
|
154
154
|
}
|
|
155
|
+
/** Fit image in layout box preserving aspect ratio (like CSS object-fit: contain). Stretch only when objectFit is fill/stretch. */
|
|
156
|
+
function computeImageDrawRect(x, y, boxWidth, boxHeight, imageWidth, imageHeight, objectFit) {
|
|
157
|
+
let drawX = x;
|
|
158
|
+
let drawY = y;
|
|
159
|
+
let drawWidth = boxWidth;
|
|
160
|
+
let drawHeight = boxHeight;
|
|
161
|
+
const stretch = objectFit === 'fill' || objectFit === 'stretch';
|
|
162
|
+
if (!stretch && imageWidth > 0 && imageHeight > 0) {
|
|
163
|
+
const scale = Math.min(boxWidth / imageWidth, boxHeight / imageHeight);
|
|
164
|
+
drawWidth = imageWidth * scale;
|
|
165
|
+
drawHeight = imageHeight * scale;
|
|
166
|
+
drawX = x + (boxWidth - drawWidth) / 2;
|
|
167
|
+
drawY = y + (boxHeight - drawHeight) / 2;
|
|
168
|
+
}
|
|
169
|
+
return { x: drawX, y: drawY, width: drawWidth, height: drawHeight };
|
|
170
|
+
}
|
|
171
|
+
exports.computeImageDrawRect = computeImageDrawRect;
|
|
155
172
|
function getImageDimensions(bytes, format) {
|
|
156
173
|
if (format === 'PNG' && bytes.length >= 24 && isPng(bytes)) {
|
|
157
174
|
return { width: readUint32BE(bytes, 16), height: readUint32BE(bytes, 20) };
|
|
@@ -246,9 +263,10 @@ function embedFontsInDoc(doc, usedFontFamilies, options) {
|
|
|
246
263
|
if (def.variants.length === 0) {
|
|
247
264
|
continue;
|
|
248
265
|
}
|
|
266
|
+
const loadFont = options.loadFont || fetchFontAsBase64;
|
|
249
267
|
for (const variant of def.variants) {
|
|
250
268
|
const vfsName = `${family}-${variant.style}.ttf`;
|
|
251
|
-
const base64 = yield
|
|
269
|
+
const base64 = yield loadFont(variant.url);
|
|
252
270
|
doc.addFileToVFS(vfsName, base64);
|
|
253
271
|
doc.addFont(vfsName, family, variant.style, 'Identity-H');
|
|
254
272
|
}
|
|
@@ -269,7 +287,7 @@ function truncateTextToFitWidth(doc, text, maxWidth) {
|
|
|
269
287
|
return truncatedText + ELLIPSIS;
|
|
270
288
|
}
|
|
271
289
|
function drawSimpleObject(x, y, doc, object, printBuffer, width, height, options, context) {
|
|
272
|
-
var _a, _b;
|
|
290
|
+
var _a, _b, _c, _d;
|
|
273
291
|
let text = ''; // Will hold text to display for either 'text' or 'field'
|
|
274
292
|
// Get text from object for text objects
|
|
275
293
|
if (object.type == 'text') {
|
|
@@ -350,18 +368,7 @@ function drawSimpleObject(x, y, doc, object, printBuffer, width, height, options
|
|
|
350
368
|
const urlLower = String(imgUrl).toLowerCase();
|
|
351
369
|
const imageAsset = (_a = context.imageAssets) === null || _a === void 0 ? void 0 : _a.get(String(imgUrl));
|
|
352
370
|
const format = (imageAsset === null || imageAsset === void 0 ? void 0 : imageAsset.format) || (urlLower.endsWith('.png') ? 'PNG' : urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg') ? 'JPEG' : 'JPEG');
|
|
353
|
-
|
|
354
|
-
let drawY = y;
|
|
355
|
-
let drawWidth = width;
|
|
356
|
-
let drawHeight = height;
|
|
357
|
-
// Preserve aspect ratio when objectFit is 'contain' and we have dimensions
|
|
358
|
-
if (imgObj.objectFit === 'contain' && (imageAsset === null || imageAsset === void 0 ? void 0 : imageAsset.width) && (imageAsset === null || imageAsset === void 0 ? void 0 : imageAsset.height)) {
|
|
359
|
-
const scale = Math.min(width / imageAsset.width, height / imageAsset.height);
|
|
360
|
-
drawWidth = imageAsset.width * scale;
|
|
361
|
-
drawHeight = imageAsset.height * scale;
|
|
362
|
-
drawX = x + (width - drawWidth) / 2;
|
|
363
|
-
drawY = y + (height - drawHeight) / 2;
|
|
364
|
-
}
|
|
371
|
+
const { x: drawX, y: drawY, width: drawWidth, height: drawHeight } = computeImageDrawRect(x, y, width, height, (_b = imageAsset === null || imageAsset === void 0 ? void 0 : imageAsset.width) !== null && _b !== void 0 ? _b : 0, (_c = imageAsset === null || imageAsset === void 0 ? void 0 : imageAsset.height) !== null && _c !== void 0 ? _c : 0, imgObj.objectFit);
|
|
365
372
|
try {
|
|
366
373
|
doc.addImage((imageAsset === null || imageAsset === void 0 ? void 0 : imageAsset.dataUrl) || imgUrl, format, drawX, drawY, drawWidth, drawHeight);
|
|
367
374
|
}
|
|
@@ -387,7 +394,7 @@ function drawSimpleObject(x, y, doc, object, printBuffer, width, height, options
|
|
|
387
394
|
return;
|
|
388
395
|
const fontStyleStr = FONT_STYLES[object.fontStyle & 3] || 'normal';
|
|
389
396
|
// Use object font if it's a custom font; otherwise layout default overrides standard fonts (Helvetica etc.)
|
|
390
|
-
const fontFamily = object.fontFamily && ((
|
|
397
|
+
const fontFamily = object.fontFamily && ((_d = options.isCustomPdfFont) === null || _d === void 0 ? void 0 : _d.call(options, object.fontFamily)) ? object.fontFamily : context.defaultFontFamily || object.fontFamily || 'Helvetica';
|
|
391
398
|
doc.setFont(fontFamily, fontStyleStr);
|
|
392
399
|
doc.setFontSize(object.fontSize / 4);
|
|
393
400
|
doc.text(truncateTextToFitWidth(doc, text, width), x, y, { align: textAlign, baseline: 'hanging' });
|
|
@@ -685,7 +692,7 @@ function genPDF(layout, printBuffer, options = {}) {
|
|
|
685
692
|
object.url = logoUrl;
|
|
686
693
|
}
|
|
687
694
|
});
|
|
688
|
-
// Preload image bytes for Node.js and dimensions for
|
|
695
|
+
// Preload image bytes for Node.js and pixel dimensions for aspect-ratio fitting
|
|
689
696
|
const imageAssets = new Map();
|
|
690
697
|
const loadImage = options.loadImage || loadImageForPdf;
|
|
691
698
|
const urls = collectImageUrlsFromLayout(layout, printBuffer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softwear/latestcollectioncore",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.195",
|
|
4
4
|
"description": "Core functions for LatestCollections applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc -p tsconfig.json",
|
|
19
19
|
"build:watch": "tsc -p tsconfig.json --watch",
|
|
20
|
-
"test": "vitest run",
|
|
21
|
-
"test:watch": "vitest"
|
|
20
|
+
"test": "TZ=Europe/Amsterdam vitest run",
|
|
21
|
+
"test:watch": "TZ=Europe/Amsterdam vitest"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
package/src/reports.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface GenPdfOptions {
|
|
|
42
42
|
resolveLogoUrl?: () => string | undefined
|
|
43
43
|
isCustomPdfFont?: (fontFamily: string) => boolean
|
|
44
44
|
getPdfFontDefinition?: (fontFamily: string) => PdfFontDefinition | undefined
|
|
45
|
+
/** Load TTF bytes as base64 (e.g. from disk in Node). Default: fetch variant.url. */
|
|
46
|
+
loadFont?: (url: string) => Promise<string>
|
|
45
47
|
loadImage?: (url: string) => Promise<PdfImageAsset | undefined>
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -217,6 +219,31 @@ function getImageMimeType(format: 'PNG' | 'JPEG'): string {
|
|
|
217
219
|
return format === 'PNG' ? 'image/png' : 'image/jpeg'
|
|
218
220
|
}
|
|
219
221
|
|
|
222
|
+
/** Fit image in layout box preserving aspect ratio (like CSS object-fit: contain). Stretch only when objectFit is fill/stretch. */
|
|
223
|
+
export function computeImageDrawRect(
|
|
224
|
+
x: number,
|
|
225
|
+
y: number,
|
|
226
|
+
boxWidth: number,
|
|
227
|
+
boxHeight: number,
|
|
228
|
+
imageWidth: number,
|
|
229
|
+
imageHeight: number,
|
|
230
|
+
objectFit?: string
|
|
231
|
+
): { x: number; y: number; width: number; height: number } {
|
|
232
|
+
let drawX = x
|
|
233
|
+
let drawY = y
|
|
234
|
+
let drawWidth = boxWidth
|
|
235
|
+
let drawHeight = boxHeight
|
|
236
|
+
const stretch = objectFit === 'fill' || objectFit === 'stretch'
|
|
237
|
+
if (!stretch && imageWidth > 0 && imageHeight > 0) {
|
|
238
|
+
const scale = Math.min(boxWidth / imageWidth, boxHeight / imageHeight)
|
|
239
|
+
drawWidth = imageWidth * scale
|
|
240
|
+
drawHeight = imageHeight * scale
|
|
241
|
+
drawX = x + (boxWidth - drawWidth) / 2
|
|
242
|
+
drawY = y + (boxHeight - drawHeight) / 2
|
|
243
|
+
}
|
|
244
|
+
return { x: drawX, y: drawY, width: drawWidth, height: drawHeight }
|
|
245
|
+
}
|
|
246
|
+
|
|
220
247
|
function getImageDimensions(bytes: Uint8Array, format: 'PNG' | 'JPEG'): { width?: number; height?: number } {
|
|
221
248
|
if (format === 'PNG' && bytes.length >= 24 && isPng(bytes)) {
|
|
222
249
|
return { width: readUint32BE(bytes, 16), height: readUint32BE(bytes, 20) }
|
|
@@ -312,9 +339,10 @@ async function embedFontsInDoc(doc: jsPDF, usedFontFamilies: Set<string>, option
|
|
|
312
339
|
if (def.variants.length === 0) {
|
|
313
340
|
continue
|
|
314
341
|
}
|
|
342
|
+
const loadFont = options.loadFont || fetchFontAsBase64
|
|
315
343
|
for (const variant of def.variants) {
|
|
316
344
|
const vfsName = `${family}-${variant.style}.ttf`
|
|
317
|
-
const base64 = await
|
|
345
|
+
const base64 = await loadFont(variant.url)
|
|
318
346
|
doc.addFileToVFS(vfsName, base64)
|
|
319
347
|
doc.addFont(vfsName, family, variant.style, 'Identity-H')
|
|
320
348
|
}
|
|
@@ -408,18 +436,15 @@ function drawSimpleObject(x: number, y: number, doc: jsPDF, object: LayoutObject
|
|
|
408
436
|
const urlLower = String(imgUrl).toLowerCase()
|
|
409
437
|
const imageAsset = context.imageAssets?.get(String(imgUrl))
|
|
410
438
|
const format = imageAsset?.format || (urlLower.endsWith('.png') ? 'PNG' : urlLower.endsWith('.jpg') || urlLower.endsWith('.jpeg') ? 'JPEG' : 'JPEG')
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
drawX = x + (width - drawWidth) / 2
|
|
421
|
-
drawY = y + (height - drawHeight) / 2
|
|
422
|
-
}
|
|
439
|
+
const { x: drawX, y: drawY, width: drawWidth, height: drawHeight } = computeImageDrawRect(
|
|
440
|
+
x,
|
|
441
|
+
y,
|
|
442
|
+
width,
|
|
443
|
+
height,
|
|
444
|
+
imageAsset?.width ?? 0,
|
|
445
|
+
imageAsset?.height ?? 0,
|
|
446
|
+
imgObj.objectFit
|
|
447
|
+
)
|
|
423
448
|
try {
|
|
424
449
|
doc.addImage(imageAsset?.dataUrl || imgUrl, format, drawX, drawY, drawWidth, drawHeight)
|
|
425
450
|
} catch (_e) {
|
|
@@ -770,7 +795,7 @@ export async function genPDF(layout: Layout, printBuffer: any, options: GenPdfOp
|
|
|
770
795
|
if (logoUrl) object.url = logoUrl
|
|
771
796
|
}
|
|
772
797
|
})
|
|
773
|
-
// Preload image bytes for Node.js and dimensions for
|
|
798
|
+
// Preload image bytes for Node.js and pixel dimensions for aspect-ratio fitting
|
|
774
799
|
const imageAssets = new Map<string, PdfImageAsset>()
|
|
775
800
|
const loadImage = options.loadImage || loadImageForPdf
|
|
776
801
|
const urls = collectImageUrlsFromLayout(layout, printBuffer)
|
package/test/reports.spec.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { genPDF } from '../src/reports'
|
|
2
|
+
import { computeImageDrawRect, genPDF } from '../src/reports'
|
|
3
3
|
import type { Layout } from '../src/pdf'
|
|
4
4
|
|
|
5
|
+
describe('computeImageDrawRect', () => {
|
|
6
|
+
it('preserves aspect ratio by default (object-fit: contain)', () => {
|
|
7
|
+
const rect = computeImageDrawRect(10, 20, 40, 30, 500, 500)
|
|
8
|
+
expect(rect).toEqual({ x: 15, y: 20, width: 30, height: 30 })
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('stretches when objectFit is fill', () => {
|
|
12
|
+
const rect = computeImageDrawRect(10, 20, 40, 30, 500, 375, 'fill')
|
|
13
|
+
expect(rect).toEqual({ x: 10, y: 20, width: 40, height: 30 })
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
5
17
|
/** Tiny valid JPEG as data URL — avoids flaky network-dependent image fetch in CI/sandbox. */
|
|
6
18
|
function offlineJpegFixture(): string {
|
|
7
19
|
const b64 =
|