@sythos/js_barcode_universal 1.5.7 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/LICENSE +5 -4
  2. package/NOTICE.md +2 -2
  3. package/README.md +50 -37
  4. package/bundle/sythos-barcode.esm.js +12051 -11484
  5. package/bundle/sythos-barcode.js +12051 -11484
  6. package/package.json +52 -45
  7. package/src/index.d.ts +363 -0
  8. package/src/index.js +573 -428
  9. package/src/js/aztec/decoder.js +326 -0
  10. package/src/js/aztec/detector.js +251 -0
  11. package/src/js/aztec/encoder.js +276 -0
  12. package/src/js/aztec/high-level.js +216 -0
  13. package/src/js/aztec/index.js +34 -0
  14. package/src/js/aztec/tables.js +196 -0
  15. package/src/js/aztecrune/decoder.js +146 -0
  16. package/src/js/aztecrune/detector.js +180 -0
  17. package/src/js/aztecrune/encoder.js +103 -0
  18. package/src/js/aztecrune/index.js +34 -0
  19. package/src/js/aztecrune/tables.js +142 -0
  20. package/src/js/compactpdf417/decoder.js +123 -0
  21. package/src/js/compactpdf417/detector.js +138 -0
  22. package/src/js/compactpdf417/encoder.js +127 -0
  23. package/src/js/compactpdf417/index.js +5 -0
  24. package/src/js/compactpdf417/tables.js +164 -0
  25. package/src/js/core/bit-buffer.js +158 -0
  26. package/src/js/core/bit-matrix.js +243 -0
  27. package/src/js/core/errors.js +59 -0
  28. package/src/js/core/galois-field.js +193 -0
  29. package/src/js/core/index.js +39 -0
  30. package/src/js/core/reed-solomon.js +318 -0
  31. package/src/js/databar/codec.js +166 -0
  32. package/src/js/databar/decoder.js +224 -0
  33. package/src/js/databar/encoder.js +88 -0
  34. package/src/js/databar/gs1.js +176 -0
  35. package/src/js/databar/index.js +36 -0
  36. package/src/js/databar/patterns.js +139 -0
  37. package/src/js/databar/tables.js +123 -0
  38. package/src/js/datamatrix/decoder.js +296 -0
  39. package/src/js/datamatrix/detector.js +254 -0
  40. package/src/js/datamatrix/encoder.js +250 -0
  41. package/src/js/datamatrix/index.js +34 -0
  42. package/src/js/datamatrix/tables.js +122 -0
  43. package/src/js/frameqr/decoder.js +216 -0
  44. package/src/js/frameqr/detector.js +199 -0
  45. package/src/js/frameqr/encoder.js +143 -0
  46. package/src/js/frameqr/index.js +33 -0
  47. package/src/js/frameqr/tables.js +252 -0
  48. package/src/js/image/binarizer.js +254 -0
  49. package/src/js/image/grid-sampler.js +148 -0
  50. package/src/js/image/index.js +38 -0
  51. package/src/js/image/luminance.js +185 -0
  52. package/src/js/image/perspective.js +158 -0
  53. package/src/js/micropdf417/compaction.js +115 -0
  54. package/src/js/micropdf417/decoder.js +180 -0
  55. package/src/js/micropdf417/detector.js +152 -0
  56. package/src/js/micropdf417/encoder.js +184 -0
  57. package/src/js/micropdf417/error-correction.js +50 -0
  58. package/src/js/micropdf417/index.js +35 -0
  59. package/src/js/micropdf417/tables.js +197 -0
  60. package/src/js/microqr/decoder.js +240 -0
  61. package/src/js/microqr/detector.js +339 -0
  62. package/src/js/microqr/encoder.js +314 -0
  63. package/src/js/microqr/index.js +34 -0
  64. package/src/js/microqr/tables.js +324 -0
  65. package/src/js/oned/addons.js +396 -0
  66. package/src/js/oned/index.js +75 -0
  67. package/src/js/oned/patterns.js +365 -0
  68. package/src/js/oned/reader.js +1383 -0
  69. package/src/js/oned/writers.js +691 -0
  70. package/src/js/pdf417/compaction.js +391 -0
  71. package/src/js/pdf417/decoder.js +110 -0
  72. package/src/js/pdf417/detector.js +484 -0
  73. package/src/js/pdf417/encoder.js +102 -0
  74. package/src/js/pdf417/error-correction.js +44 -0
  75. package/src/{pdf417 → js/pdf417}/index.js +0 -5
  76. package/src/js/pdf417/tables.js +315 -0
  77. package/src/js/qr/decoder.js +539 -0
  78. package/src/js/qr/detector.js +613 -0
  79. package/src/js/qr/encoder.js +873 -0
  80. package/src/js/qr/index.js +42 -0
  81. package/src/js/qr/tables.js +690 -0
  82. package/src/js/render/image-data.js +120 -0
  83. package/src/js/render/index.js +128 -0
  84. package/src/js/render/options.js +152 -0
  85. package/src/js/render/png.js +270 -0
  86. package/src/js/render/svg.js +117 -0
  87. package/src/js/render/webgl.js +200 -0
  88. package/src/js/render/webgpu.js +357 -0
  89. package/src/js/rmqr/decoder.js +209 -0
  90. package/src/js/rmqr/detector.js +122 -0
  91. package/src/js/rmqr/encoder.js +321 -0
  92. package/src/js/rmqr/index.js +33 -0
  93. package/src/js/rmqr/tables.js +177 -0
  94. package/src/{aztec/decoder.js → ts/aztec/decoder.ts} +0 -1
  95. package/{typings → src/ts}/aztec/detector.d.ts +2 -2
  96. package/src/{aztec/detector.js → ts/aztec/detector.ts} +0 -1
  97. package/src/{aztec/encoder.js → ts/aztec/encoder.ts} +0 -1
  98. package/src/{aztec/high-level.js → ts/aztec/high-level.ts} +0 -1
  99. package/src/{aztec/index.js → ts/aztec/index.ts} +0 -1
  100. package/src/{aztec/tables.js → ts/aztec/tables.ts} +0 -1
  101. package/src/{aztecrune/decoder.js → ts/aztecrune/decoder.ts} +0 -1
  102. package/src/{aztecrune/detector.js → ts/aztecrune/detector.ts} +0 -1
  103. package/src/{aztecrune/encoder.js → ts/aztecrune/encoder.ts} +0 -1
  104. package/src/{aztecrune/index.js → ts/aztecrune/index.ts} +0 -1
  105. package/src/{aztecrune/tables.js → ts/aztecrune/tables.ts} +0 -1
  106. package/src/{compactpdf417/decoder.js → ts/compactpdf417/decoder.ts} +0 -1
  107. package/src/{compactpdf417/detector.js → ts/compactpdf417/detector.ts} +0 -1
  108. package/src/{compactpdf417/encoder.js → ts/compactpdf417/encoder.ts} +0 -1
  109. package/src/{compactpdf417/index.js → ts/compactpdf417/index.ts} +0 -5
  110. package/src/{compactpdf417/tables.js → ts/compactpdf417/tables.ts} +0 -1
  111. package/src/{core/bit-buffer.js → ts/core/bit-buffer.ts} +0 -1
  112. package/src/{core/bit-matrix.js → ts/core/bit-matrix.ts} +0 -1
  113. package/src/{core/errors.js → ts/core/errors.ts} +0 -1
  114. package/src/{core/galois-field.js → ts/core/galois-field.ts} +0 -1
  115. package/src/{core/index.js → ts/core/index.ts} +0 -1
  116. package/src/{core/reed-solomon.js → ts/core/reed-solomon.ts} +0 -1
  117. package/src/{databar/codec.js → ts/databar/codec.ts} +0 -1
  118. package/src/{databar/decoder.js → ts/databar/decoder.ts} +0 -1
  119. package/src/{databar/encoder.js → ts/databar/encoder.ts} +0 -1
  120. package/src/{databar/gs1.js → ts/databar/gs1.ts} +0 -1
  121. package/src/{databar/index.js → ts/databar/index.ts} +0 -1
  122. package/src/{databar/patterns.js → ts/databar/patterns.ts} +0 -1
  123. package/src/{databar/tables.js → ts/databar/tables.ts} +0 -1
  124. package/src/{datamatrix/decoder.js → ts/datamatrix/decoder.ts} +0 -1
  125. package/{typings → src/ts}/datamatrix/detector.d.ts +2 -2
  126. package/src/{datamatrix/detector.js → ts/datamatrix/detector.ts} +0 -1
  127. package/src/{datamatrix/encoder.js → ts/datamatrix/encoder.ts} +0 -1
  128. package/src/{datamatrix/index.js → ts/datamatrix/index.ts} +0 -1
  129. package/src/{datamatrix/tables.js → ts/datamatrix/tables.ts} +0 -1
  130. package/src/{frameqr/decoder.js → ts/frameqr/decoder.ts} +15 -16
  131. package/src/{frameqr/detector.js → ts/frameqr/detector.ts} +2 -3
  132. package/src/{frameqr/encoder.js → ts/frameqr/encoder.ts} +7 -8
  133. package/src/{frameqr/index.js → ts/frameqr/index.ts} +0 -1
  134. package/src/{frameqr/tables.js → ts/frameqr/tables.ts} +10 -11
  135. package/src/{image/binarizer.js → ts/image/binarizer.ts} +0 -1
  136. package/src/{image/grid-sampler.js → ts/image/grid-sampler.ts} +0 -1
  137. package/src/{image/index.js → ts/image/index.ts} +0 -1
  138. package/src/{image/luminance.js → ts/image/luminance.ts} +0 -1
  139. package/src/{image/perspective.js → ts/image/perspective.ts} +0 -1
  140. package/{typings → src/ts}/index.d.ts +3 -3
  141. package/src/ts/index.ts +790 -0
  142. package/src/{micropdf417/compaction.js → ts/micropdf417/compaction.ts} +0 -1
  143. package/src/{micropdf417/decoder.js → ts/micropdf417/decoder.ts} +0 -1
  144. package/src/{micropdf417/detector.js → ts/micropdf417/detector.ts} +0 -1
  145. package/src/{micropdf417/encoder.js → ts/micropdf417/encoder.ts} +0 -1
  146. package/src/{micropdf417/error-correction.js → ts/micropdf417/error-correction.ts} +0 -1
  147. package/src/{micropdf417/index.js → ts/micropdf417/index.ts} +0 -1
  148. package/src/{micropdf417/tables.js → ts/micropdf417/tables.ts} +0 -1
  149. package/src/{microqr/decoder.js → ts/microqr/decoder.ts} +0 -1
  150. package/src/{microqr/detector.js → ts/microqr/detector.ts} +0 -1
  151. package/src/{microqr/encoder.js → ts/microqr/encoder.ts} +0 -1
  152. package/src/{microqr/index.js → ts/microqr/index.ts} +0 -1
  153. package/src/{microqr/tables.js → ts/microqr/tables.ts} +0 -1
  154. package/src/{oned/addons.js → ts/oned/addons.ts} +0 -1
  155. package/src/{oned/index.js → ts/oned/index.ts} +0 -1
  156. package/src/{oned/patterns.js → ts/oned/patterns.ts} +0 -1
  157. package/src/{oned/reader.js → ts/oned/reader.ts} +0 -1
  158. package/src/{oned/writers.js → ts/oned/writers.ts} +0 -1
  159. package/src/{pdf417/compaction.js → ts/pdf417/compaction.ts} +0 -1
  160. package/src/{pdf417/decoder.js → ts/pdf417/decoder.ts} +0 -1
  161. package/src/{pdf417/detector.js → ts/pdf417/detector.ts} +0 -1
  162. package/src/{pdf417/encoder.js → ts/pdf417/encoder.ts} +0 -1
  163. package/src/{pdf417/error-correction.js → ts/pdf417/error-correction.ts} +0 -1
  164. package/src/ts/pdf417/index.ts +6 -0
  165. package/src/{pdf417/tables.js → ts/pdf417/tables.ts} +0 -1
  166. package/src/{qr/decoder.js → ts/qr/decoder.ts} +0 -1
  167. package/src/{qr/detector.js → ts/qr/detector.ts} +0 -1
  168. package/src/{qr/encoder.js → ts/qr/encoder.ts} +0 -1
  169. package/src/{qr/index.js → ts/qr/index.ts} +0 -1
  170. package/src/{qr/tables.js → ts/qr/tables.ts} +0 -1
  171. package/src/{render/image-data.js → ts/render/image-data.ts} +0 -1
  172. package/src/{render/index.js → ts/render/index.ts} +0 -1
  173. package/src/{render/options.js → ts/render/options.ts} +0 -1
  174. package/src/{render/png.js → ts/render/png.ts} +0 -1
  175. package/src/{render/svg.js → ts/render/svg.ts} +0 -1
  176. package/src/{render/webgl.js → ts/render/webgl.ts} +0 -1
  177. package/src/{render/webgpu.js → ts/render/webgpu.ts} +0 -1
  178. package/src/{rmqr/decoder.js → ts/rmqr/decoder.ts} +0 -1
  179. package/src/{rmqr/detector.js → ts/rmqr/detector.ts} +0 -1
  180. package/src/{rmqr/encoder.js → ts/rmqr/encoder.ts} +0 -1
  181. package/src/{rmqr/index.js → ts/rmqr/index.ts} +0 -1
  182. package/src/{rmqr/tables.js → ts/rmqr/tables.ts} +0 -1
  183. /package/{typings → src/ts}/aztec/decoder.d.ts +0 -0
  184. /package/{typings → src/ts}/aztec/encoder.d.ts +0 -0
  185. /package/{typings → src/ts}/aztec/high-level.d.ts +0 -0
  186. /package/{typings → src/ts}/aztec/index.d.ts +0 -0
  187. /package/{typings → src/ts}/aztec/tables.d.ts +0 -0
  188. /package/{typings → src/ts}/aztecrune/decoder.d.ts +0 -0
  189. /package/{typings → src/ts}/aztecrune/detector.d.ts +0 -0
  190. /package/{typings → src/ts}/aztecrune/encoder.d.ts +0 -0
  191. /package/{typings → src/ts}/aztecrune/index.d.ts +0 -0
  192. /package/{typings → src/ts}/aztecrune/tables.d.ts +0 -0
  193. /package/{typings → src/ts}/bundle/sythos-barcode.d.ts +0 -0
  194. /package/{typings → src/ts}/bundle/sythos-barcode.esm.d.ts +0 -0
  195. /package/{typings → src/ts}/compactpdf417/decoder.d.ts +0 -0
  196. /package/{typings → src/ts}/compactpdf417/detector.d.ts +0 -0
  197. /package/{typings → src/ts}/compactpdf417/encoder.d.ts +0 -0
  198. /package/{typings → src/ts}/compactpdf417/index.d.ts +0 -0
  199. /package/{typings → src/ts}/compactpdf417/tables.d.ts +0 -0
  200. /package/{typings → src/ts}/core/bit-buffer.d.ts +0 -0
  201. /package/{typings → src/ts}/core/bit-matrix.d.ts +0 -0
  202. /package/{typings → src/ts}/core/errors.d.ts +0 -0
  203. /package/{typings → src/ts}/core/galois-field.d.ts +0 -0
  204. /package/{typings → src/ts}/core/index.d.ts +0 -0
  205. /package/{typings → src/ts}/core/reed-solomon.d.ts +0 -0
  206. /package/{typings → src/ts}/databar/codec.d.ts +0 -0
  207. /package/{typings → src/ts}/databar/decoder.d.ts +0 -0
  208. /package/{typings → src/ts}/databar/encoder.d.ts +0 -0
  209. /package/{typings → src/ts}/databar/gs1.d.ts +0 -0
  210. /package/{typings → src/ts}/databar/index.d.ts +0 -0
  211. /package/{typings → src/ts}/databar/patterns.d.ts +0 -0
  212. /package/{typings → src/ts}/databar/tables.d.ts +0 -0
  213. /package/{typings → src/ts}/datamatrix/decoder.d.ts +0 -0
  214. /package/{typings → src/ts}/datamatrix/encoder.d.ts +0 -0
  215. /package/{typings → src/ts}/datamatrix/index.d.ts +0 -0
  216. /package/{typings → src/ts}/datamatrix/tables.d.ts +0 -0
  217. /package/{typings → src/ts}/frameqr/decoder.d.ts +0 -0
  218. /package/{typings → src/ts}/frameqr/detector.d.ts +0 -0
  219. /package/{typings → src/ts}/frameqr/encoder.d.ts +0 -0
  220. /package/{typings → src/ts}/frameqr/index.d.ts +0 -0
  221. /package/{typings → src/ts}/frameqr/tables.d.ts +0 -0
  222. /package/{typings → src/ts}/image/binarizer.d.ts +0 -0
  223. /package/{typings → src/ts}/image/grid-sampler.d.ts +0 -0
  224. /package/{typings → src/ts}/image/index.d.ts +0 -0
  225. /package/{typings → src/ts}/image/luminance.d.ts +0 -0
  226. /package/{typings → src/ts}/image/perspective.d.ts +0 -0
  227. /package/{typings → src/ts}/micropdf417/compaction.d.ts +0 -0
  228. /package/{typings → src/ts}/micropdf417/decoder.d.ts +0 -0
  229. /package/{typings → src/ts}/micropdf417/detector.d.ts +0 -0
  230. /package/{typings → src/ts}/micropdf417/encoder.d.ts +0 -0
  231. /package/{typings → src/ts}/micropdf417/error-correction.d.ts +0 -0
  232. /package/{typings → src/ts}/micropdf417/index.d.ts +0 -0
  233. /package/{typings → src/ts}/micropdf417/tables.d.ts +0 -0
  234. /package/{typings → src/ts}/microqr/decoder.d.ts +0 -0
  235. /package/{typings → src/ts}/microqr/detector.d.ts +0 -0
  236. /package/{typings → src/ts}/microqr/encoder.d.ts +0 -0
  237. /package/{typings → src/ts}/microqr/index.d.ts +0 -0
  238. /package/{typings → src/ts}/microqr/tables.d.ts +0 -0
  239. /package/{typings → src/ts}/oned/addons.d.ts +0 -0
  240. /package/{typings → src/ts}/oned/index.d.ts +0 -0
  241. /package/{typings → src/ts}/oned/patterns.d.ts +0 -0
  242. /package/{typings → src/ts}/oned/reader.d.ts +0 -0
  243. /package/{typings → src/ts}/oned/writers.d.ts +0 -0
  244. /package/{typings → src/ts}/pdf417/compaction.d.ts +0 -0
  245. /package/{typings → src/ts}/pdf417/decoder.d.ts +0 -0
  246. /package/{typings → src/ts}/pdf417/detector.d.ts +0 -0
  247. /package/{typings → src/ts}/pdf417/encoder.d.ts +0 -0
  248. /package/{typings → src/ts}/pdf417/error-correction.d.ts +0 -0
  249. /package/{typings → src/ts}/pdf417/index.d.ts +0 -0
  250. /package/{typings → src/ts}/pdf417/tables.d.ts +0 -0
  251. /package/{typings → src/ts}/qr/decoder.d.ts +0 -0
  252. /package/{typings → src/ts}/qr/detector.d.ts +0 -0
  253. /package/{typings → src/ts}/qr/encoder.d.ts +0 -0
  254. /package/{typings → src/ts}/qr/index.d.ts +0 -0
  255. /package/{typings → src/ts}/qr/tables.d.ts +0 -0
  256. /package/{typings → src/ts}/render/image-data.d.ts +0 -0
  257. /package/{typings → src/ts}/render/index.d.ts +0 -0
  258. /package/{typings → src/ts}/render/options.d.ts +0 -0
  259. /package/{typings → src/ts}/render/png.d.ts +0 -0
  260. /package/{typings → src/ts}/render/svg.d.ts +0 -0
  261. /package/{typings → src/ts}/render/webgl.d.ts +0 -0
  262. /package/{typings → src/ts}/render/webgpu.d.ts +0 -0
  263. /package/{typings → src/ts}/rmqr/decoder.d.ts +0 -0
  264. /package/{typings → src/ts}/rmqr/detector.d.ts +0 -0
  265. /package/{typings → src/ts}/rmqr/encoder.d.ts +0 -0
  266. /package/{typings → src/ts}/rmqr/index.d.ts +0 -0
  267. /package/{typings → src/ts}/rmqr/tables.d.ts +0 -0
@@ -0,0 +1,613 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * SPDX-License-Identifier: MIT
27
+ *
28
+ * Original work. No code from any other barcode implementation.
29
+ */
30
+ /**
31
+ * QR Code detection — finding symbols in a binarized image.
32
+ *
33
+ * The whole thing hangs off one property of the finder pattern: along any line
34
+ * through its centre, in any direction, the dark and light runs are in the
35
+ * ratio 1:1:3:1:1. That is true horizontally, vertically and diagonally, it is
36
+ * scale-independent, and no ordinary printed matter reproduces it by accident.
37
+ * So the search is: scan rows for that ratio, confirm each hit by scanning the
38
+ * column through it, cluster what survives, and look for three clusters
39
+ * arranged in a right isoceles triangle.
40
+ *
41
+ * Three finders give three corners. The fourth is the problem: extrapolating
42
+ * `topRight + bottomLeft - topLeft` assumes the symbol is a parallelogram,
43
+ * which is only true if it was photographed square-on. For version 2 and up the
44
+ * bottom-right alignment pattern pins that corner down properly, which is what
45
+ * makes a tilted symbol readable. When the alignment pattern cannot be found,
46
+ * detection degrades to the parallelogram estimate rather than failing — a
47
+ * slightly wrong corner still decodes on a flat image, and Reed-Solomon absorbs
48
+ * the rest.
49
+ *
50
+ * @module qr/detector
51
+ */
52
+ import { NotFoundError } from '../core/errors.js';
53
+ import { PerspectiveTransform } from '../image/perspective.js';
54
+ import { sampleQuad } from '../image/grid-sampler.js';
55
+ import { decodeQR } from './decoder.js';
56
+ /** Finder pattern run ratios, centre run first in the array's own order. */
57
+ const FINDER_RATIOS = [1, 1, 3, 1, 1];
58
+ /** Alignment pattern run ratios. */
59
+ const ALIGNMENT_RATIOS = [1, 1, 1, 1, 1];
60
+ /** Smallest and largest legal symbol dimensions, in modules. */
61
+ const MIN_DIMENSION = 21;
62
+ const MAX_DIMENSION = 177;
63
+ /**
64
+ * Do five alternating runs match the expected ratios?
65
+ *
66
+ * Tolerance is half a module, scaled by the ratio, which is the widest band
67
+ * that still rejects ordinary text and rules while accepting the blur and
68
+ * rounding of a real scan.
69
+ *
70
+ * @param {number[]} counts Five run lengths, dark first.
71
+ * @param {number[]} ratios
72
+ * @returns {number} The implied module size, or 0 if the ratios do not match.
73
+ */
74
+ function matchRatios(counts, ratios) {
75
+ let total = 0;
76
+ let units = 0;
77
+ for (let i = 0; i < 5; i++) {
78
+ if (counts[i] === 0)
79
+ return 0;
80
+ total += counts[i];
81
+ units += ratios[i];
82
+ }
83
+ if (total < units)
84
+ return 0;
85
+ const moduleSize = total / units;
86
+ const tolerance = moduleSize / 2;
87
+ for (let i = 0; i < 5; i++) {
88
+ if (Math.abs(counts[i] - moduleSize * ratios[i]) > tolerance * ratios[i])
89
+ return 0;
90
+ }
91
+ return moduleSize;
92
+ }
93
+ /**
94
+ * Scan one row for the finder run ratio.
95
+ *
96
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
97
+ * @param {number} y
98
+ * @param {number[]} ratios
99
+ * @param {(centreX: number, moduleSize: number) => void} onHit
100
+ */
101
+ function scanRow(image, y, ratios, onHit) {
102
+ const width = image.width;
103
+ const counts = [0, 0, 0, 0, 0];
104
+ let state = 0;
105
+ for (let x = 0; x < width; x++) {
106
+ const dark = image.get(x, y);
107
+ // Even states are dark runs, odd states light.
108
+ if (dark === ((state & 1) === 0)) {
109
+ counts[state]++;
110
+ continue;
111
+ }
112
+ // A leading light margin is not part of any pattern.
113
+ if (state === 0 && counts[0] === 0)
114
+ continue;
115
+ if (state < 4) {
116
+ state++;
117
+ counts[state] = 1;
118
+ continue;
119
+ }
120
+ // Five runs complete, and the sixth has begun.
121
+ const moduleSize = matchRatios(counts, ratios);
122
+ if (moduleSize > 0) {
123
+ onHit(x - counts[4] - counts[3] - counts[2] / 2, moduleSize);
124
+ }
125
+ // Slide the window on by two runs: the trailing dark run of a rejected
126
+ // candidate is often the leading dark run of the real one.
127
+ counts[0] = counts[2];
128
+ counts[1] = counts[3];
129
+ counts[2] = counts[4];
130
+ counts[3] = 1;
131
+ counts[4] = 0;
132
+ state = 3;
133
+ }
134
+ if (state === 4) {
135
+ const moduleSize = matchRatios(counts, ratios);
136
+ if (moduleSize > 0) {
137
+ onHit(width - counts[4] - counts[3] - counts[2] / 2, moduleSize);
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Walk a line through a candidate centre and confirm the ratio holds there too.
143
+ *
144
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
145
+ * @param {number} x @param {number} y
146
+ * @param {number} dx @param {number} dy Unit step defining the line.
147
+ * @param {number[]} ratios
148
+ * @param {number} maxRun Guard against running the length of a dark image.
149
+ * @returns {number} Refined centre offset along the line, or NaN.
150
+ */
151
+ function crossCheck(image, x, y, dx, dy, ratios, maxRun) {
152
+ const width = image.width;
153
+ const height = image.height;
154
+ /** @returns {boolean | null} Null when the sample falls outside the image. */
155
+ const at = (i) => {
156
+ const px = x + dx * i;
157
+ const py = y + dy * i;
158
+ if (px < 0 || py < 0 || px >= width || py >= height)
159
+ return null;
160
+ return image.get(px, py);
161
+ };
162
+ if (at(0) !== true)
163
+ return NaN;
164
+ const counts = [0, 0, 0, 0, 0];
165
+ let i = 0;
166
+ // Forward from the centre: rest of the centre run, then light, then dark.
167
+ while (at(i) === true && counts[2] < maxRun) {
168
+ counts[2]++;
169
+ i++;
170
+ }
171
+ if (at(i) === null)
172
+ return NaN;
173
+ const centreForward = counts[2];
174
+ while (at(i) === false && counts[3] < maxRun) {
175
+ counts[3]++;
176
+ i++;
177
+ }
178
+ if (at(i) === null || counts[3] === 0)
179
+ return NaN;
180
+ while (at(i) === true && counts[4] < maxRun) {
181
+ counts[4]++;
182
+ i++;
183
+ }
184
+ if (counts[4] === 0)
185
+ return NaN;
186
+ // Backward from the centre.
187
+ i = -1;
188
+ while (at(i) === true && counts[2] < maxRun * 2) {
189
+ counts[2]++;
190
+ i--;
191
+ }
192
+ if (at(i) === null)
193
+ return NaN;
194
+ const centreBackward = counts[2] - centreForward;
195
+ while (at(i) === false && counts[1] < maxRun) {
196
+ counts[1]++;
197
+ i--;
198
+ }
199
+ if (at(i) === null || counts[1] === 0)
200
+ return NaN;
201
+ while (at(i) === true && counts[0] < maxRun) {
202
+ counts[0]++;
203
+ i--;
204
+ }
205
+ if (counts[0] === 0)
206
+ return NaN;
207
+ if (matchRatios(counts, ratios) === 0)
208
+ return NaN;
209
+ // The centre run spans offsets [-centreBackward, centreForward - 1].
210
+ return (centreForward - 1 - centreBackward) / 2;
211
+ }
212
+ /**
213
+ * @typedef {object} Candidate
214
+ * @property {number} x
215
+ * @property {number} y
216
+ * @property {number} moduleSize
217
+ * @property {number} hits
218
+ */
219
+ /**
220
+ * Locate pattern centres of a given run ratio.
221
+ *
222
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
223
+ * @param {number[]} ratios
224
+ * @param {number} unitsWide Modules the pattern spans (7 or 5).
225
+ * @param {{x0: number, y0: number, x1: number, y1: number}} [region]
226
+ * @returns {Candidate[]}
227
+ */
228
+ function findPatterns(image, ratios, unitsWide, region) {
229
+ /** @type {Candidate[]} */
230
+ const found = [];
231
+ const y0 = region ? Math.max(0, region.y0) : 0;
232
+ const y1 = region ? Math.min(image.height, region.y1) : image.height;
233
+ for (let y = y0; y < y1; y++) {
234
+ scanRow(image, y, ratios, (centreX, moduleSize) => {
235
+ if (region && (centreX < region.x0 || centreX > region.x1))
236
+ return;
237
+ const px = Math.floor(centreX);
238
+ const maxRun = Math.ceil(moduleSize * unitsWide);
239
+ const offsetY = crossCheck(image, px, y, 0, 1, ratios, maxRun);
240
+ if (Number.isNaN(offsetY))
241
+ return;
242
+ const cy = y + offsetY;
243
+ // Re-check horizontally at the refined row, which both confirms the hit
244
+ // and gives a better x than the original scan line did.
245
+ const offsetX = crossCheck(image, px, Math.round(cy), 1, 0, ratios, maxRun);
246
+ if (Number.isNaN(offsetX))
247
+ return;
248
+ const cx = px + offsetX;
249
+ // Merge with an existing centre when they describe the same pattern.
250
+ for (let i = 0; i < found.length; i++) {
251
+ const c = found[i];
252
+ if (Math.abs(c.x - cx) <= c.moduleSize &&
253
+ Math.abs(c.y - cy) <= c.moduleSize &&
254
+ Math.abs(c.moduleSize - moduleSize) <= Math.max(1, c.moduleSize / 2)) {
255
+ const n = c.hits + 1;
256
+ c.x = (c.x * c.hits + cx) / n;
257
+ c.y = (c.y * c.hits + cy) / n;
258
+ c.moduleSize = (c.moduleSize * c.hits + moduleSize) / n;
259
+ c.hits = n;
260
+ return;
261
+ }
262
+ }
263
+ found.push({ x: cx, y: cy, moduleSize, hits: 1 });
264
+ });
265
+ }
266
+ return found;
267
+ }
268
+ /**
269
+ * @param {{x: number, y: number}} a @param {{x: number, y: number}} b
270
+ * @returns {number}
271
+ */
272
+ function distance(a, b) {
273
+ return Math.hypot(a.x - b.x, a.y - b.y);
274
+ }
275
+ /**
276
+ * Order three finder centres as top-left, top-right, bottom-left.
277
+ *
278
+ * The corner is the centre opposite the longest side. Which of the remaining
279
+ * two is "top right" follows from the sign of the cross product: in image
280
+ * coordinates, with y increasing downward, a symbol the right way round has
281
+ * (topRight - topLeft) x (bottomLeft - topLeft) positive.
282
+ *
283
+ * @param {Candidate[]} three
284
+ * @returns {{tl: Candidate, tr: Candidate, bl: Candidate} | null}
285
+ */
286
+ function orientFinders(three) {
287
+ const [a, b, c] = three;
288
+ const ab = distance(a, b);
289
+ const bc = distance(b, c);
290
+ const ca = distance(c, a);
291
+ let tl, p, q, hypotenuse, leg1, leg2;
292
+ if (ab >= bc && ab >= ca) {
293
+ tl = c;
294
+ p = a;
295
+ q = b;
296
+ hypotenuse = ab;
297
+ leg1 = ca;
298
+ leg2 = bc;
299
+ }
300
+ else if (bc >= ab && bc >= ca) {
301
+ tl = a;
302
+ p = b;
303
+ q = c;
304
+ hypotenuse = bc;
305
+ leg1 = ab;
306
+ leg2 = ca;
307
+ }
308
+ else {
309
+ tl = b;
310
+ p = c;
311
+ q = a;
312
+ hypotenuse = ca;
313
+ leg1 = bc;
314
+ leg2 = ab;
315
+ }
316
+ if (leg1 === 0 || leg2 === 0)
317
+ return null;
318
+ // The two legs must be near enough equal, and Pythagoras must hold: this is
319
+ // what rejects three unrelated finder-lookalikes that happen to co-occur.
320
+ const ratio = leg1 / leg2;
321
+ if (ratio < 0.7 || ratio > 1.4)
322
+ return null;
323
+ const expected = Math.hypot(leg1, leg2);
324
+ if (Math.abs(hypotenuse - expected) > expected * 0.25)
325
+ return null;
326
+ const cross = (p.x - tl.x) * (q.y - tl.y) - (p.y - tl.y) * (q.x - tl.x);
327
+ return cross >= 0 ? { tl, tr: p, bl: q } : { tl, tr: q, bl: p };
328
+ }
329
+ /**
330
+ * Snap a measured dimension to a legal symbol size.
331
+ *
332
+ * Every QR dimension is 17 + 4v, so `dimension % 4 === 1`. A measurement one
333
+ * off is rounding; two off means the module size estimate is wrong and the
334
+ * candidate is not worth pursuing.
335
+ *
336
+ * @param {number} raw
337
+ * @returns {number} 0 if it cannot be reconciled.
338
+ */
339
+ function snapDimension(raw) {
340
+ let d = Math.round(raw);
341
+ switch (d & 3) {
342
+ case 0:
343
+ d--;
344
+ break;
345
+ case 2:
346
+ d++;
347
+ break;
348
+ case 3: return 0;
349
+ default: break;
350
+ }
351
+ if (d < MIN_DIMENSION || d > MAX_DIMENSION)
352
+ return 0;
353
+ return d;
354
+ }
355
+ /** The alignment pattern, as modules. 1 is dark. */
356
+ const ALIGNMENT_MODULES = [
357
+ [1, 1, 1, 1, 1],
358
+ [1, 0, 0, 0, 1],
359
+ [1, 0, 1, 0, 1],
360
+ [1, 0, 0, 0, 1],
361
+ [1, 1, 1, 1, 1],
362
+ ];
363
+ /**
364
+ * Confirm a candidate by reading the 5x5 module block it claims to be.
365
+ *
366
+ * The run-ratio scan alone is not enough here. A finder pattern's 1:1:3:1:1 is
367
+ * rare enough to stand on its own, but an alignment pattern's 1:1:1:1:1 occurs
368
+ * constantly in ordinary data modules, so an unverified match near the expected
369
+ * position is more likely to be payload than pattern — and a false match drags
370
+ * the fourth corner off by several modules, which is worse than having no
371
+ * alignment pattern at all.
372
+ *
373
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
374
+ * @param {number} cx @param {number} cy @param {number} moduleSize
375
+ * @returns {boolean}
376
+ */
377
+ function verifyAlignment(image, cx, cy, moduleSize) {
378
+ let good = 0;
379
+ for (let j = 0; j < 5; j++) {
380
+ for (let i = 0; i < 5; i++) {
381
+ const px = Math.round(cx + (i - 2) * moduleSize);
382
+ const py = Math.round(cy + (j - 2) * moduleSize);
383
+ if (px < 0 || py < 0 || px >= image.width || py >= image.height)
384
+ return false;
385
+ if (image.get(px, py) === (ALIGNMENT_MODULES[j][i] === 1))
386
+ good++;
387
+ }
388
+ }
389
+ // Allow two modules of slop for blur and sampling, but no more.
390
+ return good >= 23;
391
+ }
392
+ /**
393
+ * Look for the bottom-right alignment pattern near where the geometry predicts.
394
+ *
395
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
396
+ * @param {{x: number, y: number}} expected
397
+ * @param {number} moduleSize
398
+ * @returns {{x: number, y: number} | null}
399
+ */
400
+ function findAlignment(image, expected, moduleSize) {
401
+ // Three modules of slack. The parallelogram estimate is good to well under
402
+ // that on any symbol flat enough to decode, and a wider net only admits more
403
+ // data modules as candidates.
404
+ const radius = Math.max(3, Math.ceil(moduleSize * 3));
405
+ const region = {
406
+ x0: expected.x - radius,
407
+ x1: expected.x + radius,
408
+ y0: Math.floor(expected.y - radius),
409
+ y1: Math.ceil(expected.y + radius),
410
+ };
411
+ const found = findPatterns(image, ALIGNMENT_RATIOS, 5, region);
412
+ let best = null;
413
+ let bestDistance = Infinity;
414
+ for (let i = 0; i < found.length; i++) {
415
+ // The alignment pattern is 5 modules across, so its implied module size
416
+ // should agree with the one the finders reported.
417
+ if (found[i].moduleSize > moduleSize * 1.5 || found[i].moduleSize < moduleSize / 1.5)
418
+ continue;
419
+ if (!verifyAlignment(image, found[i].x, found[i].y, moduleSize))
420
+ continue;
421
+ const d = distance(found[i], expected);
422
+ if (d < bestDistance) {
423
+ bestDistance = d;
424
+ best = found[i];
425
+ }
426
+ }
427
+ // Last resort: the pattern is exactly where predicted but its runs were
428
+ // mangled by blur. Reading the modules directly still confirms it.
429
+ if (!best && verifyAlignment(image, expected.x, expected.y, moduleSize)) {
430
+ best = { x: expected.x, y: expected.y };
431
+ }
432
+ return best;
433
+ }
434
+ /**
435
+ * @typedef {object} Detection
436
+ * @property {Array<{x: number, y: number}>} corners Outer corners of the
437
+ * symbol, ordered top-left, top-right, bottom-right, bottom-left.
438
+ * @property {number} dimension Modules per side.
439
+ * @property {number} version
440
+ * @property {number} moduleSize Estimated pixels per module.
441
+ * @property {boolean} alignmentFound
442
+ */
443
+ /**
444
+ * Find QR Code symbols in a binarized image.
445
+ *
446
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage Set bit = dark.
447
+ * @returns {Detection[]} Possibly empty; ordered by descending module size, so
448
+ * the most prominent symbol comes first.
449
+ */
450
+ export function detectQR(binaryImage) {
451
+ if (!binaryImage || !binaryImage.width) {
452
+ throw new NotFoundError('detectQR: no image supplied');
453
+ }
454
+ const finders = findPatterns(binaryImage, FINDER_RATIOS, 7);
455
+ // A single stray row hit is noise; a real finder is crossed many times.
456
+ const solid = finders.filter((f) => f.hits >= 2);
457
+ const pool = solid.length >= 3 ? solid : finders;
458
+ if (pool.length < 3)
459
+ return [];
460
+ // Prefer the largest patterns, and cap the combinatorics on noisy images.
461
+ pool.sort((a, b) => b.moduleSize - a.moduleSize || b.hits - a.hits);
462
+ const limit = Math.min(pool.length, 12);
463
+ /** @type {Detection[]} */
464
+ const detections = [];
465
+ const used = new Set();
466
+ for (let i = 0; i < limit; i++) {
467
+ for (let j = i + 1; j < limit; j++) {
468
+ for (let k = j + 1; k < limit; k++) {
469
+ const three = [pool[i], pool[j], pool[k]];
470
+ // All three finders belong to one symbol, so they share a module size.
471
+ const sizes = three.map((f) => f.moduleSize);
472
+ if (Math.max(...sizes) > Math.min(...sizes) * 1.6)
473
+ continue;
474
+ const oriented = orientFinders(three);
475
+ if (!oriented)
476
+ continue;
477
+ const { tl, tr, bl } = oriented;
478
+ const moduleSize = (tl.moduleSize + tr.moduleSize + bl.moduleSize) / 3;
479
+ if (moduleSize <= 0)
480
+ continue;
481
+ // Centre-to-centre spans dimension - 7 modules.
482
+ const across = (distance(tl, tr) + distance(tl, bl)) / 2;
483
+ const dimension = snapDimension(across / moduleSize + 7);
484
+ if (dimension === 0)
485
+ continue;
486
+ const version = (dimension - 17) / 4;
487
+ if (version < 1 || version > 40)
488
+ continue;
489
+ const key = `${Math.round(tl.x)},${Math.round(tl.y)},${dimension}`;
490
+ if (used.has(key))
491
+ continue;
492
+ used.add(key);
493
+ detections.push(buildDetection(binaryImage, tl, tr, bl, dimension, version, moduleSize));
494
+ }
495
+ }
496
+ }
497
+ detections.sort((a, b) => b.moduleSize - a.moduleSize);
498
+ return detections;
499
+ }
500
+ /**
501
+ * Turn three finder centres into four symbol corners.
502
+ *
503
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
504
+ * @param {Candidate} tl @param {Candidate} tr @param {Candidate} bl
505
+ * @param {number} dimension @param {number} version @param {number} moduleSize
506
+ * @returns {Detection}
507
+ */
508
+ function buildDetection(image, tl, tr, bl, dimension, version, moduleSize) {
509
+ const d = dimension;
510
+ // Finder centres sit on module (3, 3) and friends, so at grid coordinate 3.5.
511
+ const gridTl = [3.5, 3.5];
512
+ const gridTr = [d - 3.5, 3.5];
513
+ const gridBl = [3.5, d - 3.5];
514
+ // Parallelogram estimate of the far corner, used both as the search seed for
515
+ // the alignment pattern and as the fallback when it is not there.
516
+ const guess = { x: tr.x + bl.x - tl.x, y: tr.y + bl.y - tl.y };
517
+ /** @type {PerspectiveTransform | null} */
518
+ let transform = null;
519
+ let alignmentFound = false;
520
+ if (version >= 2) {
521
+ // The bottom-right alignment pattern is centred on module (d - 7, d - 7).
522
+ const gridAlign = [d - 6.5, d - 6.5];
523
+ // Where that module lands under the parallelogram assumption.
524
+ const seed = {
525
+ x: tl.x + ((gridAlign[0] - 3.5) / (d - 7)) * (tr.x - tl.x) +
526
+ ((gridAlign[1] - 3.5) / (d - 7)) * (bl.x - tl.x),
527
+ y: tl.y + ((gridAlign[0] - 3.5) / (d - 7)) * (tr.y - tl.y) +
528
+ ((gridAlign[1] - 3.5) / (d - 7)) * (bl.y - tl.y),
529
+ };
530
+ const align = findAlignment(image, seed, moduleSize);
531
+ if (align) {
532
+ alignmentFound = true;
533
+ transform = PerspectiveTransform.quadToQuad(gridTl[0], gridTl[1], gridTr[0], gridTr[1], gridAlign[0], gridAlign[1], gridBl[0], gridBl[1], tl.x, tl.y, tr.x, tr.y, align.x, align.y, bl.x, bl.y);
534
+ }
535
+ }
536
+ const plain = PerspectiveTransform.quadToQuad(gridTl[0], gridTl[1], gridTr[0], gridTr[1], d - 3.5, d - 3.5, gridBl[0], gridBl[1], tl.x, tl.y, tr.x, tr.y, guess.x, guess.y, bl.x, bl.y);
537
+ const corners = cornersOf(transform ?? plain, d);
538
+ // Keep the parallelogram corners as a second opinion whenever an alignment
539
+ // pattern steered the first set. Verification makes a false match unlikely,
540
+ // not impossible, and one extra sampling attempt is far cheaper than losing
541
+ // a symbol to it.
542
+ const altCorners = transform ? cornersOf(plain, d) : null;
543
+ return { corners, altCorners, dimension, version, moduleSize, alignmentFound };
544
+ }
545
+ /**
546
+ * The four outer corners of the symbol under a grid-to-image transform.
547
+ *
548
+ * @param {PerspectiveTransform} transform @param {number} d
549
+ * @returns {Array<{x: number, y: number}>}
550
+ */
551
+ function cornersOf(transform, d) {
552
+ return [
553
+ transform.transformPoint(0, 0),
554
+ transform.transformPoint(d, 0),
555
+ transform.transformPoint(d, d),
556
+ transform.transformPoint(0, d),
557
+ ];
558
+ }
559
+ /**
560
+ * Find and decode every QR Code in a binarized image.
561
+ *
562
+ * Each candidate gets up to four attempts: a plain centre sample, a 3x3
563
+ * majority vote for noisy input, and both of those rotated 180 degrees. The
564
+ * rotation retry matters because three finders in a right isoceles triangle
565
+ * look identical to the same three rotated half a turn — the orientation is
566
+ * only settled once the format information reads cleanly, which is to say once
567
+ * the decode succeeds.
568
+ *
569
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage
570
+ * @returns {Array<import('./decoder.js').DecodeResult & {corners: Array<{x: number, y: number}>}>}
571
+ * Empty when nothing decodes; never throws for "no symbol here".
572
+ */
573
+ export function detectAndDecodeQR(binaryImage) {
574
+ let detections;
575
+ try {
576
+ detections = detectQR(binaryImage);
577
+ }
578
+ catch (e) {
579
+ return [];
580
+ }
581
+ const results = [];
582
+ const seen = new Set();
583
+ for (let i = 0; i < detections.length; i++) {
584
+ const det = detections[i];
585
+ for (let attempt = 0; attempt < 4; attempt++) {
586
+ const voting = attempt === 1 || attempt === 3;
587
+ const rotated = attempt >= 2;
588
+ let matrix;
589
+ try {
590
+ matrix = sampleQuad(binaryImage, det.dimension, det.corners, voting);
591
+ }
592
+ catch (e) {
593
+ continue;
594
+ }
595
+ if (rotated)
596
+ matrix.rotate180();
597
+ try {
598
+ const result = decodeQR(matrix);
599
+ // The same symbol can be detected through more than one finder triple.
600
+ const key = `${result.version}|${result.text}`;
601
+ if (!seen.has(key)) {
602
+ seen.add(key);
603
+ results.push(Object.assign({ corners: det.corners }, result));
604
+ }
605
+ break;
606
+ }
607
+ catch (e) {
608
+ /* Try the next sampling strategy. */
609
+ }
610
+ }
611
+ }
612
+ return results;
613
+ }