@softwear/latestcollectioncore 1.0.205 → 1.0.206

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.
@@ -0,0 +1,11 @@
1
+ export interface BarcodeCanvasResult {
2
+ dataUrl: string;
3
+ width: number;
4
+ height: number;
5
+ }
6
+ /**
7
+ * Render a barcode to a PNG data URL.
8
+ * Module width is fixed; the caller must scale the image uniformly into the layout box
9
+ * so bars are not distorted.
10
+ */
11
+ export declare function renderBarcodeToDataUrl(value: string): BarcodeCanvasResult | null;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderBarcodeToDataUrl = void 0;
7
+ const jsbarcode_1 = __importDefault(require("jsbarcode"));
8
+ /** Vite / CJS interop sometimes wraps the function as `{ default: fn }`. */
9
+ function resolveJsBarcode() {
10
+ const mod = jsbarcode_1.default;
11
+ if (typeof mod === 'function')
12
+ return mod;
13
+ if (mod && typeof mod.default === 'function')
14
+ return mod.default;
15
+ return null;
16
+ }
17
+ function createCanvasElement() {
18
+ if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
19
+ return document.createElement('canvas');
20
+ }
21
+ return null;
22
+ }
23
+ /**
24
+ * Render a barcode to a PNG data URL.
25
+ * Module width is fixed; the caller must scale the image uniformly into the layout box
26
+ * so bars are not distorted.
27
+ */
28
+ function renderBarcodeToDataUrl(value) {
29
+ if (!value)
30
+ return null;
31
+ const canvas = createCanvasElement();
32
+ if (!canvas)
33
+ return null;
34
+ const JsBarcode = resolveJsBarcode();
35
+ if (!JsBarcode)
36
+ return null;
37
+ try {
38
+ JsBarcode(canvas, value, {
39
+ // Fixed module geometry — do not stretch independently in X/Y when placing on the page.
40
+ // CODE128 accepts arbitrary values (EAN auto-detect can throw on bad check digits).
41
+ format: 'CODE128',
42
+ width: 2,
43
+ height: 60,
44
+ displayValue: false,
45
+ margin: 0,
46
+ });
47
+ }
48
+ catch (e) {
49
+ if (typeof console !== 'undefined')
50
+ console.warn('Barcode render failed', value, e);
51
+ return null;
52
+ }
53
+ if (!canvas.width || !canvas.height)
54
+ return null;
55
+ return {
56
+ dataUrl: canvas.toDataURL('image/png'),
57
+ width: canvas.width,
58
+ height: canvas.height,
59
+ };
60
+ }
61
+ exports.renderBarcodeToDataUrl = renderBarcodeToDataUrl;
package/dist/pdf.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface PaperSize {
5
5
  footerHeight: number;
6
6
  }
7
7
  export type TextAlign = 1 | 2 | 3;
8
- export type ObjectType = 'text' | 'field' | 'rectangle' | 'image' | 'container';
8
+ export type ObjectType = 'text' | 'field' | 'rectangle' | 'image' | 'container' | 'barcode';
9
9
  export type RepeatContainer = 'horizontal' | 'vertical' | undefined;
10
10
  export interface BaseLayoutObject {
11
11
  type: ObjectType;
@@ -44,6 +44,15 @@ export interface ImageLayoutObject extends BaseLayoutObject {
44
44
  url?: string;
45
45
  image?: HTMLImageElement;
46
46
  }
47
+ /**
48
+ * Renders a 1D barcode from printBuffer via `source` (default `barcode`).
49
+ * Drawn with uniform scale to fit width×height (never independently stretched).
50
+ */
51
+ export interface BarcodeLayoutObject extends BaseLayoutObject {
52
+ type: 'barcode';
53
+ /** Path into the current print buffer item; defaults to `barcode`. */
54
+ source?: string;
55
+ }
47
56
  export interface ContainerLayoutObject extends BaseLayoutObject {
48
57
  type: 'container';
49
58
  source: string;
@@ -51,7 +60,10 @@ export interface ContainerLayoutObject extends BaseLayoutObject {
51
60
  repeatContainer?: RepeatContainer;
52
61
  repeatOnOverflow?: boolean;
53
62
  pageBreak?: boolean;
54
- /** Minimum mm of remaining space before drawing next item; if less, break to next page first */
63
+ /**
64
+ * Minimum remaining space before drawing the next item; if less, break to the next page first.
65
+ * Same units as layout x/y/width/height (tenths of mm), e.g. 220 ≈ 22mm.
66
+ */
55
67
  minHeightBeforeBreak?: number;
56
68
  /** When true, container and children are drawn only once at the end of the report, not on every page */
57
69
  printOnlyAtEnd?: boolean;
@@ -61,7 +73,7 @@ export interface ContainerLayoutObject extends BaseLayoutObject {
61
73
  */
62
74
  printOnlyAtStart?: boolean;
63
75
  }
64
- export type LayoutObject = TextLayoutObject | FieldLayoutObject | RectangleLayoutObject | ImageLayoutObject | ContainerLayoutObject;
76
+ export type LayoutObject = TextLayoutObject | FieldLayoutObject | RectangleLayoutObject | ImageLayoutObject | BarcodeLayoutObject | ContainerLayoutObject;
65
77
  export interface Layout {
66
78
  name?: string;
67
79
  group?: string;
package/dist/reports.js CHANGED
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.genPDF = exports.computeImageDrawRect = void 0;
16
16
  const jspdf_1 = require("jspdf");
17
17
  const date_fns_1 = require("date-fns");
18
+ const barcodeCanvas_1 = require("./barcodeCanvas");
18
19
  const deepCopy_1 = __importDefault(require("./deepCopy"));
19
20
  const layoutFieldFormat_1 = require("./layoutFieldFormat");
20
21
  /** Measurement uses context `currentPageNumber` (1-based). Rendering uses jsPDF’s `getCurrentPageInfo().pageNumber`, which is already 1-based (first page is 1). */
@@ -287,7 +288,7 @@ function truncateTextToFitWidth(doc, text, maxWidth) {
287
288
  return truncatedText + ELLIPSIS;
288
289
  }
289
290
  function drawSimpleObject(x, y, doc, object, printBuffer, width, height, options, context) {
290
- var _a, _b, _c, _d;
291
+ var _a, _b, _c, _d, _f;
291
292
  let text = ''; // Will hold text to display for either 'text' or 'field'
292
293
  // Get text from object for text objects
293
294
  if (object.type == 'text') {
@@ -377,6 +378,28 @@ function drawSimpleObject(x, y, doc, object, printBuffer, width, height, options
377
378
  }
378
379
  }
379
380
  }
381
+ // Draw barcode for barcode objects (uniform scale into the layout box — never stretch bars)
382
+ if (object.type == 'barcode') {
383
+ const barcodeObj = object;
384
+ const source = barcodeObj.source || 'barcode';
385
+ const raw = getProperty(source, printBuffer);
386
+ const value = raw == null ? '' : String(raw);
387
+ if (!value) {
388
+ (_d = options.onAlert) === null || _d === void 0 ? void 0 : _d.call(options, { header: 'fieldnotfound', body: source, type: 'warning', timeout: 10000 });
389
+ }
390
+ else {
391
+ const rendered = (0, barcodeCanvas_1.renderBarcodeToDataUrl)(value);
392
+ if (rendered) {
393
+ const { x: drawX, y: drawY, width: drawWidth, height: drawHeight } = computeImageDrawRect(x, y, width, height, rendered.width, rendered.height);
394
+ try {
395
+ doc.addImage(rendered.dataUrl, 'PNG', drawX, drawY, drawWidth, drawHeight);
396
+ }
397
+ catch (_e) {
398
+ // Skip if jsPDF cannot embed the canvas data URL
399
+ }
400
+ }
401
+ }
402
+ }
380
403
  // Draw text for text and field objets
381
404
  if ((object.type == 'text' || object.type == 'field') && text) {
382
405
  let textAlign;
@@ -394,7 +417,7 @@ function drawSimpleObject(x, y, doc, object, printBuffer, width, height, options
394
417
  return;
395
418
  const fontStyleStr = FONT_STYLES[object.fontStyle & 3] || 'normal';
396
419
  // Use object font if it's a custom font; otherwise layout default overrides standard fonts (Helvetica etc.)
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';
420
+ const fontFamily = object.fontFamily && ((_f = options.isCustomPdfFont) === null || _f === void 0 ? void 0 : _f.call(options, object.fontFamily)) ? object.fontFamily : context.defaultFontFamily || object.fontFamily || 'Helvetica';
398
421
  doc.setFont(fontFamily, fontStyleStr);
399
422
  doc.setFontSize(object.fontSize / 4);
400
423
  doc.text(truncateTextToFitWidth(doc, text, width), x, y, { align: textAlign, baseline: 'hanging' });
@@ -531,9 +554,10 @@ addObjectToPDF = function (originX, originY, doc, object, printBuffer, paperSize
531
554
  });
532
555
  const container = source[containerIndex];
533
556
  absoluteLowerRightHandSide = { x: originX / 10, y: originY / 10 };
534
- const minHeightMm = object.minHeightBeforeBreak;
535
- const useOrphanCheck = minHeightMm != null && minHeightMm > 0;
536
- if (useOrphanCheck && (object.pageBreak || originY + minHeightMm > paperSize.height - paperSize.footerHeight)) {
557
+ // Layout units (tenths of mm), same as originY / paperSize after genPDF scales them ×10.
558
+ const minHeightBeforeBreak = object.minHeightBeforeBreak;
559
+ const useOrphanCheck = minHeightBeforeBreak != null && minHeightBeforeBreak > 0;
560
+ if (useOrphanCheck && (object.pageBreak || originY + minHeightBeforeBreak > paperSize.height - paperSize.footerHeight)) {
537
561
  // We ran out of paper
538
562
  originX = layoutXtenths;
539
563
  // Continuation sheets must not reuse the page-1-only overlap bump: addPage hasn't run yet so getCurrentPageNumber is still 1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softwear/latestcollectioncore",
3
- "version": "1.0.205",
3
+ "version": "1.0.206",
4
4
  "description": "Core functions for LatestCollections applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "date-fns": "^2.29.3",
40
40
  "filtrex": "^2.2.3",
41
+ "jsbarcode": "^3.12.3",
41
42
  "jspdf": "^4.2.1"
42
43
  }
43
44
  }
@@ -0,0 +1,59 @@
1
+ import JsBarcodeImport from 'jsbarcode'
2
+
3
+ export interface BarcodeCanvasResult {
4
+ dataUrl: string
5
+ width: number
6
+ height: number
7
+ }
8
+
9
+ /** Vite / CJS interop sometimes wraps the function as `{ default: fn }`. */
10
+ function resolveJsBarcode(): ((element: unknown, value: string, options?: object) => void) | null {
11
+ const mod: any = JsBarcodeImport
12
+ if (typeof mod === 'function') return mod
13
+ if (mod && typeof mod.default === 'function') return mod.default
14
+ return null
15
+ }
16
+
17
+ function createCanvasElement(): HTMLCanvasElement | null {
18
+ if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
19
+ return document.createElement('canvas')
20
+ }
21
+ return null
22
+ }
23
+
24
+ /**
25
+ * Render a barcode to a PNG data URL.
26
+ * Module width is fixed; the caller must scale the image uniformly into the layout box
27
+ * so bars are not distorted.
28
+ */
29
+ export function renderBarcodeToDataUrl(value: string): BarcodeCanvasResult | null {
30
+ if (!value) return null
31
+ const canvas = createCanvasElement()
32
+ if (!canvas) return null
33
+
34
+ const JsBarcode = resolveJsBarcode()
35
+ if (!JsBarcode) return null
36
+
37
+ try {
38
+ JsBarcode(canvas, value, {
39
+ // Fixed module geometry — do not stretch independently in X/Y when placing on the page.
40
+ // CODE128 accepts arbitrary values.
41
+ format: 'CODE128',
42
+ width: 2,
43
+ height: 60,
44
+ displayValue: false,
45
+ margin: 0,
46
+ })
47
+ } catch (e) {
48
+ if (typeof console !== 'undefined') console.warn('Barcode render failed', value, e)
49
+ return null
50
+ }
51
+
52
+ if (!canvas.width || !canvas.height) return null
53
+
54
+ return {
55
+ dataUrl: canvas.toDataURL('image/png'),
56
+ width: canvas.width,
57
+ height: canvas.height,
58
+ }
59
+ }
package/src/pdf.ts CHANGED
@@ -7,7 +7,7 @@ export interface PaperSize {
7
7
  }
8
8
 
9
9
  export type TextAlign = 1 | 2 | 3 // 1: left, 2: center, 3: right
10
- export type ObjectType = 'text' | 'field' | 'rectangle' | 'image' | 'container'
10
+ export type ObjectType = 'text' | 'field' | 'rectangle' | 'image' | 'container' | 'barcode'
11
11
  export type RepeatContainer = 'horizontal' | 'vertical' | undefined
12
12
 
13
13
  export interface BaseLayoutObject {
@@ -52,6 +52,15 @@ export interface ImageLayoutObject extends BaseLayoutObject {
52
52
  image?: HTMLImageElement
53
53
  }
54
54
 
55
+ /**
56
+ * Renders a 1D barcode from printBuffer via `source` (default `barcode`).
57
+ * Drawn with uniform scale to fit width×height (never independently stretched).
58
+ */
59
+ export interface BarcodeLayoutObject extends BaseLayoutObject {
60
+ type: 'barcode'
61
+ source?: string
62
+ }
63
+
55
64
  export interface ContainerLayoutObject extends BaseLayoutObject {
56
65
  type: 'container'
57
66
  source: string
@@ -70,7 +79,7 @@ export interface ContainerLayoutObject extends BaseLayoutObject {
70
79
  printOnlyAtStart?: boolean
71
80
  }
72
81
 
73
- export type LayoutObject = TextLayoutObject | FieldLayoutObject | RectangleLayoutObject | ImageLayoutObject | ContainerLayoutObject
82
+ export type LayoutObject = TextLayoutObject | FieldLayoutObject | RectangleLayoutObject | ImageLayoutObject | BarcodeLayoutObject | ContainerLayoutObject
74
83
 
75
84
  export interface Layout {
76
85
  name?: string
package/src/reports.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { jsPDF } from 'jspdf'
2
2
  import { format } from 'date-fns'
3
+ import { renderBarcodeToDataUrl } from './barcodeCanvas'
3
4
  import deepCopy from './deepCopy'
4
5
  import { resolveLayoutFieldText } from './layoutFieldFormat'
5
- import { PaperSize, Layout, LayoutObject, ContainerLayoutObject } from './pdf'
6
+ import { PaperSize, Layout, LayoutObject, ContainerLayoutObject, BarcodeLayoutObject } from './pdf'
6
7
 
7
8
  type RenderMode = 'measurement' | 'rendering'
8
9
 
@@ -453,6 +454,27 @@ function drawSimpleObject(x: number, y: number, doc: jsPDF, object: LayoutObject
453
454
  }
454
455
  }
455
456
 
457
+ // Draw barcode for barcode objects (uniform scale into the layout box — never stretch bars)
458
+ if (object.type == 'barcode') {
459
+ const barcodeObj = object as BarcodeLayoutObject
460
+ const source = barcodeObj.source || 'barcode'
461
+ const raw = getProperty(source, printBuffer)
462
+ const value = raw == null ? '' : String(raw)
463
+ if (!value) {
464
+ options.onAlert?.({ header: 'fieldnotfound', body: source, type: 'warning', timeout: 10000 })
465
+ } else {
466
+ const rendered = renderBarcodeToDataUrl(value)
467
+ if (rendered) {
468
+ const { x: drawX, y: drawY, width: drawWidth, height: drawHeight } = computeImageDrawRect(x, y, width, height, rendered.width, rendered.height)
469
+ try {
470
+ doc.addImage(rendered.dataUrl, 'PNG', drawX, drawY, drawWidth, drawHeight)
471
+ } catch (_e) {
472
+ // Skip if jsPDF cannot embed the canvas data URL
473
+ }
474
+ }
475
+ }
476
+ }
477
+
456
478
  // Draw text for text and field objets
457
479
  if ((object.type == 'text' || object.type == 'field') && text) {
458
480
  let textAlign: 'left' | 'center' | 'right' | undefined
@@ -455,4 +455,82 @@ describe('genPDF', () => {
455
455
  const pdfText = Buffer.from(doc.output('arraybuffer')).toString('latin1')
456
456
  expect(pdfText).toMatch(/\(\s*.\s*5\.00\) Tj/)
457
457
  })
458
+
459
+ /**
460
+ * Check multi-page vertical repeat with minHeightBeforeBreak.
461
+ */
462
+ function barcodeSheetLayout(): Layout {
463
+ return {
464
+ name: 'barcodesheet',
465
+ paperSize: { width: 210, height: 297, footerHeight: 10 },
466
+ objects: [
467
+ {
468
+ type: 'container',
469
+ name: 'items',
470
+ source: 'items',
471
+ x: 100,
472
+ y: 100,
473
+ width: 1900,
474
+ height: 220,
475
+ active: true,
476
+ repeatContainer: 'vertical',
477
+ minHeightBeforeBreak: 220,
478
+ children: [
479
+ {
480
+ type: 'barcode',
481
+ name: 'barcode',
482
+ source: 'barcode',
483
+ x: 0,
484
+ y: 20,
485
+ width: 900,
486
+ height: 160,
487
+ active: true,
488
+ },
489
+ {
490
+ type: 'field',
491
+ name: 'barcodeText',
492
+ source: 'barcode',
493
+ x: 0,
494
+ y: 185,
495
+ width: 900,
496
+ height: 30,
497
+ active: true,
498
+ fontFamily: 'Helvetica',
499
+ fontSize: 20,
500
+ fontStyle: 0,
501
+ textAlign: 2,
502
+ },
503
+ ],
504
+ },
505
+ ],
506
+ }
507
+ }
508
+
509
+ function barcodeMarksOnPage(doc: Awaited<ReturnType<typeof genPDF>>, page: number): string[] {
510
+ const pageOps = (doc as any).internal.pages[page] as string[] | undefined
511
+ if (!pageOps) return []
512
+ return [...pageOps.join('\n').matchAll(/\(BC-\d{3}\)/g)].map((m) => m[0].slice(1, -1))
513
+ }
514
+
515
+ it('keeps barcode rows that fit under minHeightBeforeBreak on a single page', async () => {
516
+ const items = Array.from({ length: 12 }, (_, i) => ({ barcode: `BC-${String(i).padStart(3, '0')}` }))
517
+ const doc = await genPDF(barcodeSheetLayout(), { items })
518
+ expect(doc.getNumberOfPages()).toBe(1)
519
+ expect(barcodeMarksOnPage(doc, 1)).toEqual(items.map((item) => item.barcode))
520
+ })
521
+
522
+ it('breaks barcode rows before drawing when minHeightBeforeBreak would overflow', async () => {
523
+ const items = Array.from({ length: 15 }, (_, i) => ({ barcode: `BC-${String(i).padStart(3, '0')}` }))
524
+ const doc = await genPDF(barcodeSheetLayout(), { items })
525
+
526
+ expect(doc.getNumberOfPages()).toBe(2)
527
+ expect(barcodeMarksOnPage(doc, 1)).toEqual(items.slice(0, 12).map((item) => item.barcode))
528
+ expect(barcodeMarksOnPage(doc, 2)).toEqual(items.slice(12).map((item) => item.barcode))
529
+
530
+ const pdfText = Buffer.from(doc.output('arraybuffer')).toString('latin1')
531
+ for (const item of items) {
532
+ const matches = pdfText.match(new RegExp(`\\(${item.barcode}\\)`, 'g'))
533
+ expect(matches, `${item.barcode} should appear exactly once`).toHaveLength(1)
534
+ }
535
+ })
458
536
  })