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 { api } from "./api"; import { imageSizeFromDataUri } from "./imageSize"; import type { InvoicePdfProps } from "./pdfProps"; import type { BankAccount, Invoice, Settings } from "./types"; function bytesToBase64(bytes: Uint8Array): string { let bin = ""; const step = 0x8000; for (let i = 0; i < bytes.length; i += step) bin += String.fromCharCode(...bytes.subarray(i, i + step)); 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 { // 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. */ export async function renderInvoicePdf( invoice: Invoice, settings: Settings, bank: BankAccount | null, prefs: RenderPrefsV1 = DEFAULT_PREFS, ): Promise { 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({ invoice, vendor: settings, bank, logo: logoUri ? { dataUri: logoUri, ...(imageSizeFromDataUri(logoUri) ?? UNKNOWN_LOGO_SIZE) } : null, signatureDataUri, appVersion: pkg.version, }); return renderModelPdf({ model, prefs }); } /** Asks for a save location and writes the bytes there; null when the dialog is cancelled. */ export async function savePdfBytes(bytes: Uint8Array, fileName: string): Promise { const suggested = fileName.replace(/[^A-Za-z0-9._-]+/g, "-") || "invoice"; const path = await save({ defaultPath: `${suggested}.pdf`, filters: [{ name: "PDF document", extensions: ["pdf"] }], }); if (!path) return null; await api.saveBinaryFile(path, bytesToBase64(bytes)); return path; } export async function exportInvoicePdf( invoice: Invoice, settings: Settings, bank: BankAccount | null, prefs: RenderPrefsV1 = DEFAULT_PREFS, ): Promise { const { bytes } = await renderInvoicePdf(invoice, settings, bank, prefs); return savePdfBytes(bytes, invoice.number); }