Replace the HTML preview with a worker-rendered PDF preview
Rendering moves into a module Web Worker behind a latest-wins RenderClient with a watchdog and a handshake-based main-thread fallback. The preview shows the real PDF through pdf.js at 2x with zoom, page indicator, audit issues popover and a focus modal. A4/Letter and margin controls are frozen into each invoice at issue and can be saved as the default (migration M3). react-pdf and pdf.js are lazy-loaded; the main chunk shrinks from 1.8 MB to 0.5 MB. The HTML twin and its styles are removed.
This commit is contained in:
+47
-62
@@ -1,13 +1,13 @@
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import pkg from "../../package.json";
|
||||
import { DEFAULT_PREFS, type RenderPrefsV1 } from "../pdf/model/prefs";
|
||||
import { buildRenderModel } from "../pdf/model/build";
|
||||
import { bankFromSnapshot, vendorFromSnapshot } from "../pdf/model/vendor";
|
||||
import { initBrowserRendering } from "../pdf/render/browserInit";
|
||||
import { renderInvoicePdf as renderModelPdf, type RenderInvoiceResult } from "../pdf/render/invoice";
|
||||
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 { imageSizeFromDataUri } from "./imageSize";
|
||||
import type { InvoicePdfProps } from "./pdfProps";
|
||||
import { getRenderClient, nextRenderJobId } from "./renderClient";
|
||||
import type { BankAccount, Invoice, Settings } from "./types";
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
@@ -17,68 +17,21 @@ function bytesToBase64(bytes: Uint8Array): string {
|
||||
return btoa(bin);
|
||||
}
|
||||
|
||||
/** Flat props for the on-screen preview; the PDF itself is built by renderInvoicePdf. */
|
||||
export async function buildPdfProps(
|
||||
invoice: Invoice,
|
||||
settings: Settings,
|
||||
bank: BankAccount | null,
|
||||
): Promise<InvoicePdfProps> {
|
||||
// Re-exports use the vendor details frozen at issue; live settings are only a fallback for
|
||||
// legacy rows without a snapshot.
|
||||
const vendor = vendorFromSnapshot(invoice.vendorSnapshot, settings);
|
||||
const logoDataUri = vendor.logoPath ? await api.readAssetDataUri(vendor.logoPath) : null;
|
||||
const signatureDataUri = invoice.signaturePath
|
||||
? await api.readAssetDataUri(invoice.signaturePath)
|
||||
: null;
|
||||
|
||||
const resolvedBank = bankFromSnapshot(invoice.bankSnapshot, bank);
|
||||
|
||||
return {
|
||||
vendor,
|
||||
bank: resolvedBank,
|
||||
logoDataUri,
|
||||
signatureDataUri,
|
||||
number: invoice.number,
|
||||
invoiceDate: invoice.invoiceDate,
|
||||
dueDate: invoice.dueDate,
|
||||
clientName: invoice.clientName,
|
||||
clientAddress: invoice.clientAddress,
|
||||
clientGstin: invoice.clientGstin,
|
||||
poNumber: invoice.poNumber,
|
||||
placeOfSupplyStateCode: invoice.placeOfSupplyStateCode,
|
||||
items: invoice.items,
|
||||
subtotal: invoice.subtotal,
|
||||
discount: invoice.discount,
|
||||
taxType: invoice.taxType,
|
||||
taxRate: invoice.taxRate,
|
||||
cgst: invoice.cgstAmount,
|
||||
sgst: invoice.sgstAmount,
|
||||
igst: invoice.igstAmount,
|
||||
total: invoice.total,
|
||||
amountInWords: invoice.amountInWords,
|
||||
notes: invoice.notes,
|
||||
};
|
||||
}
|
||||
|
||||
/** A logo with no readable size is fitted as if it filled Classic's 170 x 54 box. */
|
||||
const UNKNOWN_LOGO_SIZE = { width: 170, height: 54 };
|
||||
|
||||
/**
|
||||
* Renders an issued invoice through the engine: model (supplier and bank from the snapshots frozen at
|
||||
* issue, live settings only as a fallback) -> frame -> template -> PDF bytes, with the layout audit
|
||||
* issues and fingerprint.
|
||||
* 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 async function renderInvoicePdf(
|
||||
export function modelFromAssets(
|
||||
invoice: Invoice,
|
||||
settings: Settings,
|
||||
bank: BankAccount | null,
|
||||
prefs: RenderPrefsV1 = DEFAULT_PREFS,
|
||||
): Promise<RenderInvoiceResult> {
|
||||
await initBrowserRendering();
|
||||
const vendor = vendorFromSnapshot(invoice.vendorSnapshot, settings);
|
||||
const logoUri = vendor.logoPath ? await api.readAssetDataUri(vendor.logoPath) : null;
|
||||
const signatureDataUri = invoice.signaturePath ? await api.readAssetDataUri(invoice.signaturePath) : null;
|
||||
const model = buildRenderModel({
|
||||
logoUri: string | null,
|
||||
signatureDataUri: string | null,
|
||||
): RenderModel {
|
||||
return buildRenderModel({
|
||||
invoice,
|
||||
vendor: settings,
|
||||
bank,
|
||||
@@ -86,7 +39,39 @@ export async function renderInvoicePdf(
|
||||
signatureDataUri,
|
||||
appVersion: pkg.version,
|
||||
});
|
||||
return renderModelPdf({ model, prefs });
|
||||
}
|
||||
|
||||
/** 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 logoUri = vendor.logoPath ? await api.readAssetDataUri(vendor.logoPath) : null;
|
||||
const signatureDataUri = invoice.signaturePath ? await api.readAssetDataUri(invoice.signaturePath) : null;
|
||||
return modelFromAssets(invoice, settings, bank, logoUri, 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);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Asks for a save location and writes the bytes there; null when the dialog is cancelled. */
|
||||
@@ -105,7 +90,7 @@ export async function exportInvoicePdf(
|
||||
invoice: Invoice,
|
||||
settings: Settings,
|
||||
bank: BankAccount | null,
|
||||
prefs: RenderPrefsV1 = DEFAULT_PREFS,
|
||||
prefs?: RenderPrefsV1,
|
||||
): Promise<string | null> {
|
||||
const { bytes } = await renderInvoicePdf(invoice, settings, bank, prefs);
|
||||
return savePdfBytes(bytes, invoice.number);
|
||||
|
||||
Reference in New Issue
Block a user