QrCodeGenerator is a library to generate QR code. It is designed to be easy to use and performant.
The library only supports limited graphics types and options in order to work without any graphics libraries, which might not run all platforms. The library can provide the QR code as a list of rectangles or as a two-dimensional array of pixels (called modules by the QR code standard). It is then up to the application to display the QR code. Many demo projects show how to use this approach for different graphics libraries and UI frameworks.
The main target of the library is .NET Standard 2.0 so it runs in virtually any current .NET environment.
# Build
dotnet build
dotnet build --configuration Release
# Test (all)
dotnet test
# Test (single class)
dotnet test --filter "FullyQualifiedName~QrCodeTest"
# Test (single method)
dotnet test --filter "FullyQualifiedName=Net.Codecrete.QrCodeGenerator.Test.QrCodeTest.Constants"
# Pack NuGet
dotnet pack --no-buildQrCodeGenerator/(the library) targetsnetstandard2.0;net6.0. Thenet6.0target exists only to enable trimming (IsTrimmable). Keep public API and language usage compatible with netstandard2.0 — don't reach for newer BCL/SpanAPIs that aren't available there.QrCodeGeneratorTest/(the unit tests) targetsnet8.0;net10.0, plusnet481on Windows.dotnet testruns every target framework.QrCodeGeneratorProfiling/targetsnet10.0(BenchmarkDotNet).QrCodeAnalyzer/is a separate WPF tool in its own solution (QrCodeAnalyzer/QrCodeAnalyzer.sln), not part ofQrCodeGenerator.sln.
QrCode is the only substantial public surface: factory methods create immutable QrCode instances. They can be rendered to
sVG, PNG, BMP or a list of rectangles. It holds a single BitMatrix of modules. Almost all real work lives in internal types.
Text/bytes → segments → codewords → matrix, in this order:
-
DataSegment.FromText/FromBinaryData— chooses the text encoding. For automatic ECI: tries Latin-1 (ISO-8859-1) and adds no ECI; falls back to UTF-8 with an ECI designator if Latin-1 is lossy. Segments retain the original unencoded bytes as anArraySegmentover the caller's array until the bit stream is built — the source array must not be mutated in the meantime. -
SegmentCompaction— picks the cheapest per-byte mode (numeric > alphanumeric > Kanji > binary), groups consecutive bytes into blocks, then greedily merges adjacent blocks when the mode-switch overhead outweighs the savings. Merge cost depends onversion(count-indicator width changes at versions 1/10/27). This is what produces the "smallest possible QR code" claim. -
QrCodeBuilder.Buildis a thin orchestrator that wires the remaining stages, each its owninternal staticmodule:VersionPlanner.Plan— smallest version that fits, then boost ECC level for free if it doesn't grow the version. Returns a named(int Version, int Ecc).Codewords.BuildData— segments →BitStream→ byte codewords + terminator + 0xEC/0x11 padding.Codewords.AddErrorCorrection— splits into blocks, computes Reed-Solomon ECC (ReedSolomon), interleaves data and ECC codewords per spec.MatrixEncoder.Encode— matrix layout then mask selection:FixedPatterns.BuildFixedPatternsdeals with the fixed-pattern geometry of a version: one walk emits both the drawn matrix (finder/timing/alignment/version) and the reserved-module mask. The reserved mask, inverted, is the payload-area map (GetPayloadAreaMap). ThenFillPayloadzig-zags the codewords into the free modules.ApplyBestPattern— XORs each of the 8 mask patterns, scores it (Penalty), keeps the lowest, and draws the format information.EncodingInfo.ForcedDataMaskcan override the choice.
The ISO/IEC 18004 lookup tables these stages share live in
QrCodeParameters.
BitMatrix (internal readonly struct) is the central data structure: a square bit grid stored as 4 ulongs per row (256-bit rows regardless of size), in row-major order. It exposes fast whole-matrix And/Xor/Invert/PopCount and an in-place 64×64 block transpose.
The transpose is load-bearing, not a convenience: every penalty/format rule that operates on columns is implemented by transposing the matrix and reusing the row-wise algorithm. ApplyBestPattern keeps a modules and a transposed copy in lock-step, and Penalty takes both. Penalty itself is bit-parallel (operates on whole ulong words, not per-module) and has an early-stop path that bails once the running score exceeds the best-so-far.
Two orderings are deliberately ordered by profiling data (see QrCodeGeneratorProfiling/README.md), not arbitrary: MatrixEncoder.PatternEvaluationOrder (evaluate likely-best masks first to tighten the early-stop bound) and the rule order inside Penalty.CalculatePenalty. Reordering them changes performance, not output. Per-version BitMatrix results are cached in ConcurrentDictionary instances — FixedPatterns caches the drawn fixed patterns and the payload-area map; MatrixEncoder caches the data-mask patterns. Cached BitMatrix instances are shared and must not be mutated (callers Copy() first).
Everything is keyed by version (1–40) and ecc (0–3 = L/M/Q/H). The large static readonly lookup tables in QrCodeParameters (codeword capacity, block counts, alignment positions, format/version info bits) come straight from ISO/IEC 18004 tables — comments cite the table numbers.
RectangleBuildermerges adjacent dark modules into the largest rectangles (greedy, non-overlapping, union == dark modules) to shrink output. It is the single source of truth for that geometry:QrCode.ToRectanglesexposes the list publicly (asQrRectangles, inGetModulecoordinates with no border), andSvgBuilder(SVG document + SVG/XAML path) consumes the same list, adding the border at emit time.BmpBuilder(BMP) andPngBuilder(PNG) take the finished modules directly.QrCodedelegates to all of them.StructuredAppendsplits long text across up to 16 linked QR codes (used byEncodeTextInMultipleCodes).EncodingInfo/PenaltyScoreare opt-in diagnostics: pass anEncodingInfoto capture per-mask penalty breakdowns and the chosen segments. This forces full penalty evaluation (disables early-stop), so it is slower — it exists for theQrCodeAnalyzertool, not normal use.
Uses xUnit v3 with Verify.XunitV3 for snapshot/approval testing and Xunit.Combinatorial for parameterized matrices.
The test strategy is characterization + cross-validation, not just unit tests:
QrCodeDataProvider.csis a large generated[ClassData]source feedingQrCodeTest.TestQrCodeasserts the exact module layout, version, ECC, and mask for each case (golden tests).VerifyWithZXingdecodes every generated QR code with ZXing.Net and asserts the text round-trips with zero errors corrected.CorruptedModule_IsCorrected_AndReportsErrorsCorrectedis a negative control that flips one module to prove that assertion has teeth. (Some ECI+Kanji combinations are skipped due to a ZXing 0.16.x decode bug — the QR is still valid.)ReedSolomonTestcross-checks ECC against the STH1123.ReedSolomon package.- Verify snapshot tests cover rendered output (SVG/PNG/BMP). Snapshot
.verified.*files live alongside the test source inQrCodeGeneratorTest/. When a snapshot changes intentionally, rundotnet testonce — Verify fails and writes the new.verified.*file; accept it by replacing the old one (or via the Verify tooling).