Your QR Code Might Be Drawing a Swastika, and This Rust Crate Won't Let It
Every QR code is a small field of black and white squares placed by an algorithm that has exactly one goal: be readable by a scanner. It does not know what the squares look like. Most of the time that is fine. Occasionally the algorithm, pursuing maximum readability with the innocence of a subroutine, arranges five or seven of those squares into a neat little hooked cross and hands it to you with the quiet confidence of a job well done, even though it just slipped a big fat swastika into the mix.
This is a real thing that happens. It is not common, but “not common” is not the same as “acceptable” when the failure mode is printing a swastika on your conference badge. qr-swastika-avoider is a rust™ crate™ that detects the pattern and, if you let it generate the code, guarantees it isn’t there.

1. The standard is not on your side (it just isn’t looking)
A QR code is not drawn once. During encoding, the same data is rendered eight times, each time XOR-ed with one of eight fixed mask patterns. Masking exists to break up large same-colour regions and awkward alignments that confuse scanners. The encoder scores all eight results with a penalty function and keeps the lowest-scoring one, which is to say the most readable, to a poor, dumb machine.
Same text, drawn eight ways.
Here is the part that matters. That penalty function, defined in ISO/IEC 18004 (a standard that will run you a cool CHF 227, because naturally the one spec governing whether your code draws a swastika is locked behind a paywall), is exactly four rules, and not one of them knows what a symbol is:
- Runs. Five or more same-colour modules in a row or column cost
3 + (length − 5). - Blocks. Every 2×2 block of a single colour costs
3. - Finder-lookalikes. The
1:1:3:1:1dark/light ratio of a finder pattern, appearing anywhere in the data, costs40. - Balance. Every 5% that the overall dark/light ratio drifts from 50% costs
10.
These rules optimise for scannability: no long monotone runs, no big blocks, nothing that impersonates the three big corner squares, roughly balanced ink. A swastika trips them only by accident. A thin 5×5 hooked cross is small, reasonably balanced, has short runs and no 1:1:3:1:1 sequence, so it adds maybe six to twelve penalty points out of hundreds. That is nowhere near enough for the “avoid a swastika” mask to reliably beat the “looks slightly cleaner” mask. The standard will happily pick the swastika if the swastika scans a hair better.
2. What is a swastika, to a program that has never seen one? (a so-called nazi-unaware program)
First it needs a definition, one that is strict enough to avoid crying wolf on every stray cluster of pixels and loose enough to catch the real thing when a module or two is off.
The crate builds the ideal shape rather than hard-coding a bitmap. It takes a single “gamma” arm (a straight run plus a right-angle hook) and rotates it by 0°, 90°, 180° and 270° about the centre, unioning the four copies. Rotating a single arm four times forces exact four-fold rotational symmetry, which is the mathematical definition of a gammadion. You cannot accidentally build a lopsided one.
Rust// One arm along +x, plus its right-angle hook, then 4 rotations:
fn gammadion(arm: i32, hand: i32) -> Vec<Vec<bool>> {
// gamma = the straight run (arm cells) + the hook (arm cells, turned by `hand`)
// each point is stamped at 0°/90°/180°/270° via (dx, dy) -> (dy, -dx)
// `hand = 1` hooks clockwise (卐), `hand = -1` anticlockwise (卍)
// ...
}
Doing this for arm length 2 and 3 gives the two sizes the detector scans for, 5×5 and 7×7; doing it for both hooking directions gives both chiralities (卐 and 卍). Each is also checked in both polarities: a dark cross on a light field, and a light cross on a dark field. Four stencils, two polarities, and the shape is pinned down without a single magic bitmap.
Matching is a sliding window. At every position the detector counts how many modules differ from the ideal stencil and accepts the match only if the count stays within a small tolerance (±1 for the 5×5, ±2 for the 7×7). The count is reported back as mismatch on each Detection, so 0 means a textbook-perfect cross and 2 means “this cross bears an odd resemblance to a familiar symbol once adopted by a certain 1940s German regime, which strikes me as strange and unsolicited, so: close enough to worry about.”
Rustuse qr_swastika_avoider::contains_swastika;
// Any `impl QrModules`, here a raw module grid (`true` = dark).
let grid: Vec<Vec<bool>> = vec![vec![false; 21]; 21];
assert!(!contains_swastika(&grid));
draw one, or plant it.
3. The hard part is not crying wolf
A naive and stupid shape matcher on a QR code fires constantly, because a QR code is full of geometric furniture that looks vaguely cross-like. The three big corner squares (the finder patterns), the dotted timing lines, the alignment patterns, the format and version strips: all high-contrast, blocky, and structured. Slide a stencil across them and you will “find” swastikas that are just the code’s own skeleton.
Worse: these modules are identical across all eight masks. They are structural, not data. So a detection that overlaps them is a double failure. It is a false positive (like a fake nazi), and it is uncorrectable, because switching masks changes nothing there. Reporting it would be noise you can’t even act on.
The crate solves this with a function map. Given only the grid’s side length n (the version follows, since n = 4·version + 17), it reconstructs the position of every function module: finders and separators, timing, the alignment-pattern grid from the ISO Annex E table, format and version information. Any detection whose bounding box touches one is dropped.
This is the bug most naive implementations ship with: they flag the corners. It was, in fact, the first bug this crate hit. The function map is the unglamorous 60 lines that make the detector trustworthy instead of annoying.
4. A swastika nobody can see is not a swastika
There is one more way to be wrong: the pattern of modules can be correct and still be invisible. If a hooked cross is drawn in dark modules but every module around it is also dark, there is no cross, just a dark blob. No human will ever perceive a nazi symbol that has no contrast with its surroundings. Flagging it would be pedantically true and practically useless.
So the detector adds a contrast ring check: it looks at the ring of modules immediately surrounding a candidate and requires that enough of them (65% by default) carry the background colour. A motif buried in same-colour modules fails the check and is ignored. The threshold is tunable through Config { min_surround_contrast, .. }, and setting it to 0.0 disables the check entirely.
The modules never change. Only the background does.
Between the function map (rule out the impossible) and the contrast ring (rule out the invisible), the detector’s remaining hits are things a person would actually recognise. Overlapping hits are then collapsed to the single strongest one, so a visible motif yields exactly one Detection, not a cluster.
5. Removing it: encode_safe
Detection is the whole library on its own. Point it at whatever crate you already use to generate codes:
Rust// cargo add qr-swastika-avoider --features qrcodegen
let qr = qrcodegen::QrCode::encode_text("https://example.com", qrcodegen::QrCodeEcc::Medium)?;
if qr_swastika_avoider::contains_swastika(&qr) {
// warn, refuse, or regenerate (pick the last one and, well, you might be a nazi, so be careful)
}
But if you let the crate generate, it can guarantee a clean result. Recall that the eight masks are all valid encodings of the same data. That is the entire trick: at least one of the eight is almost always swastika-free, so pick from those, and stop busting my balls about it, I’m tired of you.
encode_safe renders all eight masks, scans each, keeps the clean ones, and among those returns the mask with the lowest ISO penalty (the most easily scannable one). You keep the standard’s readability optimisation and the guarantee at the same time. If every one of the eight is dirty (astronomically unlikely, but we don’t joke about swastikas, it’s not nice), it returns an explicit EncodeError::Unavoidable carrying the least-bad mask’s detections, never a silent swastika.
Rust// cargo add qr-swastika-avoider --features generate
use qr_swastika_avoider::{encode_safe, Ecc};
let safe = encode_safe("https://quoicoubeh.com", Ecc::Medium)?; // clean, or Err
render(safe.matrix()); // `safe.mask()` is the mask that was chosen
Type or paste to check a code.
The contract is the part I care about most: the return type is Result<SafeQr, EncodeError>. There is no code path that yields a swastika and a smile. Either you get a code that is provably clean under the configured detector, or you get an error that tells you why.
6. The crate itself
The design goals were “boring to depend on” and “impossible to misuse”:
- Zero dependencies by default. The detector is pure hardcore Rust over a two-method trait. The
generatefeature and theqrcode/fast_qradapters each pull exactly the crate they name. #![forbid(unsafe_code)]. No unsafe, anywhere, enforced by the sadistic Rust™ compiler.- Plugs into what you already have. Implement
QrModules(side()andis_dark(x, y)) for any representation and the whole detector works on it. Adapters forqrcodegen,qrcodeandfast_qrship behind features; a blanket impl covers a rawVec<Vec<bool>>.
Rustpub trait QrModules {
fn side(&self) -> usize;
fn is_dark(&self, x: usize, y: usize) -> bool;
}
Every adapter is verified in tests by rendering the extracted grid and decoding it back with a real QR reader (rqrr). If an adapter got the stride, orientation or polarity wrong, the round-trip would fail. encode_safe’s output is checked the same way: clean and still scannable.
Get it now (100% discount for non-nazi)
TOML[dependencies]
qr-swastika-avoider = "0.2" # detector, zero deps
# qr-swastika-avoider = { version = "0.2", features = ["generate"] } # + encode_safe



- crates.io: crates.io/crates/qr-swastika-avoider
- docs.rs: docs.rs/qr-swastika-avoider
- source: github.com/marc-alexis-com/qr-swastika-avoider
MIT licensed. Blazingly fast. Memory-safe. Fearless swastika avoidance.
