diff --git a/frontend/src/lib/invoice-pdf.ts b/frontend/src/lib/invoice-pdf.ts
index 365e4d4..eb2b66d 100644
--- a/frontend/src/lib/invoice-pdf.ts
+++ b/frontend/src/lib/invoice-pdf.ts
@@ -220,27 +220,65 @@ export function buildInvoiceHtml(data: InvoicePdfData): string {
`;
}
+// 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 `
+${buildInvoiceHtml(data)}`;
+}
+
export async function downloadInvoicePdf(data: InvoicePdfData): Promise {
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 {
pdf.save(data.fileName);
} finally {
- document.body.removeChild(host);
+ document.body.removeChild(iframe);
}
}