diff --git a/amethyst/build.gradle.kts b/amethyst/build.gradle.kts index 9e234da0b4..2277f7391d 100644 --- a/amethyst/build.gradle.kts +++ b/amethyst/build.gradle.kts @@ -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()) } } } } diff --git a/amethyst/plans/2026-09-15-qr-reader-overhaul.md b/amethyst/plans/2026-09-15-qr-reader-overhaul.md index 9f4c49b50d..96c78d9aab 100644 --- a/amethyst/plans/2026-09-15-qr-reader-overhaul.md +++ b/amethyst/plans/2026-09-15-qr-reader-overhaul.md @@ -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 +``` diff --git a/amethyst/src/androidTest/assets/qr/baseline.tsv b/amethyst/src/androidTest/assets/qr/baseline.tsv new file mode 100644 index 0000000000..258e4f2397 --- /dev/null +++ b/amethyst/src/androidTest/assets/qr/baseline.tsv @@ -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 diff --git a/amethyst/src/androidTest/assets/qr/blur-heavy-nevent.png b/amethyst/src/androidTest/assets/qr/blur-heavy-nevent.png new file mode 100644 index 0000000000..e97301b636 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/blur-heavy-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/blur-heavy-nprofile.png b/amethyst/src/androidTest/assets/qr/blur-heavy-nprofile.png new file mode 100644 index 0000000000..493418b194 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/blur-heavy-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/blur-heavy-npub.png b/amethyst/src/androidTest/assets/qr/blur-heavy-npub.png new file mode 100644 index 0000000000..93f957f537 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/blur-heavy-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/blur-nevent.png b/amethyst/src/androidTest/assets/qr/blur-nevent.png new file mode 100644 index 0000000000..740559b130 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/blur-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/blur-nprofile.png b/amethyst/src/androidTest/assets/qr/blur-nprofile.png new file mode 100644 index 0000000000..4c75240dee Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/blur-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/blur-npub.png b/amethyst/src/androidTest/assets/qr/blur-npub.png new file mode 100644 index 0000000000..a948a0d920 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/blur-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/clean-nevent.png b/amethyst/src/androidTest/assets/qr/clean-nevent.png new file mode 100644 index 0000000000..a9f8e18221 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/clean-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/clean-nprofile.png b/amethyst/src/androidTest/assets/qr/clean-nprofile.png new file mode 100644 index 0000000000..e001e8123d Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/clean-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/clean-npub.png b/amethyst/src/androidTest/assets/qr/clean-npub.png new file mode 100644 index 0000000000..44f757dba1 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/clean-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/expected.tsv b/amethyst/src/androidTest/assets/qr/expected.tsv new file mode 100644 index 0000000000..733a533f41 --- /dev/null +++ b/amethyst/src/androidTest/assets/qr/expected.tsv @@ -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 diff --git a/amethyst/src/androidTest/assets/qr/far-1.5px-nevent.png b/amethyst/src/androidTest/assets/qr/far-1.5px-nevent.png new file mode 100644 index 0000000000..9c5cb21fae Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-1.5px-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-1.5px-nprofile.png b/amethyst/src/androidTest/assets/qr/far-1.5px-nprofile.png new file mode 100644 index 0000000000..ea8e5b36fb Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-1.5px-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-1.5px-npub.png b/amethyst/src/androidTest/assets/qr/far-1.5px-npub.png new file mode 100644 index 0000000000..9c3a9529b0 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-1.5px-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-2px-nevent.png b/amethyst/src/androidTest/assets/qr/far-2px-nevent.png new file mode 100644 index 0000000000..adb983aff5 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-2px-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-2px-nprofile.png b/amethyst/src/androidTest/assets/qr/far-2px-nprofile.png new file mode 100644 index 0000000000..6ce86d338c Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-2px-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-2px-npub.png b/amethyst/src/androidTest/assets/qr/far-2px-npub.png new file mode 100644 index 0000000000..df60e3f43c Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-2px-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-3px-nevent.png b/amethyst/src/androidTest/assets/qr/far-3px-nevent.png new file mode 100644 index 0000000000..111342f7f1 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-3px-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-3px-nprofile.png b/amethyst/src/androidTest/assets/qr/far-3px-nprofile.png new file mode 100644 index 0000000000..94e109453c Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-3px-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/far-3px-npub.png b/amethyst/src/androidTest/assets/qr/far-3px-npub.png new file mode 100644 index 0000000000..9d6327047b Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/far-3px-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/glare-nevent.png b/amethyst/src/androidTest/assets/qr/glare-nevent.png new file mode 100644 index 0000000000..9acda03a4b Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/glare-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/glare-nprofile.png b/amethyst/src/androidTest/assets/qr/glare-nprofile.png new file mode 100644 index 0000000000..1b7a565b29 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/glare-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/glare-npub.png b/amethyst/src/androidTest/assets/qr/glare-npub.png new file mode 100644 index 0000000000..b80e94182f Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/glare-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/inverted-nevent.png b/amethyst/src/androidTest/assets/qr/inverted-nevent.png new file mode 100644 index 0000000000..4694627b92 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/inverted-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/inverted-nprofile.png b/amethyst/src/androidTest/assets/qr/inverted-nprofile.png new file mode 100644 index 0000000000..ef5ef50660 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/inverted-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/inverted-npub.png b/amethyst/src/androidTest/assets/qr/inverted-npub.png new file mode 100644 index 0000000000..9ed3a1012d Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/inverted-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/low-contrast-nevent.png b/amethyst/src/androidTest/assets/qr/low-contrast-nevent.png new file mode 100644 index 0000000000..aea754fdce Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/low-contrast-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/low-contrast-nprofile.png b/amethyst/src/androidTest/assets/qr/low-contrast-nprofile.png new file mode 100644 index 0000000000..1d46beaac4 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/low-contrast-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/low-contrast-npub.png b/amethyst/src/androidTest/assets/qr/low-contrast-npub.png new file mode 100644 index 0000000000..ea06e35e17 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/low-contrast-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/moire-nevent.png b/amethyst/src/androidTest/assets/qr/moire-nevent.png new file mode 100644 index 0000000000..f69f39c267 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/moire-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/moire-nprofile.png b/amethyst/src/androidTest/assets/qr/moire-nprofile.png new file mode 100644 index 0000000000..1e539b8189 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/moire-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/moire-npub.png b/amethyst/src/androidTest/assets/qr/moire-npub.png new file mode 100644 index 0000000000..2dd8553f4f Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/moire-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/noise-nevent.png b/amethyst/src/androidTest/assets/qr/noise-nevent.png new file mode 100644 index 0000000000..722169c289 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/noise-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/noise-nprofile.png b/amethyst/src/androidTest/assets/qr/noise-nprofile.png new file mode 100644 index 0000000000..b57e7a4aed Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/noise-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/noise-npub.png b/amethyst/src/androidTest/assets/qr/noise-npub.png new file mode 100644 index 0000000000..6ad3d5b0f8 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/noise-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/perspective-nevent.png b/amethyst/src/androidTest/assets/qr/perspective-nevent.png new file mode 100644 index 0000000000..c1f16b570e Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/perspective-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/perspective-nprofile.png b/amethyst/src/androidTest/assets/qr/perspective-nprofile.png new file mode 100644 index 0000000000..f9121f7aaa Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/perspective-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/perspective-npub.png b/amethyst/src/androidTest/assets/qr/perspective-npub.png new file mode 100644 index 0000000000..f2d3ebe03e Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/perspective-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt15-nevent.png b/amethyst/src/androidTest/assets/qr/tilt15-nevent.png new file mode 100644 index 0000000000..ad30e30592 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt15-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt15-nprofile.png b/amethyst/src/androidTest/assets/qr/tilt15-nprofile.png new file mode 100644 index 0000000000..0574a02ef6 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt15-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt15-npub.png b/amethyst/src/androidTest/assets/qr/tilt15-npub.png new file mode 100644 index 0000000000..c9c469b5dd Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt15-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt30-nevent.png b/amethyst/src/androidTest/assets/qr/tilt30-nevent.png new file mode 100644 index 0000000000..0b83d2cb94 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt30-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt30-nprofile.png b/amethyst/src/androidTest/assets/qr/tilt30-nprofile.png new file mode 100644 index 0000000000..d650c7a382 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt30-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt30-npub.png b/amethyst/src/androidTest/assets/qr/tilt30-npub.png new file mode 100644 index 0000000000..8bad92980a Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt30-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt45-nevent.png b/amethyst/src/androidTest/assets/qr/tilt45-nevent.png new file mode 100644 index 0000000000..d32fd0a077 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt45-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt45-nprofile.png b/amethyst/src/androidTest/assets/qr/tilt45-nprofile.png new file mode 100644 index 0000000000..09ca065a07 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt45-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/tilt45-npub.png b/amethyst/src/androidTest/assets/qr/tilt45-npub.png new file mode 100644 index 0000000000..73cca26568 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/tilt45-npub.png differ diff --git a/amethyst/src/androidTest/assets/qr/very-low-contrast-nevent.png b/amethyst/src/androidTest/assets/qr/very-low-contrast-nevent.png new file mode 100644 index 0000000000..9dc8fa196d Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/very-low-contrast-nevent.png differ diff --git a/amethyst/src/androidTest/assets/qr/very-low-contrast-nprofile.png b/amethyst/src/androidTest/assets/qr/very-low-contrast-nprofile.png new file mode 100644 index 0000000000..ca492a1fb2 Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/very-low-contrast-nprofile.png differ diff --git a/amethyst/src/androidTest/assets/qr/very-low-contrast-npub.png b/amethyst/src/androidTest/assets/qr/very-low-contrast-npub.png new file mode 100644 index 0000000000..ee78ae601c Binary files /dev/null and b/amethyst/src/androidTest/assets/qr/very-low-contrast-npub.png differ diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrDecodeCorpusTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrDecodeCorpusTest.kt index d50330e03f..3fb2ff284d 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrDecodeCorpusTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrDecodeCorpusTest.kt @@ -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) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt index bef8fe833e..0f5ad25dae 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/UriToRouteTest.kt @@ -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() @@ -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), ) } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpus.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpus.kt index e442abe215..a20f15de17 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpus.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpus.kt @@ -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() 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 diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpusBaselineTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpusBaselineTest.kt index 9ccadfd116..9b53c8db0d 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpusBaselineTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/scanner/QrCorpusBaselineTest.kt @@ -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" }