import * as fabric from 'fabric'; import { PDFDocument } from 'pdf-lib'; import { CANVAS_HEIGHT, CANVAS_WIDTH } from './schema'; import { FabricRenderOptions, renderFabricLayout } from './DesignerCanvas'; const PDF_PAGE_SIZES: Record = { a4: { width: 595.28, height: 841.89 }, a5: { width: 419.53, height: 595.28 }, letter: { width: 612, height: 792 }, }; export async function withFabricCanvas( options: FabricRenderOptions, handler: (canvas: fabric.Canvas, element: HTMLCanvasElement) => Promise, ): Promise { const canvasElement = document.createElement('canvas'); canvasElement.width = CANVAS_WIDTH; canvasElement.height = CANVAS_HEIGHT; const canvas = new fabric.Canvas(canvasElement, { selection: false, }); try { await renderFabricLayout(canvas, { ...options, readOnly: true, }); return await handler(canvas, canvasElement); } finally { canvas.dispose(); canvasElement.remove(); } } export async function generatePngDataUrl( options: FabricRenderOptions, multiplier = 2, ): Promise { return withFabricCanvas(options, async (canvas) => canvas.toDataURL({ format: 'png', multiplier }), ); } export async function generatePdfBytes( options: FabricRenderOptions, paper: string, orientation: string, multiplier = 2, ): Promise { const dataUrl = await generatePngDataUrl(options, multiplier); return createPdfFromPng(dataUrl, paper, orientation); } export async function createPdfFromPng( dataUrl: string, paper: string, orientation: string, ): Promise { const pdfDoc = await PDFDocument.create(); const baseSize = PDF_PAGE_SIZES[paper.toLowerCase()] ?? PDF_PAGE_SIZES.a4; const landscape = orientation === 'landscape'; const pageWidth = landscape ? baseSize.height : baseSize.width; const pageHeight = landscape ? baseSize.width : baseSize.height; const page = pdfDoc.addPage([pageWidth, pageHeight]); const pngBytes = dataUrlToUint8Array(dataUrl); const pngImage = await pdfDoc.embedPng(pngBytes); const imageWidth = pngImage.width; const imageHeight = pngImage.height; const scale = Math.min(pageWidth / imageWidth, pageHeight / imageHeight); const drawWidth = imageWidth * scale; const drawHeight = imageHeight * scale; page.drawImage(pngImage, { x: (pageWidth - drawWidth) / 2, y: (pageHeight - drawHeight) / 2, width: drawWidth, height: drawHeight, }); return pdfDoc.save(); } export function triggerDownloadFromDataUrl(dataUrl: string, filename: string): Promise { return fetch(dataUrl) .then((response) => response.blob()) .then((blob) => triggerDownloadFromBlob(blob, filename)); } export function triggerDownloadFromBlob(blob: Blob, filename: string): void { const objectUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = objectUrl; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => URL.revokeObjectURL(objectUrl), 1000); } export async function openPdfInNewTab(pdfBytes: Uint8Array): Promise { const arrayBuffer = pdfBytes.buffer.slice(pdfBytes.byteOffset, pdfBytes.byteOffset + pdfBytes.byteLength) as ArrayBuffer; const blob = new Blob([arrayBuffer], { type: 'application/pdf' }); const blobUrl = URL.createObjectURL(blob); const printWindow = window.open(blobUrl, '_blank', 'noopener,noreferrer'); if (!printWindow) { URL.revokeObjectURL(blobUrl); throw new Error('window-blocked'); } printWindow.onload = () => { try { printWindow.focus(); printWindow.print(); } catch (error) { console.error('[FabricExport] Browser print failed', error); } }; setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000); } function dataUrlToUint8Array(dataUrl: string): Uint8Array { const [, base64] = dataUrl.split(','); const decoded = atob(base64 ?? ''); const bytes = new Uint8Array(decoded.length); for (let index = 0; index < decoded.length; index += 1) { bytes[index] = decoded.charCodeAt(index); } return bytes; }