| | |
| | | import QRCode from "qrcode"; |
| | | |
| | | const QRCODE_KEY = "recycleQrcodeBase64"; |
| | | function u32(buf: Uint8Array, offset: number, value: number) { |
| | | buf[offset] = (value >>> 24) & 255; |
| | | buf[offset + 1] = (value >>> 16) & 255; |
| | | buf[offset + 2] = (value >>> 8) & 255; |
| | | buf[offset + 3] = value & 255; |
| | | } |
| | | |
| | | export function generateQrcode(text: string): Promise<string> { |
| | | return new Promise((resolve) => { |
| | | QRCode.toDataURL( |
| | | text, |
| | | { |
| | | width: 300, |
| | | margin: 2, |
| | | }, |
| | | (err, url) => { |
| | | if (err) { |
| | | console.error("❌ 二维码生成失败", err); |
| | | resolve(""); |
| | | return; |
| | | } |
| | | resolve(url); |
| | | }, |
| | | ); |
| | | function crc32(data: Uint8Array) { |
| | | let c = 0xffffffff; |
| | | for (let i = 0; i < data.length; i++) { |
| | | c ^= data[i]; |
| | | for (let k = 0; k < 8; k++) { |
| | | c = (c >>> 1) ^ (c & 1 ? 0xedb88320 : 0); |
| | | } |
| | | } |
| | | return (c ^ 0xffffffff) >>> 0; |
| | | } |
| | | |
| | | function adler32(data: Uint8Array) { |
| | | let a = 1; |
| | | let b = 0; |
| | | for (let i = 0; i < data.length; i++) { |
| | | a = (a + data[i]) % 65521; |
| | | b = (b + a) % 65521; |
| | | } |
| | | return ((b << 16) | a) >>> 0; |
| | | } |
| | | |
| | | function chunk(type: string, data: Uint8Array) { |
| | | const out = new Uint8Array(12 + data.length); |
| | | u32(out, 0, data.length); |
| | | out[4] = type.charCodeAt(0); |
| | | out[5] = type.charCodeAt(1); |
| | | out[6] = type.charCodeAt(2); |
| | | out[7] = type.charCodeAt(3); |
| | | out.set(data, 8); |
| | | u32(out, 8 + data.length, crc32(out.subarray(4, 8 + data.length))); |
| | | return out; |
| | | } |
| | | |
| | | function concat(parts: Uint8Array[]) { |
| | | const len = parts.reduce((sum, part) => sum + part.length, 0); |
| | | const out = new Uint8Array(len); |
| | | let offset = 0; |
| | | for (const part of parts) { |
| | | out.set(part, offset); |
| | | offset += part.length; |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | function zlibStore(raw: Uint8Array) { |
| | | const blocks: Uint8Array[] = []; |
| | | let offset = 0; |
| | | while (offset < raw.length) { |
| | | const len = Math.min(65535, raw.length - offset); |
| | | const last = offset + len >= raw.length; |
| | | const block = new Uint8Array(5 + len); |
| | | block[0] = last ? 1 : 0; |
| | | block[1] = len & 255; |
| | | block[2] = (len >>> 8) & 255; |
| | | const nlen = (~len) & 0xffff; |
| | | block[3] = nlen & 255; |
| | | block[4] = (nlen >>> 8) & 255; |
| | | block.set(raw.subarray(offset, offset + len), 5); |
| | | blocks.push(block); |
| | | offset += len; |
| | | } |
| | | const bodyLen = blocks.reduce((sum, block) => sum + block.length, 0); |
| | | const out = new Uint8Array(2 + bodyLen + 4); |
| | | out[0] = 0x78; |
| | | out[1] = 0x01; |
| | | let cursor = 2; |
| | | for (const block of blocks) { |
| | | out.set(block, cursor); |
| | | cursor += block.length; |
| | | } |
| | | u32(out, cursor, adler32(raw)); |
| | | return out; |
| | | } |
| | | |
| | | function bytesToBase64(bytes: Uint8Array) { |
| | | const api = typeof uni === "undefined" ? undefined : uni; |
| | | if (api && typeof api.arrayBufferToBase64 === "function") { |
| | | const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); |
| | | return api.arrayBufferToBase64(buffer as ArrayBuffer); |
| | | } |
| | | const table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
| | | let out = ""; |
| | | for (let i = 0; i < bytes.length; i += 3) { |
| | | const a = bytes[i]; |
| | | const b = i + 1 < bytes.length ? bytes[i + 1] : 0; |
| | | const c = i + 2 < bytes.length ? bytes[i + 2] : 0; |
| | | const n = (a << 16) | (b << 8) | c; |
| | | out += table[(n >> 18) & 63]; |
| | | out += table[(n >> 12) & 63]; |
| | | out += i + 1 < bytes.length ? table[(n >> 6) & 63] : "="; |
| | | out += i + 2 < bytes.length ? table[n & 63] : "="; |
| | | } |
| | | return out; |
| | | } |
| | | |
| | | /** 不依赖浏览器 canvas,直接画出可在小程序 image 中显示的 PNG。 */ |
| | | export function createQrPng(text: string) { |
| | | const qr = QRCode.create(text, { errorCorrectionLevel: "M" }); |
| | | const modules = qr.modules; |
| | | const count = modules.size; |
| | | const margin = 4; |
| | | const scale = 8; |
| | | const size = (count + margin * 2) * scale; |
| | | const raw = new Uint8Array(size * (size + 1)); |
| | | let cursor = 0; |
| | | for (let y = 0; y < size; y++) { |
| | | raw[cursor++] = 0; |
| | | const row = Math.floor(y / scale) - margin; |
| | | for (let x = 0; x < size; x++) { |
| | | const col = Math.floor(x / scale) - margin; |
| | | const dark = |
| | | row >= 0 && col >= 0 && row < count && col < count && !!modules.get(row, col); |
| | | raw[cursor++] = dark ? 0 : 255; |
| | | } |
| | | } |
| | | const ihdr = new Uint8Array(13); |
| | | u32(ihdr, 0, size); |
| | | u32(ihdr, 4, size); |
| | | ihdr[8] = 8; |
| | | const signature = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]); |
| | | return concat([ |
| | | signature, |
| | | chunk("IHDR", ihdr), |
| | | chunk("IDAT", zlibStore(raw)), |
| | | chunk("IEND", new Uint8Array(0)), |
| | | ]); |
| | | } |
| | | |
| | | function fileName(text: string) { |
| | | let hash = 0; |
| | | for (let i = 0; i < text.length; i++) { |
| | | hash = (hash * 31 + text.charCodeAt(i)) >>> 0; |
| | | } |
| | | return hash.toString(16); |
| | | } |
| | | |
| | | function savePng(bytes: Uint8Array, text: string) { |
| | | const base64 = bytesToBase64(bytes); |
| | | const dataUrl = `data:image/png;base64,${base64}`; |
| | | const root = (globalThis as any).wx?.env?.USER_DATA_PATH; |
| | | if (!root || typeof uni === "undefined" || typeof uni.getFileSystemManager !== "function") { |
| | | return Promise.resolve(dataUrl); |
| | | } |
| | | const filePath = `${root}/recycle-qr-${fileName(text)}.png`; |
| | | return new Promise<string>((resolve) => { |
| | | uni.getFileSystemManager().writeFile({ |
| | | filePath, |
| | | data: base64, |
| | | encoding: "base64", |
| | | success: () => resolve(filePath), |
| | | fail: () => resolve(dataUrl), |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | export function generateRecycleQrcode(text: string): Promise<string> { |
| | | return new Promise((resolve) => { |
| | | const cached = uni.getStorageSync(QRCODE_KEY); |
| | | if (cached) return resolve(cached); |
| | | export function generateQrcode(text: string) { |
| | | return savePng(createQrPng(text), text); |
| | | } |
| | | |
| | | generateQrcode(text).then((url) => { |
| | | if (url) { |
| | | uni.setStorageSync(QRCODE_KEY, url); |
| | | } |
| | | resolve(url); |
| | | }); |
| | | }); |
| | | export function generateRecycleQrcode(text: string) { |
| | | return generateQrcode(text); |
| | | } |
| | | |
| | | /** |
| | |
| | | export function clearQrcodeCache(key: string) { |
| | | uni.removeStorageSync(key); |
| | | } |
| | | |
| | | /** |
| | | * 清除所有二维码缓存 |
| | | */ |
| | | export function clearAllQrcodeCache() { |
| | | uni.clearStorageSync(); |
| | | } |