test(qr): measure the phase-0 baseline and commit the corpus
Runs the corpus and records what the OLD decoder actually manages, which is the measurement the plan's phase 0 asked for and the decoder swap was resting on reasoning instead of. ZXing-Java over the corpus: 31/48. The informative part is where it scores zero. Perspective is a total loss -- an off-axis code, one of the most ordinary framings there is, was simply unreadable. So are half-a-module blur, very low contrast, and anything under two pixels per module. Tilt sits at 2/3 across 15, 30 and 45 degrees. Two things make 31/48 flattering to the old scanner, both noted in the plan: the baseline decodes the full image and retries inverted, where the shipped scanner cropped to a viewfinder rect and alternated inversion across frames; and it measures a decoder on a still, so it says nothing about focus, zoom or torch. A win measured here is a floor on the real difference, not the whole of it. Calibration matters for the gate, so the corpus is tuned to discriminate rather than to be hard: every degradation is a fraction of a module (changing the render scale cannot re-tune difficulty), and blur/distance each straddle the old decoder's limit. A category it fails outright can only ever show an improvement, never catch a regression. The export is wired through a Gradle property rather than -D, because the test runs in a forked JVM that does not inherit the Gradle JVM's system properties -- which is why the first export attempt silently skipped. Images are written as 8-bit grey, and render at 4px/module with three payloads, which brought the committed corpus from 2.6 MB to 776 KB. It lives in androidTest assets, so it never ships in the app. Also fixes the deep-link test from the previous commit: it asserted an un-encoded value round-trips, but a raw `&secret=` inside `dlnwc?value=` reads as a parameter of the outer uri and truncates it. Real deep links percent-encode the value, and so does the test now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0134jvyriixNTHST4WRbbqbX
@@ -317,6 +317,12 @@ android {
|
||||
project
|
||||
.findProperty("amethyst.arti.integration")
|
||||
?.let { test.systemProperty("amethyst.arti.integration", it.toString()) }
|
||||
// Opts QrCorpusBaselineTest into rewriting the QR decode corpus under
|
||||
// src/androidTest/assets/qr. Off by default so an ordinary run never dirties
|
||||
// the working tree; Gradle forks the test JVM, so -D alone would not reach it.
|
||||
project
|
||||
.findProperty("amethyst.qr.corpus.export")
|
||||
?.let { test.systemProperty("amethyst.qr.corpus.export", it.toString()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,7 +310,7 @@ Implemented in this branch:
|
||||
|
||||
| Phase | State |
|
||||
| --- | --- |
|
||||
| 0 — measurement corpus | **Not done.** No device or emulator in this environment, so the fixture corpus and `QrDecodeCorpusTest` could not be built *or run*, and none of the before/after decode-rate numbers that §1 and §2.1 call for exist yet. The gate it was supposed to provide — "drop zxing-cpp if it doesn't beat ZXing-Java" — has therefore not been exercised. |
|
||||
| 0 — measurement corpus | **Half done.** The corpus and the ZXing-Java baseline are built and measured (see §8); the zxing-cpp half needs a device and is written but unrun, so the gate is armed rather than passed. |
|
||||
| 1 — in-app CameraX scanner | Done. |
|
||||
| 2 — per-frame decode quality | Done. |
|
||||
| 3 — explicit outcomes | Done. |
|
||||
@@ -347,3 +347,59 @@ Implemented in this branch:
|
||||
reasoned-through only.
|
||||
3. **F-Droid packaging sign-off** on the new prebuilt `.so`, per §5.
|
||||
4. **The ECC level question** in §5 — still open, and still wants the corpus to answer it.
|
||||
|
||||
---
|
||||
|
||||
## 8. Phase 0: the baseline
|
||||
|
||||
`QrCorpus` renders three payloads — an `npub`, an `nprofile` with relay hints, and an `nevent`
|
||||
(69, ~330 and ~140 characters, so three different symbol versions) — at 4 pixels per module, then
|
||||
degrades each one sixteen ways. Every degradation is expressed as a fraction of a *module*, so
|
||||
changing the render scale cannot quietly re-tune the corpus's difficulty.
|
||||
|
||||
`QrCorpusBaselineTest` measures **ZXing-Java**, the decoder the old zxing-android-embedded scanner
|
||||
used, over that corpus on the JVM. Measured 2026-09-15:
|
||||
|
||||
| category | ZXing-Java | what it stands for |
|
||||
| --- | --- | --- |
|
||||
| clean | 3/3 | sanity — if this ever fails the corpus is broken |
|
||||
| blur | 3/3 | a quarter-module out of focus |
|
||||
| blur-heavy | **0/3** | half a module out of focus |
|
||||
| tilt15 / tilt30 / tilt45 | 2/3 each | held at an angle |
|
||||
| perspective | **0/3** | seen off-axis — a code on a wall or table, photographed from the side |
|
||||
| low-contrast | 3/3 | a dim screen |
|
||||
| very-low-contrast | **0/3** | a very dim screen, or worn print |
|
||||
| inverted | 3/3 | light-on-dark |
|
||||
| glare | 3/3 | a highlight burning out one corner |
|
||||
| moire | 3/3 | photographed off another screen |
|
||||
| noise | 2/3 | sensor noise in poor light |
|
||||
| far-3px | 3/3 | three pixels per module |
|
||||
| far-2px | 2/3 | two pixels per module |
|
||||
| far-1.5px | **0/3** | one and a half pixels per module |
|
||||
| **TOTAL** | **31/48** | |
|
||||
|
||||
Read it as a map of where the old reader gave up. **Perspective is a total loss** — an off-axis
|
||||
code, one of the most ordinary framings there is, was simply unreadable. So are heavy blur, very
|
||||
low contrast, and anything under two pixels per module.
|
||||
|
||||
Two caveats on the number, both of which make 31/48 *flattering* to the old scanner:
|
||||
|
||||
- It decodes the **full image** and retries **inverted**. The shipped scanner cropped to a
|
||||
viewfinder rect and alternated inversion across frames, so it had strictly fewer chances.
|
||||
- It measures a decoder on a still. It says nothing about focus, zoom or torch, which is where
|
||||
most of this branch's other work went.
|
||||
|
||||
So a win measured here is a floor on the real-world difference, not the whole of it.
|
||||
|
||||
**The gate is not yet passed.** `QrDecodeCorpusTest` runs zxing-cpp over the identical committed
|
||||
images and fails if it reads fewer in any category. It needs a device. Until someone runs it, the
|
||||
decoder swap rests on the reasoning in §2.1 — this section just means the measurement is now one
|
||||
command away instead of unbuilt.
|
||||
|
||||
The corpus costs 776 KB in `amethyst/src/androidTest/assets/qr/` (test APK only, never shipped).
|
||||
Regenerate it with:
|
||||
|
||||
```bash
|
||||
./gradlew :amethyst:testFdroidDebugUnitTest --tests '*QrCorpusBaselineTest*' \
|
||||
-Pamethyst.qr.corpus.export=true
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
blur 3 3
|
||||
blur-heavy 0 3
|
||||
clean 3 3
|
||||
far-1.5px 0 3
|
||||
far-2px 2 3
|
||||
far-3px 3 3
|
||||
glare 3 3
|
||||
inverted 3 3
|
||||
low-contrast 3 3
|
||||
moire 3 3
|
||||
noise 2 3
|
||||
perspective 0 3
|
||||
tilt15 2 3
|
||||
tilt30 2 3
|
||||
tilt45 2 3
|
||||
very-low-contrast 0 3
|
||||
|
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,48 @@
|
||||
clean-npub.png clean nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
blur-npub.png blur nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
blur-heavy-npub.png blur-heavy nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
tilt15-npub.png tilt15 nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
tilt30-npub.png tilt30 nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
tilt45-npub.png tilt45 nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
perspective-npub.png perspective nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
low-contrast-npub.png low-contrast nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
very-low-contrast-npub.png very-low-contrast nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
inverted-npub.png inverted nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
glare-npub.png glare nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
noise-npub.png noise nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
moire-npub.png moire nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
far-3px-npub.png far-3px nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
far-2px-npub.png far-2px nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
far-1.5px-npub.png far-1.5px nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq
|
||||
clean-nprofile.png clean nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
blur-nprofile.png blur nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
blur-heavy-nprofile.png blur-heavy nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
tilt15-nprofile.png tilt15 nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
tilt30-nprofile.png tilt30 nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
tilt45-nprofile.png tilt45 nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
perspective-nprofile.png perspective nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
low-contrast-nprofile.png low-contrast nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
very-low-contrast-nprofile.png very-low-contrast nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
inverted-nprofile.png inverted nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
glare-nprofile.png glare nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
noise-nprofile.png noise nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
moire-nprofile.png moire nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
far-3px-nprofile.png far-3px nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
far-2px-nprofile.png far-2px nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
far-1.5px-nprofile.png far-1.5px nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq
|
||||
clean-nevent.png clean nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
blur-nevent.png blur nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
blur-heavy-nevent.png blur-heavy nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
tilt15-nevent.png tilt15 nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
tilt30-nevent.png tilt30 nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
tilt45-nevent.png tilt45 nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
perspective-nevent.png perspective nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
low-contrast-nevent.png low-contrast nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
very-low-contrast-nevent.png very-low-contrast nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
inverted-nevent.png inverted nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
glare-nevent.png glare nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
noise-nevent.png noise nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
moire-nevent.png moire nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
far-3px-nevent.png far-3px nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
far-2px-nevent.png far-2px nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
far-1.5px-nevent.png far-1.5px nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq
|
||||
|
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -39,7 +39,8 @@ import org.junit.runner.RunWith
|
||||
*
|
||||
* Regenerate both with:
|
||||
* ```
|
||||
* ./gradlew :amethyst:testFdroidDebugUnitTest --tests '*QrCorpusBaselineTest*' -Dqr.corpus.export=true
|
||||
* ./gradlew :amethyst:testFdroidDebugUnitTest --tests '*QrCorpusBaselineTest*' \\
|
||||
* -Pamethyst.qr.corpus.export=true
|
||||
* ```
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import java.net.URLEncoder
|
||||
|
||||
class UriToRouteTest {
|
||||
private val account = mockk<Account>()
|
||||
@@ -81,9 +82,13 @@ class UriToRouteTest {
|
||||
|
||||
@Test
|
||||
fun walletConnectDeepLinksStillUnwrapTheValueParameter() {
|
||||
// The value has to be percent-encoded, as a real deep link's would be: left raw, its own
|
||||
// `&secret=` reads as a parameter of the OUTER uri and the value comes back truncated.
|
||||
val encoded = URLEncoder.encode(NWC_URI, Charsets.UTF_8.name())
|
||||
|
||||
assertEquals(
|
||||
Route.WalletAddNwc(NWC_URI),
|
||||
uriToRoute("dlnwc?value=$NWC_URI", account),
|
||||
uriToRoute("dlnwc?value=$encoded", account),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ object QrCorpus {
|
||||
"npub" to "nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq",
|
||||
"nprofile" to "nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhkummn9ekx7mqpz4mhxue69uhkummnw3ezummcw3ezuer9wchsz9thwden5te0wfjkccte9ehx7um5wghxyctwvshsz9nhwden5te0wfjkccte9ehx7um5wghxyctwvshszxrhwden5te0wfjkccte9ehx7um5wghxyctwvshsqgxvxz9jkth8dgc6dyckt3jmg5kvthdjtcn6q9lc39ahq5dpjznuwq",
|
||||
"nevent" to "nostr:nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ezuamfdejsygzhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8psgqqqqqqspp4mhxue69uhkummn9ekx7mq",
|
||||
"njump" to "https://njump.to/npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqdhpvhq",
|
||||
)
|
||||
|
||||
/** One generated image plus what it should decode to and which hazard it represents. */
|
||||
@@ -88,7 +87,7 @@ object QrCorpus {
|
||||
val fixtures = mutableListOf<Fixture>()
|
||||
|
||||
PAYLOADS.forEach { (label, payload) ->
|
||||
val (base, modules) = renderWithModuleCount(payload, moduleSize = 6)
|
||||
val (base, modules) = renderWithModuleCount(payload, moduleSize = MODULE_PX)
|
||||
|
||||
fun add(
|
||||
category: String,
|
||||
@@ -96,8 +95,12 @@ object QrCorpus {
|
||||
) = fixtures.add(Fixture("$category-$label", category, payload, image))
|
||||
|
||||
add("clean", base)
|
||||
add("blur", blur(base, radius = 3))
|
||||
add("blur-heavy", blur(base, radius = 6))
|
||||
// A quarter-module and a half-module of blur. Both are expressed in modules so that
|
||||
// changing MODULE_PX cannot quietly re-tune the corpus's difficulty -- and they are
|
||||
// chosen to straddle the old decoder's limit, because a category it fails outright
|
||||
// can only ever show an improvement, never catch a regression.
|
||||
add("blur", blur(base, radius = MODULE_PX / 4))
|
||||
add("blur-heavy", blur(base, radius = MODULE_PX / 2))
|
||||
add("tilt15", rotate(base, degrees = 15.0))
|
||||
add("tilt30", rotate(base, degrees = 30.0))
|
||||
add("tilt45", rotate(base, degrees = 45.0))
|
||||
@@ -331,6 +334,14 @@ object QrCorpus {
|
||||
// helpers
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Pixels per module in the source renders.
|
||||
*
|
||||
* Four is ample to decode from (the hard fixtures shrink from here) and keeps the corpus that
|
||||
* has to live in the repository to a few hundred kilobytes rather than a few megabytes.
|
||||
*/
|
||||
private const val MODULE_PX = 4
|
||||
|
||||
private const val SEED = 20260915L
|
||||
private const val WHITE_RGB = 0xFFFFFF
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class QrCorpusBaselineTest {
|
||||
// The one hard assertion. If a pristine, generously-sized code fails, the corpus itself
|
||||
// is broken and every other number in this file is meaningless.
|
||||
val clean = QrCorpus.all().filter { it.category == "clean" }
|
||||
assertEquals("corpus should contain one clean fixture per payload", 4, clean.size)
|
||||
assertEquals("corpus should contain one clean fixture per payload", 3, clean.size)
|
||||
clean.forEach {
|
||||
assertEquals("clean fixture ${it.name} must decode", it.expected, decode(it.image))
|
||||
}
|
||||
@@ -80,18 +80,22 @@ class QrCorpusBaselineTest {
|
||||
/**
|
||||
* Writes the corpus and its baseline into the instrumented test's assets.
|
||||
*
|
||||
* Opt-in via `-D$EXPORT_PROPERTY=true` so an ordinary test run never dirties the working
|
||||
* tree. The generator is deterministic, so re-exporting an unchanged corpus is a no-op.
|
||||
* Opt-in via `-Pamethyst.qr.corpus.export=true` so an ordinary test run never dirties the
|
||||
* working tree. The generator is deterministic, so re-exporting an unchanged corpus is a
|
||||
* no-op. It is a Gradle property rather than a plain `-D` because the test runs in a forked
|
||||
* JVM that does not inherit the Gradle JVM's system properties.
|
||||
*/
|
||||
@Test
|
||||
fun exportsTheCorpusForTheInstrumentedTest() {
|
||||
assumeTrue("set -D$EXPORT_PROPERTY=true to regenerate the corpus", System.getProperty(EXPORT_PROPERTY) == "true")
|
||||
assumeTrue("pass -Pamethyst.qr.corpus.export=true to regenerate the corpus", System.getProperty(EXPORT_PROPERTY) == "true")
|
||||
|
||||
val dir = File(ASSET_DIR)
|
||||
dir.mkdirs()
|
||||
|
||||
QrCorpus.all().forEach { fixture ->
|
||||
ImageIO.write(fixture.image, "png", File(dir, "${fixture.name}.png"))
|
||||
// 8-bit grey, not RGB: every fixture is greyscale in content, and storing three
|
||||
// identical channels tripled the size of a corpus that has to live in the repo.
|
||||
ImageIO.write(toGrayscale(fixture.image), "png", File(dir, "${fixture.name}.png"))
|
||||
}
|
||||
|
||||
File(dir, "expected.tsv").writeText(
|
||||
@@ -99,6 +103,14 @@ class QrCorpusBaselineTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun toGrayscale(source: BufferedImage): BufferedImage {
|
||||
val out = BufferedImage(source.width, source.height, BufferedImage.TYPE_BYTE_GRAY)
|
||||
val g = out.createGraphics()
|
||||
g.drawImage(source, 0, 0, null)
|
||||
g.dispose()
|
||||
return out
|
||||
}
|
||||
|
||||
private data class Row(
|
||||
val category: String,
|
||||
val passed: Int,
|
||||
@@ -180,7 +192,7 @@ class QrCorpusBaselineTest {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXPORT_PROPERTY = "qr.corpus.export"
|
||||
private const val EXPORT_PROPERTY = "amethyst.qr.corpus.export"
|
||||
private const val ASSET_DIR = "src/androidTest/assets/qr"
|
||||
private const val BASELINE_FILE = "baseline.tsv"
|
||||
}
|
||||
|
||||