yomu 1.3.0 copy "yomu: ^1.3.0" to clipboard
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 both decode and decodeAll. 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, in decode, barcode scanning) has failed. Decoding a normal code costs up to 3% more (qr_images at fast: 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 at thorough. With every QR fixture color-inverted, balanced reads 169 of 182 and thorough 173 - the same as the originals.
    • readLightOnDark (default true) turns it all off. The cost falls on frames holding no code: on a textured Full HD frame, fast takes 3.6ms instead of 2.7ms, balanced 41ms instead of 15ms and thorough 130ms instead of 63ms. A blank frame pays under 0.4ms (8.3ms to 8.6ms at thorough).
    • decodeAll returns 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.
  • Mirror imaging: QR codes printed as a mirror image (ISO/IEC 18004:2015, 6.2) now decode at every DecodeEffort, in both decode and decodeAll, 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, fast reads 148 of 187, balanced 169 and thorough 173 (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.all on barcode images: 0.40ms to 0.365ms.
  • decodeAll picks 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. decodeAll returns 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 at fast and balanced and from 237ms to 101ms at thorough.

Fixes #

  • decodeAll pairs 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.
  • decode no longer throws ArgumentError at DecodeEffort.fast: finder patterns spaced for a symbol outside versions 1-40 reached the version lookup, which threw ArgumentError rather than a YomuException, and the barcode fallback was skipped. The detector now rejects such spacing with a DetectionException. A textured Full HD frame holding no code hit this too, and now reaches barcode scanning: it costs 2.5ms at fast instead 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, thorough returned an empty text for multi_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 threw ArgumentError and relied on a catch-all further out to turn it - and any other error - into a DecodeException, while every retry stage caught whatever was thrown. Each check now throws the YomuException that describes it, and every catch takes only YomuException.
    • 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. readVersion now rejects the first and ignores such version information in the second.
    • A class implementing YomuImage skips its constructor's checks; bytes that do not fit its size are now an ArgumentException before decoding starts, and an exception its own getters throw reaches the caller unchanged instead of as an ImageProcessingException. ImageProcessingException is no longer thrown and is deprecated.
    • BarcodeException extends YomuException.
  • Exceptions name themselves in minified and obfuscated builds: YomuException.toString named the class by its runtimeType, which dart2js -O2 and --obfuscate builds rename - a DecodeException printed as minified:I: ... on the web and Mi: ... 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 #

  • DecodeEffort replaces the tryHarder flag: the retry ladder was a boolean, with one fast point, one exhaustive point and nothing between them. It is now a three-level effort parameter, 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.
    • balanced recovers 21 of the 25 codes thorough adds over fast, for a third of the cost on a textured frame.
  • Yomu.responsive preset: all formats at DecodeEffort.balanced, for streams that can afford more per frame than Yomu.realtime.
  • Alternate-threshold retry sweep: when every stage at the configured binarizerThreshold fails, decode re-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.8 and composite_scan_blur_5.5 go from undecodable to decoding.
  • decodeAll threshold 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.
  • tryHarder is deprecated but still honored: false maps to DecodeEffort.fast, true to DecodeEffort.thorough, and yomu.tryHarder still reads back, so existing call sites keep compiling and keep their behavior.
  • DecodeEffort.thorough is slower than the old tryHarder: true on images holding no code at all: a heavily textured Full HD frame costs 51ms against ~20ms, the extra work being the threshold sweep. Yomu.responsive and Yomu.realtime are 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 <= threshold is now the sign bit of luminance - (threshold + 1) rather than an if. 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.8 and composite_scan_blur_5.5 moved from fixtures/unsupported_images to fixtures/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.decode now 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: false for the previous fast-only behavior (latency-critical per-frame scanning).
  • decodeAll retry 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.realtime preset: all formats with tryHarder disabled, tuned for per-frame camera scanning where a missed frame is cheaper than a slower failure path.
  • With tryHarder enabled, a detected-but-undecodable QR code now falls through to barcode scanning instead of propagating a DecodeException.

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_images now contains only images beyond the capability boundary; everything rescued by the retry strategies moved to fixtures/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 YomuImage container supporting various pixel formats including RGBA, BGRA, Grayscale, and YUV420 (camera stream Y-plane).
2
likes
160
points
115
downloads

Documentation

API reference

Publisher

verified publisherkoji-1009.com

Weekly Downloads

Pure Dart QR code and barcode reader library with zero dependencies.

Repository (GitHub)
View/report issues

Topics

#qr-code #barcode #scanner #reader

License

MIT (license)

More

Packages that depend on yomu