Skip to content

Commit 8be38de

Browse files
committed
List of rectangles
1 parent cd87978 commit 8be38de

7 files changed

Lines changed: 474 additions & 140 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Version 3 is a complete rewrite (≈10x faster, more standard-compliant) of what
4646

4747
## Architecture
4848

49-
`QrCode` is the only substantial public surface: immutable factory methods (`EncodeText`, `EncodeTextAdvanced`, `EncodeBinary`, `EncodeSegments`, `EncodeTextInMultipleCodes`) plus rendering (`ToSvgString`, `ToGraphicsPath`, `ToPngBitmap`, `ToBmpBitmap`, `GetModule`). It holds a single `BitMatrix` of modules. Almost all real work lives in `internal` types.
49+
`QrCode` is the only substantial public surface: immutable factory methods (`EncodeText`, `EncodeTextAdvanced`, `EncodeBinary`, `EncodeSegments`, `EncodeTextInMultipleCodes`) plus rendering (`ToSvgString`, `ToGraphicsPath`, `ToPngBitmap`, `ToBmpBitmap`, `ToRectangles`, `GetModule`). It holds a single `BitMatrix` of modules. Almost all real work lives in `internal` types. The one other public type is the `QrRectangle` value struct returned by `ToRectangles`.
5050

5151
### Encoding pipeline
5252

@@ -78,7 +78,7 @@ Everything is keyed by `version` (1–40) and `ecc` (0–3 = L/M/Q/H). The large
7878

7979
### Rendering and diagnostics
8080

81-
- `Graphics` (SVG/XAML path, BMP) and `PngBuilder` (PNG) take the finished modules; `QrCode` delegates to them. The SVG path merges adjacent dark modules into the largest rectangles to shrink output.
81+
- `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.
8282
- `StructuredAppend` splits long text across up to 16 linked QR codes (used by `EncodeTextInMultipleCodes`).
8383
- `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.
8484

Lines changed: 3 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* QR code generator library (.NET)
33
*
44
* Copyright (c) Manuel Bleichenbacher (MIT License)
@@ -7,14 +7,12 @@
77

88
using System;
99
using System.Diagnostics.CodeAnalysis;
10-
using System.Globalization;
11-
using System.Text;
1210

1311
namespace Net.Codecrete.QrCodeGenerator
1412
{
15-
internal class Graphics
13+
internal class BmpBuilder
1614
{
17-
internal Graphics(int size, bool[,] modules)
15+
internal BmpBuilder(int size, bool[,] modules)
1816
{
1917
_size = size;
2018
_modules = modules;
@@ -26,45 +24,6 @@ internal Graphics(int size, bool[,] modules)
2624
// Immutable after constructor finishes.
2725
private readonly bool[,] _modules;
2826

29-
internal string ToSvgString(int border, string foreground, string background)
30-
{
31-
if (border < 0)
32-
{
33-
throw new ArgumentOutOfRangeException(nameof(border), "Border must be non-negative");
34-
}
35-
36-
var dim = _size + border * 2;
37-
var sb = new StringBuilder()
38-
.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
39-
.Append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
40-
.Append($"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {dim} {dim}\" stroke=\"none\">\n")
41-
.Append($"\t<rect width=\"100%\" height=\"100%\" fill=\"{background}\"/>\n")
42-
.Append("\t<path d=\"");
43-
44-
// Work on copy as it is destructive
45-
var modules = CopyModules();
46-
CreatePath(sb, modules, border);
47-
48-
return sb
49-
.Append($"\" fill=\"{foreground}\"/>\n")
50-
.Append("</svg>\n")
51-
.ToString();
52-
}
53-
54-
internal string ToGraphicsPath(int border)
55-
{
56-
if (border < 0)
57-
{
58-
throw new ArgumentOutOfRangeException(nameof(border), "Border must be non-negative");
59-
}
60-
61-
// Work on copy as it is destructive
62-
var modules = CopyModules();
63-
var path = new StringBuilder();
64-
CreatePath(path, modules, border);
65-
return path.ToString();
66-
}
67-
6827
[SuppressMessage("csharpsquid", "S3776")]
6928
internal byte[] ToBmpBitmap(int border, int scale, int foreground, int background)
7029
{
@@ -227,94 +186,5 @@ internal byte[] ToBmpBitmap(int border, int scale, int foreground, int backgroun
227186

228187
return buf;
229188
}
230-
231-
// Append an SVG/XAML path for the QR code to the provided string builder
232-
private static void CreatePath(StringBuilder path, bool[,] modules, int border)
233-
{
234-
// Simple algorithm to reduce the number of rectangles for drawing the QR code
235-
// and reduce SVG/XAML size.
236-
var size = modules.GetLength(0);
237-
for (var y = 0; y < size; y++)
238-
{
239-
for (var x = 0; x < size; x++)
240-
{
241-
if (modules[y, x])
242-
{
243-
DrawLargestRectangle(path, modules, x, y, border);
244-
}
245-
}
246-
}
247-
}
248-
249-
// Find, draw and clear largest rectangle with (x, y) as the top left corner
250-
private static void DrawLargestRectangle(StringBuilder path, bool[,] modules, int x, int y, int border)
251-
{
252-
var size = modules.GetLength(0);
253-
254-
var bestW = 1;
255-
var bestH = 1;
256-
var maxArea = 1;
257-
258-
var xLimit = size;
259-
var iy = y;
260-
while (iy < size && modules[iy, x])
261-
{
262-
var w = 0;
263-
while (x + w < xLimit && modules[iy, x + w])
264-
{
265-
w++;
266-
}
267-
268-
var area = w * (iy - y + 1);
269-
if (area > maxArea)
270-
{
271-
maxArea = area;
272-
bestW = w;
273-
bestH = iy - y + 1;
274-
}
275-
xLimit = x + w;
276-
iy++;
277-
}
278-
279-
// append path command
280-
if (x != 0 || y != 0)
281-
{
282-
path.Append(' ');
283-
}
284-
285-
// Different locales use different minus signs.
286-
FormattableString pathElement = $"M{x + border},{y + border}h{bestW}v{bestH}h{-bestW}z";
287-
path.Append(pathElement.ToString(CultureInfo.InvariantCulture));
288-
289-
// clear processed modules
290-
ClearRectangle(modules, x, y, bestW, bestH);
291-
}
292-
293-
// Clear a rectangle of modules
294-
private static void ClearRectangle(bool[,] modules, int x, int y, int width, int height)
295-
{
296-
for (var iy = y; iy < y + height; iy++)
297-
{
298-
for (var ix = x; ix < x + width; ix++)
299-
{
300-
modules[iy, ix] = false;
301-
}
302-
}
303-
}
304-
305-
// Create a copy of the modules (in row-major order)
306-
private bool[,] CopyModules()
307-
{
308-
var modules = new bool[_size, _size];
309-
for (var y = 0; y < _size; y++)
310-
{
311-
for (var x = 0; x < _size; x++)
312-
{
313-
modules[y, x] = _modules[y, x];
314-
}
315-
}
316-
317-
return modules;
318-
}
319189
}
320190
}

QrCodeGenerator/QrCode.cs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ public bool GetModule(int x, int y)
387387
/// <param name="background">The background color. Defaults to white.</param>
388388
public string ToSvgString(int border, string foreground = "#000000", string background = "#ffffff")
389389
{
390-
return AsGraphics().ToSvgString(border, foreground, background);
390+
return SvgBuilder.ToSvgString(ToRectangles(), Size, border, foreground, background);
391391
}
392392

393393
/// <summary>
@@ -413,7 +413,34 @@ public string ToSvgString(int border, string foreground = "#000000", string back
413413
/// <exception cref="ArgumentOutOfRangeException">Thrown if border is negative</exception>
414414
public string ToGraphicsPath(int border = 0)
415415
{
416-
return AsGraphics().ToGraphicsPath(border);
416+
return SvgBuilder.ToGraphicsPath(ToRectangles(), border);
417+
}
418+
419+
/// <summary>
420+
/// Gets the dark modules of this QR code as a list of rectangles.
421+
/// <para>
422+
/// Adjacent dark modules are merged into larger rectangles so that most rectangles
423+
/// cover more than a single module, reducing the number of shapes to draw. This is
424+
/// the same set of rectangles used internally to render SVG and XAML output.
425+
/// </para>
426+
/// <para>
427+
/// The rectangles use the same coordinate system as <see cref="GetModule"/>: the
428+
/// top-left module is at (x=0, y=0) and each unit is one module (no border is included).
429+
/// </para>
430+
/// <para>
431+
/// The rectangles are non-overlapping and their union is exactly the set of dark modules.
432+
/// The order in which the rectangles appear in the list is unspecified.
433+
/// </para>
434+
/// <para>
435+
/// This method is intended for rendering the QR code to graphics formats that are not
436+
/// directly supported by this library.
437+
/// </para>
438+
/// </summary>
439+
/// <returns>The list of rectangles covering the dark modules.</returns>
440+
/// <seealso cref="QrRectangle"/>
441+
public IReadOnlyList<QrRectangle> ToRectangles()
442+
{
443+
return RectangleBuilder.Build(_modules);
417444
}
418445

419446
/// <summary>
@@ -434,7 +461,7 @@ public string ToGraphicsPath(int border = 0)
434461
/// <paramref name="scale"/> is less than 1 or the resulting image is wider than 32,768 pixels.</exception>
435462
public byte[] ToBmpBitmap(int border = 0, int scale = 1, int foreground = 0x000000, int background = 0xffffff)
436463
{
437-
return AsGraphics().ToBmpBitmap(border, scale, foreground, background);
464+
return AsBmpBuilder().ToBmpBitmap(border, scale, foreground, background);
438465
}
439466

440467
/// <summary>
@@ -467,9 +494,9 @@ public static int RgbColor(byte red, byte green, byte blue)
467494

468495
#region Private helper methods
469496

470-
private Graphics AsGraphics()
497+
private BmpBuilder AsBmpBuilder()
471498
{
472-
return new Graphics(Size, _modules.ToBoolArray());
499+
return new BmpBuilder(Size, _modules.ToBoolArray());
473500
}
474501

475502
#endregion

0 commit comments

Comments
 (0)