import jsQR from 'jsqr' export const CANVAS_ID = 'scanDecodeCanvas' const RETRY_SIZES = [600, 400, 800] const MAX_CANVAS = 1200 function prepareImagePath(path) { // #ifdef APP-PLUS return plus.io.convertLocalFileSystemURL(path) // #endif // #ifndef APP-PLUS return path // #endif } /** 灰度二值化,消除屏幕摩尔纹、反光 */ function binarizeImageData(dataArr) { const data = new Uint8ClampedArray(dataArr) for (let i = 0; i < data.length; i += 4) { const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114 const val = gray > 130 ? 255 : 0 data[i] = val data[i + 1] = val data[i + 2] = val } return data } function decodeFromPixels(data, width, height) { const cleanData = binarizeImageData(data) return jsQR(cleanData, width, height, { inversionAttempts: 'attemptBoth' }) } function calcDrawSize(width, height, maxSide) { const longest = Math.max(width, height) const scale = Math.min(1, maxSide / longest, MAX_CANVAS / longest) return { width: Math.max(1, Math.floor(width * scale)), height: Math.max(1, Math.floor(height * scale)), } } function compressImage(path, maxSize) { const quality = maxSize < 500 ? 70 : 90 return new Promise((resolve) => { uni.getImageInfo({ src: prepareImagePath(path), success: (info) => { const longest = Math.max(info.width, info.height) const scale = longest > maxSize ? maxSize / longest : 1 const compressedWidth = Math.max(1, Math.floor(info.width * scale)) const compressedHeight = Math.max(1, Math.floor(info.height * scale)) uni.compressImage({ src: prepareImagePath(path), quality, compressedWidth, compressedHeight, success: (res) => resolve(res.tempFilePath), fail: () => resolve(path), }) }, fail: () => { uni.compressImage({ src: prepareImagePath(path), quality, width: `${maxSize}px`, success: (res) => resolve(res.tempFilePath), fail: () => resolve(path), }) }, }) }) } function getComponentScope(component) { return component?.proxy || component } function decodeByLegacyCanvas(imagePath, component, maxSide) { const scope = getComponentScope(component) return new Promise((resolve, reject) => { uni.getImageInfo({ src: prepareImagePath(imagePath), success: (info) => { const { width, height } = calcDrawSize(info.width, info.height, maxSide) const ctx = uni.createCanvasContext(CANVAS_ID, scope) ctx.drawImage(info.path, 0, 0, width, height) ctx.draw(false, () => { uni.canvasGetImageData({ canvasId: CANVAS_ID, x: 0, y: 0, width, height, success: (res) => { const code = decodeFromPixels( new Uint8ClampedArray(res.data), res.width, res.height, ) if (code?.data) { resolve(code.data) } else { reject(new Error('decode failed')) } }, fail: reject, }) }) }, fail: reject, }) }) } // #ifdef H5 function decodeByCanvas2d(imagePath, component, maxSide) { const scope = getComponentScope(component) return new Promise((resolve, reject) => { uni.getImageInfo({ src: prepareImagePath(imagePath), success: (info) => { const query = uni.createSelectorQuery().in(scope) query .select(`#${CANVAS_ID}`) .fields({ node: true, size: true }) .exec((res) => { const canvas = res?.[0]?.node if (!canvas || typeof canvas.getContext !== 'function') { reject(new Error('canvas 2d not supported')) return } const { width, height } = calcDrawSize(info.width, info.height, maxSide) const ctx = canvas.getContext('2d', { willReadFrequently: true }) const image = canvas.createImage() canvas.width = width canvas.height = height image.onload = () => { ctx.clearRect(0, 0, width, height) ctx.drawImage(image, 0, 0, width, height) const imageData = ctx.getImageData(0, 0, width, height) const code = decodeFromPixels(imageData.data, width, height) if (code?.data) { resolve(code.data) } else { reject(new Error('decode failed')) } } image.onerror = () => reject(new Error('image load failed')) image.src = info.path }) }, fail: reject, }) }) } // #endif async function decodeOnce(imagePath, component, maxSide) { // #ifdef H5 try { return await decodeByCanvas2d(imagePath, component, maxSide) } catch { return decodeByLegacyCanvas(imagePath, component, maxSide) } // #endif // #ifndef H5 return decodeByLegacyCanvas(imagePath, component, maxSide) // #endif } export async function decodeQrFromAlbumImage(path, component) { if (!component) { throw new Error('component required') } let lastError = new Error('decode failed') for (const size of RETRY_SIZES) { try { const compressedPath = await compressImage(path, size) return await decodeOnce(compressedPath, component, size) } catch (err) { lastError = err } } throw lastError }