diff --git a/src/components/tools/QrCode.vue b/src/components/tools/QrCode.vue index a8e7064..9c15e98 100644 --- a/src/components/tools/QrCode.vue +++ b/src/components/tools/QrCode.vue @@ -14,6 +14,12 @@ const text = useLocalStorage('text', '', 'qr-code') const ecc = useLocalStorage('ecc', 'M', 'qr-code') const size = useLocalStorage('size', 300, 'qr-code') const format = useLocalStorage('format', 'png', 'qr-code') +const isBgTransparent = useLocalStorage('isBgTransparent', true, 'qr-code') +const bgColor = useLocalStorage('bgColor', '#ffffff', 'qr-code') +const fgType = useLocalStorage('fgType', 'solid', 'qr-code') +const fgColor1 = useLocalStorage('fgColor1', '#000000', 'qr-code') +const fgColor2 = useLocalStorage('fgColor2', '#10b981', 'qr-code') + const svgContent = ref('') const previewRef = ref(null) @@ -52,11 +58,16 @@ const generateQR = () => { worker.postMessage({ id: latestJobId, text: text.value, - ecc: ecc.value + ecc: ecc.value, + isBgTransparent: isBgTransparent.value, + bgColor: bgColor.value, + fgType: fgType.value, + fgColor1: fgColor1.value, + fgColor2: fgColor2.value }) } -watch([text, ecc], () => { +watch([text, ecc, isBgTransparent, bgColor, fgType, fgColor1, fgColor2], () => { generateQR() }) @@ -92,36 +103,43 @@ onUnmounted(() => { }) const downloadFile = async () => { - if (!text.value) return + if (!text.value || !svgContent.value) return const filename = `qr-code-${Date.now()}.${format.value}` if (format.value === 'svg') { - // For SVG download, we might want to inject the size if user specifically requested it, - // but usually raw SVG is better. - // If we want to support the "Size" dropdown for SVG download, we can regenerate with specific width. - const svgWithSize = await QRCode.toString(text.value, { - type: 'svg', - errorCorrectionLevel: ecc.value, - margin: 1, - width: size.value - }) - const blob = new Blob([svgWithSize], { type: 'image/svg+xml' }) + let finalSvg = svgContent.value + if (!finalSvg.includes('width=')) { + finalSvg = finalSvg.replace(' { - if (blob) triggerDownload(blob, filename) - }, mime) + const img = new Image() + const svgSource = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent.value)}` + + img.onload = () => { + if (format.value === 'jpeg' && isBgTransparent.value) { + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, size.value, size.value) + } + ctx.drawImage(img, 0, 0, size.value, size.value) + + const mime = format.value === 'jpeg' ? 'image/jpeg' : `image/${format.value}` + canvas.toBlob((blob) => { + if (blob) triggerDownload(blob, filename) + }, mime, 1.0) + } + img.onerror = (e) => { + console.error('Failed to load SVG for conversion', e) + } + img.src = svgSource } catch (err) { console.error('Download failed', err) } @@ -167,11 +185,38 @@ const triggerDownload = (blob, filename) => { + +
+ + +
+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
-
+
@@ -252,6 +297,42 @@ const triggerDownload = (blob, filename) => { color: var(--text-secondary); } +.color-picker-wrapper { + display: flex; + align-items: center; + gap: 0.5rem; + height: 38px; +} + +.color-input { + width: 38px; + height: 38px; + padding: 0; + border: 1px solid var(--panel-border); + border-radius: 6px; + cursor: pointer; + background: var(--panel-bg); +} + +.color-input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + cursor: pointer; + color: var(--text-secondary); + user-select: none; +} +.checkbox-label input { + cursor: pointer; + width: 16px; + height: 16px; +} .preview-section { display: flex; diff --git a/src/workers/qrcode.worker.js b/src/workers/qrcode.worker.js index 4caa0ea..6dad57d 100644 --- a/src/workers/qrcode.worker.js +++ b/src/workers/qrcode.worker.js @@ -1,7 +1,7 @@ import QRCode from 'qrcode' self.onmessage = async (e) => { - const { id, text, ecc } = e.data + const { id, text, ecc, isBgTransparent, bgColor, fgType, fgColor1, fgColor2 } = e.data if (!text) { self.postMessage({ id, svgContent: '' }) @@ -9,12 +9,27 @@ self.onmessage = async (e) => { } try { - const svgContent = await QRCode.toString(text, { + let svgContent = await QRCode.toString(text, { type: 'svg', errorCorrectionLevel: ecc, margin: 1, + color: { + dark: fgType === 'solid' ? fgColor1 : '#000000', + light: isBgTransparent ? '#00000000' : bgColor + } }) + if (fgType !== 'solid') { + const isLinear = fgType === 'linear' + const defs = isLinear + ? `` + : `` + + svgContent = svgContent.replace('${defs}`) + // qrcode outputs so it's safe to replace + svgContent = svgContent.replace(/stroke="#000000"/g, 'stroke="url(#qr-grad)"') + } + self.postMessage({ id, svgContent }) } catch (err) { self.postMessage({ id, error: err.message })