From 1cd0f1ed1dac2ba6b788af1a44fb3e5d151e5b1d Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Mon, 6 Apr 2026 05:46:32 +0200 Subject: [PATCH] fix svg loading to remove garbage icons --- server/templates/dashboard.html | 86 ++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/server/templates/dashboard.html b/server/templates/dashboard.html index 38b9b12..eb6defe 100644 --- a/server/templates/dashboard.html +++ b/server/templates/dashboard.html @@ -1112,6 +1112,90 @@ } } + function parseViewBox(svgEl) { + const raw = String(svgEl.getAttribute("viewBox") || "").trim(); + const parts = raw.split(/\s+/).map((item) => Number(item)); + if (parts.length !== 4 || parts.some((v) => Number.isNaN(v))) { + return { minX: 0, minY: 0, width: 100, height: 100 }; + } + return { minX: parts[0], minY: parts[1], width: parts[2], height: parts[3] }; + } + + function groupLooksOffCanvas(groupEl, viewBox) { + const attrNames = new Set(["x", "y", "cx", "cy", "x1", "y1", "x2", "y2", "d", "points"]); + const allElements = [groupEl, ...groupEl.querySelectorAll("*")]; + let farOutsideCount = 0; + let numericCount = 0; + const minAllowedX = viewBox.minX - Math.max(40, viewBox.width * 0.8); + const minAllowedY = viewBox.minY - Math.max(40, viewBox.height * 0.8); + const maxAllowedX = viewBox.minX + viewBox.width + Math.max(40, viewBox.width * 0.8); + const maxAllowedY = viewBox.minY + viewBox.height + Math.max(40, viewBox.height * 0.8); + + for (const node of allElements) { + for (const attr of node.getAttributeNames()) { + if (!attrNames.has(attr)) continue; + const value = node.getAttribute(attr); + if (!value) continue; + const matches = value.match(/-?\d*\.?\d+/g); + if (!matches) continue; + + for (let idx = 0; idx < matches.length; idx += 1) { + const num = Number(matches[idx]); + if (Number.isNaN(num)) continue; + numericCount += 1; + const isXCoord = idx % 2 === 0; + if (isXCoord) { + if (num < minAllowedX || num > maxAllowedX) farOutsideCount += 1; + } else { + if (num < minAllowedY || num > maxAllowedY) farOutsideCount += 1; + } + } + } + } + + if (numericCount < 10) return false; + return farOutsideCount / numericCount > 0.55; + } + + function maxNestedGroupDepth(groupEl) { + let maxDepth = 1; + const stack = [{ node: groupEl, depth: 1 }]; + while (stack.length > 0) { + const entry = stack.pop(); + if (!entry) continue; + maxDepth = Math.max(maxDepth, entry.depth); + for (const child of Array.from(entry.node.children)) { + if (!child.tagName || child.tagName.toLowerCase() !== "g") continue; + stack.push({ node: child, depth: entry.depth + 1 }); + } + } + return maxDepth; + } + + function normalizeHeadSvgMarkup(svgMarkup) { + if (!svgMarkup) return null; + try { + const parser = new DOMParser(); + const parsed = parser.parseFromString(svgMarkup, "image/svg+xml"); + const svgEl = parsed.querySelector("svg"); + if (!svgEl) return svgMarkup; + + const topLevelGroups = Array.from(svgEl.children).filter((el) => el.tagName && el.tagName.toLowerCase() === "g"); + if (topLevelGroups.length > 1) { + const viewBox = parseViewBox(svgEl); + const firstGroup = topLevelGroups[0]; + const deeplyNestedGroup = maxNestedGroupDepth(firstGroup) >= 3; + if (groupLooksOffCanvas(firstGroup, viewBox) || deeplyNestedGroup) { + firstGroup.remove(); + } + } + + return new XMLSerializer().serializeToString(svgEl); + } catch { + return svgMarkup; + } + } + async function preloadReplaySvgs() { if (!replay || !Array.isArray(replay.turns)) return; const urls = new Set(); @@ -1135,7 +1219,7 @@ if (type === "head") { const svgMarkup = svgCache.get(iconUrl); if (svgMarkup) { - layer.innerHTML = svgMarkup; + layer.innerHTML = normalizeHeadSvgMarkup(svgMarkup); const svgEl = layer.querySelector("svg"); if (svgEl) { svgEl.style.width = "100%";