Add Rust logo pipeline: trim, metrics, knockout and Branding settings

Logos (PNG, JPEG, WebP) are decoded, trimmed to their ink box, downscaled
to 1200 px and re-encoded as a content-addressed print PNG plus a white
knockout variant when the logo has alpha; aspect, ink density, mean colour
and kind (wordmark, mark, tall) are stored with the settings and frozen in
the issue snapshot. Legacy logos are derived at startup. The Test Vendor logo
PDF shrinks from 334 KB to 92 KB. Settings gains a Branding section with
white and dark previews and a business-name toggle (migration M5).
This commit is contained in:
2026-10-04 06:31:30 +05:30
parent 36aeaa6bce
commit bd83326d54
27 changed files with 1615 additions and 37 deletions
+15 -1
View File
@@ -12,6 +12,10 @@ const vendor: Settings = {
vendorGstin: "",
vendorStateCode: "29",
logoPath: null,
logoOriginalPath: null,
logoKnockoutPath: null,
logoMeta: "",
logoIncludesName: "auto",
signaturePath: null,
defaultBankId: 1,
defaultTaxRate: 18,
@@ -229,7 +233,17 @@ describe("cancelled and logo", () => {
expect(make({ status: "draft" }).cancelled).toBe(false);
});
it("passes the logo and signature through, and keeps null when absent", () => {
const logo = { dataUri: "data:image/png;base64,AA==", width: 200, height: 40 };
const logo = {
dataUri: "data:image/png;base64,AA==",
width: 200,
height: 40,
aspect: 5,
density: 0.43,
kind: "wordmark" as const,
includesName: true,
hasKnockout: true,
knockoutDataUri: "data:image/png;base64,CC==",
};
const m = make({}, {}, { logo, signatureDataUri: "data:image/png;base64,BB==" });
expect(m.logo).toEqual(logo);
expect(m.signature.dataUri).toBe("data:image/png;base64,BB==");
+12
View File
@@ -11,9 +11,21 @@ import { sanitize, splitLines } from "./text";
import { bankFromSnapshot, vendorFromSnapshot } from "./vendor";
export interface LogoInput {
/** The trimmed print image. */
dataUri: string;
/** Pixel size of `dataUri`. */
width: number;
height: number;
/** Width / height of the ink box. */
aspect: number;
/** Share of the box that is ink, 0 to 1. */
density: number;
kind: "wordmark" | "mark" | "tall";
/** The logo already carries the business name (the user's setting, resolved from Automatic). */
includesName: boolean;
/** A white-ink variant exists, for dark surfaces. */
hasKnockout?: boolean;
knockoutDataUri?: string;
}
export interface BuildInput {
+9 -1
View File
@@ -7,7 +7,15 @@ import type { BankAccount, Settings } from "../../lib/types";
export function vendorFromSnapshot(snapshot: string | undefined, live: Settings): Settings {
if (!snapshot) return live;
try {
return { ...live, ...(JSON.parse(snapshot) as Partial<Settings>) };
const frozen = JSON.parse(snapshot) as Partial<Settings>;
// Snapshots from before the logo pipeline lack these keys; the live values describe a different logo.
return {
...live,
logoKnockoutPath: null,
logoMeta: "",
logoIncludesName: "auto",
...frozen,
};
} catch {
return live;
}
+8 -3
View File
@@ -38,6 +38,10 @@ const baseVendor: Settings = {
vendorGstin: "",
vendorStateCode: "29",
logoPath: null,
logoOriginalPath: null,
logoKnockoutPath: null,
logoMeta: "",
logoIncludesName: "auto",
signaturePath: null,
defaultBankId: 1,
defaultTaxRate: 18,
@@ -136,12 +140,13 @@ export function fixtureLogo(kind: LogoKind): LogoInput | null {
if (kind === "real") {
// 2172 x 724: the untrimmed Test Vendor logo, a 3:1 aspect.
const png = readFileSync(path.resolve(process.cwd(), "public/brand/sample-wordmark-logo.png"));
hit = { dataUri: dataUri(png), width: png.readUInt32BE(16), height: png.readUInt32BE(20) };
const [width, height] = [png.readUInt32BE(16), png.readUInt32BE(20)];
hit = { dataUri: dataUri(png), width, height, aspect: width / height, density: 0.21, kind: "wordmark", includesName: true };
} else if (kind === "wide") {
// The trimmed Test Vendor ink box is 4.6:1.
hit = { dataUri: dataUri(solidPng(460, 100)), width: 460, height: 100 };
hit = { dataUri: dataUri(solidPng(460, 100)), width: 460, height: 100, aspect: 4.6, density: 1, kind: "wordmark", includesName: true };
} else {
hit = { dataUri: dataUri(solidPng(120, 120)), width: 120, height: 120 };
hit = { dataUri: dataUri(solidPng(120, 120)), width: 120, height: 120, aspect: 1, density: 1, kind: "mark", includesName: false };
}
cache.set(kind, hit);
}
+4
View File
@@ -28,6 +28,10 @@ const VENDOR: Settings = {
vendorGstin: "",
vendorStateCode: "29",
logoPath: null,
logoOriginalPath: null,
logoKnockoutPath: null,
logoMeta: "",
logoIncludesName: "auto",
signaturePath: null,
defaultBankId: 1,
defaultTaxRate: 18,