Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 16 additions & 4 deletions projects/kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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 `W<w>H<h>` 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({
Expand All @@ -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,
});
```

---
Expand Down
4 changes: 4 additions & 0 deletions projects/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -54,6 +55,9 @@
},
"dom-to-image-more": {
"optional": true
},
"pdf-lib": {
"optional": true
}
},
"dependencies": {
Expand Down
55 changes: 55 additions & 0 deletions projects/kit/printer/src/kit-pdf-printer.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
125 changes: 125 additions & 0 deletions projects/kit/printer/src/kit-pdf-printer.ts
Original file line number Diff line number Diff line change
@@ -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<string, KitPrintPaper>;

/** 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<Uint8Array> => {
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();
};
1 change: 1 addition & 0 deletions projects/kit/printer/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';