Files
TheXamlGuy bc55c4649b tidy
2024-04-26 23:05:36 +01:00

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
}
}