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 Arun P 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 afd07071c3
commit 74ba04e17f
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;
}