# Why the full-page QR is unreliable

The QR matrices are valid, but the rendered page is not a robust presentation of those matrices. Two separate failures are demonstrated: tiny marks over arbitrary page content break module interpretation, while stretching the QR to a wide rectangle breaks OpenCV's finder detection. This is an offline investigation of actual browser screenshots, not a physical-phone test or a measurement of every scanner.

## Evidence

- Both version 6 / error-correction L / mask 0 matrices reconstruct **bit for bit** with independent `qrcodegen` from their stored raw data. The encoded destination is `https://qr.jdt.engineering`.
- The six supplied browser frame captures all decode in ZXing; **none decode in OpenCV**. OpenCV detects the three near-square frames but cannot decode them. Providing the exact known grid still gives **0/6**, so this is not merely a finder-detection issue.
- Replace the live page with full black/white modules: ZXing **6/6**, OpenCV **3/6**. OpenCV decodes all near-square controls and detects none of the wide controls. Supplying the known grid makes all six full-module controls decode.
- Undo the wide stretch: all three plain-matrix controls decode in both decoders. The three original page overlays remain **3/3 ZXing, 0/3 OpenCV**, despite OpenCV now detecting all three.
- Reduce the original captures to 320 pixels wide and apply Gaussian blur sigma 0.75 pixels: ZXing drops to **4/6**. Repainting half-module circular pins restores **6/6** for these specific inputs, but OpenCV remains **0/6**. This is controlled degradation, not a simulation certified to represent a phone camera.
- Restoring full timing/function modules alone does not cure the page overlays. Compression of page contrast also does not cure them in this experiment.
- Near-square overlays reach OpenCV **3/3** at native and 480px widths when square marks cover **76.6%** of a data cell, or circular marks cover **78.5%**. Those changes heavily obscure the page. The wide versions still fail OpenCV detection; after unstretching they decode **3/3**. At 320px width plus blur, the near-square large squares fall to 2/3, while full-size circles retain 3/3.

## What the browser is drawing

The QR has 41×41 modules and a four-module quiet zone (49×49 total). The ordinary data mark is a circle with diameter **1/3 of a module**, so it controls only **8.73% of that cell's area**. The other 91.27% comes from the live page. Only the finder regions and lower-right alignment region receive full solid function modules; the timing lines also use microdots. There is no center alignment square.

The matrix was optimized for an earlier homepage image, but it stays fixed while the live content changes with scroll, reflow, and new cards. Exact center samples still match all 1,681 intended module bits in all six native captures. That does **not** mean the cell is recognizably black or white to every scanner: binarization, resampling, module-size estimation and sample positioning can involve nearby pixels. Cell means disagree with hundreds of intended bits. This diagnostic demonstrates the narrow visual margin; it does not reproduce any decoder's internal threshold algorithm.

The actual frame captures are **1080×1020** and **1920×1020**, excluding the 60px navigation. The older capture metadata incorrectly describes their height as 1080. For the wide frame, a data module is about 39.18×20.82 pixels and its pin is 13.06×6.94 pixels. At 320px image width those pins are approximately **2.18×1.16 pixels**, so modest blur or sample displacement can remove them. Stretching also changes the finder-pattern geometry, which the known-grid controls isolate from the page-content failure.

## Error correction is not enough

Version 6-L has **two Reed–Solomon blocks, each correcting at most 9 erroneous codewords** (18 parity codewords per block). One wrong bit corrupts its entire codeword. Error correction also cannot repair a grid which was never detected or repair function-pattern detection itself.

At exact native pixel centers there are zero wrong codewords. In the wide play capture, sampling whole-cell means instead gives **61 and 47 wrong codewords** in the two blocks; at 320px width plus blur, the simple thresholded center sample gives **57 and 43**. Those figures exceed the 9-per-block bound dramatically. They are known-grid threshold diagnostics, **not** the actual errors seen internally by ZXing or OpenCV. Indeed ZXing can succeed on some images where this deliberately simple sampler fails. `sampling-phase.json` similarly records sensitivity to small known-grid sample offsets without claiming those are real decoder offsets.

## Recommended compromise

Keep this full-page mode explicitly **experimental / unreliable for scanning**. Preserve its large interactive site area and provide a clearly visible conventional square QR as the dependable scan target, or offer a single-action square, opaque scan view. The protected-center layout remains the conservative alternate, with much less room for content.

If the aesthetic overlay itself must remain the scan target, half-module pins plus full function modules are a modest experimentally supported improvement for ZXing under this blur test. They are **not a cross-scanner fix**. Keeping the code square removes the demonstrated geometry problem. Reaching both tested decoders with the same page background required roughly three-quarters of every cell to be opaque, which defeats much of the unobstructed-page benefit. A higher error-correction level or re-optimizing after scroll cannot guarantee arbitrary changing content will scan.

## Reproduce and inspect

`inputs/` contains six unchanged supplied browser screenshots and the two original matrix JSON files. SHA-256 provenance is in `results.json`. All 324 tested rendered images are retained in `output/`, including each transformed input. No live UI was modified. The optional isolated `gallery.html` opens locally without a server and compares every result.

From an existing activated virtual environment with Pillow, numpy, opencv-python, zxing-cpp and qrcodegen:

```sh
source /home/jim/code/cool-qr-work/.venv/bin/activate
python analyze.py
python report.py
```

Tests use three page sections (play, collections, code), two viewport shapes, twelve rendering variants and five transformations (the square correction only applies to the three wide frames). `native` is the supplied image; `480` is a Lanczos resize preserving aspect ratio; `480-blur` and `320-blur` additionally apply sigma 0.75px Gaussian blur; `square` rescales wide frames to 1020×1020. Repainted prototypes are offline pixel overlays over the original screenshots, not new browser captures; they preserve the underlying page pixels but do not verify resulting UI interaction or layout. White-background controls are freshly rendered from the same exact matrix.

OpenCV's exact-grid test bypasses detection, using the known QR bounds inside the quiet zone. It is a diagnostic and does not represent an ordinary user's scanner. Decoder versions, returned positions, per-frame results and approximate sampling/RS diagnostics are recorded in JSON. No camera exposure, autofocus, screen moiré, reflection, perspective sweep or actual phone was tested. Existing protected-layout test results were not rerun in this investigation.

![Visual comparison of increasing opacity](comparison.jpg)

## Complete decoder table

| Rendering | Transform | ZXing | OpenCV | OpenCV detects | OpenCV with known grid |
|---|---|---:|---:|---:|---:|
| original | native | 6/6 | 0/6 | 3/6 | 0/6 |
| original | 480 | 6/6 | 0/6 | 3/6 | 0/6 |
| original | 480-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| original | 320-blur | 4/6 | 0/6 | 3/6 | 0/6 |
| original | square | 3/3 | 0/3 | 3/3 | 0/3 |
| functions-only | native | 6/6 | 0/6 | 3/6 | 0/6 |
| functions-only | 480 | 6/6 | 0/6 | 3/6 | 0/6 |
| functions-only | 480-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| functions-only | 320-blur | 4/6 | 0/6 | 3/6 | 0/6 |
| functions-only | square | 3/3 | 0/3 | 3/3 | 0/3 |
| circle-half | native | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-half | 480 | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-half | 480-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-half | 320-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-half | square | 3/3 | 0/3 | 3/3 | 0/3 |
| circle-two-thirds | native | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-two-thirds | 480 | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-two-thirds | 480-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-two-thirds | 320-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| circle-two-thirds | square | 3/3 | 0/3 | 3/3 | 0/3 |
| square-half | native | 6/6 | 0/6 | 3/6 | 0/6 |
| square-half | 480 | 6/6 | 0/6 | 3/6 | 0/6 |
| square-half | 480-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| square-half | 320-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| square-half | square | 3/3 | 0/3 | 3/3 | 0/3 |
| square-three-quarters | native | 6/6 | 2/6 | 3/6 | 2/6 |
| square-three-quarters | 480 | 6/6 | 1/6 | 3/6 | 2/6 |
| square-three-quarters | 480-blur | 6/6 | 0/6 | 3/6 | 1/6 |
| square-three-quarters | 320-blur | 6/6 | 1/6 | 3/6 | 0/6 |
| square-three-quarters | square | 3/3 | 0/3 | 3/3 | 0/3 |
| square-seven-eighths | native | 6/6 | 3/6 | 3/6 | 6/6 |
| square-seven-eighths | 480 | 6/6 | 3/6 | 3/6 | 6/6 |
| square-seven-eighths | 480-blur | 6/6 | 3/6 | 3/6 | 3/6 |
| square-seven-eighths | 320-blur | 6/6 | 2/6 | 3/6 | 0/6 |
| square-seven-eighths | square | 3/3 | 3/3 | 3/3 | 3/3 |
| circle-full | native | 6/6 | 3/6 | 3/6 | 6/6 |
| circle-full | 480 | 6/6 | 3/6 | 3/6 | 6/6 |
| circle-full | 480-blur | 6/6 | 3/6 | 3/6 | 6/6 |
| circle-full | 320-blur | 6/6 | 3/6 | 3/6 | 0/6 |
| circle-full | square | 3/3 | 3/3 | 3/3 | 3/3 |
| compressed-half | native | 6/6 | 0/6 | 3/6 | 0/6 |
| compressed-half | 480 | 6/6 | 0/6 | 3/6 | 0/6 |
| compressed-half | 480-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| compressed-half | 320-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| compressed-half | square | 3/3 | 0/3 | 3/3 | 0/3 |
| white-pins | native | 6/6 | 0/6 | 3/6 | 0/6 |
| white-pins | 480 | 6/6 | 3/6 | 3/6 | 6/6 |
| white-pins | 480-blur | 6/6 | 3/6 | 3/6 | 6/6 |
| white-pins | 320-blur | 6/6 | 3/6 | 3/6 | 6/6 |
| white-pins | square | 3/3 | 3/3 | 3/3 | 3/3 |
| white-pins-functions | native | 6/6 | 0/6 | 3/6 | 0/6 |
| white-pins-functions | 480 | 6/6 | 3/6 | 3/6 | 3/6 |
| white-pins-functions | 480-blur | 6/6 | 3/6 | 3/6 | 3/6 |
| white-pins-functions | 320-blur | 6/6 | 3/6 | 3/6 | 0/6 |
| white-pins-functions | square | 3/3 | 0/3 | 3/3 | 0/3 |
| matrix | native | 6/6 | 3/6 | 3/6 | 6/6 |
| matrix | 480 | 6/6 | 3/6 | 3/6 | 6/6 |
| matrix | 480-blur | 6/6 | 3/6 | 3/6 | 3/6 |
| matrix | 320-blur | 6/6 | 0/6 | 3/6 | 0/6 |
| matrix | square | 3/3 | 3/3 | 3/3 | 3/3 |
