diff --git a/src/plays/svg-optimizer/COVER_IMAGE_NOTE.md b/src/plays/svg-optimizer/COVER_IMAGE_NOTE.md new file mode 100644 index 000000000..bccad233f --- /dev/null +++ b/src/plays/svg-optimizer/COVER_IMAGE_NOTE.md @@ -0,0 +1,10 @@ +# Cover Image Placeholder + +Please add a `cover.png` file to this directory. + +Recommended specifications: +- Format: PNG +- Dimensions: Approximately 800x600 pixels (or similar aspect ratio) +- Content: A representative screenshot or icon for the SVG Optimizer play + +You can use a screenshot of the SVG Optimizer interface or create a custom graphic that represents the tool's functionality. diff --git a/src/plays/svg-optimizer/OptimizationPanel.jsx b/src/plays/svg-optimizer/OptimizationPanel.jsx new file mode 100644 index 000000000..97832b245 --- /dev/null +++ b/src/plays/svg-optimizer/OptimizationPanel.jsx @@ -0,0 +1,91 @@ +import React from 'react'; + +function OptimizationPanel({ options, onChange }) { + const handleToggle = (optionKey) => { + onChange({ + ...options, + [optionKey]: !options[optionKey] + }); + }; + + return ( +
+

Optimization Options

+
+ + + + + + + + + + + + + + + +
+
+ ); +} + +export default OptimizationPanel; diff --git a/src/plays/svg-optimizer/PreviewPanel.jsx b/src/plays/svg-optimizer/PreviewPanel.jsx new file mode 100644 index 000000000..ee3a4eab4 --- /dev/null +++ b/src/plays/svg-optimizer/PreviewPanel.jsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; + +function PreviewPanel({ originalSvg, optimizedSvg }) { + const [activePreview, setActivePreview] = useState('optimized'); + + const renderSVG = (svgString) => { + if (!svgString) { + return
No SVG to preview
; + } + + try { + return
; + } catch (error) { + return
Error rendering SVG preview
; + } + }; + + return ( +
+
+

Visual Preview

+
+ + + +
+
+ +
+ {activePreview === 'original' && ( +
{renderSVG(originalSvg)}
+ )} + + {activePreview === 'optimized' && ( +
{renderSVG(optimizedSvg)}
+ )} + + {activePreview === 'comparison' && ( +
+
+

Original

+ {renderSVG(originalSvg)} +
+
+

Optimized

+ {renderSVG(optimizedSvg)} +
+
+ )} +
+
+ ); +} + +export default PreviewPanel; diff --git a/src/plays/svg-optimizer/Readme.md b/src/plays/svg-optimizer/Readme.md new file mode 100644 index 000000000..a96203447 --- /dev/null +++ b/src/plays/svg-optimizer/Readme.md @@ -0,0 +1,78 @@ +# SVG Optimizer + +A powerful React-based SVG optimizer that allows users to paste or upload SVG code and optimize it by removing unnecessary elements while preserving visual output. This tool helps reduce file sizes significantly without compromising quality. + +## Play Demographic + +- Language: js +- Level: Intermediate + +## Creator Information + +- User: Abhrxdip +- Github Link: https://github.com/Abhrxdip +- Blog: +- Video: + +## Implementation Details + +This SVG Optimizer is built using React.js with the following features and concepts: + +### React Concepts Used: +- **Functional Components**: All components are functional components using modern React syntax +- **React Hooks**: + - `useState` for managing component state (SVG input, optimization options, file sizes) + - `useEffect` for automatically optimizing SVG when input or options change +- **Controlled Inputs**: Text areas and checkboxes are fully controlled components +- **Conditional Rendering**: Error messages, preview modes, and button states render conditionally +- **Component Composition**: Reusable components (OptimizationPanel, PreviewPanel) + +### Key Features: +1. **Multiple Input Methods**: + - Paste SVG code directly + - Upload SVG files + - Load sample SVG for testing + +2. **Optimization Options**: + - Remove comments + - Remove metadata (title, desc, metadata tags) + - Remove hidden elements + - Remove empty attributes + - Minify colors (hex shortening, named colors to hex) + - Remove default attribute values + - Optional XMLNS removal + - Code prettification + +3. **Real-time Processing**: + - Automatic optimization on input change + - Live file size calculation + - Percentage reduction display + +4. **Visual Preview**: + - Original SVG preview + - Optimized SVG preview + - Side-by-side comparison view + +5. **Export Options**: + - Copy to clipboard + - Download optimized SVG file + +### Technical Implementation: +- **Client-side Processing**: All optimization happens in the browser with no backend required +- **File API**: Uses FileReader for handling file uploads +- **Blob API**: Creates downloadable files without server interaction +- **Clipboard API**: Enables one-click copying of optimized code +- **Regular Expressions**: Pattern matching for removing unnecessary SVG elements + +## Considerations + +- This is a client-side optimizer and doesn't perform advanced path optimization or vector calculations +- Very complex SVG files with thousands of elements may require additional processing time +- Some optimization options might affect specific SVG features (test thoroughly before use) +- The tool preserves the main visual output but may remove accessibility features (like title/desc tags) if selected + +## Resources + +- [MDN SVG Documentation](https://developer.mozilla.org/en-US/docs/Web/SVG) +- [SVG Optimization Guidelines](https://www.w3.org/TR/SVG11/) +- [SVGO - SVG Optimizer Library](https://github.com/svg/svgo) (for reference) diff --git a/src/plays/svg-optimizer/SVGOptimizer.jsx b/src/plays/svg-optimizer/SVGOptimizer.jsx new file mode 100644 index 000000000..ac53ccd6e --- /dev/null +++ b/src/plays/svg-optimizer/SVGOptimizer.jsx @@ -0,0 +1,322 @@ +import PlayHeader from 'common/playlists/PlayHeader'; +import './styles.css'; +import { useState, useEffect } from 'react'; +import OptimizationPanel from './OptimizationPanel'; +import PreviewPanel from './PreviewPanel'; + +function SVGOptimizer(props) { + const [svgInput, setSvgInput] = useState(''); + const [optimizedSvg, setOptimizedSvg] = useState(''); + const [originalSize, setOriginalSize] = useState(0); + const [optimizedSize, setOptimizedSize] = useState(0); + const [error, setError] = useState(''); + const [optimizationOptions, setOptimizationOptions] = useState({ + removeComments: true, + removeMetadata: true, + removeHiddenElements: true, + removeEmptyAttributes: true, + minifyColors: true, + removeDefaultAttributes: true, + removeXMLNS: false, + prettify: false + }); + + // Sample SVG for demo + const sampleSVG = ` + + + + + image/svg+xml + + + + Sample SVG Icon + A colorful circle and rectangle + + + + + + +`; + + useEffect(() => { + if (svgInput) { + optimizeSVG(); + } else { + setOptimizedSvg(''); + setOriginalSize(0); + setOptimizedSize(0); + } + }, [svgInput, optimizationOptions]); + + const calculateSize = (str) => { + return new Blob([str]).size; + }; + + const getPercentageReduction = () => { + if (originalSize === 0) return 0; + return (((originalSize - optimizedSize) / originalSize) * 100).toFixed(2); + }; + + const optimizeSVG = () => { + try { + setError(''); + let svg = svgInput.trim(); + + if (!svg) { + setOptimizedSvg(''); + return; + } + + // Check if it's valid SVG + if (!svg.includes(' element'); + return; + } + + setOriginalSize(calculateSize(svg)); + + // Remove XML comments + if (optimizationOptions.removeComments) { + svg = svg.replace(//g, ''); + } + + // Remove metadata tags + if (optimizationOptions.removeMetadata) { + svg = svg.replace(//gi, ''); + svg = svg.replace(//gi, ''); + svg = svg.replace(//gi, ''); + svg = svg.replace(/\s*<\/defs>/gi, ''); + } + + // Remove hidden elements + if (optimizationOptions.removeHiddenElements) { + svg = svg.replace(/]*class=["']hidden["'][^>]*>[\s\S]*?<\/g>/gi, ''); + svg = svg.replace( + /<[^>]+(?:display\s*:\s*none|visibility\s*:\s*hidden)[^>]*>[\s\S]*?<\/[^>]+>/gi, + '' + ); + } + + // Remove empty attributes + if (optimizationOptions.removeEmptyAttributes) { + svg = svg.replace(/\s+[a-zA-Z-]+=""\s*/g, ' '); + } + + // Minify colors + if (optimizationOptions.minifyColors) { + // Convert named colors to hex + const colorMap = { + red: '#f00', + blue: '#00f', + green: '#0f0', + white: '#fff', + black: '#000' + }; + Object.keys(colorMap).forEach((colorName) => { + const regex = new RegExp(`(fill|stroke)="${colorName}"`, 'gi'); + svg = svg.replace(regex, `$1="${colorMap[colorName]}"`); + }); + + // Shorten hex colors where possible (#AABBCC -> #ABC) + svg = svg.replace(/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3/gi, '#$1$2$3'); + } + + // Remove default attribute values + if (optimizationOptions.removeDefaultAttributes) { + svg = svg.replace(/\s+opacity="1(\.0)?"/g, ''); + svg = svg.replace(/\s+fill-opacity="1(\.0)?"/g, ''); + svg = svg.replace(/\s+stroke-opacity="1(\.0)?"/g, ''); + } + + // Remove xmlns if specified + if (optimizationOptions.removeXMLNS) { + svg = svg.replace(/\s+xmlns(:[a-zA-Z]+)?="[^"]*"/g, ''); + } + + // Clean up extra whitespace + if (!optimizationOptions.prettify) { + svg = svg.replace(/>\s+<'); + svg = svg.replace(/\s{2,}/g, ' '); + svg = svg.trim(); + } else { + // Simple prettify + svg = svg.replace(/>\n<'); + const lines = svg.split('\n'); + let indentLevel = 0; + svg = lines + .map((line) => { + line = line.trim(); + if (line.startsWith('')) { + if (!line.match(/<[^>]+>.*<\/[^>]+>/)) { + indentLevel++; + } + } + return indented; + }) + .join('\n'); + } + + setOptimizedSvg(svg); + setOptimizedSize(calculateSize(svg)); + } catch (err) { + setError(`Error optimizing SVG: ${err.message}`); + } + }; + + const handleFileUpload = (e) => { + const file = e.target.files[0]; + if (file) { + if (file.type !== 'image/svg+xml' && !file.name.endsWith('.svg')) { + setError('Please upload a valid SVG file'); + return; + } + const reader = new FileReader(); + reader.onload = (event) => { + setSvgInput(event.target.result); + }; + reader.onerror = () => { + setError('Error reading file'); + }; + reader.readAsText(file); + } + }; + + const handleDownload = () => { + if (!optimizedSvg) return; + const blob = new Blob([optimizedSvg], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'optimized.svg'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const handleCopy = async () => { + if (!optimizedSvg) return; + try { + await navigator.clipboard.writeText(optimizedSvg); + alert('Copied to clipboard!'); + } catch (err) { + setError('Failed to copy to clipboard'); + } + }; + + const loadSample = () => { + setSvgInput(sampleSVG); + }; + + const clearAll = () => { + setSvgInput(''); + setOptimizedSvg(''); + setError(''); + }; + + return ( + <> +
+ +
+
+
+

SVG Optimizer

+

Paste or upload your SVG code to optimize and reduce file size

+
+ + {error &&
{error}
} + +
+ + + +
+ + + +
+
+
+

Input SVG

+ {originalSize} bytes +
+