| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- 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
- }
|