import * as resvg from '@resvg/resvg-wasm'; import wasmUrl from '@resvg/resvg-wasm/index_bg.wasm?url'; import { jsPDF } from 'jspdf'; import type { ConversionResult, SVGConversionOptions } from '../../types/conversion-types'; import { ValidationUtils } from '../../utils/validation-utils'; export class ResvgConverter { private static initialized = false; static async initialize(): Promise { if (!this.initialized) { try { await resvg.initWasm(fetch(wasmUrl)); this.initialized = true; } catch (error) { throw new Error('Failed to initialize RESVG converter'); } } } static async convert(file: File, options: SVGConversionOptions = {}): Promise { ValidationUtils.validateFile(file, ['svg']); if (!this.initialized) { await this.initialize(); } const svgContent = await this.readFileAsText(file); const { mmWidth, mmHeight } = this.getSvgDimensions(svgContent, options); options.onProgress?.({ stage: 'processing', progress: 25 }); const renderOptions = { background: options.backgroundColor || 'white', fitTo: options.width ? { mode: 'width' as const, value: options.width } : { mode: 'width' as const, value: Math.round((mmWidth * 96) / 25.4) } }; const resvgJS = new resvg.Resvg(svgContent, renderOptions); const pngData = resvgJS.render(); const pngBuffer = pngData.asPng(); options.onProgress?.({ stage: 'processing', progress: 75 }); const pdf = new jsPDF({ orientation: mmWidth > mmHeight ? 'landscape' : 'portrait', unit: 'mm', format: [mmWidth, mmHeight] }); const pngBlob = new Blob([pngBuffer.slice()], { type: 'image/png' }); const pngUrl = URL.createObjectURL(pngBlob); try { await new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { pdf.addImage(pngUrl, 'PNG', 0, 0, mmWidth, mmHeight, '', 'FAST'); resolve(); }; img.onerror = () => reject(new Error('Failed to load rendered PNG')); img.src = pngUrl; }); options.onProgress?.({ stage: 'finalizing', progress: 100 }); const pdfBlob = pdf.output('blob'); const pdfUrl = URL.createObjectURL(pdfBlob); return { url: pdfUrl, size: pdfBlob.size, metadata: { backend: 'resvg', dimensions: { width: mmWidth, height: mmHeight } } }; } finally { URL.revokeObjectURL(pngUrl); } } private static async readFileAsText(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = () => reject(new Error('Failed to read SVG file')); reader.readAsText(file); }); } private static getSvgDimensions(svgContent: string, options: SVGConversionOptions) { const parser = new DOMParser(); const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml'); const svgElement = svgDoc.documentElement; let width = 210; let height = 297; if (options.width && options.height) { width = (options.width * 25.4) / 96; height = (options.height * 25.4) / 96; } else { const viewBox = svgElement.getAttribute('viewBox'); const svgWidth = svgElement.getAttribute('width'); const svgHeight = svgElement.getAttribute('height'); if (viewBox) { const [, , vbWidth, vbHeight] = viewBox.split(/\s+/).map(Number); width = ((vbWidth || 0) * 25.4) / 96; height = ((vbHeight || 0) * 25.4) / 96; } else if (svgWidth && svgHeight) { const w = parseFloat(svgWidth); const h = parseFloat(svgHeight); width = svgWidth.includes('mm') ? w : svgWidth.includes('cm') ? w * 10 : svgWidth.includes('in') ? w * 25.4 : (w * 25.4) / 96; height = svgHeight.includes('mm') ? h : svgHeight.includes('cm') ? h * 10 : svgHeight.includes('in') ? h * 25.4 : (h * 25.4) / 96; } } if (options.scale) { width *= options.scale; height *= options.scale; } width = Math.max(10, Math.min(500, width)); height = Math.max(10, Math.min(500, height)); return { mmWidth: Math.round(width), mmHeight: Math.round(height) }; } }