Skip to content

Commit a5fb69f

Browse files
committed
Split QrCodebuilder into multiple classes
1 parent 779a0fb commit a5fb69f

12 files changed

Lines changed: 831 additions & 734 deletions

CLAUDE.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,15 @@ Text/bytes → segments → codewords → matrix, in this order:
5454

5555
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.
5656
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.
57-
3. **`QrCodeBuilder.Build`** (the heart of the library) does the rest:
58-
- `FindVersionAndEcc` — smallest version that fits, then boost ECC level for free if it doesn't grow the version.
59-
- `BuildCodewords` — segments → `BitStream` → byte codewords + terminator + 0xEC/0x11 padding.
60-
- `AddErrorCorrection` — splits into blocks, computes Reed-Solomon ECC (`ReedSolomon`), interleaves data and ECC codewords per spec.
61-
- Matrix layout — `CreateWithFixedPatterns` (finder/timing/alignment/format/version) 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. `EncodingInfo.ForcedDataMask` can override the choice.
57+
3. **`QrCodeBuilder.Build`** is a thin orchestrator that wires the remaining stages, each its own `internal static` module:
58+
- **`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)`.
59+
- **`Codewords.BuildData`** — segments → `BitStream` → byte codewords + terminator + 0xEC/0x11 padding.
60+
- **`Codewords.AddErrorCorrection`** — splits into blocks, computes Reed-Solomon ECC (`ReedSolomon`), interleaves data and ECC codewords per spec.
61+
- **`MatrixEncoder.Encode`** — matrix layout then mask selection:
62+
- `FixedPatterns.BuildFixedPatterns` is the single source of truth for the fixed-pattern geometry of a version: one walk emits both the *drawn* matrix (finder/timing/alignment/version) and the *reserved-module* mask, which cannot be derived from each other (a footprint reserves light modules too). Format info is reserve-only here. The reserved mask, inverted, is the *payload-area map* (`GetPayloadAreaMap`). Then `FillPayload` zig-zags the codewords into the free modules.
63+
- `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.
64+
65+
The ISO/IEC 18004 lookup tables these stages share live in **`QrCodeParameters`**.
6366

6467
### `BitMatrix` and the transpose trick
6568

@@ -69,9 +72,9 @@ The transpose is load-bearing, not a convenience: every penalty/format rule that
6972

7073
### Performance-tuned constants
7174

72-
Two orderings in `QrCodeBuilder`/`Penalty` are deliberately ordered by profiling data (see `QrCodeGeneratorProfiling/README.md`), not arbitrary: `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. `QrCodeBuilder` also caches fixed patterns and data masks per version in `ConcurrentDictionary` instances; cached `BitMatrix` instances are shared and must not be mutated (callers `Copy()` first).
75+
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).
7376

74-
Everything is keyed by `version` (1–40) and `ecc` (0–3 = L/M/Q/H). The large `static readonly` lookup tables in `QrCodeBuilder` (codeword capacity, block counts, alignment positions, format/version info bits) come straight from ISO/IEC 18004 tables — comments cite the table numbers.
77+
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.
7578

7679
### Rendering and diagnostics
7780

CONTEXT.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,16 @@ _Avoid_: "data mask" — see Flagged ambiguities.
3333
One of the 8 XOR patterns (index 0–7) applied to the payload area and chosen by
3434
lowest penalty score. Exposed as `QrCode.Mask`.
3535

36+
**Codewords**:
37+
The 8-bit symbols the payload becomes after a version and error correction level are
38+
chosen: data codewords (segment bits + terminator + padding) followed by Reed-Solomon
39+
error correction codewords, interleaved per spec, ready to fill into the matrix.
40+
3641
## Relationships
3742

43+
- The encode pipeline is: text → data segments → **codewords****module** matrix.
44+
A **version**/error-correction level is planned first, then the **codewords** are
45+
built, then filled into the matrix and a **mask pattern** is chosen.
3846
- A **version** (1–40) fully determines the **fixed patterns** and therefore the
3947
**reserved modules** and the **payload-area map**.
4048
- The **reserved modules** are the union of every fixed-pattern **footprint**;

QrCodeGenerator/Codewords.cs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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;
9+
using System.Collections.Generic;
10+
11+
namespace Net.Codecrete.QrCodeGenerator
12+
{
13+
/// <summary>
14+
/// Turns data segments into the codeword stream the matrix is filled with:
15+
/// first the data codewords (terminator + padding), then the Reed-Solomon error
16+
/// correction codewords, interleaved per specification.
17+
/// </summary>
18+
internal static class Codewords
19+
{
20+
/// <summary>
21+
/// Builds the data codewords for the given data segments.
22+
/// <para>
23+
/// The result includes the terminator and the padding for the
24+
/// given QR code version and error correction level.
25+
/// </para>
26+
/// </summary>
27+
/// <param name="dataSegments">The data segments to encode.</param>
28+
/// <param name="version">The QR code version.</param>
29+
/// <param name="ecc">The error correction level.</param>
30+
/// <returns>The data codewords.</returns>
31+
internal static byte[] BuildData(List<DataSegment> dataSegments, int version, int ecc)
32+
{
33+
var capacity = QrCodeParameters.GetCodewordDataCapacity(version, ecc);
34+
var bitStream = DataSegment.CreateBitStream(dataSegments, version, capacity);
35+
var bitstreamLength = bitStream.Length;
36+
37+
// TODO: avoid allocation of another array
38+
var result = new byte[capacity];
39+
bitStream.CopyCodewords(result, 0);
40+
41+
// add padding
42+
for (var index = (bitstreamLength + 7) / 8; index < capacity; index += 2)
43+
{
44+
result[index] = 0b1110_1100;
45+
}
46+
for (var index = (bitstreamLength + 15) / 8; index < capacity; index += 2)
47+
{
48+
result[index] = 0b0001_0001;
49+
}
50+
51+
return result;
52+
}
53+
54+
/// <summary>
55+
/// Adds the error correction to the given codewords and returns the combined result.
56+
/// <para>
57+
/// The result is transposed and interleaved as per specification.
58+
/// </para>
59+
/// </summary>
60+
/// <param name="codewords">The data codewords.</param>
61+
/// <param name="version">The QR code version.</param>
62+
/// <param name="ecc">The error correction level.</param>
63+
/// <returns>The combined result of data and error correction codewords.</returns>
64+
internal static byte[] AddErrorCorrection(byte[] codewords, int version, int ecc)
65+
{
66+
var numDataCodewords = codewords.Length;
67+
var numBlocks = QrCodeParameters.GetNumBlocks(version, ecc);
68+
var smallBlockDataLength = numDataCodewords / numBlocks;
69+
var eccBlockLength = (QrCodeParameters.GetCodewordCapacity(version) - numDataCodewords) / numBlocks;
70+
var numLargeBlocks = numDataCodewords % numBlocks;
71+
var numSmallBlocks = numBlocks - numLargeBlocks;
72+
73+
var result = new byte[QrCodeParameters.GetCodewordCapacity(version)];
74+
var dataOffset = 0;
75+
var reedSolomon = ReedSolomon.GeneratorForCapacity(eccBlockLength);
76+
77+
// split into blocks and process each block separately
78+
for (var block = 0; block < numBlocks; block += 1)
79+
{
80+
var dataLength = block < numSmallBlocks ? smallBlockDataLength : smallBlockDataLength + 1;
81+
82+
// compute the error correction codewords
83+
var eccCodewords = reedSolomon.ComputeErrorCorrection(new ArraySegment<byte>(codewords, dataOffset, dataLength));
84+
85+
// copy the data and error correction codewords to the transposed result
86+
for (var i = 0; i < smallBlockDataLength; i += 1)
87+
result[i * numBlocks + block] = codewords[dataOffset + i];
88+
if (block >= numSmallBlocks)
89+
result[numBlocks * smallBlockDataLength + block - numSmallBlocks] = codewords[dataOffset + dataLength - 1];
90+
for (var i = 0; i < eccBlockLength; i += 1)
91+
result[numDataCodewords + i * numBlocks + block] = eccCodewords[i];
92+
93+
dataOffset += dataLength;
94+
}
95+
96+
return result;
97+
}
98+
}
99+
}

QrCodeGenerator/FixedPatterns.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ namespace Net.Codecrete.QrCodeGenerator
3131
/// <para>
3232
/// The format information is reserve-only here; its bits depend on the error
3333
/// correction level and the chosen mask pattern and are drawn later by
34-
/// <see cref="QrCodeBuilder.DrawFormatInformation(BitMatrix, int, int)"/>.
34+
/// <see cref="MatrixEncoder.DrawFormatInformation(BitMatrix, int, int)"/>.
3535
/// </para>
3636
/// </summary>
3737
internal static class FixedPatterns
@@ -110,7 +110,7 @@ internal static BitMatrix GetPayloadAreaMap(int version)
110110
/// <returns>The drawn modules and the reserved-module mask.</returns>
111111
internal static (BitMatrix Drawn, BitMatrix Reserved) BuildFixedPatterns(int version)
112112
{
113-
var size = QrCodeBuilder.GetSize(version);
113+
var size = QrCodeParameters.GetSize(version);
114114
var drawn = new BitMatrix(size);
115115
var reserved = new BitMatrix(size);
116116

@@ -190,7 +190,7 @@ private static void DrawAndReserveAlignmentPatterns(BitMatrix drawn, BitMatrix r
190190
return;
191191
}
192192

193-
var positions = QrCodeBuilder.GetAlignmentPatternPosition(version);
193+
var positions = QrCodeParameters.GetAlignmentPatternPosition(version);
194194
var numPositions = positions.Length;
195195

196196
for (var x = 0; x < numPositions; x += 1)
@@ -270,7 +270,7 @@ private static void DrawVersionInformation(BitMatrix modules, int version)
270270
}
271271

272272
var size = modules.Size;
273-
var bits = QrCodeBuilder.GetVersionInformationBits(version);
273+
var bits = QrCodeParameters.GetVersionInformationBits(version);
274274

275275
for (var bit = 0; bit < 18; bit += 1)
276276
{

0 commit comments

Comments
 (0)