Files
aza/APP/logo-tool/script.js
2026-03-30 07:59:11 +02:00

693 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Logo AI Pixel-Perfect Optimizer
*
* Architektur:
* 1. Bild laden → anzeigen + segmentieren
* 2. Slider = reine Mathematik (keine KI)
* 3. KI-Feld = zwei Modi:
* a) SVG-Modus: schickt kompakten SVG-Code → KI gibt modifiziertes SVG zurück
* b) Bild-Modus: schickt Bild + Segment-Map → KI gibt JSON-Befehle zurück
* (translate, scale, stroke KEINE Bild-Generierung)
*/
// ─── State ───────────────────────────────────────────────────
const state = {
originalImage: null,
originalDataUrl: null,
originalSvgCode: null,
currentSvgCode: null,
segments: [],
history: [],
mode: null, // 'image' | 'svg'
currentView: 'result',
// KI-Bild-Modus: Anpassungen als JSON-Befehle
segmentAdjustments: {}, // { segIndex: { dx, dy, scaleX, scaleY, thicknessBoost } }
};
// ─── DOM ─────────────────────────────────────────────────────
const previewContainer = document.getElementById('preview-container');
const logoInput = document.getElementById('logo-input');
const uploadZone = document.getElementById('upload-zone');
const aiPrompt = document.getElementById('ai-prompt');
const aiApiKey = document.getElementById('ai-apikey');
const aiProvider = document.getElementById('ai-provider');
const btnAiApply = document.getElementById('btn-ai-apply');
const aiLog = document.getElementById('ai-log');
// ─── Upload ──────────────────────────────────────────────────
uploadZone.addEventListener('click', () => logoInput.click());
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('drag-over'); });
uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over'));
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('drag-over');
if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
});
logoInput.addEventListener('change', (e) => { if (e.target.files[0]) handleFile(e.target.files[0]); });
function handleFile(file) {
const name = file.name.toLowerCase();
if (name.endsWith('.svg')) {
const reader = new FileReader();
reader.onload = (ev) => {
const svgCode = ev.target.result;
state.mode = 'svg';
state.originalSvgCode = svgCode;
state.currentSvgCode = svgCode;
state.history = [svgCode];
state.segmentAdjustments = {};
const optimized = optimizeSvg(svgCode);
const pathCount = (optimized.match(/<path/g) || []).length;
updateStatus('SVG (Vektor)', pathCount + ' Pfade', getSvgDimensions(svgCode));
renderResult();
logAi('info', `SVG geladen: ${pathCount} Pfade, ${(svgCode.length / 1024).toFixed(0)} KB`);
logAi('info', `Optimiert für KI: ${(optimized.length / 1024).toFixed(0)} KB`);
};
reader.readAsText(file);
} else {
const reader = new FileReader();
reader.onload = (ev) => {
const img = new Image();
img.onload = () => {
state.originalImage = img;
state.originalDataUrl = ev.target.result;
state.mode = 'image';
state.segments = detectSegments(img);
state.segmentAdjustments = {};
state.history = [JSON.stringify({})];
updateStatus('Bild (Pixel-Segmente)', state.segments.length + ' Segmente', `${img.width}×${img.height}`);
renderResult();
logAi('info', `Bild geladen: ${img.width}×${img.height}`);
logAi('info', `${state.segments.length} Buchstaben/Segmente erkannt bereit für KI-Steuerung.`);
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
}
}
// ─── Segment-Erkennung ──────────────────────────────────────
function detectSegments(img) {
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = img.width;
tmpCanvas.height = img.height;
const ctx = tmpCanvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const px = imageData.data;
const w = img.width, h = img.height;
const colActive = new Uint8Array(w);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
const i = (y * w + x) * 4;
if (px[i + 3] > 20 && (px[i] < 240 || px[i + 1] < 240 || px[i + 2] < 240)) {
colActive[x] = 1;
break;
}
}
}
const segs = [];
let inside = false, start = 0;
for (let x = 0; x <= w; x++) {
const a = x < w ? colActive[x] : 0;
if (a && !inside) { start = x; inside = true; }
else if (!a && inside) {
segs.push({ x: start, width: x - start, index: segs.length });
inside = false;
}
}
if (segs.length === 0) segs.push({ x: 0, width: w, index: 0 });
return segs;
}
// ─── SVG-Optimierung (für KI-Token-Limit) ───────────────────
function optimizeSvg(svgCode) {
let s = svgCode;
// Koordinaten-Präzision auf 1 Dezimalstelle reduzieren
s = s.replace(/(\d+\.\d{2,})/g, (m) => parseFloat(m).toFixed(1));
// Überflüssige Whitespace entfernen
s = s.replace(/\s+/g, ' ');
s = s.replace(/>\s+</g, '><');
// Leere Pfade entfernen
s = s.replace(/<path[^>]*d="[^"]{0,5}"[^>]*\/>/g, '');
return s.trim();
}
// ─── Rendering ───────────────────────────────────────────────
const sliderIds = ['spacing', 'thickness', 'scale', 'flatten'];
sliderIds.forEach(id => {
document.getElementById(id).addEventListener('input', () => {
updateSliderDisplay();
renderResult();
});
});
function updateSliderDisplay() {
document.getElementById('val-spacing').textContent = document.getElementById('spacing').value + ' px';
document.getElementById('val-thickness').textContent = document.getElementById('thickness').value + ' px';
document.getElementById('val-scale').textContent = document.getElementById('scale').value + '%';
document.getElementById('val-flatten').textContent = document.getElementById('flatten').value + '%';
}
updateSliderDisplay();
function renderResult() {
if (state.currentView === 'original') { renderOriginal(); return; }
if (state.currentView === 'diff') { renderDiff(); return; }
if (!state.originalImage && !state.originalSvgCode) return;
const spacing = parseFloat(document.getElementById('spacing').value);
const thickness = parseFloat(document.getElementById('thickness').value);
const scalePct = parseFloat(document.getElementById('scale').value) / 100;
const flattenPct = parseFloat(document.getElementById('flatten').value) / 100;
if (state.mode === 'image' && state.originalImage) {
renderImageSegments(spacing, thickness, scalePct, flattenPct);
} else if (state.currentSvgCode) {
renderSvgWithParams(spacing, thickness, scalePct, flattenPct);
}
}
function renderImageSegments(spacing, thickness, scalePct, flattenPct) {
const img = state.originalImage;
const segments = state.segments;
const adj = state.segmentAdjustments;
let totalW = 0;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
const a = adj[i] || {};
const sx = (a.scaleX || 1) * scalePct;
totalW += seg.width * sx + spacing + (a.dx || 0);
}
totalW = Math.max(totalW - spacing, img.width * scalePct);
const scaledH = img.height * scalePct * flattenPct;
const pad = Math.max(thickness * 2, 4) + 10;
const cw = Math.ceil(totalW + pad * 2);
const ch = Math.ceil(scaledH + pad * 2 + 40);
const canvas = document.createElement('canvas');
canvas.width = cw;
canvas.height = ch;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, cw, ch);
let cx = pad;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
const a = adj[i] || {};
const sx = (a.scaleX || 1) * scalePct;
const sy = (a.scaleY || 1) * scalePct * flattenPct;
const tb = (a.thicknessBoost || 0) + thickness;
const dx = a.dx || 0;
const dy = a.dy || 0;
const sw = seg.width * sx;
const sh = img.height * sy;
if (tb > 0) {
ctx.filter = `blur(${tb / 2}px) brightness(1.15)`;
ctx.drawImage(img, seg.x, 0, seg.width, img.height, cx + dx, pad + dy, sw, sh);
}
ctx.filter = 'none';
ctx.drawImage(img, seg.x, 0, seg.width, img.height, cx + dx, pad + dy, sw, sh);
cx += sw + spacing + dx;
}
previewContainer.innerHTML = '';
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
previewContainer.appendChild(canvas);
}
function renderSvgWithParams(spacing, thickness, scalePct, flattenPct) {
const parser = new DOMParser();
const doc = parser.parseFromString(state.currentSvgCode, 'image/svg+xml');
const svg = doc.querySelector('svg');
if (!svg) return;
let rootG = svg.querySelector('g#logo-root');
if (!rootG) {
rootG = doc.createElementNS('http://www.w3.org/2000/svg', 'g');
rootG.setAttribute('id', 'logo-root');
while (svg.childNodes.length > 0) rootG.appendChild(svg.childNodes[0]);
svg.appendChild(rootG);
}
rootG.setAttribute('transform', `scale(${scalePct}, ${scalePct * flattenPct})`);
if (thickness > 0) {
svg.querySelectorAll('path').forEach(p => {
const fill = p.getAttribute('fill') || '#000';
p.setAttribute('stroke', fill);
p.setAttribute('stroke-width', thickness.toString());
p.setAttribute('stroke-linejoin', 'round');
});
}
const vb = svg.getAttribute('viewBox');
if (vb && spacing !== 0) {
const parts = vb.split(/[\s,]+/).map(Number);
parts[2] += spacing * 2;
svg.setAttribute('viewBox', parts.join(' '));
}
previewContainer.innerHTML = new XMLSerializer().serializeToString(svg);
}
function renderOriginal() {
previewContainer.innerHTML = '';
if (state.originalDataUrl) {
const img = document.createElement('img');
img.src = state.originalDataUrl;
previewContainer.appendChild(img);
} else if (state.originalSvgCode) {
previewContainer.innerHTML = state.originalSvgCode;
}
}
function renderDiff() {
previewContainer.innerHTML = '';
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display:flex;gap:20px;align-items:center;';
const leftDiv = document.createElement('div');
leftDiv.style.cssText = 'text-align:center;flex:1;';
const leftLabel = document.createElement('div');
leftLabel.textContent = 'ORIGINAL';
leftLabel.style.cssText = 'font-size:11px;color:#999;margin-bottom:8px;letter-spacing:1px;';
leftDiv.appendChild(leftLabel);
if (state.originalDataUrl) {
const im = document.createElement('img');
im.src = state.originalDataUrl;
im.style.maxWidth = '100%';
leftDiv.appendChild(im);
} else if (state.originalSvgCode) {
const d = document.createElement('div');
d.innerHTML = state.originalSvgCode;
leftDiv.appendChild(d);
}
const arrow = document.createElement('div');
arrow.textContent = '\u2192';
arrow.style.cssText = 'font-size:2em;color:#666;';
const rightDiv = document.createElement('div');
rightDiv.style.cssText = 'text-align:center;flex:1;';
const rightLabel = document.createElement('div');
rightLabel.textContent = 'OPTIMIERT';
rightLabel.style.cssText = 'font-size:11px;color:#4a9eff;margin-bottom:8px;letter-spacing:1px;';
rightDiv.appendChild(rightLabel);
const tmpContainer = document.createElement('div');
state.currentView = 'result';
renderResult();
tmpContainer.innerHTML = previewContainer.innerHTML;
state.currentView = 'diff';
rightDiv.appendChild(tmpContainer);
wrapper.appendChild(leftDiv);
wrapper.appendChild(arrow);
wrapper.appendChild(rightDiv);
previewContainer.innerHTML = '';
previewContainer.appendChild(wrapper);
}
// ─── View Toggle ─────────────────────────────────────────────
document.querySelectorAll('.view-toggle button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.view-toggle button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.currentView = btn.dataset.view;
renderResult();
});
});
// ═══════════════════════════════════════════════════════════════
// ─── KI-Integration ──────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════
// Zwei verschiedene System-Prompts: einer für SVG, einer für Bild-Segment-Modus
const SVG_SYSTEM_PROMPT = `Du bist ein SVG-Pfad-Manipulations-Experte.
STRIKTE REGELN Verstoß = Ergebnis wird verworfen:
1. Du erhältst SVG-Code und eine Anweisung.
2. Du darfst NUR mathematische Transformationen anwenden:
translate(), scale(), stroke-width, transform, fill, opacity.
3. Alle Pfad-Daten (d="...") bleiben EXAKT erhalten,
außer der Benutzer sagt explizit "Pfad ändern/korrigieren".
4. KEINE neuen Pfade oder Formen erfinden.
5. Antworte NUR mit dem modifizierten SVG-Code.
Kein Markdown, keine Erklärungen, kein \`\`\`-Wrapper.
Nur <svg>...</svg>.
6. Wenn unmöglich, antworte: FEHLER: [Begründung]`;
const IMAGE_SYSTEM_PROMPT = `Du bist ein Logo-Analyse-Experte. Du erhältst ein Logo-Bild und eine Liste erkannter Segmente (Buchstaben/Formen).
Deine Aufgabe: Analysiere das Bild, verstehe welches Segment welcher Buchstabe/Form ist,
und gib NUR mathematische Transformations-Befehle als JSON zurück.
Erlaubte Befehle pro Segment:
- dx: Horizontale Verschiebung in Pixeln (positiv = rechts)
- dy: Vertikale Verschiebung in Pixeln (positiv = unten)
- scaleX: Horizontale Skalierung (1.0 = unverändert)
- scaleY: Vertikale Skalierung (1.0 = unverändert)
- thicknessBoost: Zusätzliche Strichdicke in Pixeln
STRIKTE REGELN:
1. Antworte NUR mit einem JSON-Objekt. Kein Text, kein Markdown.
2. Keys sind Segment-Indizes (als String), Values sind Objekte mit den obigen Feldern.
3. Nur Segmente auflisten, die sich ÄNDERN. Unveränderte weglassen.
4. KEINE Farben ändern, KEINE Formen neuzeichnen, KEINE Pixel manipulieren.
5. Du verschiebst/skalierst nur existierende Bildausschnitte.
Beispiel-Antwort:
{"0": {"dx": 15}, "2": {"scaleX": 1.1, "thicknessBoost": 0.5}, "4": {"dx": -5, "dy": 2}}`;
aiApiKey.addEventListener('input', () => {
btnAiApply.disabled = !aiApiKey.value.trim();
});
btnAiApply.addEventListener('click', () => {
const prompt = aiPrompt.value.trim();
const apiKey = aiApiKey.value.trim();
if (!prompt || !apiKey) return;
applyAiCorrection(prompt, apiKey);
});
async function applyAiCorrection(userPrompt, apiKey) {
btnAiApply.disabled = true;
btnAiApply.innerHTML = '<span class="spinner"></span> KI analysiert...';
logAi('info', `Sende an KI: "${userPrompt}"`);
const provider = aiProvider.value;
try {
if (state.mode === 'svg' && state.currentSvgCode) {
await handleSvgAi(userPrompt, apiKey, provider);
} else if (state.mode === 'image' && state.originalImage) {
await handleImageAi(userPrompt, apiKey, provider);
} else {
logAi('error', 'Bitte zuerst ein Logo laden.');
}
} catch (err) {
logAi('error', 'API-Fehler: ' + err.message);
} finally {
btnAiApply.disabled = false;
btnAiApply.textContent = 'KI-Korrektur anwenden';
}
}
// ─── SVG-Modus: kompakten SVG-Code an KI ────────────────────
async function handleSvgAi(userPrompt, apiKey, provider) {
const optimized = optimizeSvg(state.currentSvgCode);
const sizeKB = (optimized.length / 1024).toFixed(0);
logAi('info', `SVG optimiert: ${sizeKB} KB`);
if (optimized.length > 80000) {
logAi('error', `SVG immer noch zu gross (${sizeKB} KB). Versuche, ein einfacheres SVG zu laden oder das Logo als PNG zu importieren.`);
return;
}
const messages = [
{ role: 'user', content: `SVG-Code:\n\n${optimized}\n\nAnweisung: ${userPrompt}` }
];
let result;
if (provider === 'openai') {
result = await callOpenAI(messages, SVG_SYSTEM_PROMPT, apiKey);
} else {
result = await callAnthropic(messages, SVG_SYSTEM_PROMPT, apiKey);
}
if (result.startsWith('FEHLER:')) { logAi('error', result); return; }
if (!result.includes('<svg') || !result.includes('</svg>')) {
logAi('error', 'KI hat kein gültiges SVG zurückgegeben. Verworfen.');
return;
}
const origPaths = (state.currentSvgCode.match(/<path/g) || []).length;
const newPaths = (result.match(/<path/g) || []).length;
if (Math.abs(origPaths - newPaths) > 2) {
logAi('error', `Halluzinations-Schutz: ${origPaths}${newPaths} Pfade. Verworfen.`);
return;
}
state.history.push(state.currentSvgCode);
state.currentSvgCode = result;
renderResult();
logAi('info', `Korrektur angewendet. Pfade: ${origPaths}${newPaths}.`);
}
// ─── Bild-Modus: Bild + Segment-Map an KI ──────────────────
async function handleImageAi(userPrompt, apiKey, provider) {
const segments = state.segments;
// Kleine Version des Bildes erstellen (max 512px breit) für die KI
const maxW = 512;
const img = state.originalImage;
const ratio = Math.min(maxW / img.width, 1);
const thumbCanvas = document.createElement('canvas');
thumbCanvas.width = Math.round(img.width * ratio);
thumbCanvas.height = Math.round(img.height * ratio);
const tCtx = thumbCanvas.getContext('2d');
tCtx.drawImage(img, 0, 0, thumbCanvas.width, thumbCanvas.height);
const thumbDataUrl = thumbCanvas.toDataURL('image/png', 0.8);
const segmentDesc = segments.map((s, i) =>
`Segment ${i}: x=${s.x}, breite=${s.width}px (${((s.x / img.width) * 100).toFixed(0)}% von links)`
).join('\n');
logAi('info', `Bild-Modus: ${segments.length} Segmente, Thumbnail ${thumbCanvas.width}×${thumbCanvas.height}`);
let result;
if (provider === 'openai') {
const messages = [{
role: 'user',
content: [
{ type: 'image_url', image_url: { url: thumbDataUrl, detail: 'low' } },
{ type: 'text', text: `Segmente:\n${segmentDesc}\n\nAnweisung: ${userPrompt}` }
]
}];
result = await callOpenAI(messages, IMAGE_SYSTEM_PROMPT, apiKey);
} else {
const base64 = thumbDataUrl.split(',')[1];
const messages = [{
role: 'user',
content: [
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: base64 } },
{ type: 'text', text: `Segmente:\n${segmentDesc}\n\nAnweisung: ${userPrompt}` }
]
}];
result = await callAnthropic(messages, IMAGE_SYSTEM_PROMPT, apiKey);
}
// JSON parsen
let adjustments;
try {
const jsonMatch = result.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error('Kein JSON in Antwort');
adjustments = JSON.parse(jsonMatch[0]);
} catch (e) {
logAi('error', `KI-Antwort konnte nicht gelesen werden: ${e.message}`);
logAi('error', `Rohe Antwort: ${result.substring(0, 200)}`);
return;
}
// Validierung: nur erlaubte Felder, keine verrückten Werte
const allowedFields = ['dx', 'dy', 'scaleX', 'scaleY', 'thicknessBoost'];
for (const [key, val] of Object.entries(adjustments)) {
const idx = parseInt(key);
if (isNaN(idx) || idx < 0 || idx >= segments.length) {
logAi('error', `Ungültiger Segment-Index: ${key}. Verworfen.`);
return;
}
for (const field of Object.keys(val)) {
if (!allowedFields.includes(field)) {
logAi('error', `Verbotenes Feld "${field}" in Segment ${key}. Verworfen.`);
return;
}
}
if (val.scaleX && (val.scaleX < 0.3 || val.scaleX > 3)) {
logAi('error', `scaleX=${val.scaleX} in Segment ${key} zu extrem. Verworfen.`);
return;
}
if (val.scaleY && (val.scaleY < 0.3 || val.scaleY > 3)) {
logAi('error', `scaleY=${val.scaleY} in Segment ${key} zu extrem. Verworfen.`);
return;
}
if (val.dx && Math.abs(val.dx) > 200) {
logAi('error', `dx=${val.dx} in Segment ${key} zu extrem. Verworfen.`);
return;
}
}
// Undo-Stack
state.history.push(JSON.stringify(state.segmentAdjustments));
state.segmentAdjustments = { ...state.segmentAdjustments };
for (const [key, val] of Object.entries(adjustments)) {
state.segmentAdjustments[key] = {
...(state.segmentAdjustments[key] || {}),
...val
};
}
renderResult();
const changedCount = Object.keys(adjustments).length;
logAi('info', `${changedCount} Segment(e) angepasst: ${JSON.stringify(adjustments)}`);
}
// ─── API-Calls ───────────────────────────────────────────────
async function callOpenAI(messages, systemPrompt, apiKey) {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
body: JSON.stringify({
model: 'gpt-4o',
temperature: 0,
max_tokens: 8000,
messages: [{ role: 'system', content: systemPrompt }, ...messages],
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || `HTTP ${res.status}`);
}
const data = await res.json();
let content = data.choices[0].message.content.trim();
content = content.replace(/^```(?:xml|svg|html|json)?\n?/i, '').replace(/\n?```$/i, '');
return content;
}
async function callAnthropic(messages, systemPrompt, apiKey) {
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true',
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 8000,
temperature: 0,
system: systemPrompt,
messages: messages,
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error?.message || `HTTP ${res.status}`);
}
const data = await res.json();
let content = data.content[0].text.trim();
content = content.replace(/^```(?:xml|svg|html|json)?\n?/i, '').replace(/\n?```$/i, '');
return content;
}
// ─── Log ─────────────────────────────────────────────────────
function logAi(type, msg) {
aiLog.classList.add('visible');
const line = document.createElement('div');
line.className = type;
const time = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
line.textContent = `[${time}] ${msg}`;
aiLog.appendChild(line);
aiLog.scrollTop = aiLog.scrollHeight;
}
// ─── Undo ────────────────────────────────────────────────────
document.getElementById('btn-undo').addEventListener('click', () => {
if (state.history.length <= 1) return;
state.history.pop();
const prev = state.history[state.history.length - 1];
if (state.mode === 'svg') {
state.currentSvgCode = prev;
} else {
try { state.segmentAdjustments = JSON.parse(prev); }
catch { state.segmentAdjustments = {}; }
}
renderResult();
logAi('info', 'Rückgängig gemacht.');
});
// ─── Download ────────────────────────────────────────────────
document.getElementById('btn-download').addEventListener('click', () => {
const svg = previewContainer.querySelector('svg');
const canvas = previewContainer.querySelector('canvas');
if (svg) {
const svgData = new XMLSerializer().serializeToString(svg);
downloadFile('optimiertes-logo.svg', 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgData));
const tmpImg = new Image();
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
tmpImg.onload = () => {
const c = document.createElement('canvas');
c.width = tmpImg.width * 2;
c.height = tmpImg.height * 2;
const ct = c.getContext('2d');
ct.drawImage(tmpImg, 0, 0, c.width, c.height);
downloadFile('optimiertes-logo.png', c.toDataURL('image/png'));
URL.revokeObjectURL(url);
};
tmpImg.src = url;
} else if (canvas) {
downloadFile('optimiertes-logo.png', canvas.toDataURL('image/png'));
}
});
function downloadFile(name, href) {
const a = document.createElement('a');
a.download = name;
a.href = href;
a.click();
}
document.getElementById('btn-fit').addEventListener('click', () => renderResult());
// ─── Status ──────────────────────────────────────────────────
function updateStatus(mode, segments, size) {
document.getElementById('status-mode').textContent = mode;
document.getElementById('status-segments').textContent = segments;
document.getElementById('status-size').textContent = size;
}
function getSvgDimensions(svgCode) {
const m = svgCode.match(/viewBox="([^"]+)"/);
if (m) { const p = m[1].split(/[\s,]+/); return `${p[2]}×${p[3]}`; }
return '\u2013';
}