Skip to content

Commit cd87978

Browse files
committed
More analyzer features
1 parent a5fb69f commit cd87978

9 files changed

Lines changed: 181 additions & 34 deletions

File tree

QrCodeAnalyzer/MainWindow.xaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,14 @@
6565
PreviewMouseLeftButtonDown="HighlightLabel_MouseDown" PreviewMouseLeftButtonUp="HighlightLabel_MouseUp"/>
6666
<Label Content="{Binding Path=PenaltyInfo.Blocks}" Grid.Row="9" Grid.Column="1" Background="Transparent" Cursor="Hand" Tag="Blocks"
6767
PreviewMouseLeftButtonDown="HighlightLabel_MouseDown" PreviewMouseLeftButtonUp="HighlightLabel_MouseUp"/>
68-
<Label Content="Horz. Finder:" Grid.Row="10" Grid.Column="0"/>
69-
<Label Content="{Binding Path=PenaltyInfo.HorizontalFinderPatterns}" Grid.Row="10" Grid.Column="1"/>
70-
<Label Content="Vert. Finder:" Grid.Row="11" Grid.Column="0"/>
71-
<Label Content="{Binding Path=PenaltyInfo.VerticalFinderPatterns}" Grid.Row="11" Grid.Column="1"/>
68+
<Label Content="Horz. Finder:" Grid.Row="10" Grid.Column="0" Background="Transparent" Cursor="Hand" Tag="HorizontalFinderPatterns"
69+
PreviewMouseLeftButtonDown="HighlightLabel_MouseDown" PreviewMouseLeftButtonUp="HighlightLabel_MouseUp"/>
70+
<Label Content="{Binding Path=PenaltyInfo.HorizontalFinderPatterns}" Grid.Row="10" Grid.Column="1" Background="Transparent" Cursor="Hand" Tag="HorizontalFinderPatterns"
71+
PreviewMouseLeftButtonDown="HighlightLabel_MouseDown" PreviewMouseLeftButtonUp="HighlightLabel_MouseUp"/>
72+
<Label Content="Vert. Finder:" Grid.Row="11" Grid.Column="0" Background="Transparent" Cursor="Hand" Tag="VerticalFinderPatterns"
73+
PreviewMouseLeftButtonDown="HighlightLabel_MouseDown" PreviewMouseLeftButtonUp="HighlightLabel_MouseUp"/>
74+
<Label Content="{Binding Path=PenaltyInfo.VerticalFinderPatterns}" Grid.Row="11" Grid.Column="1" Background="Transparent" Cursor="Hand" Tag="VerticalFinderPatterns"
75+
PreviewMouseLeftButtonDown="HighlightLabel_MouseDown" PreviewMouseLeftButtonUp="HighlightLabel_MouseUp"/>
7276
<Label Content="Balance:" Grid.Row="12" Grid.Column="0"/>
7377
<Label Content="{Binding Path=PenaltyInfo.ColorBalance}" Grid.Row="12" Grid.Column="1"/>
7478
<Label Content="Total:" Grid.Row="13" Grid.Column="0"/>

QrCodeAnalyzer/MainWindowViewModel.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
namespace Net.Codecrete.QrCodeGenerator.Analyzer;
2020

21-
public enum HighlightKind { None, Blocks, HorizontalStreaks, VerticalStreaks }
21+
public enum HighlightKind { None, Blocks, HorizontalStreaks, VerticalStreaks, HorizontalFinderPatterns, VerticalFinderPatterns }
2222

2323
public sealed record SegmentRow(string Mode, string Content);
2424

@@ -247,12 +247,30 @@ private static SegmentRow BuildSegmentRow(DataSegment segment)
247247
DataSegmentMode.Alphanumeric => new SegmentRow("Alphanumeric", DecodeAscii(segment.DataBytes)),
248248
DataSegmentMode.Binary => new SegmentRow("Byte", FormatByteContent(segment.DataBytes)),
249249
DataSegmentMode.Kanji => new SegmentRow("Kanji", FormatHex(segment.DataBytes)),
250-
DataSegmentMode.ECI => new SegmentRow("ECI", "TODO"),
251-
DataSegmentMode.StructuredAppend => new SegmentRow("Structured Append", "TODO"),
250+
DataSegmentMode.ECI => new SegmentRow("ECI", FormatEci(segment.EciDesignator)),
251+
DataSegmentMode.StructuredAppend => new SegmentRow("Structured Append", FormatStructuredAppend(segment)),
252252
_ => new SegmentRow(segment.Mode.ToString(), FormatHex(segment.DataBytes)),
253253
};
254254
}
255255

256+
private static string FormatEci(ECI eci)
257+
{
258+
try
259+
{
260+
return $"{eci.Value} ({eci.GetEncoding().WebName})";
261+
}
262+
catch (ECIException)
263+
{
264+
return eci.Value.ToString();
265+
}
266+
}
267+
268+
private static string FormatStructuredAppend(DataSegment segment)
269+
{
270+
return $"Part {segment.StructuredAppendPosition} of {segment.StructuredAppendTotal}, "
271+
+ $"parity 0x{segment.StructuredAppendParity:X2}";
272+
}
273+
256274
private static string DecodeAscii(ArraySegment<byte> data)
257275
{
258276
return data.Array == null ? string.Empty : Encoding.ASCII.GetString(data.Array, data.Offset, data.Count);
@@ -301,6 +319,8 @@ private void RenderQrCodeImage()
301319
HighlightKind.Blocks => Penalty.GetBlocks(_currentQrCode),
302320
HighlightKind.HorizontalStreaks => Penalty.GetHorizontalStreaks(_currentQrCode),
303321
HighlightKind.VerticalStreaks => Penalty.GetVerticalStreaks(_currentQrCode),
322+
HighlightKind.HorizontalFinderPatterns => Penalty.GetHorizontalFinderPatterns(_currentQrCode),
323+
HighlightKind.VerticalFinderPatterns => Penalty.GetVerticalFinderPatterns(_currentQrCode),
304324
_ => null
305325
};
306326
QrCodeImage = QrCodeDrawing.CreateDrawing(_currentQrCode, 192, _borderWidth, highlights);

QrCodeAnalyzer/Penalty.cs

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ internal static List<Rectangle> GetBlocks(QrCode qr)
2323
}
2424
}
2525

26-
return FilterOutsideFinderArea(rectangles, qr.Size);
26+
return FilterNotInFinderArea(rectangles, qr.Size);
2727
}
2828

2929
internal static List<Rectangle> GetHorizontalStreaks(QrCode qr)
@@ -56,7 +56,7 @@ internal static List<Rectangle> GetHorizontalStreaks(QrCode qr)
5656
}
5757
}
5858

59-
return FilterOutsideFinderArea(rectangles, qr.Size);
59+
return FilterNotInFinderArea(rectangles, qr.Size);
6060
}
6161

6262

@@ -89,10 +89,95 @@ internal static List<Rectangle> GetVerticalStreaks(QrCode qr)
8989
rectangles.Add(new Rectangle(x, size - count, 1, count));
9090
}
9191
}
92-
return FilterOutsideFinderArea(rectangles, qr.Size);
92+
return FilterNotInFinderArea(rectangles, qr.Size);
9393
}
9494

95-
private static List<Rectangle> FilterOutsideFinderArea(List<Rectangle> rectangles, int size)
95+
internal static List<Rectangle> GetHorizontalFinderPatterns(QrCode qr)
96+
{
97+
return GetFinderPatterns(qr, false);
98+
}
99+
100+
internal static List<Rectangle> GetVerticalFinderPatterns(QrCode qr)
101+
{
102+
return GetFinderPatterns(qr, true);
103+
}
104+
105+
// Locates finder-like patterns (1:1:3:1:1, i.e. the 7 modules "1011101")
106+
// with at least 4 white modules on one side and at least 1 on the other.
107+
// Modules outside the QR code are treated as white (the quiet zone).
108+
// When 'vertical' is true, the scan runs along columns instead of rows.
109+
private static List<Rectangle> GetFinderPatterns(QrCode qr, bool vertical)
110+
{
111+
var rectangles = new List<Rectangle>();
112+
var size = qr.Size;
113+
114+
// The 7-module core spans positions p..p+6; the side checks look 4 modules
115+
// beyond each end, so the scan ranges over every position where the core
116+
// can sit while its required margins fall within the quiet zone or matrix.
117+
for (var line = 0; line < size; line++)
118+
{
119+
for (var p = 0; p <= size - 7; p++)
120+
{
121+
if (!IsFinderCore(qr, line, p, vertical))
122+
{
123+
continue;
124+
}
125+
126+
var leftWhite = WhiteCount(qr, line, p - 1, -1, vertical);
127+
var rightWhite = WhiteCount(qr, line, p + 7, 1, vertical);
128+
if ((leftWhite >= 4 && rightWhite >= 1) || (leftWhite >= 1 && rightWhite >= 4))
129+
{
130+
rectangles.Add(vertical
131+
? new Rectangle(line, p, 1, 7)
132+
: new Rectangle(p, line, 7, 1));
133+
}
134+
}
135+
}
136+
137+
return FilterNotInFinderArea(rectangles, size);
138+
}
139+
140+
// Returns whether the 7 modules starting at 'p' match the core "1011101".
141+
private static bool IsFinderCore(QrCode qr, int line, int p, bool vertical)
142+
{
143+
return Module(qr, line, p, vertical)
144+
&& !Module(qr, line, p + 1, vertical)
145+
&& Module(qr, line, p + 2, vertical)
146+
&& Module(qr, line, p + 3, vertical)
147+
&& Module(qr, line, p + 4, vertical)
148+
&& !Module(qr, line, p + 5, vertical)
149+
&& Module(qr, line, p + 6, vertical);
150+
}
151+
152+
// Counts consecutive white (and out-of-bounds) modules from 'start', stepping
153+
// by 'step', up to a maximum of 4 (the largest margin the rule cares about).
154+
private static int WhiteCount(QrCode qr, int line, int start, int step, bool vertical)
155+
{
156+
var count = 0;
157+
for (var i = 0; i < 4; i++)
158+
{
159+
if (Module(qr, line, start + i * step, vertical))
160+
{
161+
break;
162+
}
163+
count++;
164+
}
165+
return count;
166+
}
167+
168+
// Reads a module, treating coordinates outside the QR code as white (false).
169+
private static bool Module(QrCode qr, int line, int pos, bool vertical)
170+
{
171+
var x = vertical ? line : pos;
172+
var y = vertical ? pos : line;
173+
if (x < 0 || y < 0 || x >= qr.Size || y >= qr.Size)
174+
{
175+
return false;
176+
}
177+
return qr.GetModule(x, y);
178+
}
179+
180+
private static List<Rectangle> FilterNotInFinderArea(List<Rectangle> rectangles, int size)
96181
{
97182
return [.. rectangles.Where(r => !IsInFinderArea(r.X, r.Y, r.Width, r.Height, size))];
98183
}

QrCodeGenerator/DataSegment.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,43 @@ public abstract class DataSegment
6262
/// <param name="version">The QR code version.</param>
6363
/// <returns>The segment length, in bits.</returns>
6464
public int GetTotalLength(int version) => GetHeaderLength(Mode, version) + EncodedLength;
65-
65+
66+
/// <summary>
67+
/// Gets the extended character set indicator (ECI).
68+
/// <para>
69+
/// This value is only valid if this segment uses the segment mode <see cref="DataSegmentMode.ECI"/>.
70+
/// </para>
71+
/// </summary>
72+
public virtual ECI EciDesignator => ECI.None;
73+
74+
/// <summary>
75+
/// The position of the QR code within a structured append message.
76+
/// <para>
77+
/// Valid positions are between 1 and 16.
78+
/// The value is only valuid if this segment uses the segment mode <see cref="DataSegmentMode.StructuredAppend"/>.
79+
/// </para>
80+
/// </summary>
81+
public virtual int StructuredAppendPosition => 0;
82+
83+
/// <summary>
84+
/// The total number of QR codes used for the structured append message.
85+
/// <para>
86+
/// Valid numbers are between 1 and 16.
87+
/// The value is only valuid if this segment uses the segment mode <see cref="DataSegmentMode.StructuredAppend"/>.
88+
/// </para>
89+
/// </summary>
90+
public virtual int StructuredAppendTotal => 0;
91+
92+
/// <summary>
93+
/// The data parity in the structured append messages.
94+
/// <para>
95+
/// The value is only valuid if this segment uses the segment mode <see cref="DataSegmentMode.StructuredAppend"/>.
96+
/// </para>
97+
/// </summary>
98+
public virtual byte StructuredAppendParity => 0;
99+
66100
#endregion
67-
101+
68102
#region Factory methods
69103

70104
/// <summary>
@@ -488,7 +522,7 @@ public static string GetText(IEnumerable<DataSegment> segments)
488522
{
489523
if (segment is DataSegmentEci eci)
490524
{
491-
encoding = eci.Designator.GetEncoding();
525+
encoding = eci.EciDesignator.GetEncoding();
492526
}
493527
else if (segment is DataSegmentStructuredAppend)
494528
{

QrCodeGenerator/DataSegmentEci.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal class DataSegmentEci : DataSegment
1717
/// <summary>
1818
/// Extended Channel Interpretation (ECI) designator.
1919
/// </summary>
20-
internal ECI Designator { get; }
20+
override public ECI EciDesignator { get; }
2121

2222
internal DataSegmentEci(ECI eci)
2323
: base(DataSegmentMode.ECI, GetEciBitLength(eci.Value))
@@ -27,7 +27,7 @@ internal DataSegmentEci(ECI eci)
2727
throw new ArgumentOutOfRangeException(nameof(eci), eci, "ECI value must be between 0 and 999999");
2828
}
2929

30-
Designator = eci;
30+
EciDesignator = eci;
3131
}
3232

3333
private static int GetEciBitLength(int value)
@@ -46,7 +46,7 @@ private static int GetEciBitLength(int value)
4646

4747
internal override void WriteToBitStream(BitStream bitStream)
4848
{
49-
var eciValue = (uint)Designator.Value;
49+
var eciValue = (uint)EciDesignator.Value;
5050
if (eciValue <= 127)
5151
{
5252
bitStream.AppendBits(eciValue, 8);

QrCodeGenerator/DataSegmentNumeric.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ internal override void WriteToBitStream(BitStream bitStream)
4646
{
4747
var bytes = DataBytes;
4848
Debug.Assert(bytes.Array != null);
49-
for (var i = 0; i < bytes.Count; )
49+
var i = 0;
50+
while (i < bytes.Count)
5051
{
5152
// 3 characters are encoded into 10 bits
5253
var n = Math.Min(bytes.Count - i, 3);

QrCodeGenerator/DataSegmentStructuredAppend.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ internal class DataSegmentStructuredAppend : DataSegment
1515
/// Valid positions are between 1 and 16.
1616
/// </para>
1717
/// </summary>
18-
internal readonly int Position;
18+
public override int StructuredAppendPosition { get; }
1919

2020
/// <summary>
2121
/// The total number of QR code used for the message.
2222
/// <para>
2323
/// Valid numbers are between 1 and 16.
2424
/// </para>
2525
/// </summary>
26-
internal readonly int Total;
27-
26+
public override int StructuredAppendTotal { get; }
27+
2828
/// <summary>
2929
/// The parity of the encoded data.
3030
/// </summary>
31-
internal readonly byte Parity;
31+
public override byte StructuredAppendParity { get; }
3232

3333
/// <summary>
3434
/// Creates a new instance.
@@ -54,17 +54,17 @@ internal DataSegmentStructuredAppend(int position, int total, byte parity)
5454
throw new ArgumentOutOfRangeException(nameof(position), position.ToString(), "position must be less or equal to total");
5555
}
5656

57-
Position = position;
58-
Total = total;
59-
Parity = parity;
57+
StructuredAppendPosition = position;
58+
StructuredAppendTotal = total;
59+
StructuredAppendParity = parity;
6060
}
6161

6262
/// <inheritdoc/>
6363
internal override void WriteToBitStream(BitStream bitStream)
6464
{
65-
bitStream.AppendBits((uint)Position - 1, 4);
66-
bitStream.AppendBits((uint)Total - 1, 4);
67-
bitStream.AppendBits(Parity, 8);
65+
bitStream.AppendBits((uint)StructuredAppendPosition - 1, 4);
66+
bitStream.AppendBits((uint)StructuredAppendTotal - 1, 4);
67+
bitStream.AppendBits(StructuredAppendParity, 8);
6868
}
6969
}
7070
}

QrCodeGenerator/QrCodeGenerator.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ Advanced features:
6767

6868
<ItemGroup>
6969
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.300" PrivateAssets="All" />
70+
<!-- Build-time-only transitive dependency of SourceLink. Pin it and drop its build assets
71+
so its .targets (which warns about net6.0 not being a supported TFM) isn't imported. -->
72+
<PackageReference Include="System.IO.Hashing" Version="10.0.8" PrivateAssets="All" ExcludeAssets="build;buildTransitive" />
7073
</ItemGroup>
7174

7275
<Target Name="ValidateNuGetPackage" AfterTargets="Pack">

QrCodeGeneratorTest/DataSegmentTest.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public void EncodeFromTextKanji(string text, int numSegments)
217217
var segments = DataSegment.FromText(text, ECI.ShiftJIS);
218218
Assert.Equal(numSegments, segments.Count);
219219
Assert.Equal(DataSegmentMode.ECI, segments[0].Mode);
220-
Assert.Equal(ECI.ShiftJIS, (segments[0] as DataSegmentEci)?.Designator);
220+
Assert.Equal(ECI.ShiftJIS, (segments[0] as DataSegmentEci)?.EciDesignator);
221221
Assert.Contains(segments, segment => segment.Mode == DataSegmentMode.Kanji);
222222
var decodedText = DataSegment.GetText(segments);
223223
Assert.Equal(text, decodedText);
@@ -293,7 +293,7 @@ public void SelectEncoding()
293293
segments = DataSegment.FromText(text);
294294
Assert.Equal(2, segments.Count);
295295
Assert.Equal(DataSegmentMode.ECI, segments[0].Mode);
296-
Assert.Equal(ECI.UTF8, (segments[0] as DataSegmentEci)?.Designator);
296+
Assert.Equal(ECI.UTF8, (segments[0] as DataSegmentEci)?.EciDesignator);
297297
Assert.Equal(DataSegmentMode.Binary, segments[1].Mode);
298298
Assert.Equal(text, DataSegment.GetText(segments));
299299
}
@@ -305,7 +305,7 @@ public void MakeEciSegment()
305305
Assert.IsType<DataSegmentEci>(segment);
306306
var eciSegment = segment as DataSegmentEci;
307307
Debug.Assert(eciSegment != null);
308-
Assert.Equal(ECI.Latin9, eciSegment.Designator);
308+
Assert.Equal(ECI.Latin9, eciSegment.EciDesignator);
309309
}
310310

311311
[Fact]
@@ -315,9 +315,9 @@ public void MakeStructuredAppend()
315315
Assert.IsType<DataSegmentStructuredAppend>(segment);
316316
var structuredAppendSegment = segment as DataSegmentStructuredAppend;
317317
Debug.Assert(structuredAppendSegment != null);
318-
Assert.Equal(1, structuredAppendSegment.Position);
319-
Assert.Equal(8, structuredAppendSegment.Total);
320-
Assert.Equal(0x3f, structuredAppendSegment.Parity);
318+
Assert.Equal(1, structuredAppendSegment.StructuredAppendPosition);
319+
Assert.Equal(8, structuredAppendSegment.StructuredAppendTotal);
320+
Assert.Equal(0x3f, structuredAppendSegment.StructuredAppendParity);
321321
}
322322

323323
#endregion

0 commit comments

Comments
 (0)