Skip to content

Commit 56d8391

Browse files
Copilotrabbitism
andauthored
Add configurable corner ratio to QRCode symbol rendering (#865)
* Initial plan * Add SymbolCornerRatio property to QRCode control Co-authored-by: rabbitism <14807942+rabbitism@users.noreply.github.com> * Add validation for SymbolCornerRatio property (0.0 to 1.0) Co-authored-by: rabbitism <14807942+rabbitism@users.noreply.github.com> * Fix typo: QC Code -> QR Code in comment Co-authored-by: rabbitism <14807942+rabbitism@users.noreply.github.com> * fix: fix calculation. * feat: clear many useless features. * feat: simplify implementations. * feat: add hotpath to simplify non-corner case. * feat: add EccLevel enum and refactor QRCode geometry processing * fix: coerce to 0.5 * feat: fix a renderer state error. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rabbitism <14807942+rabbitism@users.noreply.github.com> Co-authored-by: Dong Bin <popmessiah@hotmail.com>
1 parent 7afb501 commit 56d8391

5 files changed

Lines changed: 452 additions & 455 deletions

File tree

demo/Ursa.Demo/Pages/QrCodeDemo.axaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
Height="400"
1515
CornerRadius="0"
1616
Data="{Binding #text.Text}"
17-
ErrorCorrection="{Binding #eccLevel.Value}" />
17+
ErrorCorrection="{Binding #eccLevel.Value}"
18+
SymbolCornerRatio="{Binding #cornerRatio.Value}">
19+
</u:QRCode>
1820
<u:Form Grid.Column="1" Width="300">
1921
<TextBox
2022
Name="text"
@@ -25,6 +27,14 @@
2527
u:FormItem.Label="Error Correction Level"
2628
EnumType="{x:Type u:EccLevel}"
2729
Value="{x:Static u:EccLevel.Medium}" />
30+
<Slider
31+
Name="cornerRatio"
32+
u:FormItem.Label="Symbol Corner Ratio"
33+
Minimum="0"
34+
Maximum="0.5"
35+
Value="0.5"
36+
TickFrequency="0.05"
37+
IsSnapToTickEnabled="True" />
2838
</u:Form>
2939
</Grid>
3040
</UserControl>

src/Ursa.Themes.Semi/Controls/QrCode.axaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
</Design.PreviewWith>
77

88
<ControlTheme x:Key="{x:Type u:QRCode}" TargetType="u:QRCode">
9-
<Setter Property="Background" Value="{DynamicResource SemiColorBackground0}" />
109
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
1110
</ControlTheme>
1211
</ResourceDictionary>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace Ursa.Controls;
2+
3+
/// <summary>
4+
/// Indicates the level of error correction available in case of data loss or corruption. The higher the correction level, the more data will be included in the QRCode
5+
/// </summary>
6+
public enum EccLevel
7+
{
8+
/// <summary>
9+
/// The lowest level of error correction where up to ~7% of data can be be recovered if lost and uses the least amount of symbols to represent the data
10+
/// </summary>
11+
Lowest,
12+
13+
/// <summary>
14+
/// The standard level of error correction where up to ~15% of data can be be recovered if lost and represents a good compromise between a small size and reliability
15+
/// </summary>
16+
Medium,
17+
18+
/// <summary>
19+
/// A high readability level of error correction where up to ~25% of data can be be recovered if lost but requires a larger footprint to represent the data
20+
/// </summary>
21+
Quality,
22+
23+
/// <summary>
24+
/// The maximum level of error correction where up to ~30% of data can be be recovered if lost and represents the maximum achievable reliability
25+
/// </summary>
26+
Highest,
27+
}
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
using Avalonia;
2+
using Avalonia.Media;
3+
using Gma.QrCodeNet.Encoding;
4+
5+
namespace Ursa.Controls;
6+
7+
public partial class QRCode
8+
{
9+
/// <summary>
10+
/// Processes a symbol if set and adds the required geometry.
11+
/// </summary>
12+
/// <param name="geometry">Geometry containing the QRCode Geometry</param>
13+
/// <param name="bitMatrix">BitMatrix containing the data</param>
14+
/// <param name="row">The row of the symbol being processed</param>
15+
/// <param name="column">The column of the symbol being processed</param>
16+
/// <param name="symbolBounds">The bounds of the symbol being processed</param>
17+
/// <param name="cornerRatio"></param>
18+
/// <returns>True if the symbol was processed, otherwise false</returns>
19+
private static void ProcessSymbolIfSet(
20+
PathGeometry geometry,
21+
BitMatrix bitMatrix,
22+
int row,
23+
int column,
24+
Rect symbolBounds,
25+
double cornerRatio)
26+
{
27+
if (cornerRatio == 0)
28+
{
29+
var simpleFigure = new PathFigure() { StartPoint = symbolBounds.TopLeft, };
30+
simpleFigure.Segments!.Add( new LineSegment { Point = symbolBounds.TopRight });
31+
simpleFigure.Segments .Add( new LineSegment { Point = symbolBounds.BottomRight });
32+
simpleFigure.Segments .Add( new LineSegment { Point = symbolBounds.BottomLeft });
33+
geometry.Figures?.Add(simpleFigure);
34+
return;
35+
}
36+
var cornerRadius = symbolBounds.Size * cornerRatio;
37+
var cornerFlags = GetSetSymbolCornerFlags(bitMatrix, row, column);
38+
var figure = new PathFigure
39+
{ StartPoint = new Point(symbolBounds.Left, symbolBounds.Top + cornerRadius.Height) };
40+
41+
// Top Left
42+
if ((cornerFlags & CornerFlags.TopLeft) != 0)
43+
{
44+
figure.Segments!.Add(new LineSegment { Point = symbolBounds.TopLeft });
45+
figure.Segments!.Add(new LineSegment
46+
{ Point = new Point(symbolBounds.Right - cornerRadius.Width, symbolBounds.Top) });
47+
}
48+
else
49+
{
50+
figure.Segments!.Add(new ArcSegment
51+
{
52+
SweepDirection = SweepDirection.Clockwise,
53+
Point = new Point(symbolBounds.Left + cornerRadius.Width, symbolBounds.Top),
54+
Size = cornerRadius
55+
});
56+
figure.Segments.Add(new LineSegment()
57+
{
58+
Point = new Point(symbolBounds.Right - cornerRadius.Width, symbolBounds.Top),
59+
});
60+
}
61+
62+
// Top Right
63+
if ((cornerFlags & CornerFlags.TopRight) != 0)
64+
{
65+
figure.Segments!.Add(new LineSegment { Point = symbolBounds.TopRight });
66+
figure.Segments.Add(new LineSegment()
67+
{
68+
Point = new Point(symbolBounds.Right, symbolBounds.Bottom - cornerRadius.Height),
69+
});
70+
}
71+
else
72+
{
73+
figure.Segments!.Add(new ArcSegment
74+
{
75+
SweepDirection = SweepDirection.Clockwise,
76+
Point = new Point(symbolBounds.Right, symbolBounds.Top + cornerRadius.Height),
77+
Size = cornerRadius
78+
});
79+
figure.Segments.Add(new LineSegment()
80+
{
81+
Point = new Point(symbolBounds.Right, symbolBounds.Bottom - cornerRadius.Height),
82+
});
83+
}
84+
85+
// Bottom Right
86+
if ((cornerFlags & CornerFlags.BottomRight) != 0)
87+
{
88+
figure.Segments!.Add(new LineSegment { Point = symbolBounds.BottomRight });
89+
figure.Segments!.Add(new LineSegment
90+
{ Point = new Point(symbolBounds.Left + cornerRadius.Width, symbolBounds.Bottom) });
91+
}
92+
else
93+
{
94+
figure.Segments!.Add(new ArcSegment
95+
{
96+
SweepDirection = SweepDirection.Clockwise,
97+
Point = new Point(symbolBounds.Right - cornerRadius.Width, symbolBounds.Bottom),
98+
Size = cornerRadius
99+
});
100+
figure.Segments!.Add(new LineSegment
101+
{ Point = new Point(symbolBounds.Left + cornerRadius.Width, symbolBounds.Bottom) });
102+
}
103+
104+
// Bottom Left
105+
if ((cornerFlags & CornerFlags.BottomLeft) != 0)
106+
{
107+
figure.Segments!.Add(new LineSegment { Point = symbolBounds.BottomLeft });
108+
figure.Segments!.Add(new LineSegment { Point = figure.StartPoint });
109+
}
110+
else
111+
{
112+
figure.Segments!.Add(new ArcSegment
113+
{
114+
SweepDirection = SweepDirection.Clockwise,
115+
Point = new Point(symbolBounds.Left, symbolBounds.Bottom - cornerRadius.Height),
116+
Size = cornerRadius
117+
});
118+
figure.Segments!.Add(new LineSegment { Point = figure.StartPoint });
119+
}
120+
121+
geometry.Figures?.Add(figure);
122+
}
123+
124+
/// <summary>
125+
/// Gets the corner flags indicating how a set symbol is to be processed
126+
/// </summary>
127+
/// <param name="bitMatrix">BitMatrix containing the data</param>
128+
/// <param name="row">The row of the symbol being processed</param>
129+
/// <param name="column">The column of the symbol being processed</param>
130+
/// <returns>The corner flags for a set symbol</returns>
131+
private static CornerFlags GetSetSymbolCornerFlags(BitMatrix bitMatrix, int row, int column)
132+
{
133+
var flags = CornerFlags.None;
134+
135+
if (!IsValid(bitMatrix, column, row))
136+
return flags;
137+
138+
if (IsValid(bitMatrix, column, row - 1) || IsValid(bitMatrix, column - 1, row))
139+
flags |= CornerFlags.TopLeft;
140+
if (IsValid(bitMatrix, column, row - 1) || IsValid(bitMatrix, column + 1, row))
141+
flags |= CornerFlags.TopRight;
142+
if (IsValid(bitMatrix, column, row + 1) || IsValid(bitMatrix, column + 1, row))
143+
flags |= CornerFlags.BottomRight;
144+
if (IsValid(bitMatrix, column, row + 1) || IsValid(bitMatrix, column - 1, row))
145+
flags |= CornerFlags.BottomLeft;
146+
147+
return flags;
148+
}
149+
150+
/// <summary>
151+
/// Processes a symbol if unset and adds the required geometry.
152+
/// </summary>
153+
/// <param name="geometry">Geometry containing the QRCode Geometry</param>
154+
/// <param name="bitMatrix">BitMatrix containing the data</param>
155+
/// <param name="row">The row of the symbol being processed</param>
156+
/// <param name="column">The column of the symbol being processed</param>
157+
/// <param name="symbolBounds">The bounds of the symbol being processed</param>
158+
/// <param name="cornerRatio"></param>
159+
private static void ProcessSymbolIfUnset(PathGeometry geometry, BitMatrix bitMatrix, int row, int column,
160+
Rect symbolBounds, double cornerRatio)
161+
{
162+
// If filled, no action required
163+
if (IsValid(bitMatrix, column, row))
164+
return;
165+
if (cornerRatio == 0) return;
166+
167+
var cornerFlags = GetUnsetSymbolCornerFlags(bitMatrix, row, column);
168+
169+
// If there are no nearby bits set, there's no need to smooth corners
170+
if (cornerFlags == CornerFlags.None)
171+
return;
172+
173+
var cornerRadius = symbolBounds.Size * cornerRatio;
174+
175+
// Top Left
176+
if ((cornerFlags & CornerFlags.TopLeft) != 0)
177+
{
178+
var start = new Point(symbolBounds.Left, symbolBounds.Top + cornerRadius.Height);
179+
180+
geometry.Figures!.Add(new PathFigure
181+
{
182+
StartPoint = start,
183+
Segments =
184+
[
185+
new LineSegment { Point = symbolBounds.TopLeft },
186+
new LineSegment { Point = new Point(symbolBounds.Left + cornerRadius.Width, symbolBounds.Top) },
187+
new ArcSegment
188+
{
189+
SweepDirection = SweepDirection.CounterClockwise,
190+
Point = start,
191+
Size = cornerRadius
192+
}
193+
]
194+
});
195+
}
196+
197+
// Top Right
198+
if ((cornerFlags & CornerFlags.TopRight) != 0)
199+
{
200+
var start = new Point(symbolBounds.Right - cornerRadius.Width, symbolBounds.Top);
201+
202+
geometry.Figures!.Add(new PathFigure
203+
{
204+
StartPoint = start,
205+
Segments =
206+
[
207+
new LineSegment { Point = symbolBounds.TopRight },
208+
new LineSegment { Point = new Point(symbolBounds.Right, symbolBounds.Top + cornerRadius.Height) },
209+
new ArcSegment
210+
{
211+
SweepDirection = SweepDirection.CounterClockwise,
212+
Point = start,
213+
Size = cornerRadius
214+
}
215+
]
216+
});
217+
}
218+
219+
// Bottom Right
220+
if ((cornerFlags & CornerFlags.BottomRight) != 0)
221+
{
222+
var start = new Point(symbolBounds.Right, symbolBounds.Bottom - cornerRadius.Height);
223+
224+
geometry.Figures!.Add(new PathFigure
225+
{
226+
StartPoint = start,
227+
Segments =
228+
[
229+
new LineSegment { Point = symbolBounds.BottomRight },
230+
new LineSegment { Point = new Point(symbolBounds.Right - cornerRadius.Width, symbolBounds.Bottom) },
231+
new ArcSegment
232+
{
233+
SweepDirection = SweepDirection.CounterClockwise,
234+
Point = start,
235+
Size = cornerRadius
236+
}
237+
]
238+
});
239+
}
240+
241+
// Bottom Left
242+
if ((cornerFlags & CornerFlags.BottomLeft) != 0)
243+
{
244+
var start = new Point(symbolBounds.Left + cornerRadius.Width, symbolBounds.Bottom);
245+
246+
geometry.Figures!.Add(new PathFigure
247+
{
248+
StartPoint = start,
249+
Segments =
250+
[
251+
new LineSegment { Point = symbolBounds.BottomLeft },
252+
new LineSegment { Point = new Point(symbolBounds.Left, symbolBounds.Bottom - cornerRadius.Height) },
253+
new ArcSegment
254+
{
255+
SweepDirection = SweepDirection.CounterClockwise,
256+
Point = start,
257+
Size = cornerRadius
258+
}
259+
]
260+
});
261+
}
262+
}
263+
264+
/// <summary>
265+
/// Gets the corner flags indicating how an unset symbol is to be processed
266+
/// </summary>
267+
/// <param name="bitMatrix">BitMatrix containing the data</param>
268+
/// <param name="row">The row of the symbol being processed</param>
269+
/// <param name="column">The column of the symbol being processed</param>
270+
/// <returns>The corner flags for an unset symbol</returns>
271+
private static CornerFlags GetUnsetSymbolCornerFlags(BitMatrix bitMatrix, int row, int column)
272+
{
273+
var flags = CornerFlags.None;
274+
275+
if (IsValid(bitMatrix, column, row))
276+
return flags;
277+
278+
if (IsValid(bitMatrix, column, row - 1) && IsValid(bitMatrix, column - 1, row - 1) &&
279+
IsValid(bitMatrix, column - 1, row))
280+
flags |= CornerFlags.TopLeft;
281+
if (IsValid(bitMatrix, column, row - 1) && IsValid(bitMatrix, column + 1, row - 1) &&
282+
IsValid(bitMatrix, column + 1, row))
283+
flags |= CornerFlags.TopRight;
284+
if (IsValid(bitMatrix, column, row + 1) && IsValid(bitMatrix, column + 1, row + 1) &&
285+
IsValid(bitMatrix, column + 1, row))
286+
flags |= CornerFlags.BottomRight;
287+
if (IsValid(bitMatrix, column, row + 1) && IsValid(bitMatrix, column - 1, row + 1) &&
288+
IsValid(bitMatrix, column - 1, row))
289+
flags |= CornerFlags.BottomLeft;
290+
291+
return flags;
292+
}
293+
294+
/// <summary>
295+
/// Returns whether or not the specified symbol should be considered "set"
296+
/// </summary>
297+
/// <param name="bitMatrix">BitMatrix containing the data</param>
298+
/// <param name="x"></param>
299+
/// <param name="y"></param>
300+
/// <returns></returns>
301+
private static bool IsValid(BitMatrix bitMatrix, int x, int y)
302+
{
303+
// Validate bounds of the bit matrix
304+
if (x < 0 || y < 0 || x >= bitMatrix.Width || y >= bitMatrix.Height)
305+
return false;
306+
if (x < 8 && y < 8) return false;
307+
if (x > bitMatrix.Width - 9 && y < 8) return false;
308+
if (x < 8 && y > bitMatrix.Height - 9) return false;
309+
return bitMatrix[y, x];
310+
}
311+
}

0 commit comments

Comments
 (0)