icon-vectorizer / index.html
SkHugger's picture
Upload folder using huggingface_hub
ebae3da verified
Raw History Blame Contribute Delete
9.59 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Icon Vectorizer — Accurate Centerline</title>
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
<style>
* { box-sizing: border-box; }
body { font-family: "Segoe UI", system-ui, sans-serif; background:#111417; color:#e8e8e8; margin:0; padding:20px; }
h1 { font-size:22px; margin:0 0 4px; }
.sub { color:#9aa; margin-bottom:16px; font-size:13px; }
.wrap { display:flex; gap:16px; flex-wrap:wrap; }
.panel { background:#1b2026; border:1px solid #2c343d; border-radius:10px; padding:16px; flex:1 1 340px; min-width:300px; }
label { display:block; font-size:13px; margin:10px 0 4px; color:#bcd; }
input[type=file] { width:100%; }
input[type=range] { width:100%; }
select, input[type=number] { background:#0f1317; color:#eee; border:1px solid #2c343d; border-radius:6px; padding:6px 8px; }
button { background:#1f7a3a; color:#fff; border:0; border-radius:8px; padding:12px 18px; font-size:15px; cursor:pointer; width:100%; margin-top:14px; }
button:disabled { background:#3a424b; cursor:wait; }
button.secondary { background:#2c343d; width:auto; margin-top:8px; margin-right:8px; padding:8px 14px; font-size:13px; }
#status { font-size:13px; color:#fc6; min-height:20px; margin-top:10px; white-space:pre-wrap; }
#preview { background:repeating-conic-gradient(#ededed 0% 25%, #fff 0% 50%) 0 0/24px 24px; border-radius:8px; padding:12px; text-align:center; min-height:200px; }
#preview svg { max-width:100%; height:auto; }
table { width:100%; border-collapse:collapse; font-size:12px; margin-top:8px; }
th, td { text-align:left; padding:4px 6px; border-bottom:1px solid #2c343d; }
th { color:#9aa; }
#stats { font-size:13px; }
.row { display:flex; gap:8px; align-items:center; }
.row output { min-width:44px; text-align:right; }
</style>
</head>
<body>
<h1>ICON VECTORIZER <span style="color:#9aa;font-weight:normal">— accurate centerline</span></h1>
<div class="sub">Black-outline icon (JPG/PNG) → clean centerline SVG. 100% browser me chalta hai — slow but accurate (~20–60s).</div>
<div class="wrap">
<div class="panel">
<label>1. Icon image (JPG/PNG)</label>
<input type="file" id="file" accept=".png,.jpg,.jpeg,.bmp,.webp">
<label>2. Threshold <span style="color:#777">(default 110)</span></label>
<div class="row"><input type="range" id="threshold" min="0" max="255" value="110"><output id="threshold_v">110</output></div>
<label>Quality <span style="color:#777">(default 3x)</span></label>
<select id="quality"><option>1x</option><option>2x</option><option selected>3x</option><option>4x</option></select>
<label>Smoothing <span style="color:#777">(default Low)</span></label>
<select id="smoothing"><option selected>Low</option><option>Medium</option><option>High</option></select>
<label>Tolerance px <span style="color:#777">(default 0.6)</span></label>
<input type="number" id="tolerance" value="0.6" step="0.1" min="0.1" max="5">
<label><input type="checkbox" id="reduce"> Reduce Anchors</label>
<button id="convert" disabled>Engine load ho raha hai…</button>
<div id="status">Pyodide + packages load ho rahe hain (pehli dafa 1–2 min lag sakte hain)…</div>
</div>
<div class="panel">
<label style="margin-top:0">SVG preview</label>
<div id="preview"><span style="color:#777">yahan result nazar aayega</span></div>
<div id="stats"></div>
<div>
<button class="secondary" id="download" disabled>Download SVG</button>
<button class="secondary" id="copy" disabled>Copy SVG</button>
</div>
<table id="layers" style="display:none">
<thead><tr><th>name</th><th>kind</th><th>anchors</th><th>max dev</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<script>
let pyodide = null;
let lastSvg = "";
const $ = id => document.getElementById(id);
$("threshold").addEventListener("input", e => { $("threshold_v").textContent = e.target.value; });
function setStatus(t){ $("status").textContent = t; }
// File ko select hote hi memory me parh lo — taake dobara convert par
// browser ki file-reference expire na ho ("could not be read" error fix).
let fileBytes = null, fileName = "icon";
$("file").addEventListener("change", async (e) => {
const f = e.target.files[0];
fileBytes = null;
if(!f) return;
try {
fileBytes = new Uint8Array(await f.arrayBuffer());
fileName = (f.name || "icon").replace(/\.[^.]+$/, "");
setStatus("File ready: " + f.name + " — Convert dabao.");
} catch(err){
setStatus("File parh nahi saki, dobara select karo.");
}
});
async function init(){
try {
setStatus("Pyodide load ho raha hai…");
pyodide = await loadPyodide();
setStatus("Packages load ho rahe hain: numpy, pillow, opencv, scipy, scikit-image… (thoda waqt lagega)");
await pyodide.loadPackage(["numpy", "pillow", "opencv-python", "scipy", "scikit-image"]);
setStatus("Tracer engine load ho raha hai…");
const core = await (await fetch("vectorizer_core.py")).text();
if(!core || core.length < 1000) throw new Error("vectorizer_core.py download nahi ho saki");
pyodide.FS.writeFile("/vectorizer_core.py", core);
if(!pyodide.FS.analyzePath("/vectorizer_core.py").exists) throw new Error("file write failed");
await pyodide.runPythonAsync("import sys; sys.path.insert(0, '/')\nimport vectorizer_core as vc");
setStatus("Tayyar! Image select karo aur Convert dabao.");
$("convert").disabled = false;
$("convert").textContent = "Convert to SVG";
} catch(err){
setStatus("Load error:\n" + err.message);
}
}
const PY_RUN = [
"import gc; gc.collect()",
"from PIL import Image as _PIL",
"_im = _PIL.open('/input.png'); _w, _h = _im.size",
"_Q = int(Q); _cap = 2048",
"NOTE = ''",
"if max(_w, _h) * _Q > _cap:",
" _s = _cap / max(_w, _h)",
" _im = _im.resize((max(1, int(_w * _s)), max(1, int(_h * _s))), _PIL.LANCZOS)",
" _im.save('/input.png')",
" NOTE = ' (note: bari image auto-resize hui taake phone ki memory me aaye)'",
"import vectorizer_core as vc",
"cfg = vc.VectorizerConfig(",
" threshold=int(TH), upscale=_Q, smoothing=SM, tolerance=float(TOL),",
" reduce_anchors=bool(RA), use_clip=False, use_sam=False)",
"out, res = vc.convert_file('/input.png', '/output.svg', cfg)",
"els = [{'name': e['name'], 'kind': e['kind'],",
" 'anchors': int(e['anchors']), 'max_dev': float(e['max_dev'])}",
" for e in res['elements']]",
"st = res['stats']",
"stats = {'paths': int(st['paths']), 'anchors': int(st['anchors']),",
" 'stroke_w': float(st['stroke_w']), 'max_dev': float(st['max_dev']),",
" 'mean_dev': float(st['mean_dev']), 'elapsed': float(st['elapsed'])}",
"RESULT = {'svg': res['svg'], 'stats': stats, 'elements': els, 'note': NOTE}",
"del _im; gc.collect()",
].join("\n");
async function convert(){
if(!fileBytes){ setStatus("Pehle image select karo (dobara select karo agar error aaye)."); return; }
if($("convert").disabled) return;
$("convert").disabled = true;
$("convert").textContent = "Converting… (20–60s)";
setStatus("Convert ho raha hai… slow but accurate, intezar karo.");
try {
pyodide.FS.writeFile("/input.png", fileBytes);
pyodide.globals.set("TH", $("threshold").value);
pyodide.globals.set("Q", $("quality").value.replace("x",""));
pyodide.globals.set("SM", $("smoothing").value);
pyodide.globals.set("TOL", $("tolerance").value);
pyodide.globals.set("RA", $("reduce").checked ? 1 : 0);
await pyodide.runPythonAsync(PY_RUN);
const r = pyodide.globals.get("RESULT").toJs({dict_converter: Object.fromEntries});
lastSvg = r.svg;
$("preview").innerHTML = lastSvg;
const st = r.stats;
$("stats").innerHTML = "<p><b>" + st.paths + "</b> paths · <b>" + st.anchors +
"</b> anchors · stroke <b>" + st.stroke_w + "px</b><br>max deviation <b>" +
st.max_dev + "px</b> · mean <b>" + st.mean_dev + "px</b> · " + st.elapsed + "s</p>";
const tb = document.querySelector("#layers tbody");
tb.innerHTML = "";
for(const e of r.elements){
const tr = document.createElement("tr");
tr.innerHTML = "<td>" + e.name + "</td><td>" + e.kind + "</td><td>" +
e.anchors + "</td><td>" + e.max_dev.toFixed(2) + "px</td>";
tb.appendChild(tr);
}
$("layers").style.display = "";
$("download").disabled = false;
$("copy").disabled = false;
setStatus("Ho gaya!" + r.note + " Neeche Download SVG dabao.");
} catch(err){
const msg = err.message || String(err);
if(/allocate|memory/i.test(msg)){
setStatus("Phone ki memory full ho gayi. Quality ko 2x ya 1x karke dobara try karo.");
} else {
setStatus("Convert error:\n" + msg.slice(0, 600));
}
}
$("convert").disabled = false;
$("convert").textContent = "Convert to SVG";
}
$("convert").addEventListener("click", convert);
$("download").addEventListener("click", () => {
const blob = new Blob([lastSvg], {type: "image/svg+xml"});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName + ".svg";
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
});
$("copy").addEventListener("click", async () => {
try { await navigator.clipboard.writeText(lastSvg); setStatus("SVG copy ho gaya."); }
catch(e){ setStatus("Copy nahi ho saka, code manually select karo."); }
});
init();
</script>
</body>
</html>