yomu 1.3.0
yomu: ^1.3.0 copied to clipboard
Pure Dart QR code and barcode reader library with zero dependencies.
CHANGELOG #
1.3.0 #
Features #
- Reflectance reversal: QR codes printed as light modules on a dark background (ISO/IEC 18004:2015, 6.2) now decode at every
DecodeEffort, in bothdecodeanddecodeAll. The finder pattern scan reads light-on-dark patterns from the same runs as the normal ones - a 1:1:3:1:1 sequence that starts on a white run - so the image is scanned once; those candidates are tried once the normal attempt (and, indecode, barcode scanning) has failed. Decoding a normal code costs up to 3% more (qr_imagesatfast: 0.43ms to 0.44ms).- Every retry stage reads light-on-dark codes the way it reads dark-on-light ones: the corner-grid rescue, despeckle and the tolerant finder at
balanced, the full-resolution retry and threshold sweep atthorough. With every QR fixture color-inverted,balancedreads 169 of 182 andthorough173 - the same as the originals. readLightOnDark(defaulttrue) turns it all off. The cost falls on frames holding no code: on a textured Full HD frame,fasttakes 3.6ms instead of 2.7ms,balanced41ms instead of 15ms andthorough130ms instead of 63ms. A blank frame pays under 0.4ms (8.3ms to 8.6ms atthorough).decodeAllreturns every code on a sheet mixing dark-on-light and light-on-dark ones; light-on-dark codes were read only when no dark-on-light one decoded.
- Every retry stage reads light-on-dark codes the way it reads dark-on-light ones: the corner-grid rescue, despeckle and the tolerant finder at
- Mirror imaging: QR codes printed as a mirror image (ISO/IEC 18004:2015, 6.2) now decode at every
DecodeEffort, in bothdecodeanddecodeAll, dark on light or light on dark. A mirror image samples as the transpose of its normal grid, so a grid that fails to decode is read again transposed - but only when its format information reads with fewer errors that way, as read backwards most format information codewords still lie within three bits of another one. With every QR fixture mirrored,fastreads 148 of 187,balanced169 andthorough173 (as is: 149, 173 and 178); none were read correctly before. There is no option. Frames holding no code cost the same as before, within measurement noise; the fixture corpus, weighted toward hard and undecodable images, takes about 1% longer (903.8ms to 912.5ms), mostly in the transposed retries of grids that fail either way.
Performance #
- Finder pattern cross-check stops at the pattern's width: the vertical check walked the center run of a 1:1:3:1:1 candidate to its end, however long. On a 1D barcode every bar is such a run, so each horizontal hit walked the full bar height before failing. A center run as long as the pattern is wide can never pass the checks that follow, so the walk now stops there; the candidates found are identical on every fixture. The finder pattern scan on barcode images: 0.086ms to 0.068ms;
Yomu.allon barcode images: 0.40ms to 0.365ms. decodeAllpicks finder triplets without trying every triple: picking codes' finder triplets smallest first (see Fixes) enumerated every triple of finder candidates, which costs the cube of the candidate count - a textured frame yields hundreds (296 dark-on-light and 736 light-on-dark on a Full HD noise frame at full resolution; picking from the latter took 2.8s). Triplets are now enumerated from their right-angle corner, only out to the longest leg a version 40 symbol allows.decodeAllreturns the same codes on every fixture, as is and color-inverted, at every effort; on a textured Full HD frame holding no code it goes from 46ms to 31ms atfastandbalancedand from 237ms to 101ms atthorough.
Fixes #
decodeAllpairs each code with its own finder patterns: in a grid of codes, the matching finder patterns of three neighboring codes form a right isosceles triangle as well, and the first valid triplet found was taken. Four codes in a 2x2 grid rotated by 5° decoded 0/4. Triplets are now taken smallest first, after those whose patterns were confirmed on a consistent number of rows, and all four decode.decodeno longer throwsArgumentErroratDecodeEffort.fast: finder patterns spaced for a symbol outside versions 1-40 reached the version lookup, which threwArgumentErrorrather than aYomuException, and the barcode fallback was skipped. The detector now rejects such spacing with aDetectionException. A textured Full HD frame holding no code hit this too, and now reaches barcode scanning: it costs 2.5ms atfastinstead of 2.4ms.- Data that is not a bit stream no longer decodes to an empty text: a mode indicator that no mode has was read as the end of the data, so a grid whose codewords passed Reed-Solomon by chance - a sampling of something that holds no symbol - decoded to an empty text instead of failing. Such an indicator now fails the decode, and the retry ladder moves on. With every QR fixture mirrored,
thoroughreturned an empty text formulti_qr_3_vertical.png; it now reports no code. Detection and decode time are unchanged. - Every failure is a
YomuException, thrown where it happens: parts of the library threwArgumentErrorand relied on a catch-all further out to turn it - and any other error - into aDecodeException, while every retry stage caught whatever was thrown. Each check now throws theYomuExceptionthat describes it, and every catch takes onlyYomuException.- Dropping the catch-alls brought out reads past the sampled grid that they had hidden: a grid not 17 + 4v modules wide, or one whose version information decoded to a version of another size, was read with that other version's layout.
readVersionnow rejects the first and ignores such version information in the second. - A class implementing
YomuImageskips its constructor's checks; bytes that do not fit its size are now anArgumentExceptionbefore decoding starts, and an exception its own getters throw reaches the caller unchanged instead of as anImageProcessingException.ImageProcessingExceptionis no longer thrown and is deprecated. BarcodeExceptionextendsYomuException.
- Dropping the catch-alls brought out reads past the sampled grid that they had hidden: a grid not 17 + 4v modules wide, or one whose version information decoded to a version of another size, was read with that other version's layout.
- Exceptions name themselves in minified and obfuscated builds:
YomuException.toStringnamed the class by itsruntimeType, which dart2js-O2and--obfuscatebuilds rename - aDecodeExceptionprinted asminified:I: ...on the web andMi: ...in an obfuscated app. Every exception in the library now spells out its name. - 1D barcodes decode in rows that start dark: the run lengths of a scan row started with the color of its first pixel, but every decoder reads even-indexed runs as white. A dark border or a dark object at the left edge of the frame flipped every run, and no symbology could decode that row: with a 10px dark band added to the left of each barcode fixture (quiet zone intact), 0 of 19 decoded. A row that starts dark now begins with an empty white run, and all 19 decode.
1.2.0 #
Features #
DecodeEffortreplaces thetryHarderflag: the retry ladder was a boolean, with one fast point, one exhaustive point and nothing between them. It is now a three-leveleffortparameter, split where the cost jumps: between stages that reuse the binarized image and stages that rebuild it from the source pixels. Successful scans are unaffected at every level.DecodeEffort.fast: no retries. 83.1% of the fixture corpus (167/201), 2.6ms on a textured Full HD frame holding no code.DecodeEffort.balanced: corner grid search, despeckle and tolerant finder. 93.5% (188/201), 18.0ms.DecodeEffort.thorough(default): adds the full-resolution retry and the threshold sweep. 95.5% (192/201), 51.3ms.balancedrecovers 21 of the 25 codesthoroughadds overfast, for a third of the cost on a textured frame.
Yomu.responsivepreset: all formats atDecodeEffort.balanced, for streams that can afford more per frame thanYomu.realtime.- Alternate-threshold retry sweep: when every stage at the configured
binarizerThresholdfails,decodere-binarizes at 0.6 / 1.0 / 1.1 and re-runs the ladder. Codes that fail by shifting contrast rather than destroying it (screen moire, heavy low-light sensor noise, casual-scan blur) come out clean at a different factor.gaussian_noise_120,moire_0.8andcomposite_scan_blur_5.5go from undecodable to decoding. decodeAllthreshold sweep: a sheet whose codes have differing contrast (one clean, one occluded) is re-scanned at the alternate factors when a pass decodes fewer codes than it detected, and the best pass wins. Passes are compared rather than merged, so two codes carrying the same text on one sheet are both preserved.tryHarderis deprecated but still honored:falsemaps toDecodeEffort.fast,truetoDecodeEffort.thorough, andyomu.tryHarderstill reads back, so existing call sites keep compiling and keep their behavior.DecodeEffort.thoroughis slower than the oldtryHarder: trueon images holding no code at all: a heavily textured Full HD frame costs 51ms against ~20ms, the extra work being the threshold sweep.Yomu.responsiveandYomu.realtimeare the ways to decline that cost.
Performance #
- Block-averaged binarizer: the adaptive threshold now comes from an integral image of per-block (4x4) mean luminance instead of a per-pixel one. The averaging window is at least 40px wide, so the threshold surface varies far more slowly than the block grid; quantizing it to blocks costs no meaningful accuracy while cutting the per-pixel work to one load and one compare.
- Branchless thresholding:
luminance <= thresholdis now the sign bit ofluminance - (threshold + 1)rather than anif. Binarizing a photograph otherwise means one unpredictable branch per pixel, and the misprediction dominated the compare; removing it cut the threshold pass by 3.4x on high-entropy input. - Whole-pixel image conversion: RGBA/BGRA to luminance (and the fused downsample) reads one 32-bit word per pixel instead of three bounds-checked bytes, falling back to per-byte access for unaligned buffers and strides that are not a whole number of pixels.
- Binarization was ~65% of decode time and now costs roughly one sequential pass over the image (1.3x a bare read loop, against 4.2x before). On the fixture corpus (AOT): standard QR -30%, high-version QR -44%, uneven lighting -40%, Full HD / 4K -39%, barcodes -22%. Per image: 4K 3.51ms -> 2.12ms, Full HD 1.99ms -> 1.24ms, version 7 2.70ms -> 1.57ms.
- Table-driven data masks: six of the eight mask patterns built their 32-bit words a bit at a time, two integer modulos per module, and unmasking runs twice per decode attempt since the second XOR is what restores the matrix. Every mask condition is periodic - 12 rows by 3 word alignments covers all of them - so those words are constants now, 1.1 KiB in total. Generating the mask words for a version 40 symbol: 23.5us -> 1.8us. On the fixture corpus (AOT): images holding no code -6.5%, distorted -4.1%, whole corpus -4.4%.
Fixes #
- Finder pattern run lengths no longer wrap at 255: the run lengths behind the 1:1:3:1:1 test were counted in a byte, but a run is bounded by the image - the white margin around a code on a megapixel frame passes 255 pixels easily. A real finder pattern was never at risk (that would need a module wider than 255 pixels, which downsampling rules out), but a run that is nothing like one could read as one:
black(10) gap(266) black(30) white(10) black(10)is a textbook 1:1:3:1:1 once the gap is taken modulo 256. Such a candidate still had to survive decoding, so this cost work on noisy input rather than producing wrong results.
Test fixtures #
gaussian_noise_120,moire_0.8andcomposite_scan_blur_5.5moved fromfixtures/unsupported_imagestofixtures/distorted_images, and the stress generator gained the next rung on each axis so the boundary is pinned from above again (moire 0.9, composite scan blur 6.0, low-light noise sigma 170).- Low-light noise is the one probabilistic axis: each sigma draws a single noise field, so a fixture near the transition asserts its own draw rather than the decoder's limit. Over 20 independent draws per sigma the decode rate runs 110 -> 100%, 130 -> 75%, 150 -> 50%, 170 -> 5%, so the rungs are taken from the flat ends (sigma 120 decodes, sigma 170 does not) instead of the 130-160 band.
- Of the 198 images that existed before, three more decode (189 -> 192); the corpus is now 201 images as the ladders were extended.
1.1.0 #
Features #
- Try-Harder Mode (default on):
Yomu.decodenow runs escalating retry strategies when the fast path fails, significantly improving the detection rate (fixture corpus: 84.3% -> 95.5%, 167/198 -> 189/198). Successful scans are unaffected; retries only run on images the fast path cannot decode.- Corner grid search: per-axis dimension candidates plus a grid search of the bottom-right corner rescue perspective-distorted codes.
- Despeckle retry: a word-parallel 3x3 majority filter (
BitMatrix.majority3x3) recovers codes under salt & pepper noise (validated up to 20% pixel noise). - Tolerant finder: clusters raw row-scan hits without the strict vertical cross-check, recovering slanted finder patterns under strong perspective.
- Full-resolution retry: re-runs detection without downsampling when a downsampled pass fails, recovering small codes in high-resolution frames.
- Retries are bounded by a deterministic work budget and grid-search deduplication, so undecodable inputs cannot make the failure path pathologically slow.
- Set
tryHarder: falsefor the previous fast-only behavior (latency-critical per-frame scanning).
decodeAllretry passes: multi-code scanning applies the same strategy. Detected-but-undecodable codes get the corner-grid rescue, and a pass that finds nothing escalates to despeckle and a full-resolution pass (a noisy 3-code sheet and two 90px codes in a 4K frame go from 0 to fully decoded).Yomu.realtimepreset: all formats withtryHarderdisabled, tuned for per-frame camera scanning where a missed frame is cheaper than a slower failure path.- With
tryHarderenabled, a detected-but-undecodable QR code now falls through to barcode scanning instead of propagating aDecodeException.
Test fixtures #
- Fixture ladders now bracket the current capability boundary on both sides, with boundary-pinning tests on each side (see the Supported Image Classes table in the README).
- New distortion axes derived from the modern imaging pipeline: low-light Gaussian noise, JPEG quantization artifacts, specular glare, screen moire, and a composite casual-scan recipe (mild perspective + lighting gradient + blur) that demonstrates composition lowering the single-axis boundary.
- The legacy
perspective_{x,y}fixtures above 0.2 cropped the finder patterns out of the canvas (invalid test images); they are replaced by a padded transform that keeps the code fully visible. fixtures/unsupported_imagesnow contains only images beyond the capability boundary; everything rescued by the retry strategies moved tofixtures/distorted_images.
1.0.0 #
Initial stable release.
Features #
- Pure Dart Implementation: A zero-dependency QR code and barcode reader library. No native code required, making it highly portable across Flutter, Web, and Server-side Dart.
- QR Code Support:
- Full support for QR Code versions 1 to 40.
- Supports all error correction levels (L, M, Q, H).
- Robust multi-QR detection and decoding in a single image.
- High resilience against perspective distortion, rotation, and uneven lighting.
- 1D Barcode Support:
- Retail: EAN-13 (including JAN), EAN-8, UPC-A.
- Industrial: Code 128, Code 39, ITF (Interleaved 2 of 5), Codabar.
- High Performance:
- Specifically optimized for AOT compilation and performance.
- Efficiently handles high-resolution images (> 1MP) using internal fused downsampling and conversion.
- Capable of real-time decoding on mobile and desktop platforms.
- Flexible Image API: Platform-agnostic
YomuImagecontainer supporting various pixel formats including RGBA, BGRA, Grayscale, and YUV420 (camera stream Y-plane).