Repository navigation
feat(android): replace ML Kit with zxing-cpp - #62
Merged
Merged
Conversation
The hook shelled out to ./tools/ktlint, which is no longer in the tree, so committing a Kotlin file failed with "no such file or directory". Spotless already owns the project's ktlint version, its Compose rule set and its editorconfig overrides, and CI checks the same configuration through spotlessCheck. Calling spotlessApply keeps formatting defined in exactly one place rather than in a checked-in binary that has to be kept in step with it.
com.google.mlkit:barcode-scanning is proprietary, which kept KScan out of apps published on F-Droid. Every dependency is now open source. Measured over 468 generated frames covering all thirteen formats at a range of angles, distances and lighting, on an API 37 x86_64 emulator: ML Kit reads 89.1% of them and zxing-cpp 79.9%, misreading 0.6% and 0.9%. zxing-cpp is ahead on QR, Data Matrix and Aztec, and on symbols small enough that ML Kit stops seeing them at all; it is behind on linear symbols rotated off axis, on strongly uneven lighting and on heavy defocus. It decodes roughly seven times faster and drops 14.6MB from the sample APK. The retry options and the local-average binarizer were chosen by measurement rather than by default: the four retries are worth 12.2 points of read rate, a global histogram binarizer costs 2.8, and accepting a single scan line leaves the read rate where it is while raising the misread rate to 1.5%. tryDenoise reaches the native side but changed no outcome, so it is left off. FrameInverter stays, and now produces an eight bit alpha bitmap that zxing-cpp reads as a luminance plane. zxing-cpp's own tryInvert is offered only to readers that declare support for reversed reflectance, which is QR, Data Matrix and Aztec but never a linear one, so a light-on-dark barcode still needs the negative handing to it explicitly. zxing-cpp decodes on the thread that calls it, so analysis moves off the main thread onto one of the library's own. The analyzer hands its results back through an executor, which keeps a caller's filter and result on the main thread where they have always been called, and confines the repeat counting and the accept decision to that thread. BREAKING CHANGE: ScannerView's autoZoom is removed. It was backed by ML Kit's zoom suggestions, which zxing-cpp has no equivalent for, and it never did anything on iOS, desktop or web. ScannerController still drives zoom. Callers passing it positionally should note that result has moved up a place. Closes ismai117#60
Naming TYPE_UNKNOWN in codeTypes matched everything. It maps to no platform format, and both decoders read an empty format set as no restriction at all, so zxing-cpp would decode its full repertoire, DataBar and MaxiCode and the rest, map each to TYPE_UNKNOWN for want of a name, and find that sitting in the requested list. Desktop reaches the same state by leaving POSSIBLE_FORMATS unset. ML Kit never showed this because its repertoire is exactly the thirteen formats KScan maps, so nothing it decoded was ever unnamed. isRequestedFormat now rejects TYPE_UNKNOWN however it was asked for, which is what the wildcard path already did, so the two agree. Every platform's scan path funnels through it or through firstMatching, so the fix is not Android's alone.
Owner
|
Great! @wodoro i will test this out on my side |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
One or more issues must be addressed before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Replaces Android ML Kit barcode decoding with open-source zxing-cpp while preserving KScan’s multiplatform API aside from removing autoZoom.
Changes:
- Added zxing-cpp decoding, format mapping, inversion, and background camera analysis.
- Removed
ScannerView.autoZoomand updated samples, APIs, documentation, and changelog. - Updated dependencies, ProGuard rules, tests, and pre-commit formatting.
File summaries
| File | Description |
|---|---|
| sample/shared/src/commonMain/kotlin/org/ncgroup/kscan/settings/SettingsScreen.kt | Updated as part of this pull request. |
| sample/shared/src/commonMain/kotlin/org/ncgroup/kscan/scanner/ScannerState.kt | Updated as part of this pull request. |
| sample/shared/src/commonMain/kotlin/org/ncgroup/kscan/scanner/ScannerScreen.kt | Updated as part of this pull request. |
| README.md | Updated as part of this pull request. |
| kscan/src/webMain/kotlin/org/ncgroup/kscan/view/ScannerViewImpl.web.kt | Updated as part of this pull request. |
| kscan/src/jvmMain/kotlin/org/ncgroup/kscan/view/ScannerViewImpl.jvm.kt | Updated as part of this pull request. |
| kscan/src/iosMain/kotlin/org/ncgroup/kscan/view/ScannerViewImpl.ios.kt | Updated as part of this pull request. |
| kscan/src/commonTest/kotlin/org/ncgroup/kscan/format/BarcodeSelectionTest.kt | Updated as part of this pull request. |
| kscan/src/commonMain/kotlin/org/ncgroup/kscan/view/ScannerViewImpl.kt | Updated as part of this pull request. |
| kscan/src/commonMain/kotlin/org/ncgroup/kscan/ScannerView.kt | Updated as part of this pull request. |
| kscan/src/commonMain/kotlin/org/ncgroup/kscan/format/BarcodeSelection.kt | Updated as part of this pull request. |
| kscan/src/commonMain/kotlin/org/ncgroup/kscan/BarcodeFormat.kt | Updated as part of this pull request. |
| kscan/src/androidMain/kotlin/org/ncgroup/kscan/view/ScannerViewImpl.android.kt | Updated as part of this pull request. |
| kscan/src/androidMain/kotlin/org/ncgroup/kscan/scanner/ZxingCppBarcode.kt | Updated as part of this pull request. |
| kscan/src/androidMain/kotlin/org/ncgroup/kscan/scanner/ScannerOptions.kt | Updated as part of this pull request. |
| kscan/src/androidMain/kotlin/org/ncgroup/kscan/scanner/FrameInverter.kt | Updated as part of this pull request. |
| kscan/src/androidMain/kotlin/org/ncgroup/kscan/scanner/BarcodeAnalyzer.kt | Updated as part of this pull request. |
| kscan/src/androidMain/kotlin/org/ncgroup/kscan/ImageScanner.android.kt | Updated as part of this pull request. |
| kscan/src/androidMain/kotlin/org/ncgroup/kscan/format/BarcodeFormatMapper.kt | Updated as part of this pull request. |
| kscan/src/androidHostTest/kotlin/org/ncgroup/kscan/scanner/InvertLuminanceTest.kt | Updated as part of this pull request. |
| kscan/src/androidHostTest/kotlin/org/ncgroup/kscan/format/BarcodeFormatMapperTest.kt | Updated as part of this pull request. |
| kscan/build.gradle.kts | Updated as part of this pull request. |
| kscan/api/kscan.klib.api | Updated as part of this pull request. |
| kscan/api/jvm/kscan.api | Updated as part of this pull request. |
| gradle/libs.versions.toml | Updated as part of this pull request. |
| consumer-rules.pro | Updated as part of this pull request. |
| CHANGELOG.md | Updated as part of this pull request. |
| .hooks/pre-commit | Updated as part of this pull request. |
Review details
Suppressed comments (3)
.hooks/pre-commit:22
git add "$file"stages the entire file afterspotlessApply. If a Kotlin file has both staged and unstaged edits, the unstaged edits are silently included in the commit; formatting the whole tree makes this easier to encounter. Preserve the staged patch or avoid re-adding complete files so the hook cannot expand the commit.
echo "$CHANGED_FILES" | while read -r file; do
if [ -f "$file" ]; then
git add "$file"
kscan/src/androidMain/kotlin/org/ncgroup/kscan/format/BarcodeFormatMapper.kt:29
- When
codeTypescontains onlyTYPE_UNKNOWN, this returns an empty set. zxing-cpp treats an emptyOptions.formatsas unrestricted, so the analyzer still performs a full decode (and periodic inversion) only to discard every result, contrary to the new no-match behavior. Short-circuit this request or pass an explicit no-format configuration to avoid the unnecessary work.
// Named rather than left empty for every format: an empty set asks zxing-cpp
// for every symbology it knows, including the ones KScan has no name for,
// which would only be decoded to be reported as TYPE_UNKNOWN and dropped.
fun toZxingCppFormats(appFormats: List<BarcodeFormat>): Set<BarcodeReader.Format> = formats.platformFormats(appFormats).toSet()
kscan/src/androidMain/kotlin/org/ncgroup/kscan/scanner/BarcodeAnalyzer.kt:53
- This callback is queued after the proxy has already been closed, so the analysis executor can enqueue a decode failure after an earlier barcode's
reportcallback has already succeeded. Because this guard only checksclosed(which is not set until disposal), callers can receiveOnFailedafterOnSuccess; also suppress failures oncehasSuccessfullyProcessedBarcodeis true.
callbackExecutor.execute { if (!closed) onFailed(e) }
- Files reviewed: 28/28 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Two things the review caught in scanImage. It decoded on the thread that called it. ML Kit's task API had kept that off the main thread and answered on it, so a caller scanning from a picker callback, which is what the sample does, was newly made to wait for a photograph to be decoded. It runs on a pool again and posts the outcome to the main thread, as before. It also never tried the negative. The camera path does, because zxing-cpp offers tryInvert only to the readers that allow reversed reflectance and never to a linear one, so a still photograph of a light-on-dark Code 128 or EAN was missed where the same barcode through the camera was found. ML Kit did not invert either, so this is new behaviour rather than a restored regression. The negative is built as an eight bit luminance bitmap, read a row at a time, so a large photograph costs one byte a pixel rather than the four a colour copy would.
Owner
|
🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #60.
Android now decodes with
io.github.zxing-cpp:android(Apache 2.0) instead ofcom.google.mlkit:barcode-scanning, so every dependency is open source and KScancan be used in apps published on F-Droid.
Two commits, no new modules. The measurement that justifies it, the corpus generator and the full per-frame data live in #61, which is kept only as the record of how the decision was made; this one is
just the change.
What it costs and what it buys
Measured over 468 generated frames, all thirteen formats KScan exposes across
distance, in- and out-of-plane rotation, lighting, contrast, defocus and noise,
on an API 37 x86_64 emulator:
9.2 points worse overall, ~7x faster, and 14.6 MB smaller (sample debug APK
44.3 MB -> 29.7 MB; ML Kit's bundled
libbarhopper_v3.sois 20.2 MB across fourABIs against zxing-cpp's 6.6 MB).
The average hides the shape. zxing-cpp is ahead on QR, Data Matrix and Aztec
(94.4% against 89.6%), on inverted and coloured symbols, and dramatically on small
ones: where the symbol spans 12% of the frame ML Kit reads 23.1% and zxing-cpp
100%. It is behind on one thing, linear barcodes rotated off axis, which
accounts for 47 of the 72 frames it lost. Right angles are fine; 15 degrees costs
it, 45 costs it badly. Excluding those conditions it is 1.0 point ahead overall.
Replaying every scene as a second of hand-held video rather than a single frame
does not rescue it: sensor noise turns out to be entirely transient (both reach
100%), but the rotation loss is persistent. A user holding a linear barcode at 30
degrees will watch it fail to scan. That is the honest cost of the switch.
Breaking
ScannerView'sautoZoomis removed. It was ML Kit'sZoomSuggestionOptions, which zxing-cpp has no equivalent for, and it never didanything on iOS, desktop or web.
ScannerControllerstill drives zoom. Callerspassing arguments positionally should note that
resultmoves up a place. ABIdumps are regenerated and the sample's Auto zoom toggle goes with it.
0.10.0 is already an unreleased breaking version, so this rides along with the
existing
ScannerViewchanges rather than adding a second migration.Behaviour worth knowing
Decoding moved off the main thread. zxing-cpp decodes on the thread that calls
it, so analysis runs on a thread of the library's own.
filterandresultarestill called on the main thread, unchanged.
FrameInverterhad to stay. zxing-cpp'stryInvertis only offered to readersdeclaring support for reversed reflectance, which is QR, Data Matrix and Aztec but
never a linear one or PDF417 (
MultiFormatReaderskips readers withoutsupportsInversionon an inverted image). Dropping the explicit negative passwould have silently lost inverted linear barcodes, which work today. It now
produces an eight bit alpha bitmap that zxing-cpp reads as a luminance plane, so
it costs one byte per pixel rather than four.
Misread profile differs. zxing-cpp's four misreads are all truncated ITF, which
has no mandatory check digit; it returned
1234for1234567890on rotated ortilted frames.
RepeatedDetectionrequires the same value on two frames beforereporting, so real exposure is lower than the single-frame 0.9%, but
validateOptionalChecksumcould be exposed if you want it stricter.Dependency audit
io.github.zxing-cpp:android:3.1.1, checked before adopting:.ascsignature present; release cut by the upstream maintainer.
providers.
libc,libm,libdl,liblogandlibjnigraphics. Nosocket,connect,getaddrinfo,exec,fork,popen,systemorptracein its import table, and no URLs in its strings. It exportsexactly two JNI entry points,
readBitmapandreadYBuffer.androidx.camera:camera-coreandkotlin-stdlib.No Play Services.
Also in here
The pre-commit hook called
./tools/ktlint, which is no longer in the tree, socommitting a Kotlin file failed. It now calls
spotlessApply, which is where thektlint version, the Compose rule set and the editorconfig overrides already live
and what CI checks.