fix(invoices): render PDF in isolated iframe to avoid oklch crash
html2canvas 1.x cannot parse Tailwind v4's oklch() colors, so generating the invoice PDF from the live document threw and the download silently failed. Render the invoice template inside a sandboxed iframe that loads only the Peyda @font-face and no app stylesheets, so the captured tree is free of oklch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -220,27 +220,65 @@ export function buildInvoiceHtml(data: InvoicePdfData): string {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Self-contained @font-face block so the PDF renders with the Peyda typeface
|
||||
// regardless of the host page. Rendering happens inside an isolated iframe (see
|
||||
// below) so the app's global CSS — which uses oklch() colors that html2canvas
|
||||
// 1.x cannot parse — never reaches the canvas.
|
||||
const FONT_FACE_CSS = [400, 500, 600, 700, 800, 900]
|
||||
.map((weight) => {
|
||||
const file =
|
||||
{ 400: 'Regular', 500: 'Medium', 600: 'SemiBold', 700: 'Bold', 800: 'ExtraBold', 900: 'Black' }[weight];
|
||||
return `@font-face{font-family:'PeydaPDF';src:url('/fonts/peyda/Peyda-${file}.woff') format('woff');font-weight:${weight};font-style:normal;font-display:swap;}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
function buildIframeDocument(data: InvoicePdfData): string {
|
||||
return `<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8">
|
||||
<style>${FONT_FACE_CSS}
|
||||
:root{--font-peyda:'PeydaPDF';}
|
||||
*{box-sizing:border-box;}
|
||||
html,body{margin:0;padding:0;background:#ffffff;}
|
||||
</style></head><body>${buildInvoiceHtml(data)}</body></html>`;
|
||||
}
|
||||
|
||||
export async function downloadInvoicePdf(data: InvoicePdfData): Promise<void> {
|
||||
const [{ default: html2canvas }, { jsPDF }] = await Promise.all([
|
||||
import('html2canvas'),
|
||||
import('jspdf'),
|
||||
]);
|
||||
|
||||
const host = document.createElement('div');
|
||||
host.style.position = 'fixed';
|
||||
host.style.left = '-10000px';
|
||||
host.style.top = '0';
|
||||
host.style.zIndex = '-1';
|
||||
host.innerHTML = buildInvoiceHtml(data);
|
||||
document.body.appendChild(host);
|
||||
// Render inside an isolated iframe so none of the app's stylesheets (Tailwind v4
|
||||
// emits oklch() colors that crash html2canvas) leak into the captured tree.
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('aria-hidden', 'true');
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.left = '-10000px';
|
||||
iframe.style.top = '0';
|
||||
iframe.style.width = '820px';
|
||||
iframe.style.height = '1200px';
|
||||
iframe.style.border = '0';
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
try {
|
||||
const target = host.firstElementChild as HTMLElement;
|
||||
const doc = iframe.contentDocument!;
|
||||
doc.open();
|
||||
doc.write(buildIframeDocument(data));
|
||||
doc.close();
|
||||
|
||||
// Wait for the Peyda webfont to load inside the iframe before capturing.
|
||||
if (doc.fonts?.ready) {
|
||||
await doc.fonts.ready;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const target = doc.body.firstElementChild as HTMLElement;
|
||||
const canvas = await html2canvas(target, {
|
||||
scale: 2,
|
||||
backgroundColor: '#ffffff',
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
windowWidth: target.scrollWidth,
|
||||
windowHeight: target.scrollHeight,
|
||||
});
|
||||
|
||||
const pdf = new jsPDF({ unit: 'pt', format: 'a4', orientation: 'portrait' });
|
||||
@@ -265,6 +303,6 @@ export async function downloadInvoicePdf(data: InvoicePdfData): Promise<void> {
|
||||
|
||||
pdf.save(data.fileName);
|
||||
} finally {
|
||||
document.body.removeChild(host);
|
||||
document.body.removeChild(iframe);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user