From 35b60257edcceca4044d6823a1494575da38b3b2 Mon Sep 17 00:00:00 2001 From: TTigger Date: Mon, 13 Jul 2026 21:57:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(qr):=20QR=20=E6=8E=83=E6=8F=8F=EF=BC=88sta?= =?UTF-8?q?ge=20C=20item=202=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 補齊 QR 產生器的另一半:相機或圖片掃描,純前端零上傳。 - 解碼引擎選 jsQR(新依賴)而非 handoff 提的 barcode-detector: 原生 BarcodeDetector 在 Safari/Firefox 缺席,wasm ponyfill 逾 1MB; jsQR 純 JS、QR-only 正好符合範圍,且 headless e2e 行為確定。 - src/lib/qrScan.ts:內容分類與解析(TDD 7 tests),是 qrFormats.ts 產生器的反向——WIFI:/vCard 跳脫反解、RFC 摺行展開、往返測試釘住; javascript:/ftp: 等連結一律當純文字,不給可點的 。 - src/tools/QrScanner.tsx:圖片分頁(點選/拖放/Ctrl+V 貼上, 大圖縮到 1024 邊長再解)+相機分頁(getUserMedia,~150ms 節流 掃描,對到自動停);網址/WiFi/名片結構化顯示與複製。 - 中英頁面、tools.ts 註冊(實用工具 📷)、OG 卡 70 張(守門測試同步)。 - e2e:qrcode 套件現做 QR 圖上傳,涵蓋網址(rel=noopener 斷言)、 WiFi 解析、無 QR 圖片的明確錯誤(+3)。 測試:typecheck:api ✓ / 537 unit ✓(+7)/ 60 e2e ✓(+3) 純前端,不碰 api/。 Co-Authored-By: Claude Fable 5 --- e2e/qr-scanner.spec.ts | 45 ++++ package-lock.json | 7 + package.json | 1 + public/og/en/qr-scanner.png | Bin 0 -> 39738 bytes public/og/zh/qr-scanner.png | Bin 0 -> 40260 bytes src/data/tools.ts | 1 + src/lib/__tests__/qrScan.test.ts | 59 +++++ src/lib/qrScan.ts | 78 +++++++ src/pages/en/tools/qr-scanner.astro | 12 + src/pages/tools/qr-scanner.astro | 9 + src/tools/QrScanner.tsx | 328 ++++++++++++++++++++++++++++ 11 files changed, 540 insertions(+) create mode 100644 e2e/qr-scanner.spec.ts create mode 100644 public/og/en/qr-scanner.png create mode 100644 public/og/zh/qr-scanner.png create mode 100644 src/lib/__tests__/qrScan.test.ts create mode 100644 src/lib/qrScan.ts create mode 100644 src/pages/en/tools/qr-scanner.astro create mode 100644 src/pages/tools/qr-scanner.astro create mode 100644 src/tools/QrScanner.tsx diff --git a/e2e/qr-scanner.spec.ts b/e2e/qr-scanner.spec.ts new file mode 100644 index 0000000..3a63875 --- /dev/null +++ b/e2e/qr-scanner.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import QRCode from 'qrcode'; +import { waitForIslands, makePngBuffer } from './helpers'; + +async function qrPng(text: string): Promise { + return QRCode.toBuffer(text, { width: 320, margin: 2 }); +} + +test('上傳網址 QR → 解出網址並給開啟連結', async ({ page }) => { + await page.goto('/tools/qr-scanner'); + await waitForIslands(page); + + const png = await qrPng('https://tiglet.vercel.app/tools'); + await page.locator('input[type="file"]').setInputFiles({ name: 'url.png', mimeType: 'image/png', buffer: png }); + + const result = page.locator('div[role="status"]'); // CopyButton 內部也有 role=status 的 span,鎖 div 才唯一 + await expect(result).toContainText('網址'); + await expect(result).toContainText('https://tiglet.vercel.app/tools'); + const link = result.getByRole('link', { name: '開啟連結 ↗' }); + await expect(link).toHaveAttribute('href', 'https://tiglet.vercel.app/tools'); + await expect(link).toHaveAttribute('rel', 'noopener noreferrer'); +}); + +test('上傳 WiFi QR → 解析出網路名稱與密碼', async ({ page }) => { + await page.goto('/tools/qr-scanner'); + await waitForIslands(page); + + const png = await qrPng('WIFI:T:WPA;S:TigletCafe;P:pw123456;;'); + await page.locator('input[type="file"]').setInputFiles({ name: 'wifi.png', mimeType: 'image/png', buffer: png }); + + const result = page.locator('div[role="status"]'); // CopyButton 內部也有 role=status 的 span,鎖 div 才唯一 + await expect(result).toContainText('WiFi'); + await expect(result).toContainText('TigletCafe'); + await expect(result).toContainText('pw123456'); +}); + +test('沒有 QR 的圖片 → 明確告知找不到', async ({ page }) => { + await page.goto('/tools/qr-scanner'); + await waitForIslands(page); + + const png = await makePngBuffer(page, 320, 320, '#d97757'); + await page.locator('input[type="file"]').setInputFiles({ name: 'plain.png', mimeType: 'image/png', buffer: png }); + + await expect(page.getByText(/找不到 QR 碼/)).toBeVisible(); +}); diff --git a/package-lock.json b/package-lock.json index 792a140..90be3da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "astro": "^6.4.3", "dompurify": "^3.4.11", "fit-file-parser": "^3.0.2", + "jsqr": "^1.4.0", "marked": "^18.0.5", "qrcode": "^1.5.4", "react": "^19.2.7", @@ -9683,6 +9684,12 @@ "node": ">=0.10.0" } }, + "node_modules/jsqr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz", + "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==", + "license": "Apache-2.0" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", diff --git a/package.json b/package.json index 3dd6901..e486290 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "astro": "^6.4.3", "dompurify": "^3.4.11", "fit-file-parser": "^3.0.2", + "jsqr": "^1.4.0", "marked": "^18.0.5", "qrcode": "^1.5.4", "react": "^19.2.7", diff --git a/public/og/en/qr-scanner.png b/public/og/en/qr-scanner.png new file mode 100644 index 0000000000000000000000000000000000000000..d7157f25bb751994ba544c9f64571d58cf58fc24 GIT binary patch literal 39738 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 B7CQg{ literal 0 HcmV?d00001 diff --git a/src/data/tools.ts b/src/data/tools.ts index e489396..d053e88 100644 --- a/src/data/tools.ts +++ b/src/data/tools.ts @@ -50,4 +50,5 @@ export const tools: Tool[] = [ { id: 'hash-id', title: '雜湊 / UUID', titleEn: 'Hash / UUID', description: '文字與檔案的 SHA 雜湊,加上 UUID 批次產生。', descriptionEn: 'SHA hashes for text and files, plus batch UUID generation.', category: '文字', path: '/tools/hash-id', icon: '🔏', status: 'available', keywords: ['hash', 'sha256', 'sha1', 'uuid', 'guid', '雜湊', 'checksum'] }, { id: 'url-audit', title: '網址健檢', titleEn: 'URL Audit', description: '檢查網址的 SEO、社群分享卡與 AI 引擎設定並評分。', descriptionEn: 'Audit a URL for SEO, social share cards and AI-engine readiness, with a score.', category: '實用工具', path: '/tools/url-audit', icon: '🔎', status: 'available', keywords: ['seo', 'og', 'open graph', 'geo', '健檢', 'meta', '分享', 'social', 'audit', 'twitter card'] }, { id: 'invoice-lottery', title: '發票對獎', titleEn: 'Invoice Lottery', description: '財政部最新開獎號碼,輸入末 3 碼立刻對獎。', descriptionEn: 'Check Taiwan uniform-invoice numbers against the latest official draw.', category: '實用工具', path: '/tools/invoice-lottery', icon: '🧧', status: 'available', keywords: ['發票', '統一發票', '對獎', '中獎號碼', 'invoice', 'lottery', 'receipt', '雲端發票', '財政部'] }, + { id: 'qr-scanner', title: 'QR 掃描', titleEn: 'QR Scanner', description: '相機或圖片掃描 QR 碼,網址、WiFi、名片自動解析。', descriptionEn: 'Scan QR codes from camera or images; URLs, WiFi and vCards parsed.', category: '實用工具', path: '/tools/qr-scanner', icon: '📷', status: 'available', keywords: ['qr', 'scan', 'scanner', '掃描', '掃碼', 'barcode', 'wifi', 'vcard', '解碼'] }, ]; diff --git a/src/lib/__tests__/qrScan.test.ts b/src/lib/__tests__/qrScan.test.ts new file mode 100644 index 0000000..7911ba7 --- /dev/null +++ b/src/lib/__tests__/qrScan.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { classifyQrContent } from '../qrScan'; +import { wifiQr, vcardQr } from '../qrFormats'; + +describe('classifyQrContent', () => { + it('http/https 連結 → url', () => { + expect(classifyQrContent('https://tiglet.vercel.app/tools')).toEqual({ type: 'url', url: 'https://tiglet.vercel.app/tools' }); + expect(classifyQrContent('HTTP://EXAMPLE.COM')).toMatchObject({ type: 'url' }); + // javascript: 之類的不當連結一律當純文字,不要給可點的 + expect(classifyQrContent('javascript:alert(1)')).toEqual({ type: 'text' }); + expect(classifyQrContent('ftp://example.com')).toEqual({ type: 'text' }); + }); + + it('WIFI: payload → 解析 ssid / 密碼 / 加密 / 隱藏', () => { + expect(classifyQrContent('WIFI:T:WPA;S:MyHome;P:secret123;;')).toEqual({ + type: 'wifi', + ssid: 'MyHome', + password: 'secret123', + security: 'WPA', + hidden: false, + }); + expect(classifyQrContent('WIFI:T:nopass;S:Cafe;H:true;;')).toMatchObject({ type: 'wifi', ssid: 'Cafe', password: '', hidden: true }); + }); + + it('WiFi 與產生器互為往返:跳脫字元要正確解回來', () => { + const payload = wifiQr({ ssid: 'A;B,C:D"E\\F', password: 'p;w,d', security: 'WPA' }); + const parsed = classifyQrContent(payload); + expect(parsed).toEqual({ type: 'wifi', ssid: 'A;B,C:D"E\\F', password: 'p;w,d', security: 'WPA', hidden: false }); + }); + + it('vCard payload → 解析常用欄位', () => { + const payload = vcardQr({ name: '王小明', phone: '0912345678', email: 'ming@example.com', org: 'Tiglet', title: '工程師', url: 'https://example.com' }); + expect(classifyQrContent(payload)).toEqual({ + type: 'vcard', + name: '王小明', + phone: '0912345678', + email: 'ming@example.com', + org: 'Tiglet', + title: '工程師', + url: 'https://example.com', + }); + }); + + it('vCard 與產生器互為往返:跳脫的逗號分號換行解得回來', () => { + const payload = vcardQr({ name: 'Wang, Xiao;Ming', org: 'A\nB' }); + expect(classifyQrContent(payload)).toMatchObject({ type: 'vcard', name: 'Wang, Xiao;Ming', org: 'A\nB' }); + }); + + it('vCard 缺欄位就略過,CRLF 與大小寫容忍', () => { + const raw = 'begin:vcard\r\nversion:3.0\r\nfn:Amy\r\ntel:123\r\nend:vcard'; + expect(classifyQrContent(raw)).toEqual({ type: 'vcard', name: 'Amy', phone: '123', email: '', org: '', title: '', url: '' }); + }); + + it('其他內容 → text', () => { + expect(classifyQrContent('hello world')).toEqual({ type: 'text' }); + expect(classifyQrContent('')).toEqual({ type: 'text' }); + expect(classifyQrContent('WIFI:')).toEqual({ type: 'text' }); // 缺 SSID 的殘缺 payload + }); +}); diff --git a/src/lib/qrScan.ts b/src/lib/qrScan.ts new file mode 100644 index 0000000..30bb352 --- /dev/null +++ b/src/lib/qrScan.ts @@ -0,0 +1,78 @@ +// QR 掃描結果的內容分類與解析:qrFormats.ts 產生器的反向操作。 +// 純函式;解碼本身(影像 → 字串)由 jsQR 在元件層處理。 + +export type QrContent = + | { type: 'url'; url: string } + | { type: 'wifi'; ssid: string; password: string; security: string; hidden: boolean } + | { type: 'vcard'; name: string; phone: string; email: string; org: string; title: string; url: string } + | { type: 'text' }; + +// WIFI: 規格:欄位以未跳脫的 ; 分隔,值內的 \ ; , " : 以反斜線跳脫 +function splitWifiFields(body: string): Map { + const fields = new Map(); + let key = ''; + let buf = ''; + let readingKey = true; + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch === '\\' && i + 1 < body.length) { + buf += body[i + 1]; + i++; + } else if (readingKey && ch === ':') { + key = buf; + buf = ''; + readingKey = false; + } else if (!readingKey && ch === ';') { + fields.set(key, buf); + buf = ''; + key = ''; + readingKey = true; + } else { + buf += ch; + } + } + if (key) fields.set(key, buf); + return fields; +} + +function unescapeVcard(s: string): string { + return s.replace(/\\(.)/g, (_, c: string) => (c === 'n' || c === 'N' ? '\n' : c)); +} + +export function classifyQrContent(text: string): QrContent { + if (/^https?:\/\/\S+$/i.test(text.trim())) return { type: 'url', url: text.trim() }; + + if (/^WIFI:/i.test(text)) { + const fields = splitWifiFields(text.slice(5)); + const ssid = fields.get('S') ?? ''; + if (ssid) { + return { + type: 'wifi', + ssid, + password: fields.get('P') ?? '', + security: fields.get('T') ?? '', + hidden: (fields.get('H') ?? '').toLowerCase() === 'true', + }; + } + } + + if (/^BEGIN:VCARD/i.test(text.trim())) { + // 展開 RFC 摺行(換行後接空白 = 前行延續),再逐行取欄位 + const unfolded = text.replace(/\r\n/g, '\n').replace(/\n[ \t]/g, ''); + const field = (key: string) => { + const m = unfolded.match(new RegExp(`^${key}(?:;[^:\\n]*)?:(.*)$`, 'im')); + return m ? unescapeVcard(m[1].trim()) : ''; + }; + return { + type: 'vcard', + name: field('FN'), + phone: field('TEL'), + email: field('EMAIL'), + org: field('ORG'), + title: field('TITLE'), + url: field('URL'), + }; + } + + return { type: 'text' }; +} diff --git a/src/pages/en/tools/qr-scanner.astro b/src/pages/en/tools/qr-scanner.astro new file mode 100644 index 0000000..f495d02 --- /dev/null +++ b/src/pages/en/tools/qr-scanner.astro @@ -0,0 +1,12 @@ +--- +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import QrScanner from '../../../tools/QrScanner.tsx'; +import { tools } from '../../../data/tools'; + +const tool = tools.find((t) => t.id === 'qr-scanner')!; +--- + + ← All tools +

{tool.titleEn}

+ + diff --git a/src/pages/tools/qr-scanner.astro b/src/pages/tools/qr-scanner.astro new file mode 100644 index 0000000..ae59e28 --- /dev/null +++ b/src/pages/tools/qr-scanner.astro @@ -0,0 +1,9 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import QrScanner from '../../tools/QrScanner.tsx'; +--- + + ← 回到所有工具 +

QR 掃描

+ +
diff --git a/src/tools/QrScanner.tsx b/src/tools/QrScanner.tsx new file mode 100644 index 0000000..288d6ff --- /dev/null +++ b/src/tools/QrScanner.tsx @@ -0,0 +1,328 @@ +import { useEffect, useRef, useState } from 'react'; +import jsQR from 'jsqr'; +import Tabs from '../components/Tabs'; +import CopyButton from '../components/CopyButton'; +import { classifyQrContent, type QrContent } from '../lib/qrScan'; +import type { Locale } from '../lib/i18n'; + +const L = { + zh: { + tabImage: '圖片掃描', + tabCamera: '相機掃描', + dropHint: '點擊選擇、拖放或貼上(Ctrl+V)含 QR 碼的圖片', + fileAria: '上傳 QR 圖片', + notFound: '這張圖片裡找不到 QR 碼,換張清晰一點的試試。', + decodeError: '圖片讀取失敗,請確認檔案是有效的圖片。', + camStart: '開啟相機掃描', + camStop: '停止相機', + camRescan: '重新掃描', + camAria: '相機預覽', + camDenied: '相機權限被拒絕。請在瀏覽器網址列的權限設定允許相機後重試。', + camMissing: '找不到可用的相機。', + camHint: '把 QR 碼對準畫面,對到就會自動停下來。', + resultLabel: '掃描結果', + typeNames: { url: '網址', wifi: 'WiFi', vcard: '名片', text: '文字' } as Record, + openLink: '開啟連結 ↗', + wifiSsid: '網路名稱', + wifiPassword: '密碼', + wifiSecurity: '加密方式', + wifiHidden: '隱藏網路', + yes: '是', + vName: '姓名', + vPhone: '電話', + vEmail: 'Email', + vOrg: '公司', + vTitle: '職稱', + vUrl: '網站', + rawLabel: '原始內容', + privacy: '解碼完全在你的瀏覽器本機進行,圖片與相機畫面都不會上傳。', + }, + en: { + tabImage: 'From image', + tabCamera: 'Camera', + dropHint: 'Click, drag & drop, or paste (Ctrl+V) an image containing a QR code', + fileAria: 'Upload QR image', + notFound: 'No QR code found in this image — try a sharper one.', + decodeError: 'Could not read the image. Make sure it is a valid image file.', + camStart: 'Start camera scan', + camStop: 'Stop camera', + camRescan: 'Scan again', + camAria: 'Camera preview', + camDenied: 'Camera permission denied. Allow camera access in your browser and retry.', + camMissing: 'No camera available.', + camHint: 'Point the QR code at the frame — it stops automatically on a hit.', + resultLabel: 'Scan result', + typeNames: { url: 'URL', wifi: 'WiFi', vcard: 'vCard', text: 'Text' } as Record, + openLink: 'Open link ↗', + wifiSsid: 'Network name', + wifiPassword: 'Password', + wifiSecurity: 'Security', + wifiHidden: 'Hidden network', + yes: 'Yes', + vName: 'Name', + vPhone: 'Phone', + vEmail: 'Email', + vOrg: 'Organization', + vTitle: 'Title', + vUrl: 'Website', + rawLabel: 'Raw content', + privacy: 'Decoding happens entirely in your browser — images and camera frames are never uploaded.', + }, +} as const; +type Dict = (typeof L)[Locale]; + +interface Scan { + text: string; + content: QrContent; +} + +const MAX_DECODE_SIZE = 1024; // 大圖先縮到這個邊長再解,太大反而讓 jsQR 變慢又難對焦 + +function decodeImageData(data: ImageData): string | null { + return jsQR(data.data, data.width, data.height, { inversionAttempts: 'attemptBoth' })?.data ?? null; +} + +async function decodeImageFile(file: File): Promise { + const bitmap = await createImageBitmap(file); + try { + const scale = Math.min(1, MAX_DECODE_SIZE / Math.max(bitmap.width, bitmap.height)); + const w = Math.max(1, Math.round(bitmap.width * scale)); + const h = Math.max(1, Math.round(bitmap.height * scale)); + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d', { willReadFrequently: true })!; + ctx.drawImage(bitmap, 0, 0, w, h); + return decodeImageData(ctx.getImageData(0, 0, w, h)); + } finally { + bitmap.close(); + } +} + +function Row({ label, value, copyable = false }: { label: string; value: string; copyable?: boolean }) { + if (!value) return null; + return ( +
+
{label}
+
+ {value} + {copyable && } +
+
+ ); +} + +function ResultCard({ scan, t }: { scan: Scan; t: Dict }) { + const c = scan.content; + return ( +
+
+ {t.resultLabel} + {t.typeNames[c.type]} +
+ + {c.type === 'url' && ( + + {t.openLink} + + )} + {c.type === 'wifi' && ( +
+ + + + {c.hidden && } +
+ )} + {c.type === 'vcard' && ( +
+ + + + + + +
+ )} + +
+
+ {t.rawLabel} + +
+
{scan.text}
+
+
+ ); +} + +function ImagePanel({ t, onScan }: { t: Dict; onScan: (s: Scan | null) => void }) { + const [error, setError] = useState(''); + const [dragging, setDragging] = useState(false); + const inputRef = useRef(null); + + async function handleFile(file: File | undefined | null) { + if (!file) return; + setError(''); + onScan(null); + try { + const text = await decodeImageFile(file); + if (!text) { + setError(t.notFound); + return; + } + onScan({ text, content: classifyQrContent(text) }); + } catch { + setError(t.decodeError); + } + } + + useEffect(() => { + function onPaste(e: ClipboardEvent) { + const file = Array.from(e.clipboardData?.items ?? []) + .find((i) => i.type.startsWith('image/')) + ?.getAsFile(); + if (file) handleFile(file); + } + window.addEventListener('paste', onPaste); + return () => window.removeEventListener('paste', onPaste); + }); + + return ( +
+ + { + handleFile(e.target.files?.[0]); + e.target.value = ''; + }} + /> + {error &&

{error}

} +
+ ); +} + +function CameraPanel({ t, onScan }: { t: Dict; onScan: (s: Scan | null) => void }) { + const [running, setRunning] = useState(false); + const [error, setError] = useState(''); + const videoRef = useRef(null); + const streamRef = useRef(null); + const rafRef = useRef(0); + const lastTickRef = useRef(0); + + function stop() { + cancelAnimationFrame(rafRef.current); + streamRef.current?.getTracks().forEach((track) => track.stop()); + streamRef.current = null; + setRunning(false); + } + + async function start() { + setError(''); + onScan(null); + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); + streamRef.current = stream; + setRunning(true); + const video = videoRef.current!; + video.srcObject = stream; + await video.play(); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d', { willReadFrequently: true })!; + const tick = (now: number) => { + // 每 ~150ms 解一次就夠了,全速跑只會燒電 + if (now - lastTickRef.current > 150 && video.videoWidth > 0) { + lastTickRef.current = now; + const scale = Math.min(1, MAX_DECODE_SIZE / Math.max(video.videoWidth, video.videoHeight)); + canvas.width = Math.round(video.videoWidth * scale); + canvas.height = Math.round(video.videoHeight * scale); + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const text = decodeImageData(ctx.getImageData(0, 0, canvas.width, canvas.height)); + if (text) { + stop(); + onScan({ text, content: classifyQrContent(text) }); + return; + } + } + rafRef.current = requestAnimationFrame(tick); + }; + rafRef.current = requestAnimationFrame(tick); + } catch (e) { + stop(); + setError(e instanceof DOMException && e.name === 'NotAllowedError' ? t.camDenied : t.camMissing); + } + } + + useEffect(() => stop, []); + + return ( +
+
+ ); +} + +export default function QrScanner({ locale = 'zh' }: { locale?: Locale }) { + const t = L[locale]; + const [tab, setTab] = useState('image'); + const [scan, setScan] = useState(null); + + return ( +
+
+ { + setTab(id); + setScan(null); + }} + /> +
+ {tab === 'image' && } + {tab === 'camera' && } + {scan && } +

{t.privacy}

+
+ ); +}