From 0ccb8e26d9e3508cf6759d65d951cc9ba373020b Mon Sep 17 00:00:00 2001 From: rdlabo Date: Thu, 16 Jul 2026 14:08:09 +0900 Subject: [PATCH 1/2] Add shared PDF label builder --- package-lock.json | 48 +++++++ package.json | 1 + projects/kit/README.md | 18 ++- projects/kit/package.json | 4 + .../kit/printer/src/kit-pdf-printer.spec.ts | 55 ++++++++ projects/kit/printer/src/kit-pdf-printer.ts | 125 ++++++++++++++++++ projects/kit/printer/src/public-api.ts | 1 + 7 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 projects/kit/printer/src/kit-pdf-printer.spec.ts create mode 100644 projects/kit/printer/src/kit-pdf-printer.ts diff --git a/package-lock.json b/package-lock.json index 3f1e011..43820d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "ng-packagr": "^21.0.0", "np": "^9.2.0", "path": "^0.12.7", + "pdf-lib": "^1.17.1", "prettier": "^3.1.1", "typescript": "~5.9.3", "typescript-eslint": "^8.61.1", @@ -8201,6 +8202,26 @@ "license": "MIT", "optional": true }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@peculiar/asn1-cms": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", @@ -20826,6 +20847,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -21073,6 +21101,26 @@ "dev": true, "license": "MIT" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", diff --git a/package.json b/package.json index 9c113f8..18c8c14 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "ng-packagr": "^21.0.0", "np": "^9.2.0", "path": "^0.12.7", + "pdf-lib": "^1.17.1", "prettier": "^3.1.1", "typescript": "~5.9.3", "typescript-eslint": "^8.61.1", diff --git a/projects/kit/README.md b/projects/kit/README.md index 23ab775..49beafb 100644 --- a/projects/kit/README.md +++ b/projects/kit/README.md @@ -547,16 +547,19 @@ await kitPresentLanguageActionSheet(inject(ActionSheetController), { --- -### Printer (Brother label plumbing) +### Printer (label image and PDF plumbing) -Three pure functions (no DI) that extract the i18n-free core of the fleet's Brother label printing, so a device-quirk or print-setting fix lands in every app at once. The UI orchestration — search/channel-selection alerts, loading overlays, and the app-specific paper list — stays in each app, since those diverge (labels, paper options, copies policy). +Pure functions (no DI) that extract the i18n-free core of the fleet's label printing, so a device-quirk, layout, or PDF fix lands in every app at once. The UI orchestration — paper-selection alerts, loading overlays, storage, printer transport, and app-specific copies policy — stays in each app. - `kitDomToPng(element, { rotate?, scale? })` — render a DOM element to a base64 PNG with the fleet's device fixes (iOS +2px to avoid bottom clipping, none on Android to avoid a black line; retries up to 10×). The caller presents its own loading UI. - `kitRotationImage(base64)` — rotate a base64 image 90° via canvas. - `kitBuildBrotherPrintSettings({ modelName, printBase64, label, numberOfCopies, halftoneThreshold })` — assemble the canonical `BRLMPrintOptions` (fit-page, centered, best quality, threshold halftone, standard margins, tape size parsed from the label's `WH` code). Merge `{ port, channelInfo }` from the selected channel before calling `BrotherPrint.printImage()`. +- `kitCalculatePrintLayout({ paper, labelWidthPx, labelHeightPx, copies, measure?, marginMm? })` — calculate row-major positions across as many pages as required. The default outer margin is 5mm. +- `kitBuildLabelPdf({ imageData, ...layoutOptions })` — embed the PNG artwork at the calculated positions and return PDF bytes. It deliberately does not open a browser tab or call a native printer. +- `kitPrintPaperSizes` — A4/B5 presets. Callers may pass any other `KitPrintPaper` dimensions without changing the kit. ```typescript -import { kitDomToPng, kitBuildBrotherPrintSettings } from '@rdlabo/ionic-angular-kit'; +import { kitBuildBrotherPrintSettings, kitBuildLabelPdf, kitDomToPng, kitPrintPaperSizes } from '@rdlabo/ionic-angular-kit/printer'; const png = await kitDomToPng(this.preview().nativeElement, { rotate: true }); const settings = kitBuildBrotherPrintSettings({ @@ -567,6 +570,15 @@ const settings = kitBuildBrotherPrintSettings({ halftoneThreshold: printOptions.halftoneThreshold, }); await BrotherPrint.printImage({ ...settings, port: channel.port, channelInfo: channel.channelInfo }); + +const pdfBytes = await kitBuildLabelPdf({ + imageData: png, + paper: kitPrintPaperSizes.a4, + labelWidthPx: 200, + labelHeightPx: 100, + copies: 6, + marginMm: 5, +}); ``` --- diff --git a/projects/kit/package.json b/projects/kit/package.json index ed8fdb4..9a59ed7 100644 --- a/projects/kit/package.json +++ b/projects/kit/package.json @@ -24,6 +24,7 @@ "@capacitor-community/apple-sign-in": "*", "@rdlabo/capacitor-brotherprint": ">=6.0.0 <9.0.0", "dom-to-image-more": "^3.0.0", + "pdf-lib": "^1.17.1", "firebase": ">=11 <13", "rxjs": "^7.8.0" }, @@ -54,6 +55,9 @@ }, "dom-to-image-more": { "optional": true + }, + "pdf-lib": { + "optional": true } }, "dependencies": { diff --git a/projects/kit/printer/src/kit-pdf-printer.spec.ts b/projects/kit/printer/src/kit-pdf-printer.spec.ts new file mode 100644 index 0000000..357f68f --- /dev/null +++ b/projects/kit/printer/src/kit-pdf-printer.spec.ts @@ -0,0 +1,55 @@ +import { PDFDocument } from 'pdf-lib'; + +import { kitBuildLabelPdf, kitCalculatePrintLayout, kitPrintPaperSizes } from './kit-pdf-printer'; + +describe('kitCalculatePrintLayout', () => { + it('uses A4 dimensions and the default 5mm margin', () => { + const layout = kitCalculatePrintLayout({ + paper: kitPrintPaperSizes.a4, + labelWidthPx: 200, + labelHeightPx: 100, + copies: 1, + }); + expect(layout.paperWidth).toBeCloseTo((210 * 72) / 25.4); + expect(layout.paperHeight).toBeCloseTo((297 * 72) / 25.4); + expect(layout.positions[0].x).toBeCloseTo((5 * 72) / 25.4); + }); + + it('supports B5 and continues labels onto additional pages', () => { + const layout = kitCalculatePrintLayout({ + paper: kitPrintPaperSizes.b5, + labelWidthPx: 600, + labelHeightPx: 900, + copies: 3, + }); + expect(layout.paperWidth).toBeCloseTo((182 * 72) / 25.4); + expect(layout.positions.at(-1)?.pageIndex).toBeGreaterThan(0); + }); + + it('applies a physical width while preserving the aspect ratio', () => { + const layout = kitCalculatePrintLayout({ + paper: kitPrintPaperSizes.a4, + labelWidthPx: 200, + labelHeightPx: 100, + copies: 1, + measure: { type: 'width', sizeMm: 80 }, + }); + expect(layout.labelWidth).toBeCloseTo((80 * 72) / 25.4); + expect(layout.labelWidth / layout.labelHeight).toBeCloseTo(2); + }); +}); + +describe('kitBuildLabelPdf', () => { + it('builds a PDF containing the required number of pages', async () => { + const png = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='; + const bytes = await kitBuildLabelPdf({ + imageData: png, + paper: kitPrintPaperSizes.a4, + labelWidthPx: 600, + labelHeightPx: 900, + copies: 2, + }); + const pdf = await PDFDocument.load(bytes); + expect(pdf.getPageCount()).toBe(2); + }); +}); diff --git a/projects/kit/printer/src/kit-pdf-printer.ts b/projects/kit/printer/src/kit-pdf-printer.ts new file mode 100644 index 0000000..3b1ab9f --- /dev/null +++ b/projects/kit/printer/src/kit-pdf-printer.ts @@ -0,0 +1,125 @@ +import { PDFDocument } from 'pdf-lib'; + +/** Physical paper dimensions used to build a PDF page. */ +export interface KitPrintPaper { + /** Human-readable paper name. */ + readonly label: string; + /** Paper width in millimetres. */ + readonly widthMm: number; + /** Paper height in millimetres. */ + readonly heightMm: number; +} + +/** Standard paper presets shared by the printing applications. */ +export const kitPrintPaperSizes = { + a4: { label: 'A4', widthMm: 210, heightMm: 297 }, + b5: { label: 'B5', widthMm: 182, heightMm: 257 }, +} as const satisfies Record; + +/** A position for one label on a PDF page, expressed in PDF points. */ +export interface KitPrintPosition { + readonly pageIndex: number; + readonly x: number; + readonly y: number; +} + +/** Calculated PDF page and label dimensions. */ +export interface KitPrintLayout { + readonly paperWidth: number; + readonly paperHeight: number; + readonly labelWidth: number; + readonly labelHeight: number; + readonly positions: readonly KitPrintPosition[]; +} + +/** Optional physical size override for a label. */ +export interface KitPrintMeasure { + readonly type: 'width' | 'height'; + readonly sizeMm: number; +} + +/** Parameters for {@link kitCalculatePrintLayout}. */ +export interface KitCalculatePrintLayoutParams { + readonly paper: KitPrintPaper; + readonly labelWidthPx: number; + readonly labelHeightPx: number; + readonly copies: number; + readonly measure?: KitPrintMeasure | null; + /** Outer page margin in millimetres. Defaults to 5mm. */ + readonly marginMm?: number; +} + +/** + * Calculate page dimensions and row-major label positions for a printable PDF. + * + * Labels keep their aspect ratio, are never enlarged beyond their requested pixel/physical size, + * and automatically continue on additional pages when the current page is full. + */ +export const kitCalculatePrintLayout = (params: KitCalculatePrintLayoutParams): KitPrintLayout => { + const pointsPerMm = 72 / 25.4; + const pointsPerPixel = 72 / 96; + const paperWidth = params.paper.widthMm * pointsPerMm; + const paperHeight = params.paper.heightMm * pointsPerMm; + const margin = (params.marginMm ?? 5) * pointsPerMm; + const aspect = params.labelWidthPx / params.labelHeightPx; + let labelWidth = params.labelWidthPx * pointsPerPixel; + let labelHeight = params.labelHeightPx * pointsPerPixel; + + if (params.measure?.type === 'width') { + labelWidth = params.measure.sizeMm * pointsPerMm; + labelHeight = labelWidth / aspect; + } else if (params.measure?.type === 'height') { + labelHeight = params.measure.sizeMm * pointsPerMm; + labelWidth = labelHeight * aspect; + } + + const scale = Math.min(1, (paperWidth - margin * 2) / labelWidth, (paperHeight - margin * 2) / labelHeight); + labelWidth *= scale; + labelHeight *= scale; + + const positions: KitPrintPosition[] = []; + let pageIndex = 0; + let x = margin; + let y = paperHeight - margin; + for (let index = 0; index < Math.max(0, params.copies); index++) { + if (x + labelWidth > paperWidth - margin + 0.01) { + x = margin; + y -= labelHeight; + } + if (y - labelHeight < margin - 0.01) { + pageIndex++; + x = margin; + y = paperHeight - margin; + } + positions.push({ pageIndex, x, y: y - labelHeight }); + x += labelWidth; + } + + return { paperWidth, paperHeight, labelWidth, labelHeight, positions }; +}; + +/** Parameters for {@link kitBuildLabelPdf}. */ +export interface KitBuildLabelPdfParams extends KitCalculatePrintLayoutParams { + /** PNG label artwork as a data URL or base64 string accepted by pdf-lib. */ + readonly imageData: string; +} + +/** Build a multi-page label PDF and return its bytes without choosing an output destination. */ +export const kitBuildLabelPdf = async (params: KitBuildLabelPdfParams): Promise => { + const pdf = await PDFDocument.create(); + const image = await pdf.embedPng(params.imageData); + const layout = kitCalculatePrintLayout(params); + const pages = layout.positions.length > 0 ? [pdf.addPage([layout.paperWidth, layout.paperHeight])] : []; + + for (const position of layout.positions) { + while (pages.length <= position.pageIndex) pages.push(pdf.addPage([layout.paperWidth, layout.paperHeight])); + pages[position.pageIndex].drawImage(image, { + x: position.x, + y: position.y, + width: layout.labelWidth, + height: layout.labelHeight, + }); + } + + return pdf.save(); +}; diff --git a/projects/kit/printer/src/public-api.ts b/projects/kit/printer/src/public-api.ts index da80126..de87c02 100644 --- a/projects/kit/printer/src/public-api.ts +++ b/projects/kit/printer/src/public-api.ts @@ -2,3 +2,4 @@ // Split into its own entry point so only apps that print pull in `@rdlabo/capacitor-brotherprint` // and `dom-to-image-more`; the core entry stays free of those native peers. export * from './kit-printer'; +export * from './kit-pdf-printer'; From 2e247462f91ee6f394468154898e2de4923ce298 Mon Sep 17 00:00:00 2001 From: rdlabo Date: Thu, 16 Jul 2026 14:11:24 +0900 Subject: [PATCH 2/2] Document PDF peer dependency --- projects/kit/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/kit/README.md b/projects/kit/README.md index 49beafb..5b9dbd3 100644 --- a/projects/kit/README.md +++ b/projects/kit/README.md @@ -41,7 +41,7 @@ Kit shares the repo `v*` release line with the other libraries (see root README | `dom-to-image-more` | `^3.0.0` | | `rxjs` | `^7.8.0` | -Feature-scoped peers are only needed by the features that use them (`status-bar` → `KitThemeController`; `preferences` + `in-app-review` → `kitRequestReview`; `capacitor-brotherprint` + `dom-to-image-more` → the printer helpers); an app that doesn't use a feature can ignore its unmet-peer warning. +Feature-scoped peers are only needed by the features that use them (`status-bar` → `KitThemeController`; `preferences` + `in-app-review` → `kitRequestReview`; `capacitor-brotherprint` + `dom-to-image-more` → the Brother/PNG printer helpers; `pdf-lib` → the PDF printer helper); an app that doesn't use a feature can ignore its unmet-peer warning. ---