decodeQrImage.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import jsQR from 'jsqr'
  2. export const CANVAS_ID = 'scanDecodeCanvas'
  3. const RETRY_SIZES = [600, 400, 800]
  4. const MAX_CANVAS = 1200
  5. function prepareImagePath(path) {
  6. // #ifdef APP-PLUS
  7. return plus.io.convertLocalFileSystemURL(path)
  8. // #endif
  9. // #ifndef APP-PLUS
  10. return path
  11. // #endif
  12. }
  13. /** 灰度二值化,消除屏幕摩尔纹、反光 */
  14. function binarizeImageData(dataArr) {
  15. const data = new Uint8ClampedArray(dataArr)
  16. for (let i = 0; i < data.length; i += 4) {
  17. const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114
  18. const val = gray > 130 ? 255 : 0
  19. data[i] = val
  20. data[i + 1] = val
  21. data[i + 2] = val
  22. }
  23. return data
  24. }
  25. function decodeFromPixels(data, width, height) {
  26. const cleanData = binarizeImageData(data)
  27. return jsQR(cleanData, width, height, { inversionAttempts: 'attemptBoth' })
  28. }
  29. function calcDrawSize(width, height, maxSide) {
  30. const longest = Math.max(width, height)
  31. const scale = Math.min(1, maxSide / longest, MAX_CANVAS / longest)
  32. return {
  33. width: Math.max(1, Math.floor(width * scale)),
  34. height: Math.max(1, Math.floor(height * scale)),
  35. }
  36. }
  37. function compressImage(path, maxSize) {
  38. const quality = maxSize < 500 ? 70 : 90
  39. return new Promise((resolve) => {
  40. uni.getImageInfo({
  41. src: prepareImagePath(path),
  42. success: (info) => {
  43. const longest = Math.max(info.width, info.height)
  44. const scale = longest > maxSize ? maxSize / longest : 1
  45. const compressedWidth = Math.max(1, Math.floor(info.width * scale))
  46. const compressedHeight = Math.max(1, Math.floor(info.height * scale))
  47. uni.compressImage({
  48. src: prepareImagePath(path),
  49. quality,
  50. compressedWidth,
  51. compressedHeight,
  52. success: (res) => resolve(res.tempFilePath),
  53. fail: () => resolve(path),
  54. })
  55. },
  56. fail: () => {
  57. uni.compressImage({
  58. src: prepareImagePath(path),
  59. quality,
  60. width: `${maxSize}px`,
  61. success: (res) => resolve(res.tempFilePath),
  62. fail: () => resolve(path),
  63. })
  64. },
  65. })
  66. })
  67. }
  68. function getComponentScope(component) {
  69. return component?.proxy || component
  70. }
  71. function decodeByLegacyCanvas(imagePath, component, maxSide) {
  72. const scope = getComponentScope(component)
  73. return new Promise((resolve, reject) => {
  74. uni.getImageInfo({
  75. src: prepareImagePath(imagePath),
  76. success: (info) => {
  77. const { width, height } = calcDrawSize(info.width, info.height, maxSide)
  78. const ctx = uni.createCanvasContext(CANVAS_ID, scope)
  79. ctx.drawImage(info.path, 0, 0, width, height)
  80. ctx.draw(false, () => {
  81. uni.canvasGetImageData({
  82. canvasId: CANVAS_ID,
  83. x: 0,
  84. y: 0,
  85. width,
  86. height,
  87. success: (res) => {
  88. const code = decodeFromPixels(
  89. new Uint8ClampedArray(res.data),
  90. res.width,
  91. res.height,
  92. )
  93. if (code?.data) {
  94. resolve(code.data)
  95. } else {
  96. reject(new Error('decode failed'))
  97. }
  98. },
  99. fail: reject,
  100. })
  101. })
  102. },
  103. fail: reject,
  104. })
  105. })
  106. }
  107. // #ifdef H5
  108. function decodeByCanvas2d(imagePath, component, maxSide) {
  109. const scope = getComponentScope(component)
  110. return new Promise((resolve, reject) => {
  111. uni.getImageInfo({
  112. src: prepareImagePath(imagePath),
  113. success: (info) => {
  114. const query = uni.createSelectorQuery().in(scope)
  115. query
  116. .select(`#${CANVAS_ID}`)
  117. .fields({ node: true, size: true })
  118. .exec((res) => {
  119. const canvas = res?.[0]?.node
  120. if (!canvas || typeof canvas.getContext !== 'function') {
  121. reject(new Error('canvas 2d not supported'))
  122. return
  123. }
  124. const { width, height } = calcDrawSize(info.width, info.height, maxSide)
  125. const ctx = canvas.getContext('2d', { willReadFrequently: true })
  126. const image = canvas.createImage()
  127. canvas.width = width
  128. canvas.height = height
  129. image.onload = () => {
  130. ctx.clearRect(0, 0, width, height)
  131. ctx.drawImage(image, 0, 0, width, height)
  132. const imageData = ctx.getImageData(0, 0, width, height)
  133. const code = decodeFromPixels(imageData.data, width, height)
  134. if (code?.data) {
  135. resolve(code.data)
  136. } else {
  137. reject(new Error('decode failed'))
  138. }
  139. }
  140. image.onerror = () => reject(new Error('image load failed'))
  141. image.src = info.path
  142. })
  143. },
  144. fail: reject,
  145. })
  146. })
  147. }
  148. // #endif
  149. async function decodeOnce(imagePath, component, maxSide) {
  150. // #ifdef H5
  151. try {
  152. return await decodeByCanvas2d(imagePath, component, maxSide)
  153. } catch {
  154. return decodeByLegacyCanvas(imagePath, component, maxSide)
  155. }
  156. // #endif
  157. // #ifndef H5
  158. return decodeByLegacyCanvas(imagePath, component, maxSide)
  159. // #endif
  160. }
  161. export async function decodeQrFromAlbumImage(path, component) {
  162. if (!component) {
  163. throw new Error('component required')
  164. }
  165. let lastError = new Error('decode failed')
  166. for (const size of RETRY_SIZES) {
  167. try {
  168. const compressedPath = await compressImage(path, size)
  169. return await decodeOnce(compressedPath, component, size)
  170. } catch (err) {
  171. lastError = err
  172. }
  173. }
  174. throw lastError
  175. }