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:
@@ -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==");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user