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).
70 lines
2.9 KiB
TypeScript
70 lines
2.9 KiB
TypeScript
import pkg from "../../package.json";
|
|
import { parsePrefs, type RenderPrefsV1 } from "../pdf/model/prefs";
|
|
import { buildRenderModel, type RenderModel } from "../pdf/model/build";
|
|
import { vendorFromSnapshot } from "../pdf/model/vendor";
|
|
import { Superseded } from "../pdf/render/client";
|
|
import type { RenderResult } from "../pdf/render/jobs";
|
|
import { api } from "./api";
|
|
import { loadLogoImages, logoForModel, type LogoImages } from "./logo";
|
|
import { getRenderClient, nextRenderJobId } from "./renderClient";
|
|
import type { BankAccount, Invoice, Settings } from "./types";
|
|
|
|
/**
|
|
* The render model for an invoice whose images are already loaded (the live preview caches them).
|
|
* Supplier and bank come from the snapshots frozen at issue; live settings are only a fallback.
|
|
*/
|
|
export function modelFromAssets(
|
|
invoice: Invoice,
|
|
settings: Settings,
|
|
bank: BankAccount | null,
|
|
logoImages: LogoImages | null,
|
|
signatureDataUri: string | null,
|
|
): RenderModel {
|
|
return buildRenderModel({
|
|
invoice,
|
|
vendor: settings,
|
|
bank,
|
|
logo: logoForModel(vendorFromSnapshot(invoice.vendorSnapshot, settings), logoImages),
|
|
signatureDataUri,
|
|
appVersion: pkg.version,
|
|
});
|
|
}
|
|
|
|
/** Loads the invoice's logo and signature, then builds its render model. */
|
|
export async function buildModelForInvoice(invoice: Invoice, settings: Settings, bank: BankAccount | null): Promise<RenderModel> {
|
|
const vendor = vendorFromSnapshot(invoice.vendorSnapshot, settings);
|
|
const logoImages = await loadLogoImages(vendor, api.readAssetDataUri);
|
|
const signatureDataUri = invoice.signaturePath ? await api.readAssetDataUri(invoice.signaturePath) : null;
|
|
return modelFromAssets(invoice, settings, bank, logoImages, signatureDataUri);
|
|
}
|
|
|
|
/** Page setup for an invoice: what was frozen at issue, else the user's current default. */
|
|
export const prefsForInvoice = (invoice: Invoice, settings: Settings): RenderPrefsV1 =>
|
|
parsePrefs(invoice.renderPrefs || settings.renderPrefs);
|
|
|
|
/**
|
|
* Renders an issued invoice to PDF bytes through the shared render client (worker, or the main
|
|
* thread if the worker cannot start), with the layout audit issues and fingerprint.
|
|
*/
|
|
export async function renderInvoicePdf(
|
|
invoice: Invoice,
|
|
settings: Settings,
|
|
bank: BankAccount | null,
|
|
prefs: RenderPrefsV1 = prefsForInvoice(invoice, settings),
|
|
): Promise<RenderResult> {
|
|
const model = await buildModelForInvoice(invoice, settings, bank);
|
|
return renderModelWithRetry(model, prefs);
|
|
}
|
|
|
|
/** Renders a ready model in the shared client. */
|
|
export async function renderModelWithRetry(model: RenderModel, prefs: RenderPrefsV1): Promise<RenderResult> {
|
|
// The live preview shares the client; if it takes the worker first this render is superseded, so go again.
|
|
for (let attempt = 0; ; attempt++) {
|
|
try {
|
|
return await getRenderClient().render({ id: nextRenderJobId(), model, prefs });
|
|
} catch (err) {
|
|
if (!(err instanceof Superseded) || attempt >= 4) throw err;
|
|
}
|
|
}
|
|
}
|