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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 Arun P logo, a 3:1 aspect.
|
||||
const png = readFileSync(path.resolve(process.cwd(), "public/brand/arun-p-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 Arun P 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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user