diff --git a/README.md b/README.md index 14213e7..3c3be6f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ L'encodage et le rendu du QR code se font **intégralement dans le navigateur** - Niveaux de correction d'erreur configurables (L / M / Q / H) - Couleurs de modules et de fond personnalisables - Superposition d'un logo au centre du QR code (force automatiquement la correction d'erreur au niveau H pour rester scannable) -- Export du QR code en PNG +- Export du QR code en PNG ou en SVG (vectoriel, redimensionnable sans perte) - Aucune dépendance réseau au runtime (CSP `connect-src 'none'`) ## Déploiement diff --git a/app/app.js b/app/app.js index c675759..f6c898d 100644 --- a/app/app.js +++ b/app/app.js @@ -26,6 +26,7 @@ const logoClearBtn = document.getElementById("qr-logo-clear"); const generateBtn = document.getElementById("generate-btn"); const downloadBtn = document.getElementById("download-btn"); + const downloadSvgBtn = document.getElementById("download-svg-btn"); const canvas = document.getElementById("qr-canvas"); const statusEl = document.getElementById("qr-status"); const ctx = canvas.getContext("2d"); @@ -137,7 +138,7 @@ el.addEventListener("input", render); }); - function render() { + function getPayload() { const mode = modeEl.value; let text; if (mode === "wifi") { @@ -147,6 +148,11 @@ } else { text = textEl.value; } + return { mode, text }; + } + + function render() { + const { mode, text } = getPayload(); statusEl.classList.remove("error"); if (!text) { @@ -229,12 +235,84 @@ }, "image/png"); } + function triggerDownload(filename, content, mimeType) { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function downloadSvg() { + const { mode, text } = getPayload(); + if (!text) { + alert("Générez d'abord un QR code avant de l'exporter."); + return; + } + + const cellSize = parseInt(cellSizeEl.value, 10) || 8; + const requestedLevel = correctionEl.value; + const level = logoImage ? "H" : requestedLevel; + const fgColor = fgColorEl.value || "#000000"; + const bgColor = bgColorEl.value || "#ffffff"; + const margin = 4; // modules + + try { + const qr = qrcode(0, level); + qr.addData(text); + qr.make(); + + const moduleCount = qr.getModuleCount(); + const size = (moduleCount + margin * 2) * cellSize; + + const parts = []; + parts.push( + `` + ); + parts.push(``); + + for (let row = 0; row < moduleCount; row++) { + for (let col = 0; col < moduleCount; col++) { + if (qr.isDark(row, col)) { + const x = (col + margin) * cellSize; + const y = (row + margin) * cellSize; + parts.push(``); + } + } + } + + if (logoImage) { + const logoSize = size * 0.22; + const pad = logoSize * 0.14; + const x = (size - logoSize) / 2; + const y = (size - logoSize) / 2; + + parts.push( + `` + ); + parts.push( + `` + ); + } + + parts.push(""); + triggerDownload("qrcode.svg", parts.join(""), "image/svg+xml"); + } catch (err) { + alert("Erreur : " + (err.message || "contenu trop long pour être encodé.")); + } + } + [textEl, cellSizeEl, correctionEl, fgColorEl, bgColorEl].forEach((el) => { el.addEventListener("input", render); }); generateBtn.addEventListener("click", render); downloadBtn.addEventListener("click", downloadPng); + downloadSvgBtn.addEventListener("click", downloadSvg); updateModeVisibility(); render(); diff --git a/app/index.html b/app/index.html index 8d81456..0d3775f 100644 --- a/app/index.html +++ b/app/index.html @@ -135,6 +135,7 @@
+