vectorizer / index.html
pg0's picture
potrace url changed
79a3e5d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Image to SVG Vectorizer</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://kilobtye.github.io/potrace/potrace.js"></script>
<style>
.dropzone {
border: 2px dashed #cbd5e0;
transition: all 0.3s ease;
}
.dropzone.active {
border-color: #4f46e5;
background-color: #eef2ff;
}
.dropzone.error {
border-color: #ef4444;
background-color: #fee2e2;
}
.progress-bar {
transition: width 0.3s ease;
}
#svgPreview {
max-height: 400px;
overflow: auto;
background-image: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
linear-gradient(-45deg, #f0f0f0 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f0f0f0 75%),
linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.tooltip {
position: relative;
}
.tooltip-text {
visibility: hidden;
width: 200px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.file-info {
display: none;
}
.settings-panel {
transition: all 0.3s ease;
}
.settings-panel.collapsed {
max-height: 0;
overflow: hidden;
opacity: 0;
}
.settings-panel.expanded {
max-height: 500px;
opacity: 1;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="text-center mb-12">
<h1 class="text-4xl font-bold text-indigo-600 mb-2">
<i class="fas fa-vector-square mr-2"></i>Advanced Image to SVG Vectorizer
</h1>
<p class="text-gray-600 max-w-2xl mx-auto">
Convert raster images to high-quality scalable vector graphics with advanced settings
</p>
</header>
<div class="bg-white rounded-xl shadow-lg overflow-hidden max-w-4xl mx-auto">
<div class="md:flex">
<!-- Left Panel - Upload and Settings -->
<div class="p-6 md:w-1/2 border-r border-gray-200">
<div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-6">
<div id="uploadUI" class="flex flex-col items-center justify-center">
<i class="fas fa-cloud-upload-alt text-4xl text-indigo-500 mb-3"></i>
<h3 class="text-lg font-medium text-gray-700 mb-1">Drag & Drop your image here</h3>
<p class="text-gray-500 text-sm mb-3">or click to browse files</p>
<input type="file" id="fileInput" class="hidden" accept="image/*">
<button id="browseBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm font-medium transition">
Select Image
</button>
</div>
<div id="fileInfo" class="file-info flex items-center justify-center">
<div class="bg-gray-100 rounded-lg p-3 flex items-center">
<i class="fas fa-file-image text-indigo-500 text-xl mr-3"></i>
<div>
<p id="fileName" class="text-sm font-medium text-gray-7truncate max-w-xs"></p>
<p id="fileSize" class="text-xs text-gray-500"></p>
</div>
</div>
</div>
</div>
<div id="previewContainer" class="hidden mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-3">Image Preview</h3>
<div class="relative">
<img id="imagePreview" class="max-h-48 w-auto mx-auto rounded-md shadow-sm border border-gray-200">
<button id="removeImage" class="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white rounded-full p-1">
<i class="fas fa-times text-xs"></i>
</button>
</div>
</div>
<div class="space-y-4">
<div class="flex justify-between items-center cursor-pointer" id="settingsToggle">
<h3 class="text-lg font-medium text-gray-700">Vectorization Settings</h3>
<i class="fas fa-chevron-down text-gray-500 transition-transform duration-300" id="toggleIcon"></i>
</div>
<div id="settingsPanel" class="settings-panel expanded space-y-4">
<div>
<label for="vectorMode" class="block text-sm font-medium text-gray-700 mb-1">Vectorization Mode</label>
<select id="vectorMode" class="w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 text-sm">
<option value="posterize">Posterize (color blocks)</option>
<option value="grayscale">Grayscale</option>
<option value="blackwhite" selected>Black & White</option>
</select>
</div>
<div id="colorSettings">
<label for="colorCount" class="block text-sm font-medium text-gray-700 mb-1">Color Count</label>
<input type="range" id="colorCount" min="2" max="16" value="4" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>2</span>
<span>4</span>
<span>8</span>
<span>16</span>
</div>
</div>
<div>
<label for="threshold" class="block text-sm font-medium text-gray-700 mb-1">Threshold</label>
<input type="range" id="threshold" min="0" max="100" value="50" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>Low</span>
<span>Medium</span>
<span>High</span>
</div>
</div>
<div>
<label for="smoothness" class="block text-sm font-medium text-gray-700 mb-1">Smoothness</label>
<input type="range" id="smoothness" min="0" max="10" value="4" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>Sharp</span>
<span>Balanced</span>
<span>Smooth</span>
</div>
</div>
<div>
<label for="detailLevel" class="block text-sm font-medium text-gray-700 mb-1">Detail Level</label>
<input type="range" id="detailLevel" min="1" max="10" value="5" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<div class="flex justify-between text-xs text-gray-500 mt-1">
<span>Low</span>
<span>Medium</span>
<span>High</span>
</div>
</div>
<div class="flex items-center">
<input type="checkbox" id="transparentBg" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="transparentBg" class="ml-2 block text-sm text-gray-7">Transparent background</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="optimizePaths" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded" checked>
<label for="optimizePaths" class="ml-2 block text-sm text-gray-7">Optimize paths</label>
</div>
</div>
<button id="convertBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-md font-medium transition disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fas fa-magic mr-2"></i> Convert to SVG
</button>
</div>
</div>
<!-- Right Panel - Results -->
<div class="p-6 md:w-1/2">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-800">SVG Output</h2>
<div class="flex space-x-2">
<button id="downloadBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm font-medium hidden">
<i class="fas fa-download mr-1"></i> Download
</button>
<button id="copyBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-sm font-medium hidden">
<i class="fas fa-copy mr-1"></i> Copy
</button>
</div>
</div>
<div id="processingUI" class="hidden text-center py-12">
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div>
<h3 class="text-lg font-medium text-gray-700">Processing Image</h3>
<p class="text-gray-500 text-sm mt-1">Vectorizing your image...</p>
<div class="w-full bg-gray-200 rounded-full h-2.5 mt-4">
<div id="progressBar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
<div id="resultContainer" class="hidden">
<div class="bg-gray-100 rounded-lg p-4 mb-4">
<div id="svgPreview" class="mx-auto"></div>
</div>
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-700">SVG Statistics</h4>
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<div class="bg-gray-100 p-2 rounded">
<div class="text-gray-500">File Size</div>
<div id="svgSize" class="font-medium">-</div>
</div>
<div class="bg-gray-100 p-2 rounded">
<div class="text-gray-500">Path Count</div>
<div id="pathCount" class="font-medium">-</div>
</div>
<div class="bg-gray-100 p-2 rounded">
<div class="text-gray-500">Colors</div>
<div id="colorCountDisplay" class="font-medium">-</div>
</div>
</div>
</div>
<div>
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-700">SVG Code</h4>
<button id="copyCodeBtn" class="text-indigo-600 hover:text-indigo-800 text-xs font-medium">
<i class="fas fa-copy mr-1"></i> Copy Code
</button>
</div>
<pre id="svgCode" class="bg-gray-800 text-gray-100 text-xs p-3 rounded-md overflow-auto max-h-40"></pre>
</div>
</div>
<div id="emptyState" class="text-center py-12">
<div class="mx-auto w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mb-4">
<i class="fas fa-image text-indigo-500 text-xl"></i>
</div>
<h3 class="text-lg font-medium text-gray-700">No image uploaded</h3>
<p class="text-gray-500 text-sm mt-1">Upload an image to convert it to SVG format</p>
</div>
</div>
</div>
</div>
<footer class="text-center mt-12 text-gray-500 text-sm">
<p>Advanced Image to SVG Vectorizer &copy; 2023 | Powered by Potrace algorithm</p>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// DOM elements
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const uploadUI = document.getElementById('uploadUI');
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const previewContainer = document.getElementById('previewContainer');
const imagePreview = document.getElementById('imagePreview');
const removeImage = document.getElementById('removeImage');
const convertBtn = document.getElementById('convertBtn');
const processingUI = document.getElementById('processingUI');
const progressBar = document.getElementById('progressBar');
const resultContainer = document.getElementById('resultContainer');
const emptyState = document.getElementById('emptyState');
const svgPreview = document.getElementById('svgPreview');
const svgCode = document.getElementById('svgCode');
const downloadBtn = document.getElementById('downloadBtn');
const copyBtn = document.getElementById('copyBtn');
const copyCodeBtn = document.getElementById('copyCodeBtn');
const settingsToggle = document.getElementById('settingsToggle');
const settingsPanel = document.getElementById('settingsPanel');
const toggleIcon = document.getElementById('toggleIcon');
const vectorMode = document.getElementById('vectorMode');
const colorCount = document.getElementById('colorCount');
const threshold = document.getElementById('threshold');
const smoothness = document.getElementById('smoothness');
const detailLevel = document.getElementById('detailLevel');
const transparentBg = document.getElementById('transparentBg');
const optimizePaths = document.getElementById('optimizePaths');
const colorSettings = document.getElementById('colorSettings');
const svgSize = document.getElementById('svgSize');
const pathCount = document.getElementById('pathCount');
const colorCountDisplay = document.getElementById('colorCountDisplay');
// Current file data
let currentFile = null;
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
let progressInterval;
// Toggle settings panel
settingsToggle.addEventListener('click', () => {
settingsPanel.classList.toggle('expanded');
settingsPanel.classList.toggle('collapsed');
toggleIcon.classList.toggle('rotate-180');
});
// Toggle color settings based on vector mode
vectorMode.addEventListener('change', () => {
if (vectorMode.value === 'posterize') {
colorSettings.style.display = 'block';
} else {
colorSettings.style.display = 'none';
}
});
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight dropzone when item is dragged over it
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropzone.classList.add('active');
}
function unhighlight() {
dropzone.classList.remove('active');
}
// Handle dropped files
dropzone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
// Handle file selection via button
browseBtn.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) {
handleFiles(fileInput.files);
}
});
// Remove image
removeImage.addEventListener('click', resetUpload);
// Convert button
convertBtn.addEventListener('click', processImage);
// Copy buttons
copyBtn.addEventListener('click', copySVG);
copyCodeBtn.addEventListener('click', copySVGCode);
// Download button
downloadBtn.addEventListener('click', downloadSVG);
// Handle selected files
function handleFiles(files) {
const file = files[0];
currentFile = file;
// Check if file is an image
if (!file.type.match('image.*')) {
showError('Please upload an image file (JPG, PNG, etc.)');
return;
}
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
showError('File size exceeds 10MB limit');
return;
}
// Display file info
uploadUI.style.display = 'none';
fileInfo.style.display = 'flex';
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
// Display image preview
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.src = e.target.result;
previewContainer.classList.remove('hidden');
convertBtn.disabled = false;
// Load image to canvas for processing
const img = new Image();
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
// Format file size
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' bytes';
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
else return (bytes / 1048576).toFixed(1) + ' MB';
}
// Show error message
function showError(message) {
dropzone.classList.add('error');
const errorElement = document.createElement('div');
errorElement.className = 'text-red-500 text-sm mt-2';
errorElement.textContent = message;
dropzone.appendChild(errorElement);
setTimeout(() => {
dropzone.classList.remove('error');
dropzone.removeChild(errorElement);
}, 3000);
}
// Reset upload
function resetUpload() {
fileInput.value = '';
currentFile = null;
uploadUI.style.display = 'flex';
fileInfo.style.display = 'none';
previewContainer.classList.add('hidden');
convertBtn.disabled = true;
emptyState.classList.remove('hidden');
resultContainer.classList.add('hidden');
// Clear any existing progress interval
if (progressInterval) {
clearInterval(progressInterval);
}
}
// Process image with vectorization
function processImage() {
// Show processing UI
processingUI.classList.remove('hidden');
emptyState.classList.add('hidden');
resultContainer.classList.add('hidden');
// Reset progress bar
progressBar.style.width = '0%';
// Simulate progress with more controlled increments
let progress = 0;
progressInterval = setInterval(() => {
// Calculate progress increment based on remaining distance to 100%
const remaining = 100 - progress;
const increment = Math.min(
Math.max(1, Math.floor(remaining * 0.15)), // Minimum 1%, maximum 15% of remaining
10 // Absolute maximum increment per step
);
progress += increment;
// Ensure we don't go over 100%
if (progress >= 100) {
progress = 100;
clearInterval(progressInterval);
}
progressBar.style.width = progress + '%';
}, 200);
// Process image after a short delay to allow UI to update
setTimeout(() => {
// Get settings from UI
const settings = {
mode: vectorMode.value,
colorCount: parseInt(colorCount.value),
threshold: parseInt(threshold.value) / 100,
smoothness: parseInt(smoothness.value),
detail: parseInt(detailLevel.value),
transparent: transparentBg.checked,
optimize: optimizePaths.checked
};
// Process the image based on selected mode
let processedImageData;
if (settings.mode === 'posterize') {
processedImageData = posterizeImage(canvas, settings.colorCount);
} else if (settings.mode === 'grayscale') {
processedImageData = convertToGrayscale(canvas);
} else { // blackwhite
processedImageData = applyThreshold(canvas, settings.threshold);
}
// Create temporary canvas with processed image
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.putImageData(processedImageData, 0, 0);
// Use Potrace to vectorize the image
const imageDataURL = tempCanvas.toDataURL('image/png');
// Vectorize using Potrace
potrace.loadImageFromUrl(imageDataURL);
potrace.setParameters({
turdsize: Math.max(1, 10 - settings.detail), // Fewer details with higher values
optcurve: settings.optimize,
alphamax: settings.smoothness * 1.4, // Controls corner threshold
opttolerance: settings.smoothness * 0.5 // Optimization tolerance
});
potrace.process(() => {
// Clear the progress interval if it's still running
if (progressInterval) {
clearInterval(progressInterval);
}
// Ensure progress bar shows 100%
progressBar.style.width = '100%';
// Get SVG data
const svg = potrace.getSVG(1, settings.transparent ? 'none' : '#ffffff');
// Show result after a small delay to allow progress bar to complete
setTimeout(() => {
showResult(svg);
}, 100);
});
}, 500);
}
// Image processing functions
function posterizeImage(canvas, levels) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Calculate the step for each channel
const step = 255 / (levels - 1);
for (let i = 0; i < data.length; i += 4) {
// Posterize each channel
data[i] = Math.round(data[i] / step) * step; // R
data[i + 1] = Math.round(data[i + 1] / step) * step; // G
data[i + 2] = Math.round(data[i + 2] / step) * step; // B
}
return imageData;
}
function convertToGrayscale(canvas) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// Convert to grayscale using luminance
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
data[i] = data[i + 1] = data[i + 2] = gray;
}
return imageData;
}
function applyThreshold(canvas, thresholdValue) {
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// Convert to grayscale first
const gray = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
// Apply threshold
const value = gray > (thresholdValue * 255) ? 255 : 0;
data[i] = data[i + 1] = data[i + 2] = value;
}
return imageData;
}
// Show result
function showResult(svgData) {
processingUI.classList.add('hidden');
resultContainer.classList.remove('hidden');
emptyState.classList.add('hidden');
downloadBtn.classList.remove('hidden');
copyBtn.classList.remove('hidden');
// Display SVG
svgPreview.innerHTML = svgData;
svgCode.textContent = svgData;
// Calculate and display statistics
const svgSizeBytes = new Blob([svgData]).size;
svgSize.textContent = formatFileSize(svgSizeBytes);
// Count paths in SVG
const pathMatches = svgData.match(/<path/g);
const pathCountValue = pathMatches ? pathMatches.length : 0;
pathCount.textContent = pathCountValue;
// Count colors in SVG
const colorMatches = svgData.match(/fill="([^"]+)"/g);
let uniqueColors = new Set();
if (colorMatches) {
colorMatches.forEach(match => {
const color = match.match(/fill="([^"]+)"/)[1];
uniqueColors.add(color.toLowerCase());
});
}
colorCountDisplay.textContent = uniqueColors.size;
}
// Copy SVG to clipboard
function copySVG() {
const svg = svgPreview.innerHTML;
navigator.clipboard.writeText(svg)
.then(() => {
showTooltip(copyBtn, 'SVG copied to clipboard!');
})
.catch(err => {
console.error('Failed to copy SVG: ', err);
});
}
// Copy SVG code to clipboard
function copySVGCode() {
const code = svgCode.textContent;
navigator.clipboard.writeText(code)
.then(() => {
showTooltip(copyCodeBtn, 'Code copied to clipboard!');
})
.catch(err => {
console.error('Failed to copy code: ', err);
});
}
// Download SVG
function downloadSVG() {
const svg = svgPreview.innerHTML;
const blob = new Blob([svg], {type: 'image/svg+xml'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = (fileName.textContent.replace(/\.[^/.]+$/, "") || 'vectorized') + '.svg';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// Show tooltip
function showTooltip(element, message) {
const tooltip = document.createElement('div');
tooltip.className = 'absolute bg-gray-800 text-white text-xs px-2 py-1 rounded -mt-8';
tooltip.textContent = message;
element.appendChild(tooltip);
setTimeout(() => {
element.removeChild(tooltip);
}, 2000);
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=pg0/vectorizer" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>