/* SVG Code Converter: upload SVG, view/clean code, copy and export. Everything is done client-side. */ (() => { const els = { dropzone: document.getElementById('dropzone'), fileInput: document.getElementById('fileInput'), uploadError: document.getElementById('uploadError'), preview: document.getElementById('preview'), svgMeta: document.getElementById('svgMeta'), codeOutput: document.getElementById('codeOutput'), copyBtn: document.getElementById('copyBtn'), minifyToggle: document.getElementById('minifyToggle'), stripComments: document.getElementById('stripComments'), stripMetadata: document.getElementById('stripMetadata'), prettyPrint: document.getElementById('prettyPrint'), keepViewBox: document.getElementById('keepViewBox'), downloadSvgBtn: document.getElementById('downloadSvgBtn'), downloadPngBtn: document.getElementById('downloadPngBtn'), themeToggle: document.getElementById('themeToggle'), themeIcon: document.getElementById('themeIcon'), codeInfo: document.getElementById('codeInfo'), }; const state = { rawSvg: '', cleanSvg: '', fileName: 'image.svg', svgEl: null, dark: null, }; // Theme handling const initTheme = () => { const stored = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; state.dark = stored ? stored === 'dark' : prefersDark; document.documentElement.classList.toggle('dark', state.dark); setThemeIcon(); }; const setThemeIcon = () => { els.themeIcon.innerHTML = state.dark ? '' : ''; }; els.themeToggle.addEventListener('click', () => { state.dark = !state.dark; document.documentElement.classList.toggle('dark', state.dark); localStorage.setItem('theme', state.dark ? 'dark' : 'light'); setThemeIcon(); }); // Utilities const showError = (msg) => { els.uploadError.textContent = msg; els.uploadError.classList.remove('hidden'); setTimeout(() => els.uploadError.classList.add('hidden'), 5000); }; const fmtBytes = (n) => { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / (1024 * 1024)).toFixed(2)} MB`; }; const decodeSvgText = (text) => { // Remove BOM and decode numeric entities robustly return text.replace(/^\uFEFF/, '').replace(/ /g, ' '); }; const stripComments = (str) => str.replace(//g, ''); const stripSvgMetadata = (str) => { // Remove sodipodi/inkscape metadata, editors' data, and XML processing instructions return str .replace(/<\?xml[\s\S]*?\?>/g, '') .replace(//gi, '') .replace(/\s+(sodipodi:|inkscape:|xml:space)="[^"]*"/gi, '') .replace(/\s+(sodipodi:|inkscape:)[^=\s>]+/gi, '') .replace(/\s+data-name="[^"]*"/gi, '') .replace(/>\s+<') .trim(); }; const ensureViewBox = (svgString) => { // If viewBox is present, keep it; otherwise try to infer from width/height const hasViewBox = /viewBox\s*=/.test(svgString); if (hasViewBox) return svgString; const widthMatch = svgString.match(/\bwidth\s*=\s*["']?([0-9.]+)(px)?["']?/i); const heightMatch = svgString.match(/\bheight\s*=\s*["']?([0-9.]+)(px)?["']?/i); if (widthMatch && heightMatch) { const w = parseFloat(widthMatch[1]); const h = parseFloat(heightMatch[1]); if (isFinite(w) && isFinite(h) && w > 0 && h > 0) { // Insert or replace viewBox right after opening return svgString.replace( /]*?)>/i, `` ); } } return svgString; }; const cleanSvg = (raw, opts) => { let s = raw; // Normalize newlines and decode s = decodeSvgText(s.replace(/\r\n?/g, '\n')); if (opts.stripComments) s = stripComments(s); if (opts.stripMetadata) s = stripSvgMetadata(s); if (opts.minify) { // Minify: remove unnecessary whitespace between tags, collapse spaces between attributes s = s .replace(/>\s+<') .replace(/\s{2,}/g, ' ') .replace(/\s*=\s*/g, '=') .replace(/\s*([<>"'])\s*/g, '$1') .trim(); } else if (opts.pretty) { s = prettyXml(s); } if (opts.keepViewBox) { s = ensureViewBox(s); } return s; }; const prettyXml = (xml) => { // Simple pretty printer for XML/SVG try { const PADDING = ' '; const reg = /(>)(<)(\/*)/g; let xmlStr = xml.replace(reg, '$1\r\n$2$3'); let pad = 0; return xmlStr.split('\r\n').map((line) => { if (line.match(/.+<\/\w[^>]*>$/)) { // Single line return (PADDING.repeat(pad)) + line; } if (line.match(/^<\/\w/)) { // Closing tag pad = Math.max(pad - 1, 0); return (PADDING.repeat(pad)) + line; } if (line.match(/^<\w[^>]*[^\/]>.*$/)) { // Opening tag const out = (PADDING.repeat(pad)) + line; pad += 1; return out; } return (PADDING.repeat(pad)) + line; }).join('\n'); } catch { return xml; } }; const updateCodeInfo = () => { const text = els.codeOutput.value; const lines = (text.match(/\n/g) || []).length + 1; const chars = text.length.toLocaleString(); els.codeInfo.textContent = `${lines} lines • ${chars} chars`; }; const renderPreview = (svgString) => { els.preview.innerHTML = ''; if (!svgString) { const div = document.createElement('div'); div.className = 'text-center text-gray-500'; div.innerHTML = 'SVG preview will appear here'; els.preview.appendChild(div); return; } const wrapper = document.createElement('div'); wrapper.className = 'w-full h-full flex items-center justify-center overflow-auto'; // Parse and insert safely const parser = new DOMParser(); const doc = parser.parseFromString(svgString, 'image/svg+xml'); const parseError = doc.querySelector('parsererror'); if (parseError) { const err = document.createElement('div'); err.className = 'text-red-600 text-sm'; err.textContent = 'Invalid SVG: ' + parseError.textContent; els.preview.appendChild(err); return; } const svg = doc.documentElement; svg.classList.remove('hidden'); svg.removeAttribute('width'); svg.removeAttribute('height'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); svg.style.maxWidth = '100%'; svg.style.maxHeight = '100%'; wrapper.appendChild(svg); els.preview.appendChild(wrapper); state.svgEl = svg; }; const updateAll = () => { const opts = { minify: els.minifyToggle.checked, stripComments: els.stripComments.checked, stripMetadata: els.stripMetadata.checked, pretty: els.prettyPrint.checked && !els.minifyToggle.checked, keepViewBox: els.keepViewBox.checked, }; const cleaned = cleanSvg(state.rawSvg, opts); state.cleanSvg = cleaned; els.codeOutput.value = cleaned; updateCodeInfo(); renderPreview(cleaned); els.downloadSvgBtn.disabled = !cleaned; els.downloadPngBtn.disabled = !cleaned; }; const handleFile = (file) => { if (!file) return; if (!/\.svg$/i.test(file.name) && file.type !== 'image/svg+xml') { showError('Please select a valid SVG file.'); return; } const maxSize = 5 * 1024 * 1024; if (file.size > maxSize) { showError('File is too large. Max 5 MB.'); return; } state.fileName = file.name.endsWith('.svg') ? file.name : (file.name.replace(/\.[^.]+$/, '') + '.svg'); const reader = new FileReader(); reader.onload = () => { state.rawSvg = String(reader.result || ''); updateAll(); els.svgMeta.textContent = `${file.name} • ${fmtBytes(file.size)}`; }; reader.onerror = () => showError('Failed to read the file.'); reader.readAsText(file); }; // Drag & drop const preventDefaults = (e) => { e.preventDefault(); e.stopPropagation(); }; ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((evt) => { els.dropzone.addEventListener(evt, preventDefaults, false); }); els.dropzone.addEventListener('dragover', () => { els.dropzone.classList.add('border-primary-400'); }); els.dropzone.addEventListener('dragleave', () => { els.dropzone.classList.remove('border-primary-400'); }); els.dropzone.addEventListener('drop', (e) => { els.dropzone.classList.remove('border-primary-400'); const file = e.dataTransfer.files?.[0]; handleFile(file); }); els.dropzone.addEventListener('click', () => els.fileInput.click()); els.fileInput.addEventListener('change', () => handleFile(els.fileInput.files?.[0])); // Controls els.copyBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(els.codeOutput.value); const original = els.copyBtn.textContent; els.copyBtn.textContent = 'Copied!'; setTimeout(() => (els.copyBtn.textContent = original), 1200); } catch { showError('Clipboard write failed. Select the code and copy manually.'); } }); [els.minifyToggle, els.stripComments, els.stripMetadata, els.prettyPrint, els.keepViewBox].forEach((el) => { el.addEventListener('change', updateAll); }); els.downloadSvgBtn.addEventListener('click', () => { if (!state.cleanSvg) return; const blob = new Blob([state.cleanSvg], { type: 'image/svg+xml;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = state.fileName || 'image.svg'; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); }); els.downloadPngBtn.addEventListener('click', async () => { if (!state.cleanSvg) return; try { const { image, width, height } = await rasterizeSvg(state.cleanSvg, 2); // 2x scale const a = document.createElement('a'); a.href = image.src; a.download = (state.fileName || 'image').replace(/\.svg$/i, '') + '.png'; document.body.appendChild(a); a.click(); a.remove(); } catch (err) { console.error(err); showError('Failed to export PNG.'); } }); const rasterizeSvg = (svgString, scale = 1) => { return new Promise((resolve, reject) => { const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }); const url = URL.createObjectURL(svgBlob); const img = new Image(); img.onload = () => { // Determine final dimensions const width = Math.max(1, Math.ceil(img.naturalWidth * scale) || 1024); const height = Math.max(1, Math.ceil(img.naturalHeight * scale) || 1024); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); // Optional: white background for non-opaque exports ctx.fillStyle = 'white'; ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); canvas.toBlob((blob) => { if (!blob) { URL.revokeObjectURL(url); reject(new Error('Canvas toBlob failed')); return; } const image = new Image(); image.onload = () => { URL.revokeObjectURL(url); resolve({ image, width, height }); }; image.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Image load failed')); }; image.src = URL.createObjectURL(blob); }, 'image/png'); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Invalid SVG')); }; img.src = url; }); }; // Initialize initTheme(); updateAll(); })();
SVG preview will appear here