136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
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<string, { width: number; height: number }> = {
|
|
a4: { width: 595.28, height: 841.89 },
|
|
a5: { width: 419.53, height: 595.28 },
|
|
letter: { width: 612, height: 792 },
|
|
};
|
|
|
|
export async function withFabricCanvas<T>(
|
|
options: FabricRenderOptions,
|
|
handler: (canvas: fabric.Canvas, element: HTMLCanvasElement) => Promise<T>,
|
|
): Promise<T> {
|
|
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<string> {
|
|
return withFabricCanvas(options, async (canvas) =>
|
|
canvas.toDataURL({ format: 'png', multiplier }),
|
|
);
|
|
}
|
|
|
|
export async function generatePdfBytes(
|
|
options: FabricRenderOptions,
|
|
paper: string,
|
|
orientation: string,
|
|
multiplier = 2,
|
|
): Promise<Uint8Array> {
|
|
const dataUrl = await generatePngDataUrl(options, multiplier);
|
|
return createPdfFromPng(dataUrl, paper, orientation);
|
|
}
|
|
|
|
export async function createPdfFromPng(
|
|
dataUrl: string,
|
|
paper: string,
|
|
orientation: string,
|
|
): Promise<Uint8Array> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|
|
}
|