Skip to content

Commit 06ea926

Browse files
committed
Rewrite of library, better standard compliance, 10x speedup
1 parent 673c6f8 commit 06ea926

163 files changed

Lines changed: 14945 additions & 10061 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/continuous-integration.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ jobs:
2020
name: Build and run tests
2121
steps:
2222
- name: Checkout git repository
23-
uses: actions/checkout@v4
23+
uses: actions/checkout@v6
2424
with:
2525
fetch-depth: 0
2626

27-
- uses: actions/setup-dotnet@v4
27+
- uses: actions/setup-dotnet@v5
2828
with:
2929
dotnet-version: |
3030
6.x
@@ -40,7 +40,7 @@ jobs:
4040
run: dotnet test --no-build --verbosity normal
4141

4242
- name: Upload test results
43-
uses: actions/upload-artifact@v4
43+
uses: actions/upload-artifact@v7
4444
if: always()
4545
with:
4646
name: TestResults-${{ runner.os }}

.github/workflows/demos.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ jobs:
1616
name: Build demo projects
1717
steps:
1818
- name: Checkout git repository
19-
uses: actions/checkout@v4
19+
uses: actions/checkout@v6
2020
with:
2121
fetch-depth: 0
2222

23-
- uses: actions/setup-dotnet@v4
23+
- uses: actions/setup-dotnet@v5
2424
with:
2525
dotnet-version: |
2626
6.x
@@ -43,6 +43,9 @@ jobs:
4343
$pkg = Resolve-Path QrCodeGenerator\bin\Release\Net.Codecrete.QrCodeGenerator.*.nupkg
4444
nuget push $pkg -Source Local
4545
46+
- name: Build Basic-Example
47+
run: dotnet build
48+
working-directory: Basic-Example
4649
- name: Build Demo-ImageSharp
4750
run: dotnet build
4851
working-directory: Demo-ImageSharp
@@ -52,9 +55,6 @@ jobs:
5255
- name: Build Demo-ImageMagick
5356
run: dotnet build
5457
working-directory: Demo-ImageMagick
55-
- name: Build Demo-QRCode-Variety
56-
run: dotnet build
57-
working-directory: Demo-QRCode-Variety
5858
- name: Build Demo-SkiaSharp
5959
run: dotnet build
6060
working-directory: Demo-SkiaSharp

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@
2121

2222
# Rider
2323
.idea/
24+
25+
# Claude
26+
.claude/
27+
TestResults-*

Demo-QRCode-Variety/Demo-QRCode-Variety.csproj renamed to Basic-Example/Basic-Example.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
55
<TargetFramework>net8.0</TargetFramework>
6-
<RootNamespace>Demo_Basic</RootNamespace>
6+
<RootNamespace>Net.Codecrete.QrCodeGenerator.Demo</RootNamespace>
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="Net.Codecrete.QrCodeGenerator" Version="2.*" />
10+
<PackageReference Include="Net.Codecrete.QrCodeGenerator" Version="3.*" />
1111
</ItemGroup>
1212

1313
</Project>

Demo-QRCode-Variety/Demo-QRCode-Variety.sln renamed to Basic-Example/Basic-Example.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 17
44
VisualStudioVersion = 17.0.31903.59
55
MinimumVisualStudioVersion = 10.0.40219.1
6-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo-QRCode-Variety", "Demo-QRCode-Variety.csproj", "{CE334406-7A4A-4455-889B-200DEBB6C08C}"
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Basic-Example", "Basic-Example.csproj", "{CE334406-7A4A-4455-889B-200DEBB6C08C}"
77
EndProject
88
Global
99
GlobalSection(SolutionConfigurationPlatforms) = preSolution

Basic-Example/Program.cs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* QR code generator library (.NET)
3+
*
4+
* Copyright (c) Manuel Bleichenbacher (MIT License)
5+
* https://github.com/manuelbl/QrCodeGenerator
6+
*/
7+
8+
using System.IO;
9+
using System.Text;
10+
11+
namespace Net.Codecrete.QrCodeGenerator.Demo
12+
{
13+
internal class Program
14+
{
15+
// The main application program.
16+
internal static void Main()
17+
{
18+
// Enable support for special encodings like Shift-JIS
19+
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
20+
21+
BasicQrCode();
22+
AlphanumericText();
23+
LongerText();
24+
Emojis();
25+
EmojisWithoutEci();
26+
BinaryData();
27+
GraphicsFormats();
28+
}
29+
30+
// Creates a single QR code, then writes it to an SVG file.
31+
private static void BasicQrCode()
32+
{
33+
const string text = "Hello, world!"; // Payload text
34+
var errCorLvl = QrCode.Ecc.Low; // Minimal error correction level
35+
36+
var qrCode = QrCode.EncodeText(text, errCorLvl); // Create the QR code symbol
37+
SaveAsSvg(qrCode, "hello-world-qr.svg"); // Save as SVG
38+
}
39+
40+
41+
// Creates QR code with digits and alphanumeric characters only.
42+
private static void AlphanumericText()
43+
{
44+
// For digits, a more compact representation will automatically be chosen (3.33 bits per digit)
45+
var qrCode = QrCode.EncodeText("27182818284590452353602874713526624977572470936999595749669676277240766",
46+
QrCode.Ecc.Medium);
47+
SaveAsSvg(qrCode, "digits-qr.svg");
48+
49+
// For an alphanumeric subset of characters (not including lower-case letters),
50+
// a more compact representation will be automatically chosen (5.5 bits per character)
51+
qrCode = QrCode.EncodeText("THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG", QrCode.Ecc.High);
52+
SaveAsSvg(qrCode, "alphanumeric-qr.svg");
53+
}
54+
55+
private static void LongerText()
56+
{
57+
// Moderately large QR code using longer text
58+
var qrCode = QrCode.EncodeText(
59+
"I was a Flower of the mountain yes when I put the rose in my hair like the Andalusian girls used " +
60+
"or shall I wear a red yes and how he kissed me under the Moorish wall and I thought well as well " +
61+
"him as another and then I asked him with my eyes to ask again yes and then he asked me would I " +
62+
"yes to say yes my mountain flower and first I put my arms around him yes and drew him down to me " +
63+
"so he could feel my breasts all perfume yes and his heart was going like mad and yes I said yes " +
64+
"I will Yes.", QrCode.Ecc.High);
65+
SaveAsSvg(qrCode, "joyce-qr.svg");
66+
}
67+
68+
private static void Emojis()
69+
{
70+
// The full Unicode character set is supported.
71+
// By default, the library uses UTF-8 encoding and indicates this with an ECI designator.
72+
var qrCode = QrCode.EncodeText("🎲 😇 🤒 🏌 ⏭ 🚍", QrCode.Ecc.Quartile);
73+
SaveAsSvg(qrCode, "emojis-qr.svg");
74+
}
75+
76+
private static void EmojisWithoutEci()
77+
{
78+
// Suppress the ECI designator.
79+
// Most QR code readers will correctly guess the encoding.
80+
// Some readers always ignore the ECI designator.
81+
var qrCode = QrCode.EncodeTextAdvanced("🎲 😇 🤒 🏌 ⏭ 🚍", QrCode.Ecc.Quartile,
82+
encoding: Encoding.UTF8, eci: ECI.None);
83+
SaveAsSvg(qrCode, "emojis-no-eci-qr.svg");
84+
}
85+
86+
private static void BinaryData()
87+
{
88+
// Encode binary data. An ECI designator will be added to indicate it.
89+
// Exchanging binary data with QR codes usually works in closed systems only.
90+
byte[] data = {
91+
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00,
92+
0x01, 0x00, 0x80, 0x01, 0x00, 0xff, 0xff, 0xff,
93+
0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x0a,
94+
0x00, 0x01, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00,
95+
0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c,
96+
0x01, 0x00, 0x3b
97+
};
98+
var qr = QrCode.EncodeBinary(data, QrCode.Ecc.Medium);
99+
SaveAsSvg(qr, "binary.svg");
100+
}
101+
102+
private static void GraphicsFormats()
103+
{
104+
// Save the QR code in various graphics formats, directly supported by the library.
105+
// See the demo applications for further graphics format and displaying options.
106+
var qrCode = QrCode.EncodeText(
107+
"Ineluctable modality of the visible: at least that if no more, thought through my eyes. Signatures " +
108+
"of all things I am here to read, seapawn and searack, the nearing tide, that rusty boot. Snotgreen, " +
109+
"bluesilver, rust: colored signs. Limits of the diaphane.", QrCode.Ecc.Medium);
110+
111+
File.WriteAllBytes("qr-code.png", qrCode.ToPngBitmap(border: 4));
112+
113+
File.WriteAllBytes("qr-code.bmp", qrCode.ToBmpBitmap(border: 4));
114+
}
115+
116+
private static void SaveAsSvg(QrCode qrCode, string filname)
117+
{
118+
string svg = qrCode.ToSvgString(4); // Convert to SVG XML code
119+
File.WriteAllText(filname, svg, Encoding.UTF8); // Write image to file
120+
}
121+
122+
}
123+
}

Basic-Example/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Example code for generating QR codes
2+
3+
This example program creates a series of QR code and saves them as SVG, PNG and BMP files.
4+
It is a simple console application without the need for any further libraries.
5+
It will run on any .NET platform compatible with .NET Standard 2.0.
6+
7+
The examples demonstrate the use of:
8+
9+
- different texxt encoding (short/long, digits, alphanumeric, full Unicode)
10+
- different error correction levels
11+
- encoding binary data

CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# CLAUDE.md
2+
3+
## Purpose
4+
5+
QrCodeGenerator is a library to generate QR code. It is designed to be easy to use and performant.
6+
7+
The library only supports limited graphics types and options in order to work without
8+
any graphics libraries, which might not run all platforms.
9+
The library can provide the QR code as a list of rectangles or as a two-dimensional array of pixels
10+
(called modules by the QR code standard). It is then up to the application to display the QR code.
11+
Many demo projects show how to use this approach for different graphics libraries and UI frameworks.
12+
13+
The main target of the library is .NET Standard 2.0 so it runs in virtually any current .NET environment.
14+
15+
16+
## Commands
17+
18+
```bash
19+
# Build
20+
dotnet build
21+
dotnet build --configuration Release
22+
23+
# Test (all)
24+
dotnet test
25+
26+
# Test (single class)
27+
dotnet test --filter "FullyQualifiedName~QrCodeTest"
28+
29+
# Test (single method)
30+
dotnet test --filter "FullyQualifiedName=Net.Codecrete.QrCodeGenerator.Test.QrCodeTest.Constants"
31+
32+
# Pack NuGet
33+
dotnet pack --no-build
34+
```
35+
36+
## Build targets
37+
38+
- **`QrCodeGenerator/`** (the library) targets `netstandard2.0;net6.0`. The `net6.0` target exists only to enable trimming (`IsTrimmable`). Keep public API and language usage compatible with netstandard2.0 — don't reach for newer BCL/`Span` APIs that aren't available there.
39+
- **`QrCodeGeneratorTest/`** (the unit tests) targets `net8.0;net10.0`, plus `net481` on Windows. `dotnet test` runs every target framework.
40+
- **`QrCodeGeneratorProfiling/`** targets `net10.0` (BenchmarkDotNet).
41+
- **`QrCodeAnalyzer/`** is a separate WPF tool in its own solution (`QrCodeAnalyzer/QrCodeAnalyzer.sln`), not part of `QrCodeGenerator.sln`.
42+
43+
44+
## Architecture
45+
46+
`QrCode` is the only substantial public surface: factory methods create immutable `QrCode` instances. They can be rendered to
47+
sVG, PNG, BMP or a list of rectangles. It holds a single `BitMatrix` of modules. Almost all real work lives in `internal` types.
48+
49+
50+
### Encoding pipeline
51+
52+
Text/bytes → segments → codewords → matrix, in this order:
53+
54+
1. **`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 an `ArraySegment` over the caller's array until the bit stream is built — **the source array must not be mutated** in the meantime.
55+
2. **`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 on `version` (count-indicator width changes at versions 1/10/27). This is what produces the "smallest possible QR code" claim.
56+
3. **`QrCodeBuilder.Build`** is a thin orchestrator that wires the remaining stages, each its own `internal static` module:
57+
- **`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)`.
58+
- **`Codewords.BuildData`** — segments → `BitStream` → byte codewords + terminator + 0xEC/0x11 padding.
59+
- **`Codewords.AddErrorCorrection`** — splits into blocks, computes Reed-Solomon ECC (`ReedSolomon`), interleaves data and ECC codewords per spec.
60+
- **`MatrixEncoder.Encode`** — matrix layout then mask selection:
61+
- `FixedPatterns.BuildFixedPatterns` deals 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`). Then `FillPayload` zig-zags the codewords into the free modules.
62+
- `ApplyBestPattern` — XORs each of the 8 mask patterns, scores it (`Penalty`), keeps the lowest, and draws the format information. `EncodingInfo.ForcedDataMask` can override the choice.
63+
64+
The ISO/IEC 18004 lookup tables these stages share live in **`QrCodeParameters`**.
65+
66+
### `BitMatrix` and the transpose trick
67+
68+
`BitMatrix` (`internal readonly struct`) is the central data structure: a square bit grid stored as 4 `ulong`s 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**.
69+
70+
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.
71+
72+
### Performance-tuned constants
73+
74+
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).
75+
76+
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.
77+
78+
### Rendering and diagnostics
79+
80+
- `RectangleBuilder` merges 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.ToRectangles` exposes the list publicly (as `QrRectangle`s, in `GetModule` coordinates with no border), and `SvgBuilder` (SVG document + SVG/XAML path) consumes the same list, adding the border at emit time. `BmpBuilder` (BMP) and `PngBuilder` (PNG) take the finished modules directly. `QrCode` delegates to all of them.
81+
- `StructuredAppend` splits long text across up to 16 linked QR codes (used by `EncodeTextInMultipleCodes`).
82+
- `EncodingInfo` / `PenaltyScore` are opt-in diagnostics: pass an `EncodingInfo` to capture per-mask penalty breakdowns and the chosen segments. This forces *full* penalty evaluation (disables early-stop), so it is slower — it exists for the `QrCodeAnalyzer` tool, not normal use.
83+
84+
## Tests
85+
86+
Uses **xUnit v3** with **Verify.XunitV3** for snapshot/approval testing and **Xunit.Combinatorial** for parameterized matrices.
87+
88+
The test strategy is characterization + cross-validation, not just unit tests:
89+
90+
- **`QrCodeDataProvider.cs`** is a large generated `[ClassData]` source feeding `QrCodeTest`. `TestQrCode` asserts the exact module layout, version, ECC, and mask for each case (golden tests).
91+
- **`VerifyWithZXing`** decodes every generated QR code with **ZXing.Net** and asserts the text round-trips with *zero* errors corrected. `CorruptedModule_IsCorrected_AndReportsErrorsCorrected` is 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.)
92+
- **`ReedSolomonTest`** cross-checks ECC against the **STH1123.ReedSolomon** package.
93+
- **Verify** snapshot tests cover rendered output (SVG/PNG/BMP). Snapshot `.verified.*` files live alongside the test source in `QrCodeGeneratorTest/`. When a snapshot changes intentionally, run `dotnet test` once — Verify fails and writes the new `.verified.*` file; accept it by replacing the old one (or via the Verify tooling).

0 commit comments

Comments
 (0)