@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 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 fetchFontAsBase64(variant.url);
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
- let drawX = x;
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 && ((_b = options.isCustomPdfFont) === null || _b === void 0 ? void 0 : _b.call(options, object.fontFamily)) ? object.fontFamily : context.defaultFontFamily || object.fontFamily || 'Helvetica';
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 objectFit:contain
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.192",
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 fetchFontAsBase64(variant.url)
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
- let drawX = x
412
- let drawY = y
413
- let drawWidth = width
414
- let drawHeight = height
415
- // Preserve aspect ratio when objectFit is 'contain' and we have dimensions
416
- if (imgObj.objectFit === 'contain' && imageAsset?.width && imageAsset?.height) {
417
- const scale = Math.min(width / imageAsset.width, height / imageAsset.height)
418
- drawWidth = imageAsset.width * scale
419
- drawHeight = imageAsset.height * scale
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 objectFit:contain
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)
@@ -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 =