766 lines
31 KiB
C#
766 lines
31 KiB
C#
using Avalonia;
|
|
using Avalonia.Animation;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Documents;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Gma.QrCodeNet.Encoding;
|
|
using System.Collections;
|
|
|
|
namespace Toolkit.UI.Controls.Avalonia;
|
|
|
|
/// <summary>
|
|
/// Avalonia implementation of a Quick Response code (QR Code) with smooth borders and support for gradient brushes
|
|
/// For spec, see: https://www.swisseduc.ch/informatik/theoretische_informatik/qr_codes/docs/qr_standard.pdf
|
|
/// </summary>
|
|
public class QrCode : Control
|
|
{
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// Property for the Background brush (i.e. the area that has no data)
|
|
/// </summary>
|
|
public static readonly StyledProperty<IBrush?> BackgroundProperty = Border.BackgroundProperty.AddOwner<QrCode>();
|
|
|
|
/// <summary>
|
|
/// Property for the Foreground brush (i.e. the actual data)
|
|
/// </summary>
|
|
public static readonly StyledProperty<IBrush?> ForegroundProperty = TextElement.ForegroundProperty.AddOwner<TemplatedControl>();
|
|
|
|
/// <summary>
|
|
/// Property indicating how rounded the corners will be
|
|
/// </summary>
|
|
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty = Border.CornerRadiusProperty.AddOwner<QrCode>();
|
|
|
|
/// <summary>
|
|
/// Property indicating the Quiet Zone (distance between the edge of the control and where the data actually starts)
|
|
///
|
|
/// Note: The Quiet Zone (aka Padding) is defined in the QC Code standard (ISO 18004) as the width of 4 modules on all
|
|
/// sides, but is implemented separately in this control. Official support may wish to remove this property as adjusting
|
|
/// it will technically make the generated QRCodes "non-standard". This implementation does not currently concern itself
|
|
/// with this as the code itself it not meant for public consumption.
|
|
/// </summary>
|
|
public static readonly StyledProperty<Thickness> PaddingProperty = Decorator.PaddingProperty.AddOwner<QrCode>();
|
|
|
|
/// <summary>
|
|
/// Property indicating whether the Quiet Zone of 4 modules should be added to the QR Code as additional padding. Default: True
|
|
///
|
|
/// Note: Disabling the Quiet Zone makes the generated QRCodes "non-standard" according to the ISO 18004 standard.
|
|
/// The padding created by the Quiet Zone depends on the module size and therefore on the amount of data. This can be
|
|
/// disabled and a fixed <see cref="Padding"/> can be set instead to have more control over the layout.
|
|
/// </summary>
|
|
public static readonly StyledProperty<bool> IsQuietZoneEnabledProperty = AvaloniaProperty.Register<QrCode, bool>(nameof(IsQuietZoneEnabled), true);
|
|
|
|
/// <summary>
|
|
/// Property indicating the Error Correction Code of the generated data. Default: Medium
|
|
///
|
|
/// Note: See <see cref="EccLevel" /> for the specific definitions of each value.
|
|
/// </summary>
|
|
public static readonly StyledProperty<EccLevel> ErrorCorrectionProperty = AvaloniaProperty.Register<QrCode, EccLevel>(nameof(ErrorCorrection), EccLevel.Medium);
|
|
|
|
/// <summary>
|
|
/// Property for the data represented in the QRCode
|
|
/// </summary>
|
|
public static readonly StyledProperty<string?> DataProperty = AvaloniaProperty.Register<QrCode, string?>(nameof(Data));
|
|
|
|
/// <inheritdoc cref="BackgroundProperty" />
|
|
public IBrush Background
|
|
{
|
|
get => GetValue(BackgroundProperty) ?? Brushes.White;
|
|
set => SetValue(BackgroundProperty, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="ForegroundProperty" />
|
|
public IBrush Foreground
|
|
{
|
|
get => GetValue(ForegroundProperty) ?? Brushes.Black;
|
|
set => SetValue(ForegroundProperty, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="CornerRadiusProperty" />
|
|
public CornerRadius CornerRadius
|
|
{
|
|
get => GetValue(CornerRadiusProperty);
|
|
set => SetValue(CornerRadiusProperty, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="PaddingProperty" />
|
|
public Thickness Padding
|
|
{
|
|
get => GetValue(PaddingProperty);
|
|
set => SetValue(PaddingProperty, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="IsQuietZoneEnabledProperty" />
|
|
public bool IsQuietZoneEnabled
|
|
{
|
|
get => GetValue(IsQuietZoneEnabledProperty);
|
|
set => SetValue(IsQuietZoneEnabledProperty, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="ErrorCorrectionProperty" />
|
|
public EccLevel ErrorCorrection
|
|
{
|
|
get => GetValue(ErrorCorrectionProperty);
|
|
set => SetValue(ErrorCorrectionProperty, value);
|
|
}
|
|
|
|
/// <inheritdoc cref="DataProperty" />
|
|
public string? Data
|
|
{
|
|
get => GetValue(DataProperty);
|
|
set => SetValue(DataProperty, value);
|
|
}
|
|
|
|
#endregion Properties
|
|
|
|
/// <summary>
|
|
/// Engine to actually calculate the bit matrix of the QRCode. Currently a Nuget package, but official support may wish to implement and remove such dependency
|
|
/// </summary>
|
|
private static readonly QrEncoder QrCodeGenerator = new();
|
|
|
|
/// <summary>
|
|
/// A cache of currently set bits in the bit matrix. This is used to potentially speed up processing.
|
|
/// </summary>
|
|
private readonly Hashtable _setBitsTable = new();
|
|
|
|
/// <summary>
|
|
/// A cache of the last encoded QRCode. This is used to reuse the last generated data whenever a style property like Width, Height or Padding was changed.
|
|
/// </summary>
|
|
private Gma.QrCodeNet.Encoding.QrCode? _encodedQrCode;
|
|
|
|
// QRCode specs mandate a standard 4-symbol-sized space on each side of the data. We support custom Padding and will ignore this zone when processing
|
|
private int QuietZoneCount => IsQuietZoneEnabled ? 4 : 0;
|
|
|
|
private int QuietMargin => QuietZoneCount * 2;
|
|
|
|
/// <summary>
|
|
/// Defines the geometry of the previously displayed QRCode
|
|
/// </summary>
|
|
private (PathGeometry, double)? _oldQrCodeGeometry;
|
|
|
|
/// <summary>
|
|
/// Defines the geometry of the currently displayed QRCode
|
|
/// </summary>
|
|
private (PathGeometry, double)? _qrCodeGeometry;
|
|
|
|
private Task? _transitionTask;
|
|
|
|
public QrCode()
|
|
{
|
|
// These properties change how the control is rendered, but not the data that's being displayed
|
|
// See "OnPropertyChanged" for the properties that require the data to be updated.
|
|
AffectsRender<QrCode>(BackgroundProperty, ForegroundProperty, CornerRadiusProperty, WidthProperty, HeightProperty);
|
|
|
|
// This is ideally how we would do transitions, but it obviously doesn't work with how we are doing it. It's left in as an example of what I was hoping for.
|
|
Transitions = new Transitions
|
|
{
|
|
new DoubleTransition
|
|
{
|
|
Property = OpacityProperty,
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised whenever a property on this control is changed.
|
|
/// </summary>
|
|
/// <param name="change">Event Args for the changed property including old and new values</param>
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
{
|
|
base.OnPropertyChanged(change);
|
|
|
|
// When any property is changed, we will recalculate the bit matrix and rerender the control
|
|
// For properties that do not require the data to be reprocessed, see the constructor.
|
|
|
|
// We can only reprocess the data when data is available to reprocess...
|
|
if (Data == null)
|
|
return;
|
|
|
|
// Invalidates the cached QRCode if needed. We do not need recreate the bit matrix for layout changes.
|
|
switch (change.Property.Name)
|
|
{
|
|
// Error Correction change requires the data to be reprocessed to recalculate the new bit matrix. This is unavoidable.
|
|
case nameof(ErrorCorrection):
|
|
// A change in data obviously indicates the need to update the bit matrix
|
|
case nameof(Data):
|
|
_encodedQrCode = null;
|
|
break;
|
|
}
|
|
|
|
// Generating the QRCode bit matrix if needed.
|
|
if (_encodedQrCode is null)
|
|
{
|
|
lock (_setBitsTable)
|
|
_setBitsTable.Clear();
|
|
|
|
QrCodeGenerator.ErrorCorrectionLevel = ToQrCoderEccLevel(ErrorCorrection);
|
|
_encodedQrCode = QrCodeGenerator.Encode(Data);
|
|
}
|
|
|
|
switch (change.Property.Name)
|
|
{
|
|
// Padding and size requires the geometry paths to be adjusted to match the new locations. ToDo: Can this be simulated with a scale to enhance performance?
|
|
case nameof(Padding):
|
|
case nameof(Width):
|
|
case nameof(Height):
|
|
case nameof(IsQuietZoneEnabled):
|
|
case nameof(ErrorCorrection):
|
|
case nameof(Data):
|
|
OnLayoutChanged(_encodedQrCode);
|
|
|
|
// This is hard coded for now as I'm sure there is a better and more "Avalonia" way to transition between renders.
|
|
// Eventually, it may be a property of some sort.
|
|
if (_transitionTask == null || _transitionTask.IsCompleted)
|
|
{
|
|
_transitionTask = Dispatcher.UIThread.Invoke(async () =>
|
|
{
|
|
while (_qrCodeGeometry is (_, < 1))
|
|
{
|
|
if (_qrCodeGeometry is var (newGeometry, newOpacity))
|
|
_qrCodeGeometry = (newGeometry, Math.Min(1, newOpacity + 0.1));
|
|
InvalidateVisual();
|
|
await Task.Delay(30);
|
|
}
|
|
|
|
_oldQrCodeGeometry = null;
|
|
InvalidateVisual();
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised whenever a property of the control changes that impacts the layout of the QRCode geometry
|
|
/// </summary>
|
|
/// <param name="qrCodeData">The QRCode Data with the underlying bit matrix</param>
|
|
private void OnLayoutChanged(Gma.QrCodeNet.Encoding.QrCode qrCodeData)
|
|
{
|
|
/*
|
|
* The following code turns the QRCode bit matrix into a geometry path. The path represents the SHAPE of the QRCode and
|
|
* thus is achieved maybe unintuitively by ensuring that the background covers the whole control and then "carving" out
|
|
* the areas where the foreground should appear. In the case of the markers, pathing over a "carved" out area will
|
|
* re-add the background color and, indeed, create the ring effects in the finished render.
|
|
*
|
|
* This logic is in place to ensure that the the whole QRCode is contained in one "Geometry" object and will thus be
|
|
* rendered with one brush to support a gradient across the whole control if so desired.
|
|
*/
|
|
|
|
// Bounds of the entire control
|
|
var bounds = new Rect(0, 0, Width, Height);
|
|
var matrix = qrCodeData.Matrix;
|
|
var columnCount = matrix.Width + QuietMargin;
|
|
var rowCount = matrix.Height + QuietMargin;
|
|
|
|
// The size of each symbol taking into account the size of the QRCode and our custom quiet zone aka padding
|
|
var symbolSize = new Size(
|
|
(Width - Padding.Left - Padding.Right) / columnCount,
|
|
(Height - Padding.Top - Padding.Bottom) / rowCount
|
|
);
|
|
|
|
// QR Code Shape
|
|
var geometry = new PathGeometry();
|
|
|
|
// The entire area is drawn here as the idea is to cover the control with the background brush and "carve" out the data showing the foreground
|
|
geometry.Figures!.Add(new PathFigure
|
|
{
|
|
Segments = new PathSegments
|
|
{
|
|
new LineSegment { Point = bounds.BottomLeft },
|
|
new LineSegment { Point = bounds.BottomRight },
|
|
new LineSegment { Point = bounds.TopRight }
|
|
// No need to have the additional line segment back to 0,0 as PathFigures are closed (IsClosed) by default and this segment will be assumed
|
|
}
|
|
});
|
|
|
|
// Adds the three Position Detection Pattern
|
|
AddPositionDetectionPattern(geometry, bounds, symbolSize);
|
|
|
|
for (var row = 0; row < matrix.Height; row++)
|
|
{
|
|
ProcessRow(geometry, matrix, row, symbolSize);
|
|
}
|
|
|
|
_oldQrCodeGeometry = _qrCodeGeometry;
|
|
_qrCodeGeometry = (geometry, 0); // start at 0% opacity
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes a full row of the the bit matrix and adds geometry as needed
|
|
/// </summary>
|
|
/// <param name="geometry">Geometry of the QR Code</param>
|
|
/// <param name="bitMatrix">The bit matrix being processed</param>
|
|
/// <param name="row">The row to process</param>
|
|
/// <param name="symbolSize">The calculated size of each symbol</param>
|
|
private void ProcessRow(PathGeometry geometry, BitMatrix bitMatrix, int row, Size symbolSize)
|
|
{
|
|
// Loop through each item within the row
|
|
for (var column = 0; column < bitMatrix.Width; column++)
|
|
{
|
|
ProcessSymbol(geometry, bitMatrix, row, column, symbolSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes a symbol and adds geometry as needed
|
|
/// </summary>
|
|
/// <param name="geometry">Geometry of the QR Code</param>
|
|
/// <param name="bitMatrix">The bit matrix being processed</param>
|
|
/// <param name="row">The row to process</param>
|
|
/// <param name="column">The column of the symbol being processed</param>
|
|
/// <param name="symbolSize">The calculated size of each symbol</param>
|
|
private void ProcessSymbol(PathGeometry geometry, BitMatrix bitMatrix, int row, int column, Size symbolSize)
|
|
{
|
|
// The full bounds of the symbol
|
|
var symbolBounds = new Rect(
|
|
(column + QuietZoneCount) * symbolSize.Width + Padding.Left,
|
|
(row + QuietZoneCount) * symbolSize.Height + Padding.Top,
|
|
symbolSize.Width,
|
|
symbolSize.Height
|
|
);
|
|
|
|
if (ProcessSymbolIfSet(geometry, bitMatrix, row, column, symbolBounds))
|
|
return;
|
|
|
|
ProcessSymbolIfUnset(geometry, bitMatrix, row, column, symbolBounds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes a symbol if set and adds the required geometry.
|
|
/// </summary>
|
|
/// <param name="geometry">Geometry containing the QRCode Geometry</param>
|
|
/// <param name="bitMatrix">BitMatrix containing the data</param>
|
|
/// <param name="row">The row of the symbol being processed</param>
|
|
/// <param name="column">The column of the symbol being processed</param>
|
|
/// <param name="symbolBounds">The bounds of the symbol being processed</param>
|
|
/// <returns>True if the symbol was processed, otherwise false</returns>
|
|
private bool ProcessSymbolIfSet(PathGeometry geometry, BitMatrix bitMatrix, int row, int column, Rect symbolBounds)
|
|
{
|
|
// If not filled, no action required
|
|
if (!IsValid(bitMatrix, column, row))
|
|
return false;
|
|
|
|
var boundsRadius = symbolBounds.Size / 2;
|
|
var cornerFlags = GetSetSymbolCornerFlags(bitMatrix, row, column);
|
|
var figure = new PathFigure { StartPoint = new Point(symbolBounds.Left, symbolBounds.Top + boundsRadius.Height) };
|
|
|
|
// Top Left
|
|
if ((cornerFlags & CornerFlags.TopLeft) != 0)
|
|
{
|
|
figure.Segments!.Add(new LineSegment { Point = symbolBounds.TopLeft });
|
|
figure.Segments!.Add(new LineSegment { Point = new Point(symbolBounds.Left + boundsRadius.Width, symbolBounds.Top) });
|
|
}
|
|
else
|
|
{
|
|
figure.Segments!.Add(new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.Clockwise,
|
|
Point = new Point(symbolBounds.Left + boundsRadius.Width, symbolBounds.Top),
|
|
Size = boundsRadius
|
|
});
|
|
}
|
|
|
|
// Top Right
|
|
if ((cornerFlags & CornerFlags.TopRight) != 0)
|
|
{
|
|
figure.Segments!.Add(new LineSegment { Point = symbolBounds.TopRight });
|
|
figure.Segments!.Add(new LineSegment { Point = new Point(symbolBounds.Right, symbolBounds.Top + boundsRadius.Height) });
|
|
}
|
|
else
|
|
{
|
|
figure.Segments!.Add(new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.Clockwise,
|
|
Point = new Point(symbolBounds.Right, symbolBounds.Top + boundsRadius.Height),
|
|
Size = boundsRadius
|
|
});
|
|
}
|
|
|
|
// Bottom Right
|
|
if ((cornerFlags & CornerFlags.BottomRight) != 0)
|
|
{
|
|
figure.Segments!.Add(new LineSegment { Point = symbolBounds.BottomRight });
|
|
figure.Segments!.Add(new LineSegment { Point = new Point(symbolBounds.Right - boundsRadius.Width, symbolBounds.Bottom) });
|
|
}
|
|
else
|
|
{
|
|
figure.Segments!.Add(new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.Clockwise,
|
|
Point = new Point(symbolBounds.Right - boundsRadius.Width, symbolBounds.Bottom),
|
|
Size = boundsRadius
|
|
});
|
|
}
|
|
|
|
// Bottom Left
|
|
if ((cornerFlags & CornerFlags.BottomLeft) != 0)
|
|
{
|
|
figure.Segments!.Add(new LineSegment { Point = symbolBounds.BottomLeft });
|
|
figure.Segments!.Add(new LineSegment { Point = figure.StartPoint });
|
|
}
|
|
else
|
|
{
|
|
figure.Segments!.Add(new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.Clockwise,
|
|
Point = figure.StartPoint,
|
|
Size = boundsRadius
|
|
});
|
|
}
|
|
|
|
geometry.Figures?.Add(figure);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the corner flags indicating how a set symbol is to be processed
|
|
/// </summary>
|
|
/// <param name="bitMatrix">BitMatrix containing the data</param>
|
|
/// <param name="row">The row of the symbol being processed</param>
|
|
/// <param name="column">The column of the symbol being processed</param>
|
|
/// <returns>The corner flags for a set symbol</returns>
|
|
private CornerFlags GetSetSymbolCornerFlags(BitMatrix bitMatrix, int row, int column)
|
|
{
|
|
var flags = CornerFlags.None;
|
|
|
|
if (!IsValid(bitMatrix, column, row))
|
|
return flags;
|
|
|
|
if (IsValid(bitMatrix, column, row - 1) || IsValid(bitMatrix, column - 1, row))
|
|
flags |= CornerFlags.TopLeft;
|
|
if (IsValid(bitMatrix, column, row - 1) || IsValid(bitMatrix, column + 1, row))
|
|
flags |= CornerFlags.TopRight;
|
|
if (IsValid(bitMatrix, column, row + 1) || IsValid(bitMatrix, column + 1, row))
|
|
flags |= CornerFlags.BottomRight;
|
|
if (IsValid(bitMatrix, column, row + 1) || IsValid(bitMatrix, column - 1, row))
|
|
flags |= CornerFlags.BottomLeft;
|
|
|
|
return flags;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes a symbol if unset and adds the required geometry.
|
|
/// </summary>
|
|
/// <param name="geometry">Geometry containing the QRCode Geometry</param>
|
|
/// <param name="bitMatrix">BitMatrix containing the data</param>
|
|
/// <param name="row">The row of the symbol being processed</param>
|
|
/// <param name="column">The column of the symbol being processed</param>
|
|
/// <param name="symbolBounds">The bounds of the symbol being processed</param>
|
|
private void ProcessSymbolIfUnset(PathGeometry geometry, BitMatrix bitMatrix, int row, int column, Rect symbolBounds)
|
|
{
|
|
// If filled, no action required
|
|
if (IsValid(bitMatrix, column, row))
|
|
return;
|
|
|
|
var cornerFlags = GetUnsetSymbolCornerFlags(bitMatrix, row, column);
|
|
|
|
// If there are no nearby bits set, there's no need to smooth corners
|
|
if (cornerFlags == CornerFlags.None)
|
|
return;
|
|
|
|
var boundsRadius = symbolBounds.Size / 2;
|
|
|
|
// Top Left
|
|
if ((cornerFlags & CornerFlags.TopLeft) != 0)
|
|
{
|
|
var start = new Point(symbolBounds.Left, symbolBounds.Top + boundsRadius.Height);
|
|
|
|
geometry.Figures!.Add(new PathFigure
|
|
{
|
|
StartPoint = start,
|
|
Segments = new PathSegments
|
|
{
|
|
new LineSegment { Point = symbolBounds.TopLeft },
|
|
new LineSegment { Point = new Point(symbolBounds.Left + boundsRadius.Width, symbolBounds.Top) },
|
|
new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.CounterClockwise,
|
|
Point = start,
|
|
Size = boundsRadius
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Top Right
|
|
if ((cornerFlags & CornerFlags.TopRight) != 0)
|
|
{
|
|
var start = new Point(symbolBounds.Right - boundsRadius.Width, symbolBounds.Top);
|
|
|
|
geometry.Figures!.Add(new PathFigure
|
|
{
|
|
StartPoint = start,
|
|
Segments = new PathSegments
|
|
{
|
|
new LineSegment { Point = symbolBounds.TopRight },
|
|
new LineSegment { Point = new Point(symbolBounds.Right, symbolBounds.Top + boundsRadius.Height) },
|
|
new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.CounterClockwise,
|
|
Point = start,
|
|
Size = boundsRadius
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Bottom Right
|
|
if ((cornerFlags & CornerFlags.BottomRight) != 0)
|
|
{
|
|
var start = new Point(symbolBounds.Right, symbolBounds.Bottom - boundsRadius.Height);
|
|
|
|
geometry.Figures!.Add(new PathFigure
|
|
{
|
|
StartPoint = start,
|
|
Segments = new PathSegments
|
|
{
|
|
new LineSegment { Point = symbolBounds.BottomRight },
|
|
new LineSegment { Point = new Point(symbolBounds.Right - boundsRadius.Width, symbolBounds.Bottom) },
|
|
new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.CounterClockwise,
|
|
Point = start,
|
|
Size = boundsRadius
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Bottom Left
|
|
if ((cornerFlags & CornerFlags.BottomLeft) != 0)
|
|
{
|
|
var start = new Point(symbolBounds.Left + boundsRadius.Width, symbolBounds.Bottom);
|
|
|
|
geometry.Figures!.Add(new PathFigure
|
|
{
|
|
StartPoint = start,
|
|
Segments = new PathSegments
|
|
{
|
|
new LineSegment { Point = symbolBounds.BottomLeft },
|
|
new LineSegment { Point = new Point(symbolBounds.Left, symbolBounds.Bottom - boundsRadius.Height) },
|
|
new ArcSegment
|
|
{
|
|
SweepDirection = SweepDirection.CounterClockwise,
|
|
Point = start,
|
|
Size = boundsRadius
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the corner flags indicating how an unset symbol is to be processed
|
|
/// </summary>
|
|
/// <param name="bitMatrix">BitMatrix containing the data</param>
|
|
/// <param name="row">The row of the symbol being processed</param>
|
|
/// <param name="column">The column of the symbol being processed</param>
|
|
/// <returns>The corner flags for an unset symbol</returns>
|
|
private CornerFlags GetUnsetSymbolCornerFlags(BitMatrix bitMatrix, int row, int column)
|
|
{
|
|
var flags = CornerFlags.None;
|
|
|
|
if (IsValid(bitMatrix, column, row))
|
|
return flags;
|
|
|
|
if (IsValid(bitMatrix, column, row - 1) && IsValid(bitMatrix, column - 1, row - 1) && IsValid(bitMatrix, column - 1, row))
|
|
flags |= CornerFlags.TopLeft;
|
|
if (IsValid(bitMatrix, column, row - 1) && IsValid(bitMatrix, column + 1, row - 1) && IsValid(bitMatrix, column + 1, row))
|
|
flags |= CornerFlags.TopRight;
|
|
if (IsValid(bitMatrix, column, row + 1) && IsValid(bitMatrix, column + 1, row + 1) && IsValid(bitMatrix, column + 1, row))
|
|
flags |= CornerFlags.BottomRight;
|
|
if (IsValid(bitMatrix, column, row + 1) && IsValid(bitMatrix, column - 1, row + 1) && IsValid(bitMatrix, column - 1, row))
|
|
flags |= CornerFlags.BottomLeft;
|
|
|
|
return flags;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether or not the specified symbol should be considered "set"
|
|
/// </summary>
|
|
/// <param name="bitMatrix">BitMatrix containing the data</param>
|
|
/// <param name="x"></param>
|
|
/// <param name="y"></param>
|
|
/// <returns></returns>
|
|
private bool IsValid(BitMatrix bitMatrix, int x, int y)
|
|
{
|
|
// Validate bounds of the bit matrix
|
|
if (x < 0 || y < 0 || x >= bitMatrix.Width || y >= bitMatrix.Height)
|
|
return false;
|
|
|
|
var key = (x, y).GetHashCode();
|
|
|
|
lock (_setBitsTable)
|
|
{
|
|
if (_setBitsTable.ContainsKey(key))
|
|
return (bool)_setBitsTable[key]!;
|
|
|
|
// Top Left Marker
|
|
if (x < 8 && y < 8)
|
|
return (bool)(_setBitsTable[key] = false);
|
|
// Top Right Marker
|
|
if (x > bitMatrix.Width - 9 && y < 8)
|
|
return (bool)(_setBitsTable[key] = false);
|
|
// Bottom Left Marker
|
|
if (x < 8 && y > bitMatrix.Height - 9)
|
|
return (bool)(_setBitsTable[key] = false);
|
|
|
|
/*
|
|
* ToDo: You can add additional logic here to exclude an additional portion of data.
|
|
* This is not supported in the example as careful consideration must be made to ensure
|
|
* that the QRCode is still readable based on the ECC Level selected. Additionally,
|
|
* you may want to accept a path to render a logo in the center to make it fit with the
|
|
* current design.
|
|
*/
|
|
|
|
return (bool)(_setBitsTable[key] = bitMatrix[y, x]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the Position Detection Patterns (the three markers
|
|
/// </summary>
|
|
/// <param name="geometry">Geometry containing the QRCode Geometry</param>
|
|
/// <param name="bounds">Bounds of the control itself</param>
|
|
/// <param name="symbolSize">The size of each symbol</param>
|
|
private void AddPositionDetectionPattern(PathGeometry geometry, Rect bounds, Size symbolSize)
|
|
{
|
|
// Pre-calculations to reduce the amount of repeat math
|
|
var dataBounds = bounds
|
|
.Deflate(Padding)
|
|
.Deflate(new Thickness(symbolSize.Width * QuietZoneCount, symbolSize.Height * QuietZoneCount));
|
|
var markerSize = symbolSize * 7;
|
|
var markerRadiusSize = markerSize / 2;
|
|
var twiceSymbolSize = symbolSize * 2;
|
|
|
|
// Three Position Patters
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
/*
|
|
* Determines the X/Y location of this marker:
|
|
* 0: Top-Left
|
|
* 1: Top-Right
|
|
* 2: Bottom-Left
|
|
*/
|
|
var markerPosition = new Point(
|
|
i == 1 ? dataBounds.Right - markerSize.Width : dataBounds.Left,
|
|
i == 2 ? dataBounds.Bottom - markerRadiusSize.Height : dataBounds.Top + markerRadiusSize.Height
|
|
);
|
|
|
|
// Starting position of the circles. These are adjusted each loop to make them smaller and smaller
|
|
var startPoint = markerPosition;
|
|
var endPoint = startPoint.WithX(startPoint.X + markerSize.Width);
|
|
var arcSize = markerRadiusSize;
|
|
|
|
// Three "rings" per marker
|
|
for (var x = 0; x < 3; x++)
|
|
{
|
|
geometry.Figures!.Add(new PathFigure
|
|
{
|
|
StartPoint = startPoint,
|
|
Segments = new PathSegments {
|
|
new ArcSegment { Size = arcSize, Point = endPoint },
|
|
new ArcSegment { Size = arcSize, Point = startPoint }
|
|
}
|
|
});
|
|
|
|
// Adjusts the "rings" to make them progressively smaller with each loop
|
|
startPoint = startPoint.WithX(startPoint.X + symbolSize.Width);
|
|
endPoint = endPoint.WithX(endPoint.X - symbolSize.Width);
|
|
arcSize -= twiceSymbolSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Render(DrawingContext context)
|
|
{
|
|
base.Render(context);
|
|
|
|
// Render nothing when there's no data.
|
|
// Note, when using in a scenario when you may not have data right away, you can render something over the QRCode like a spinner, etc
|
|
if (_qrCodeGeometry == null)
|
|
return;
|
|
|
|
var bounds = new Rect(0, 0, Width, Height);
|
|
|
|
// Rounded corners
|
|
context.PushClip(new RoundedRect(bounds, CornerRadius.TopLeft, CornerRadius.TopRight, CornerRadius.BottomRight, CornerRadius.BottomLeft));
|
|
|
|
if (_oldQrCodeGeometry is var (oldGeometry, _))
|
|
{
|
|
// The foreground will show through as the qr code will be "cut out" of the background
|
|
context.DrawRectangle(Foreground, null, bounds);
|
|
// Render background over the foreground as the geometry has "cut outs" that allow the foreground to show through
|
|
context.DrawGeometry(Background, null, oldGeometry);
|
|
}
|
|
|
|
if (_qrCodeGeometry is var (newGeometry, newOpacity))
|
|
{
|
|
using var _ = context.PushOpacity(newOpacity);
|
|
|
|
// The foreground will show through as the qr code will be "cut out" of the background
|
|
context.DrawRectangle(Foreground, null, bounds);
|
|
// Render background over the foreground as the geometry has "cut outs" that allow the foreground to show through
|
|
context.DrawGeometry(Background, null, newGeometry);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public enum EccLevel
|
|
{
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
Lowest,
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
Medium,
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
Quality,
|
|
|
|
/// <summary>
|
|
/// The maximum level of error correction where up to ~30% of data can be be recovered if lost and represents the maximum achievable reliability
|
|
/// </summary>
|
|
Highest,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts from our EccLevel to the one used by whichever algorithm being used.
|
|
/// This exists as an abstraction layer for if/when the package or namespace of the actual QR Generator changes so that breaking changes are not introduced
|
|
/// </summary>
|
|
/// <param name="eccLevel">The selected ECC Level to convert</param>
|
|
/// <returns>The appropriate ECC Level type used by the generator</returns>
|
|
/// <exception cref="ArgumentOutOfRangeException">When an unsupported ECC Level is provided</exception>
|
|
protected static ErrorCorrectionLevel ToQrCoderEccLevel(EccLevel eccLevel)
|
|
{
|
|
return eccLevel switch
|
|
{
|
|
EccLevel.Lowest => ErrorCorrectionLevel.L,
|
|
EccLevel.Medium => ErrorCorrectionLevel.M,
|
|
EccLevel.Quality => ErrorCorrectionLevel.Q,
|
|
EccLevel.Highest => ErrorCorrectionLevel.H,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(eccLevel), eccLevel, null)
|
|
};
|
|
}
|
|
|
|
[Flags]
|
|
private enum CornerFlags
|
|
{
|
|
None = 0,
|
|
TopLeft = 1 << 0,
|
|
TopRight = 1 << 1,
|
|
BottomRight = 1 << 2,
|
|
BottomLeft = 1 << 3
|
|
}
|
|
} |