Toolkit.UI.Controls.Avalonia

This commit is contained in:
TheXamlGuy
2024-04-13 11:41:33 +01:00
parent 62a7e94e19
commit 862e7b2e34
97 changed files with 8558 additions and 0 deletions
@@ -0,0 +1,8 @@
namespace Toolkit.UI.Controls.Avalonia;
public class AsyncImage :
global::Avalonia.Labs.Controls.AsyncImage
{
protected override Type StyleKeyOverride =>
typeof(global::Avalonia.Labs.Controls.AsyncImage);
}
@@ -0,0 +1,115 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using Avalonia.Styling;
using SkiaSharp;
namespace Toolkit.UI.Controls.Avalonia;
public class BlurBehind :
Control
{
public static readonly StyledProperty<ExperimentalAcrylicMaterial> MaterialProperty =
AvaloniaProperty.Register<BlurBehind, ExperimentalAcrylicMaterial>("Material");
public static readonly ImmutableExperimentalAcrylicMaterial DefaultAcrylicMaterialDark =
(ImmutableExperimentalAcrylicMaterial)new ExperimentalAcrylicMaterial()
{
MaterialOpacity = 0.25,
TintColor = Colors.Black,
TintOpacity = 0.7,
PlatformTransparencyCompensationLevel = 0
}.ToImmutable();
public static readonly ImmutableExperimentalAcrylicMaterial DefaultAcrylicMaterialLight =
(ImmutableExperimentalAcrylicMaterial)new ExperimentalAcrylicMaterial()
{
MaterialOpacity = 0.0,
TintColor = Colors.White,
TintOpacity = 0.3,
PlatformTransparencyCompensationLevel = 0
}.ToImmutable();
static BlurBehind()
{
AffectsRender<BlurBehind>(MaterialProperty);
}
public ExperimentalAcrylicMaterial Material
{
get => GetValue(MaterialProperty);
set => SetValue(MaterialProperty, value);
}
public override void Render(DrawingContext context)
{
ImmutableExperimentalAcrylicMaterial material = Material is not null
? (ImmutableExperimentalAcrylicMaterial)Material.ToImmutable()
: Application.Current?.ActualThemeVariant == ThemeVariant.Dark ? DefaultAcrylicMaterialDark : DefaultAcrylicMaterialLight;
context.Custom(new BlurBehindRenderOperation(material, new Rect(default, Bounds.Size)));
}
private class BlurBehindRenderOperation(ImmutableExperimentalAcrylicMaterial material,
Rect bounds) : ICustomDrawOperation
{
private readonly Rect bounds = bounds;
private readonly ImmutableExperimentalAcrylicMaterial material = material;
public Rect Bounds => bounds.Inflate(4);
public void Dispose()
{
}
public bool Equals(ICustomDrawOperation? other) =>
other is BlurBehindRenderOperation behindRenderOperation &&
behindRenderOperation.bounds == bounds && behindRenderOperation.material.Equals(material);
public bool HitTest(Point point) => bounds.Contains(point);
public void Render(ImmediateDrawingContext context)
{
if (context.TryGetFeature<ISkiaSharpApiLeaseFeature>() is ISkiaSharpApiLeaseFeature leaseFeature)
{
using ISkiaSharpApiLease? lease = leaseFeature.Lease();
if (lease.SkCanvas is SKCanvas canvas)
{
if (canvas.TotalMatrix.TryInvert(out SKMatrix currentInvertedTransform))
{
if (lease.SkSurface is SKSurface surface)
{
using SKImage backgroundSnapshot = surface.Snapshot();
using SKShader backdropShader = SKShader.CreateImage(backgroundSnapshot, SKShaderTileMode.Clamp,
SKShaderTileMode.Clamp, currentInvertedTransform);
using SKSurface blurred = SKSurface.Create(lease.GrContext, false,
new SKImageInfo((int)Math.Ceiling(bounds.Width), (int)Math.Ceiling(bounds.Height),
SKImageInfo.PlatformColorType, SKAlphaType.Premul));
using (SKImageFilter filter = SKImageFilter.CreateBlur(8, 8, SKShaderTileMode.Clamp))
using (SKPaint blurPaint = new() { Shader = backdropShader, ImageFilter = filter })
blurred.Canvas.DrawRect(5, 5, (float)bounds.Width - 20, (float)bounds.Height - 20, blurPaint);
using SKImage blurSnap = blurred.Snapshot();
using SKShader blurSnapShader = SKShader.CreateImage(blurSnap);
using SKPaint blurSnapPaint = new()
{
Shader = blurSnapShader,
IsAntialias = true
};
canvas.DrawRect(0, 0, (float)bounds.Width, (float)bounds.Height, blurSnapPaint);
}
}
}
}
}
}
}
@@ -0,0 +1,95 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Toolkit.UI.Controls.Avalonia">
<Design.PreviewWith>
<Border Padding="20" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type controls:CarouselView}" TargetType="controls:CarouselView">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid x:Name="Container" Background="{TemplateBinding Background}">
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Border HorizontalAlignment="Left">
<controls:CarouselViewItem />
</Border>
<Rectangle
x:Name="Indicator"
Width="10"
Height="10"
VerticalAlignment="Top" />
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type controls:CarouselViewItem}" TargetType="controls:CarouselViewItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter
x:Name="Content"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="{StaticResource ControlContentThemeFontSize}"
Foreground="{TemplateBinding Foreground}" />
</ControlTemplate>
</Setter>
<Style Selector="^:selected /template/ ContentPresenter#Content">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0.4" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="0.9" />
<Setter Property="ScaleTransform.ScaleY" Value="0.9" />
</KeyFrame>
<KeyFrame Cue="100%" KeySpline="0,0 0,1">
<Setter Property="ScaleTransform.ScaleX" Value="1.0" />
<Setter Property="ScaleTransform.ScaleY" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="^:not(:selected) /template/ ContentPresenter#Content">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0.4" />
</KeyFrame>
</Animation>
<Animation FillMode="Forward" Duration="00:00:00.500">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="1.0" />
<Setter Property="ScaleTransform.ScaleY" Value="1.0" />
</KeyFrame>
<KeyFrame Cue="100%" KeySpline="0,0 0,1">
<Setter Property="ScaleTransform.ScaleX" Value="0.9" />
<Setter Property="ScaleTransform.ScaleY" Value="0.9" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -0,0 +1,313 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
using System.Collections.Specialized;
using System.Numerics;
namespace Toolkit.UI.Controls.Avalonia;
public class CarouselView :
ItemsControl
{
private readonly TimeSpan animationDuration = TimeSpan.FromMilliseconds(500);
private readonly List<ExpressionAnimation> animations = [];
private readonly int columnCount = 5;
private readonly List<CompositionVisual> itemVisuals = [];
private readonly ScopedBatchHelper scopedBatch = new();
private readonly double spacing = 12;
private Compositor? compositor;
private Grid? container;
private Vector3D finalOffset;
private float horizontalDelta;
private Rectangle? indicator;
private Vector3DKeyFrameAnimation? indicatorAnimation;
private CompositionVisual? indicatorVisual;
private bool isAnimating;
private bool isPressed;
private List<Border>? items;
private Point? lastPosition;
private int newIndex;
private int SelectedIndex;
private Point? startPosition;
private CompositionVisual? touchAreaVisual;
protected override void OnApplyTemplate(TemplateAppliedEventArgs args)
{
container = args.NameScope.Get<Grid>("Container");
if (container is not null)
{
items = container.Children.OfType<Border>().ToList();
foreach (Border item in items)
{
if (item.Child is CarouselViewItem contentControl)
{
contentControl.ContentTemplate = ItemTemplate;
}
}
}
indicator = args.NameScope.Get<Rectangle>("Indicator");
if (indicator is not null)
{
indicatorVisual = ElementComposition.GetElementVisual(indicator);
}
ItemsView.CollectionChanged -= OnCollectionChanged;
ItemsView.CollectionChanged += OnCollectionChanged;
base.OnApplyTemplate(args);
}
protected override void OnSizeChanged(SizeChangedEventArgs args)
{
base.OnSizeChanged(args);
ArrangeItems(newIndex, isAnimating: false);
}
protected override void OnLoaded(RoutedEventArgs args)
{
if (container is not null
&& items is not null
&& indicator is not null)
{
indicatorVisual = ElementComposition.GetElementVisual(indicator);
touchAreaVisual = ElementComposition.GetElementVisual(container);
if (touchAreaVisual is not null)
{
compositor = touchAreaVisual.Compositor;
}
itemVisuals.Clear();
foreach (Border item in items)
{
if (ElementComposition.GetElementVisual(item) is CompositionVisual visual)
{
itemVisuals.Add(visual);
}
}
ArrangeItems(newIndex);
}
base.OnLoaded(args);
}
protected override void OnPointerMoved(PointerEventArgs args)
{
if (isPressed && indicatorVisual is not null && startPosition.HasValue)
{
lastPosition = args.GetPosition(container);
horizontalDelta = (float)(lastPosition.Value.X - startPosition.Value.X);
indicatorVisual.Offset = new Vector3(horizontalDelta, 0.0f, 0.0f);
}
base.OnPointerMoved(args);
}
protected override void OnPointerPressed(PointerPressedEventArgs args)
{
if (!isPressed && indicatorVisual is not null)
{
if (!isAnimating)
{
horizontalDelta = 0;
isPressed = true;
startPosition = args.GetPosition(container);
indicatorVisual.Offset = new Vector3(horizontalDelta, 0.0f, 0.0f);
PrepareAnimations();
for (int i = 0; i < itemVisuals.Count; i++)
{
itemVisuals[i].StartAnimation("Offset", animations[i]);
}
}
}
base.OnPointerPressed(args);
}
protected override void OnPointerReleased(PointerReleasedEventArgs args)
{
if (isPressed && container is not null
&& items is not null
&& indicatorVisual is not null)
{
isPressed = false;
double itemWidth = items[0].Bounds.Width;
double threshold = itemWidth / 3;
int oldSelectedIndex = newIndex;
double offset = indicatorVisual.Offset.X;
if (offset <= -threshold)
{
newIndex = (newIndex + 1) % 5;
SelectedIndex = (SelectedIndex + 1) % ItemsView.Count;
}
if (offset >= threshold)
{
newIndex = (newIndex + 4) % 5;
SelectedIndex = (SelectedIndex + ItemsView.Count - 1) % ItemsView.Count;
}
ArrangeItems(newIndex, oldSelectedIndex, true);
}
base.OnPointerReleased(args);
}
private void ArrangeItems(int newIndex,
int oldIndex = -1,
bool isAnimating = false)
{
if (compositor is not null
&& container is not null
&& items is not null
&& indicatorVisual is not null)
{
double containerHeight = Bounds.Height;
double containerWidth = Bounds.Width;
container.Height = containerHeight;
double targetSize = containerHeight;
foreach (Border item in items)
{
if (item.Child is CarouselViewItem content)
{
content.Width = targetSize;
content.Height = targetSize;
}
item.Width = targetSize;
item.Height = targetSize;
}
double centreLeft = (containerWidth - targetSize) / 2;
double leftLeft = -targetSize + centreLeft;
double rightLeft = containerWidth - centreLeft;
double[] offsets =
[
leftLeft - targetSize + spacing * 1,
leftLeft + spacing * 2,
centreLeft + spacing * 3,
rightLeft + spacing * 4,
rightLeft + targetSize + spacing * 5
];
double centreOffset = spacing * (columnCount - 1) / 2 + spacing;
if (!isAnimating)
{
for (int i = 0; i < columnCount; i++)
{
itemVisuals[(newIndex + i - 2 + columnCount) % columnCount].Offset =
new Vector3((float)(offsets[i] - centreOffset), 0, 100);
}
SetItems();
}
else
{
int difference = newIndex - oldIndex;
finalOffset = difference switch
{
0 => new Vector3D(0, 0, 0),
1 => new Vector3D((float)(-targetSize - spacing), 0, 0),
-1 => new Vector3D((float)(targetSize + spacing), 0, 0),
_ => new Vector3D((float)(targetSize * Math.Sign(difference) +
spacing * Math.Sign(difference)), 0, 0)
};
indicatorAnimation = compositor.CreateVector3DKeyFrameAnimation();
indicatorAnimation.InsertKeyFrame(1.0f, finalOffset);
indicatorAnimation.Duration = animationDuration;
indicatorAnimation.StopBehavior = AnimationStopBehavior.LeaveCurrentValue;
SetItems();
scopedBatch.Completed += () =>
{
this.isAnimating = false;
for (int i = 0; i < columnCount; i++)
{
itemVisuals[(newIndex + i - 2 + columnCount) % columnCount].Offset =
new Vector3((float)(offsets[i] - centreOffset), 0, 0);
}
};
indicatorVisual.StartAnimation("Offset", indicatorAnimation);
scopedBatch.Start(animationDuration);
this.isAnimating = true;
}
}
}
private void OnCollectionChanged(object? sender,
NotifyCollectionChangedEventArgs args) => ArrangeItems(newIndex);
private void PrepareAnimations()
{
animations.Clear();
if (compositor is not null && indicatorVisual is not null && itemVisuals is not null)
{
for (int i = 0; i < itemVisuals.Count; i++)
{
ExpressionAnimation animation = compositor.CreateExpressionAnimation();
animation.Expression = $"Source.Offset + Vector3({itemVisuals[i].Offset.X}, 0, 0)";
animation.SetReferenceParameter("Source", indicatorVisual);
animations.Add(animation);
}
}
}
private void SetItems()
{
if (items is not null)
{
int itemCount = ItemsView.Count;
if (itemCount == 0)
{
return;
}
int[] selectedIndexOffsets = new int[columnCount];
int[] indexOffsets = new int[columnCount];
SelectedIndex = SelectedIndex < 0 ? 0 : SelectedIndex;
for (int i = -2; i <= 2; i++)
{
selectedIndexOffsets[i + 2] = (SelectedIndex + i + itemCount) % itemCount;
indexOffsets[i + 2] = (newIndex + i + columnCount) % columnCount;
}
for (int i = 0; i < columnCount; i++)
{
int index = selectedIndexOffsets[i];
if (itemCount == 1)
{
index = 0;
}
if (items[indexOffsets[i]] is Border border && border.Child is
CarouselViewItem content)
{
content.Content = ItemsView[index];
content.SetSelected(indexOffsets.Length / 2 == i);
}
}
}
}
}
@@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace Toolkit.UI.Controls.Avalonia;
public class CarouselViewItem :
ContentControl
{
internal void SetSelected(bool selected)
{
PseudoClasses.Set(":selected", selected);
}
}
@@ -0,0 +1,357 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Toolkit.UI.Controls.Avalonia"
xmlns:ui="using:FluentAvalonia.UI.Controls">
<Design.PreviewWith>
<Border Padding="20" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type ui:ContentDialog}" TargetType="ui:ContentDialog">
<Setter Property="Foreground" Value="{DynamicResource ContentDialogForeground}" />
<Setter Property="Background" Value="{DynamicResource ContentDialogBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ContentDialogBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ContentDialogBorderWidth}" />
<Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="Container">
<Panel Name="LayoutRoot" Background="{DynamicResource ContentDialogSmokeFill}">
<controls:BlurBehind />
<ui:FABorder
Name="BackgroundElement"
MinWidth="{DynamicResource ContentDialogMinWidth}"
MinHeight="{DynamicResource ContentDialogMinHeight}"
MaxWidth="{DynamicResource ContentDialogMaxWidth}"
MaxHeight="{DynamicResource ContentDialogMaxHeight}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{StaticResource ContentDialogBorderWidth}"
BoxShadow="0 8 32 0 #66000000"
CornerRadius="{TemplateBinding CornerRadius}">
<!-- Even in WinUI, shadow is always black regardless of light/dark mode -->
<!--
if this border isn't here, dialog space displays outside of corner radius at top
if we put ClipToBounds=True on BackgroundElement above, it clips the shadow
-->
<Border ClipToBounds="True" CornerRadius="{TemplateBinding CornerRadius}">
<Grid
Name="DialogSpace"
ClipToBounds="True"
RowDefinitions="*,Auto">
<ScrollViewer
Name="ContentScrollViewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Border
Padding="{DynamicResource ContentDialogPadding}"
Background="{DynamicResource ContentDialogTopOverlay}"
BorderBrush="{DynamicResource ContentDialogSeparatorBorderBrush}"
BorderThickness="{StaticResource ContentDialogSeparatorThickness}">
<Grid RowDefinitions="Auto,*">
<Grid.Styles>
<!-- Make sure text wrapping is on -->
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
</Grid.Styles>
<ContentControl
Name="Title"
Margin="{StaticResource ContentDialogTitleMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Content="{TemplateBinding Title}"
ContentTemplate="{TemplateBinding TitleTemplate}"
FontFamily="Default"
FontSize="20"
FontWeight="SemiBold"
Foreground="{TemplateBinding Foreground}">
<ContentControl.Template>
<ControlTemplate>
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<ContentPresenter
Name="Content"
Grid.Row="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
FontFamily="{StaticResource ContentControlThemeFontFamily}"
FontSize="{StaticResource ControlContentThemeFontSize}"
Foreground="{TemplateBinding Foreground}" />
</Grid>
</Border>
</ScrollViewer>
<Border
Grid.Row="1"
Padding="{StaticResource ContentDialogPadding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Background="{TemplateBinding Background}">
<Grid Name="CommandSpace">
<!--
B/C we can't target Row/Column defs in Styles like WinUI
this still uses the old Col defs, but it works the same
way in the end...
-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button
Name="PrimaryButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Content="{TemplateBinding PrimaryButtonText}"
IsEnabled="{TemplateBinding IsPrimaryButtonEnabled}"
IsVisible="False" />
<Button
Name="SecondaryButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Content="{TemplateBinding SecondaryButtonText}"
IsEnabled="{TemplateBinding IsSecondaryButtonEnabled}"
IsVisible="False" />
<Button
Name="CloseButton"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Content="{TemplateBinding CloseButtonText}"
IsVisible="False" />
</Grid>
</Border>
</Grid>
</Border>
</ui:FABorder>
</Panel>
</Border>
</ControlTemplate>
</Setter>
<!-- Handle hidden dialog -->
<Style Selector="^:hidden /template/ Panel#LayoutRoot">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.167">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="0.0" />
<Setter Property="IsVisible" Value="False" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="^:hidden /template/ Border#Container">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.167">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="1.0" />
<Setter Property="ScaleTransform.ScaleY" Value="1.0" />
</KeyFrame>
<KeyFrame Cue="100%" KeySpline="0,0 0,1">
<Setter Property="ScaleTransform.ScaleX" Value="1.05" />
<Setter Property="ScaleTransform.ScaleY" Value="1.05" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- Handle open dialog -->
<Style Selector="^:open /template/ Panel#LayoutRoot">
<Setter Property="IsVisible" Value="True" />
<Style.Animations>
<!--
Animation applies with priority of LocalValue
To overrule the IsVisible=False in :hidden, set
IsVisible=True in BOTH KeyFrames here
-->
<Animation FillMode="Forward" Duration="00:00:00.250">
<KeyFrame Cue="0%">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Opacity" Value="0.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="^:open /template/ Border#Container">
<Style.Animations>
<Animation FillMode="Forward" Duration="00:00:00.250">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="1.05" />
<Setter Property="ScaleTransform.ScaleY" Value="1.05" />
</KeyFrame>
<KeyFrame Cue="100%" KeySpline="0,0 0,1">
<Setter Property="ScaleTransform.ScaleX" Value="1.00" />
<Setter Property="ScaleTransform.ScaleY" Value="1.00" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- Handle showing smoke layer -->
<Style Selector="^:nosmokelayer /template/ Panel#LayoutRoot">
<Setter Property="Background" Value="{x:Null}" />
</Style>
<!-- Handle FullDialogSizing -->
<Style Selector="^:fullsize /template/ ui|FABorder#BackgroundElement">
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<!-- Primary Button Only -->
<Style Selector="^:primary /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="2" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
<Style Selector="^:primary /template/ Button#SecondaryButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:primary /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<!-- Secondary Button Only -->
<Style Selector="^:secondary /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:secondary /template/ Button#SecondaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="2" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
<Style Selector="^:secondary /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<!-- Close Button Only -->
<Style Selector="^:close /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:close /template/ Button#SecondaryButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:close /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="2" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
<!-- Margins are defined by ContentDialogButtonSpacing (8) -->
<!-- Primary and Secondary -->
<Style Selector="^:primary:secondary /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="0 0 4 0" />
</Style>
<Style Selector="^:primary:secondary /template/ Button#SecondaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="2" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="4 0 0 0" />
</Style>
<Style Selector="^:primary:secondary /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<!-- Primary and Close Buttons -->
<Style Selector="^:primary:close /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="0 0 4 0" />
</Style>
<Style Selector="^:primary:close /template/ Button#SecondaryButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:primary:close /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="2" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="4 0 0 0" />
</Style>
<!-- Primary and Secondary Buttons -->
<Style Selector="^:primary:secondary /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="0 0 4 0" />
</Style>
<Style Selector="^:primary:secondary /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:primary:secondary /template/ Button#SecondaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="2" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="4 0 0 0" />
</Style>
<!-- Secondary and Close Buttons -->
<Style Selector="^:secondary:close /template/ Button#Secondary">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="0 0 4 0" />
</Style>
<Style Selector="^:secondary:close /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:secondary:close /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="2" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="4 0 0 0" />
</Style>
<!-- All Buttons -->
<Style Selector="^:primary:secondary:close /template/ Button#PrimaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Grid.ColumnSpan" Value="1" />
<Setter Property="Margin" Value="0 0 4 0" />
</Style>
<Style Selector="^:primary:secondary:close /template/ Button#SecondaryButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="1" />
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Margin" Value="4 0 4 0" />
</Style>
<Style Selector="^:primary:secondary:close /template/ Button#CloseButton">
<Setter Property="IsVisible" Value="True" />
<Setter Property="Grid.Column" Value="3" />
<Setter Property="Grid.ColumnSpan" Value="1" />
<Setter Property="Margin" Value="4 0 0 0" />
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -0,0 +1,8 @@
namespace Toolkit.UI.Controls.Avalonia;
public class ContentDialog :
FluentAvalonia.UI.Controls.ContentDialog
{
protected override Type StyleKeyOverride =>
typeof(FluentAvalonia.UI.Controls.ContentDialog);
}
@@ -0,0 +1,166 @@
using Avalonia.Media.Imaging;
using Avalonia.Styling;
using Avalonia;
using SukiUI.Utilities.Background;
using Avalonia.Platform;
using Avalonia.Media;
namespace Toolkit.UI.Controls.Avalonia;
public class FastNoiseBackgroundRenderer
{
private static readonly FastNoiseLite NoiseGen = new();
private static readonly Random Rand = new();
private readonly float accentAlpha;
private readonly object lockObj = new();
private readonly float primaryAlpha;
private readonly float scale;
private readonly float xSeed;
private readonly float ySeed;
private uint accentColour;
private float aOffsetX;
private float aOffsetY;
private uint baseColour;
private bool isRedrawing;
private float pOffsetX;
private float pOffsetY;
private uint themeColour;
public FastNoiseBackgroundRenderer(FastNoiseRendererOptions? options = null)
{
FastNoiseRendererOptions opt = options ??
new FastNoiseRendererOptions(FastNoiseLite.NoiseType.OpenSimplex2);
NoiseGen.SetNoiseType(opt.Type);
scale = opt.NoiseScale * 100f;
xSeed = opt.XSeed;
ySeed = opt.YSeed;
primaryAlpha = opt.PrimaryAlpha;
accentAlpha = opt.AccentAlpha;
}
public async void Render(WriteableBitmap bitmap)
{
pOffsetX += xSeed;
pOffsetY += ySeed;
aOffsetX -= xSeed;
aOffsetY -= ySeed;
if (isRedrawing) return;
lock (lockObj) { isRedrawing = true; }
await Task.Run(() =>
{
using ILockedFramebuffer frameBuffer = bitmap.Lock();
PixelSize frameSize = frameBuffer.Size;
float frameScale = 1f / frameSize.Height * scale;
unsafe
{
uint* backBuffer = (uint*)frameBuffer.Address.ToPointer();
int stride = frameBuffer.RowBytes / 4;
Parallel.For(0, frameSize.Height, (long scanline) =>
{
for (int x = 0; x < frameSize.Width; x++)
{
float noise = NoiseGen.GetNoise((pOffsetX + x) * frameScale, (pOffsetY + scanline) * frameScale);
noise = (noise + 1f) / 2f * primaryAlpha; // noise returns -1 to +1 which isn't useful.
byte alpha = (byte)(noise * 255);
uint firstLayer = BlendPixelOverlay(WithAlpha(themeColour, alpha), baseColour);
noise = NoiseGen.GetNoise((aOffsetX + x) * frameScale, (aOffsetY + scanline) * frameScale);
noise = (noise + 1f) / 2f * accentAlpha;
alpha = (byte)(noise * 255);
(backBuffer + scanline * stride + 0)[x] = BlendPixel(WithAlpha(accentColour, alpha), firstLayer);
}
});
}
});
lock (lockObj) { isRedrawing = false; }
}
public void UpdateValues(Color primary,
Color accent,
ThemeVariant baseTheme)
{
themeColour = ToUInt32(primary);
accentColour = ToUInt32(accent);
baseColour = baseTheme == ThemeVariant.Light
? new Color(255, 241, 241, 241).ToUInt32()
: GetBackgroundColour(primary);
pOffsetX = Rand.Next(1000);
pOffsetY = Rand.Next(1000);
aOffsetY = Rand.Next(1000);
aOffsetX = Rand.Next(1000);
}
private static byte A(uint col) => (byte)(col >> 24);
private static uint ARGB(byte a, byte r, byte g, byte b) =>
(uint)(a << 24 | r << 16 | g << 8 | b << 0);
private static byte B(uint col) => (byte)col;
private static uint BlendPixel(uint fore, uint back)
{
float alphaF = A(fore) / 255.0f;
byte resultR = (byte)(R(fore) * alphaF + R(back) * (1 - alphaF));
byte resultG = (byte)(G(fore) * alphaF + G(back) * (1 - alphaF));
byte resultB = (byte)(B(fore) * alphaF + B(back) * (1 - alphaF));
byte resultA = A(back);
return ARGB(resultA, resultR, resultG, resultB);
}
private static uint BlendPixelOverlay(uint fore, uint back)
{
float alphaF = A(fore) / 255.0f;
byte resultR = OverlayComponentBlend(R(fore), R(back), alphaF);
byte resultG = OverlayComponentBlend(G(fore), G(back), alphaF);
byte resultB = OverlayComponentBlend(B(fore), B(back), alphaF);
return ARGB(A(back), resultR, resultG, resultB);
}
private static byte G(uint col) =>
(byte)(col >> 8);
private static uint GetBackgroundColour(Color input)
{
int r = input.R;
int g = input.G;
int b = input.B;
int minValue = Math.Min(Math.Min(r, g), b);
int maxValue = Math.Max(Math.Max(r, g), b);
r = r == minValue ? 30 : r == maxValue ? 30 : 22;
g = g == minValue ? 30 : g == maxValue ? 30 : 22;
b = b == minValue ? 30 : b == maxValue ? 30 : 22;
return ARGB(255, (byte)r, (byte)g, (byte)b);
}
private static byte OverlayComponentBlend(byte componentF, byte componentB, float alphaF)
{
float result = componentB <= 128
? 2 * componentF * componentB / 255.0f
: 255 - 2 * (255 - componentF) * (255 - componentB) / 255.0f;
return (byte)(result * alphaF + componentB * (1 - alphaF));
}
private static byte R(uint col) => (byte)(col >> 16);
private static uint ToUInt32(Color colour) =>
(uint)(colour.A << 24 | colour.R << 16 | colour.G << 8 | colour.B);
private static uint WithAlpha(uint col, byte a) => col & 0x00FFFFFF | (uint)(a << 24);
}
@@ -0,0 +1,23 @@
namespace SukiUI.Utilities.Background;
public readonly struct FastNoiseRendererOptions(
FastNoiseLite.NoiseType type,
float noiseScale = 1.5f,
float xSeed = 2f,
float ySeed = 1f,
float primaryAlpha = 0.7f,
float accentAlpha = 0.04f,
float seedScale = 0.1f)
{
public float AccentAlpha { get; } = accentAlpha;
public float NoiseScale { get; } = noiseScale;
public float PrimaryAlpha { get; } = primaryAlpha;
public FastNoiseLite.NoiseType Type { get; } = type;
public float XSeed { get; } = xSeed * seedScale;
public float YSeed { get; } = ySeed * seedScale;
}
@@ -0,0 +1,44 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Styling;
namespace Toolkit.UI.Controls.Avalonia;
public class FastRendererBackground :
Image, IDisposable
{
private const int ImageWidth = 100;
private const int ImageHeight = 100;
private readonly WriteableBitmap bitmap = new(new PixelSize(ImageWidth, ImageHeight),
new Vector(96, 96), PixelFormat.Bgra8888);
private readonly FastNoiseBackgroundRenderer renderer = new();
public FastRendererBackground()
{
Source = bitmap;
Stretch = Stretch.UniformToFill;
}
public override void EndInit()
{
base.EndInit();
if (Application.Current?.ActualThemeVariant is ThemeVariant theme)
{
renderer.UpdateValues((Color)Application.Current.FindResource("SystemAccentColorLight3"),
(Color)Application.Current.FindResource("SystemAccentColorDark3"), theme);
}
renderer.Render(bitmap);
}
public void Dispose()
{
GC.SuppressFinalize(this);
bitmap.Dispose();
}
}
@@ -0,0 +1,8 @@
namespace Toolkit.UI.Controls.Avalonia;
public class Frame :
FluentAvalonia.UI.Controls.Frame
{
protected override Type StyleKeyOverride =>
typeof(FluentAvalonia.UI.Controls.Frame);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
using Avalonia.Threading;
namespace Toolkit.UI.Controls.Avalonia;
public class ScopedBatchHelper
{
private DispatcherTimer? timer;
public Action? Completed { get; set; }
public void Start(TimeSpan duration)
{
timer ??= new DispatcherTimer(duration, DispatcherPriority.Background, Tick);
timer.Start();
}
private void Tick(object? sender, EventArgs args)
{
timer?.Stop();
Completed?.Invoke();
Completed = null;
}
}
@@ -0,0 +1,8 @@
namespace Toolkit.UI.Controls.Avalonia;
public class NavigationView :
FluentAvalonia.UI.Controls.NavigationView
{
protected override Type StyleKeyOverride =>
typeof(FluentAvalonia.UI.Controls.NavigationView);
}
@@ -0,0 +1,8 @@
namespace Toolkit.UI.Controls.Avalonia;
public class NavigationViewItem :
FluentAvalonia.UI.Controls.NavigationViewItem
{
protected override Type StyleKeyOverride =>
typeof(FluentAvalonia.UI.Controls.NavigationViewItem);
}
@@ -0,0 +1,3 @@
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Toolkit.UI.Controls.Avalonia")]
@@ -0,0 +1,127 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Gma.QrCodeNet.Encoding;
internal sealed class BitList : IEnumerable<bool>
{
internal BitList()
{
Count = 0;
List = new List<byte>(32);
}
internal BitList(IEnumerable<byte> byteArray)
{
Count = byteArray.Count();
List = byteArray.ToList();
}
internal List<byte> List { get; }
internal int Count { get; private set; }
internal bool this[int index]
{
get
{
if (index < 0 || index >= Count)
{
throw new ArgumentOutOfRangeException(nameof(index), "Index out of range");
}
int value_Renamed = List[index >> 3] & 0xff;
return ((value_Renamed >> (7 - (index & 0x7))) & 1) == 1;
}
}
public IEnumerator<bool> GetEnumerator()
{
int numBytes = Count >> 3;
int remainder = Count & 0x7;
byte value;
for (int index = 0; index < numBytes; index++)
{
value = List[index];
for (int shiftNum = 7; shiftNum >= 0; shiftNum--)
{
yield return ((value >> shiftNum) & 1) == 1;
}
}
if (remainder > 0)
{
value = List[numBytes];
for (int index = 0; index < remainder; index++)
{
yield return ((value >> (7 - index)) & 1) == 1;
}
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private int ToBit(bool item)
{
return item ? 1 : 0;
}
internal void Add(bool item)
{
int numBitsinLastByte = Count & 0x7;
// Add one more byte to List when we have no bits in the last byte.
if (numBitsinLastByte == 0)
{
List.Add(0);
}
List[Count >> 3] |= (byte)(ToBit(item) << (7 - numBitsinLastByte));
Count++;
}
internal void Add(IEnumerable<bool> items)
{
foreach (bool item in items)
{
Add(item);
}
}
internal void Add(int value, int bitCount)
{
if (bitCount is < 0 or > 32)
{
throw new ArgumentOutOfRangeException(nameof(bitCount), $"{nameof(bitCount)} must be greater than or equal to 0");
}
int numBitsLeft = bitCount;
while (numBitsLeft > 0)
{
if ((Count & 0x7) == 0 && numBitsLeft >= 8)
{
// Add one more byte to List.
byte newByte = (byte)((value >> (numBitsLeft - 8)) & 0xFF);
AppendByte(newByte);
numBitsLeft -= 8;
}
else
{
bool bit = ((value >> (numBitsLeft - 1)) & 1) == 1;
Add(bit);
numBitsLeft--;
}
}
}
private void AppendByte(byte item)
{
List.Add(item);
Count += 8;
}
}
@@ -0,0 +1,24 @@
namespace Gma.QrCodeNet.Encoding;
public abstract class BitMatrix
{
public abstract int Width { get; }
public abstract int Height { get; }
public abstract bool[,] InternalArray { get; }
public abstract bool this[int i, int j] { get; set; }
internal void CopyTo(TriStateMatrix target, MatrixRectangle sourceArea, MatrixPoint targetPoint, MatrixStatus mstatus)
{
for (int j = 0; j < sourceArea.Size.Height; j++)
{
for (int i = 0; i < sourceArea.Size.Width; i++)
{
bool value = this[sourceArea.Location.X + i, sourceArea.Location.Y + j];
target[targetPoint.X + i, targetPoint.Y + j, mstatus] = value;
}
}
}
internal void CopyTo(TriStateMatrix target, MatrixPoint targetPoint, MatrixStatus mstatus) => CopyTo(target, new MatrixRectangle(new MatrixPoint(0, 0), new MatrixSize(Width, Height)), targetPoint, mstatus);
}
@@ -0,0 +1,31 @@
namespace Gma.QrCodeNet.Encoding;
public abstract class BitMatrixBase : BitMatrix
{
protected BitMatrixBase(int width, bool[,] internalArray)
{
Width = width;
InternalArray = internalArray;
}
protected BitMatrixBase(bool[,] internalArray)
{
InternalArray = internalArray;
int width = internalArray.GetLength(0);
Width = width;
}
public override bool[,] InternalArray { get; }
public override int Width { get; }
public static bool CanCreate(bool[,] internalArray)
{
if (internalArray is null)
{
return false;
}
return internalArray.GetLength(0) == internalArray.GetLength(1);
}
}
@@ -0,0 +1,48 @@
using System;
namespace Gma.QrCodeNet.Encoding.DataEncodation;
public static class CharCountIndicatorTable
{
/// <remarks>ISO/IEC 18004:2000 Table 3 Page 18</remarks>
public static int[] GetCharCountIndicatorSet()
{
return new int[] { 8, 16, 16 };
}
public static int GetBitCountInCharCountIndicator(int version)
{
int[] charCountIndicatorSet = GetCharCountIndicatorSet();
int versionGroup = GetVersionGroup(version);
return charCountIndicatorSet[versionGroup];
}
/// <summary>
/// Used to define length of the Character Count Indicator <see cref="GetBitCountInCharCountIndicator"/>
/// </summary>
/// <returns>Returns the 0 based index of the row from Chapter 8.4 Data encodation, Table 3 — Number of bits in Character Count Indicator. </returns>
private static int GetVersionGroup(int version)
{
if (version > 40)
{
throw new InvalidOperationException($"Unexpected version: {version}.");
}
else if (version >= 27)
{
return 2;
}
else if (version >= 10)
{
return 1;
}
else if (version > 0)
{
return 0;
}
else
{
throw new InvalidOperationException($"Unexpected version: {version}.");
}
}
}
@@ -0,0 +1,62 @@
using System;
using Gma.QrCodeNet.Encoding.DataEncodation.InputRecognition;
using Gma.QrCodeNet.Encoding.Terminate;
using Gma.QrCodeNet.Encoding.Versions;
namespace Gma.QrCodeNet.Encoding.DataEncodation;
/// <remarks>ISO/IEC 18004:2000 Chapter 8.1 Page 14
/// DataEncode is combination of Data analysis and Data encodation step.
/// Which uses sub functions under several different namespaces</remarks>
internal static class DataEncode
{
internal static EncodationStruct Encode(string content, ErrorCorrectionLevel ecLevel)
{
RecognitionStruct recognitionResult = InputRecognise.Recognise(content);
EncoderBase encoderBase = CreateEncoder(recognitionResult.EncodingName);
BitList encodeContent = encoderBase.GetDataBits(content);
int encodeContentLength = encodeContent.Count;
VersionControlStruct vcStruct =
VersionControl.InitialSetup(encodeContentLength, ecLevel, recognitionResult.EncodingName);
BitList dataCodewords = new();
// Eci header
if (vcStruct.IsContainECI && vcStruct.ECIHeader is { })
{
dataCodewords.Add(vcStruct.ECIHeader);
}
// Header
dataCodewords.Add(encoderBase.GetModeIndicator());
int numLetter = encodeContentLength >> 3;
dataCodewords.Add(encoderBase.GetCharCountIndicator(numLetter, vcStruct.VersionDetail.Version));
// Data
dataCodewords.Add(encodeContent);
// Terminator Padding
dataCodewords.TerminateBites(dataCodewords.Count, vcStruct.VersionDetail.NumDataBytes);
int dataCodewordsCount = dataCodewords.Count;
if ((dataCodewordsCount & 0x7) != 0)
{
throw new ArgumentException($"{nameof(dataCodewords)} is not byte sized.");
}
else if (dataCodewordsCount >> 3 != vcStruct.VersionDetail.NumDataBytes)
{
throw new ArgumentException($"{nameof(dataCodewords)} num of bytes not equal to {nameof(vcStruct.VersionDetail.NumDataBytes)} for current version");
}
var encStruct = new EncodationStruct(vcStruct, dataCodewords);
return encStruct;
}
private static EncoderBase CreateEncoder(string encodingName)
{
return new EightBitByteEncoder(encodingName);
}
}
@@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
namespace Gma.QrCodeNet.Encoding.DataEncodation;
public sealed class ECISet
{
/// <summary>
/// ISO/IEC 18004:2006 Chapter 6.4.2 Mode indicator = 0111 Page 23
/// </summary>
private const int ECIMode = 7;
private const int ECIIndicatorNumBits = 4;
private Dictionary<string, int>? _nameToValue;
private Dictionary<int, string>? _valueToName;
/// <summary>
/// Initialize ECI Set.
/// </summary>
/// <param name="option">AppendOption is enum under ECISet
/// Use NameToValue during Encode. ValueToName during Decode</param>
internal ECISet(AppendOption option)
{
Initialize(option);
}
public enum AppendOption
{
NameToValue,
ValueToName,
Both
}
/// <summary>
/// Length indicator for number of ECI codewords
/// </summary>
/// <remarks>ISO/IEC 18004:2006 Chapter 6.4.2 Page 24.
/// 1 codeword length = 0. Any additional codeword add 1 to front. Eg: 3 = 110</remarks>
/// <description>Bits required for each one is:
/// one = 1, two = 2, three = 3</description>
private enum ECICodewordsLength
{
One = 0,
Two = 2,
Three = 6
}
/// <remarks>ISO/IEC 18004:2006E ECI Designator Page 24</remarks>
/// <param name="eCIValue">Range: 0 ~ 999999</param>
/// <returns>Number of Codewords(Byte) for ECI Assignment Value</returns>
private static int NumOfCodewords(int eCIValue)
{
if (eCIValue is >= 0 and <= 127)
{
return 1;
}
else if (eCIValue is > 127 and <= 16383)
{
return 2;
}
else if (eCIValue is > 16383 and <= 999999)
{
return 3;
}
else
{
throw new ArgumentOutOfRangeException($"{nameof(eCIValue)} should be in range: 0 to 999999.");
}
}
/// <remarks>ISO/IEC 18004:2006E ECI Designator Page 24</remarks>
/// <param name="eCIValue">Range: 0 ~ 999999</param>
/// <returns>Number of bits for ECI Assignment Value</returns>
private static int NumOfAssignmentBits(int eCIValue) => NumOfCodewords(eCIValue) * 8;
private void AppendECI(string name, int value, AppendOption option)
{
switch (option)
{
case AppendOption.NameToValue:
_nameToValue?.Add(name, value);
break;
case AppendOption.ValueToName:
_valueToName?.Add(value, name);
break;
case AppendOption.Both:
_nameToValue?.Add(name, value);
_valueToName?.Add(value, name);
break;
default:
throw new InvalidOperationException($"There is no such {nameof(AppendOption)}.");
}
}
private void Initialize(AppendOption option)
{
switch (option)
{
case AppendOption.NameToValue:
_nameToValue = new Dictionary<string, int>();
break;
case AppendOption.ValueToName:
_valueToName = new Dictionary<int, string>();
break;
case AppendOption.Both:
_nameToValue = new Dictionary<string, int>();
_valueToName = new Dictionary<int, string>();
break;
default:
throw new InvalidOperationException($"There is no such {nameof(AppendOption)}.");
}
// ECI table. Source 01 URL: http://strokescribe.com/en/ECI.html
// ECI table. Source 02 URL: http://lab.must.or.kr/Extended-Channel-Interpretations-ECI-Encoding.ashx
// ToDo. Fill up remaining missing table.
AppendECI("iso-8859-1", 1, option);
AppendECI("IBM437", 2, option);
// AppendECI("iso-8859-1", 3, option); //ECI value 1 is default encoding.
AppendECI("iso-8859-2", 4, option);
AppendECI("iso-8859-3", 5, option);
AppendECI("iso-8859-4", 6, option);
AppendECI("iso-8859-5", 7, option);
AppendECI("iso-8859-6", 8, option);
AppendECI("iso-8859-7", 9, option);
AppendECI("iso-8859-8", 10, option);
AppendECI("iso-8859-9", 11, option);
AppendECI("windows-874", 13, option);
AppendECI("iso-8859-13", 15, option);
AppendECI("iso-8859-15", 17, option);
AppendECI("shift_jis", 20, option);
AppendECI("utf-8", 26, option);
}
/// <remarks>ISO/IEC 18004:2006E ECI Designator Page 24</remarks>
/// <param name="eCIValue">Range: 0 ~ 999999</param>
/// <returns>Number of bits for ECI Header</returns>
internal static int NumOfECIHeaderBits(int eCIValue) => NumOfAssignmentBits(eCIValue) + 4;
internal int GetECIValueByName(string encodingName)
{
if (_nameToValue is null)
{
Initialize(AppendOption.NameToValue);
}
if (_nameToValue!.TryGetValue(encodingName, out int eCIValue))
{
return eCIValue;
}
else
{
throw new ArgumentOutOfRangeException($"ECI does not contain encoding: {encodingName}.");
}
}
internal string GetECINameByValue(int eCIValue)
{
if (_valueToName is null)
{
Initialize(AppendOption.ValueToName);
}
if (_valueToName!.TryGetValue(eCIValue, out var eCIName))
{
return eCIName;
}
else
{
throw new ArgumentOutOfRangeException($"ECI does not contain value: {eCIValue}.");
}
}
/// <returns>ECI table in Dictionary collection</returns>
public Dictionary<string, int>? GetECITable()
{
if (_nameToValue is null)
{
Initialize(AppendOption.NameToValue);
}
return _nameToValue;
}
public bool ContainsECIName(string encodingName)
{
if (_nameToValue is null)
{
Initialize(AppendOption.NameToValue);
}
return _nameToValue!.ContainsKey(encodingName);
}
public bool ContainsECIValue(int eciValue)
{
if (_valueToName is null)
{
Initialize(AppendOption.ValueToName);
}
return _valueToName!.ContainsKey(eciValue);
}
/// <remarks>ISO/IEC 18004:2006 Chapter 6.4.2 Page 24.</remarks>
internal BitList GetECIHeader(string encodingName)
{
int eciValue = GetECIValueByName(encodingName);
BitList dataBits = new()
{
{ ECIMode, ECIIndicatorNumBits }
};
int eciAssignmentByte = NumOfCodewords(eciValue);
// Number of bits = Num codewords indicator + codeword value = Number of codewords * 8
// Chapter 6.4.2.1 ECI Designator ISOIEC 18004:2006 Page 24
int eciAssignmentBits;
switch (eciAssignmentByte)
{
case 1:
// Indicator = 0. Page 24. Chapter 6.4.2.1
dataBits.Add((int)ECICodewordsLength.One, 1);
eciAssignmentBits = (eciAssignmentByte * 8) - 1;
break;
case 2:
// Indicator = 10. Page 24. Chapter 6.4.2.1
dataBits.Add((int)ECICodewordsLength.Two, 2);
eciAssignmentBits = (eciAssignmentByte * 8) - 2;
break;
case 3:
// Indicator = 110. Page 24. Chapter 6.4.2.1
dataBits.Add((int)ECICodewordsLength.Three, 3);
eciAssignmentBits = (eciAssignmentByte * 8) - 3;
break;
default:
throw new InvalidOperationException("Assignment Codewords should be either 1, 2 or 3.");
}
dataBits.Add(eciValue, eciAssignmentBits);
return dataBits;
}
}
@@ -0,0 +1,80 @@
using System;
namespace Gma.QrCodeNet.Encoding.DataEncodation;
/// <summary>
/// EightBitByte is a bit complicate compare to other encoding.
/// It can accept several different encoding table from global ECI table.
/// For different country, default encoding is different. JP use shift_jis, International spec use iso-8859-1
/// China use ASCII which is first part of normal char table. Between 00 to 7E
/// Korean and Thai should have their own default encoding as well. But so far I cannot find their specification freely online.
/// QrCode.Net will use international standard which is iso-8859-1 as default encoding.
/// And use UTF8 as suboption for any string that not belong to any char table or other encoder.
/// </summary>
/// <remarks>ISO/IEC 18004:2000 Chapter 8.4.4 Page 22</remarks>
internal class EightBitByteEncoder : EncoderBase
{
private const string DefaultEncoding = QRCodeConstantVariable.DefaultEncoding;
/// <summary>
/// Bitcount, Chapter 8.4.4, P.24
/// </summary>
private const int EightBitByteBitcount = 8;
/// <summary>
/// EightBitByte encoder's encoding will change according to different region
/// </summary>
/// <param name="encoding">Default encoding is "iso-8859-1"</param>
internal EightBitByteEncoder(string encoding) : base()
{
Encoding = encoding ?? DefaultEncoding;
}
internal EightBitByteEncoder() : base()
{
Encoding = DefaultEncoding;
}
internal string Encoding { get; private set; }
protected byte[] EncodeContent(string content, string encoding) => System.Text.Encoding.GetEncoding(encoding).GetBytes(content);
internal override BitList GetDataBits(string content)
{
var eciSet = new ECISet(ECISet.AppendOption.NameToValue);
if (!eciSet.ContainsECIName(Encoding))
{
throw new ArgumentOutOfRangeException(
nameof(Encoding),
$"Current ECI table does not support this encoding. Please check {nameof(ECISet)} class for more info.");
}
byte[] contentBytes = EncodeContent(content, Encoding);
return GetDataBitsByByteArray(contentBytes, Encoding);
}
internal BitList GetDataBitsByByteArray(byte[] encodeContent, string encodingName)
{
var dataBits = new BitList();
// Current plan for UTF8 support is put Byte order Mark in front of content byte.
// Also include ECI header before encoding header. Which will be add with encoding header.
if (encodingName == "utf-8")
{
byte[] utf8BOM = QRCodeConstantVariable.UTF8ByteOrderMark;
for (int index = 0; index < utf8BOM.Length; index++)
{
dataBits.Add(utf8BOM[index], EightBitByteBitcount);
}
}
for (int index = 0; index < encodeContent.Length; index++)
{
dataBits.Add(encodeContent[index], EightBitByteBitcount);
}
return dataBits;
}
protected override int GetBitCountInCharCountIndicator(int version) => CharCountIndicatorTable.GetBitCountInCharCountIndicator(version);
}
@@ -0,0 +1,15 @@
using Gma.QrCodeNet.Encoding.Versions;
namespace Gma.QrCodeNet.Encoding.DataEncodation;
internal struct EncodationStruct
{
internal EncodationStruct(VersionControlStruct vcStruct, BitList dataCodewords)
{
VersionDetail = vcStruct.VersionDetail;
DataCodewords = dataCodewords;
}
internal VersionDetail VersionDetail { get; set; }
internal BitList DataCodewords { get; set; }
}
@@ -0,0 +1,46 @@
namespace Gma.QrCodeNet.Encoding.DataEncodation;
public abstract class EncoderBase
{
internal EncoderBase()
{
}
protected virtual int GetDataLength(string content) => content.Length;
/// <summary>
/// Returns the bit representation of input data.
/// </summary>
internal abstract BitList GetDataBits(string content);
/// <summary>
/// Returns bit representation of Modevalue.
/// </summary>
/// <remarks>See Chapter 8.4 Data encodation, Table 2 — Mode indicators</remarks>
internal BitList GetModeIndicator()
{
BitList modeIndicatorBits = new()
{
{ 0001 << 2, 4 }
};
return modeIndicatorBits;
}
internal BitList GetCharCountIndicator(int characterCount, int version)
{
BitList characterCountBits = new();
int bitCount = GetBitCountInCharCountIndicator(version);
characterCountBits.Add(characterCount, bitCount);
return characterCountBits;
}
/// <summary>
/// Defines the length of the Character Count Indicator,
/// which varies according to the mode and the symbol version in use
/// </summary>
/// <returns>Number of bits in Character Count Indicator.</returns>
/// <remarks>
/// See Chapter 8.4 Data encodation, Table 3 — Number of bits in Character Count Indicator.
/// </remarks>
protected abstract int GetBitCountInCharCountIndicator(int version);
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
namespace Gma.QrCodeNet.Encoding.DataEncodation.InputRecognition;
public static class InputRecognise
{
public static RecognitionStruct Recognise(string content)
{
string encodingName = EightBitByteRecognision(content, 0, content.Length);
return new RecognitionStruct(encodingName);
}
private static string EightBitByteRecognision(string content, int startPos, int contentLength)
{
if(string.IsNullOrEmpty(content))
throw new ArgumentNullException(nameof(content));
var eciSets = new ECISet(ECISet.AppendOption.NameToValue);
Dictionary<string, int>? eciSet = eciSets.GetECITable();
if(eciSet == null)
return string.Empty;
// we will not check for utf8 encoding.
eciSet.Remove(QRCodeConstantVariable.UTF8Encoding);
eciSet.Remove(QRCodeConstantVariable.DefaultEncoding);
int scanPos = startPos;
// default encoding as priority
scanPos = ModeEncodeCheck.TryEncodeEightBitByte(content, QRCodeConstantVariable.DefaultEncoding, scanPos, contentLength);
if (scanPos == -1)
{
return QRCodeConstantVariable.DefaultEncoding;
}
foreach (KeyValuePair<string, int> kvp in eciSet)
{
scanPos = ModeEncodeCheck.TryEncodeEightBitByte(content, kvp.Key, scanPos, contentLength);
if (scanPos == -1)
{
return kvp.Key;
}
}
if (scanPos == -1)
{
throw new ArgumentException("foreach Loop check give wrong result.");
}
else
{
return QRCodeConstantVariable.UTF8Encoding;
}
}
}
@@ -0,0 +1,72 @@
using System;
namespace Gma.QrCodeNet.Encoding.DataEncodation.InputRecognition;
public static class ModeEncodeCheck
{
/// <summary>
/// Encoding.GetEncoding.GetBytes will transform char to 0x3F if that char not belong to current encoding table.
/// 0x3F is '?'
/// </summary>
private const int QuestionMarkChar = 0x3F;
/// <summary>
/// Use given encoding to check input string from starting position. If encoding table is suitable solution.
/// it will return -1. Else it will return failed encoding position.
/// </summary>
/// <param name="content">Input string</param>
/// <param name="encodingName">Encoding name. Check ECI table</param>
/// <returns>Returns -1 if from starting position to end encoding success. Else returns fail position</returns>
internal static int TryEncodeEightBitByte(string content, string encodingName, int startingPosition, int contentLength)
{
if (string.IsNullOrEmpty(content))
{
throw new IndexOutOfRangeException("Input cannot be null or empty.");
}
System.Text.Encoding encoding;
try
{
encoding = System.Text.Encoding.GetEncoding(encodingName);
}
catch (ArgumentException)
{
return startingPosition;
}
char[] currentChar = new char[1];
byte[] bytes;
for (int index = startingPosition; index < contentLength; index++)
{
currentChar[0] = content[index];
bytes = encoding.GetBytes(currentChar);
int length = bytes.Length;
if (currentChar[0] != '?' && length == 1 && bytes[0] == QuestionMarkChar)
{
return index;
}
else if (length > 1)
{
return index;
}
}
for (int index = 0; index < startingPosition; index++)
{
currentChar[0] = content[index];
bytes = encoding.GetBytes(currentChar);
int length = bytes.Length;
if (currentChar[0] != '?' && length == 1 && bytes[0] == QuestionMarkChar)
{
return index;
}
else if (length > 1)
{
return index;
}
}
return -1;
}
}
@@ -0,0 +1,12 @@
namespace Gma.QrCodeNet.Encoding.DataEncodation.InputRecognition;
public struct RecognitionStruct
{
public RecognitionStruct(string encodingName)
: this()
{
EncodingName = encodingName;
}
public string EncodingName { get; private set; }
}
@@ -0,0 +1,60 @@
namespace Gma.QrCodeNet.Encoding.EncodingRegion;
internal static class BCHCalculator
{
/// <summary>
/// Calculate int length by search for Most significant bit
/// </summary>
/// <param name="num">Input Number</param>
/// <returns>Most significant bit</returns>
internal static int PosMSB(int num) => num == 0 ? 0 : BinarySearchPos(num, 0, 32) + 1;
/// <summary>
/// Search for right side bit of Most significant bit
/// </summary>
/// <param name="num">Input number</param>
/// <param name="lowBoundary">Lower boundary. At start should be 0</param>
/// <param name="highBoundary">Higher boundary. At start should be 32</param>
/// <returns>Most significant bit - 1</returns>
private static int BinarySearchPos(int num, int lowBoundary, int highBoundary)
{
int mid = (lowBoundary + highBoundary) / 2;
int shiftResult = num >> mid;
if (shiftResult == 1)
{
return mid;
}
else if (shiftResult < 1)
{
return BinarySearchPos(num, lowBoundary, mid);
}
else
{
return BinarySearchPos(num, mid, highBoundary);
}
}
/// <summary>
/// With input number and polynomial number. Method will calculate BCH value and return
/// </summary>
/// <param name="num">Input number</param>
/// <param name="poly">Polynomial number</param>
/// <returns>BCH value</returns>
internal static int CalculateBCH(int num, int poly)
{
int polyMSB = PosMSB(poly);
// num's length will be old length + new length - 1.
// Once divide poly number. BCH number will be one length short than Poly number's length.
num <<= (polyMSB - 1);
int numMSB = PosMSB(num);
while (PosMSB(num) >= polyMSB)
{
// left shift Poly number to same level as num. Then xor.
// Remove most significant bits of num.
num ^= poly << (numMSB - polyMSB);
numMSB = PosMSB(num);
}
return num;
}
}
@@ -0,0 +1,72 @@
using System;
namespace Gma.QrCodeNet.Encoding.EncodingRegion;
/// <remarks>ISO/IEC 18004:2000 Chapter 8.7.3 Page 46</remarks>
internal static class Codeword
{
internal static void TryEmbedCodewords(this TriStateMatrix tsMatrix, BitList codewords)
{
int sWidth = tsMatrix.Width;
int codewordsSize = codewords.Count;
int bitIndex = 0;
int directionUp = -1;
int x = sWidth - 1;
int y = sWidth - 1;
while (x > 0)
{
// Skip vertical timing pattern
if (x == 6)
{
x -= 1;
}
while (y >= 0 && y < sWidth)
{
for (int xOffset = 0; xOffset < 2; xOffset++)
{
int xPos = x - xOffset;
if (tsMatrix.MStatus(xPos, y) == MatrixStatus.None)
{
bool bit;
if (bitIndex < codewordsSize)
{
bit = codewords[bitIndex];
bitIndex++;
}
else
{
bit = false;
}
tsMatrix[xPos, y, MatrixStatus.Data] = bit;
}
}
y = NextY(y, directionUp);
}
directionUp = ChangeDirection(directionUp);
y = NextY(y, directionUp);
x -= 2;
}
if (bitIndex != codewordsSize)
{
throw new Exception($"Not all bits from {nameof(codewords)} consumed by matrix: {bitIndex} / {codewordsSize}.");
}
}
internal static int NextY(int y, int directionUp)
{
return y + directionUp;
}
internal static int ChangeDirection(int directionUp)
{
return -directionUp;
}
}
@@ -0,0 +1,114 @@
using System;
using Gma.QrCodeNet.Encoding.Masking;
namespace Gma.QrCodeNet.Encoding.EncodingRegion;
/// <summary>
/// 6.9 Format information
/// The Format Information is a 15 bit sequence containing 5 data bits, with 10 error correction bits calculated using the (15, 5) BCH code.
/// </summary>
/// <remarks>ISO/IEC 18004:2000 Chapter 8.9 Page 53</remarks>
internal static class FormatInformation
{
/// <summary>
/// From Appendix C in JISX0510:2004 (p.65).
/// </summary>
private const int FormatInfoPoly = 0x537;
/// <summary>
/// From Appendix C in JISX0510:2004 (p.65).
/// </summary>
private const int FormatInfoMaskPattern = 0x5412;
/// <summary>
/// Embed format information to tristatematrix.
/// Process combination of create info bits, BCH error correction bits calculation, embed towards matrix.
/// </summary>
/// <remarks>ISO/IEC 18004:2000 Chapter 8.9 Page 53</remarks>
internal static void EmbedFormatInformation(this TriStateMatrix triMatrix, ErrorCorrectionLevel errorLevel, Pattern pattern)
{
BitList formatInfo = GetFormatInfoBits(errorLevel, pattern);
int width = triMatrix.Width;
for (int index = 0; index < 15; index++)
{
MatrixPoint point = PointForInfo1(index);
bool bit = formatInfo[index];
triMatrix[point.X, point.Y, MatrixStatus.NoMask] = bit;
if (index < 7)
{
triMatrix[8, width - 1 - index, MatrixStatus.NoMask] = bit;
}
else
{
triMatrix[width - 8 + (index - 7), 8, MatrixStatus.NoMask] = bit;
}
}
}
private static MatrixPoint PointForInfo1(int bitsIndex)
{
if (bitsIndex <= 7)
{
return bitsIndex >= 6
? new MatrixPoint(bitsIndex + 1, 8)
: new MatrixPoint(bitsIndex, 8);
}
else
{
return bitsIndex == 8
? new MatrixPoint(8, 8 - (bitsIndex - 7))
: new MatrixPoint(8, 8 - (bitsIndex - 7) - 1);
}
}
private static BitList GetFormatInfoBits(ErrorCorrectionLevel errorLevel, Pattern pattern)
{
int formatInfo = (int)pattern.MaskPatternType;
// Pattern bits length = 3
formatInfo |= GetErrorCorrectionIndicatorBits(errorLevel) << 3;
int bchCode = BCHCalculator.CalculateBCH(formatInfo, FormatInfoPoly);
// bchCode length = 10
formatInfo = (formatInfo << 10) | bchCode;
// xor maskPattern
formatInfo ^= FormatInfoMaskPattern;
BitList resultBits = new()
{
{ formatInfo, 15 }
};
if (resultBits.Count != 15)
{
throw new Exception("FormatInfoBits length is not 15");
}
else
{
return resultBits;
}
}
/// <summary>
/// According Table 25 — Error correction level indicators
/// Using these bits as enum values would destroy their order which currently corresponds to error correction strength.
/// </summary>
internal static int GetErrorCorrectionIndicatorBits(ErrorCorrectionLevel errorLevel)
{
// L 01
// M 00
// Q 11
// H 10
return errorLevel switch
{
ErrorCorrectionLevel.H => 0x02,
ErrorCorrectionLevel.L => 0x01,
ErrorCorrectionLevel.M => 0x00,
ErrorCorrectionLevel.Q => 0x03,
_ => throw new ArgumentException($"Unsupported error correction level [{errorLevel}]", nameof(errorLevel))
};
}
}
@@ -0,0 +1,70 @@
using System;
namespace Gma.QrCodeNet.Encoding.EncodingRegion;
/// <summary>
/// Embed version information for version larger than or equal to 7.
/// </summary>
/// <remarks>ISO/IEC 18004:2000 Chapter 8.10 Page 54</remarks>
internal static class VersionInformation
{
private const int VIRectangleHeight = 3;
private const int VIRectangleWidth = 6;
private const int LengthDataBits = 6;
private const int LengthECBits = 12;
private const int VersionBCHPoly = 0x1f25;
/// <summary>
/// Embed version information to Matrix
/// Only for version greater than or equal to 7
/// </summary>
internal static void EmbedVersionInformation(this TriStateMatrix tsMatrix, int version)
{
if (version < 7)
{
return;
}
BitList versionInfo = VersionInfoBitList(version);
int matrixWidth = tsMatrix.Width;
// 1 cell between version info and position stencil
int shiftLength = QRCodeConstantVariable.PositionStencilWidth + VIRectangleHeight + 1;
// Reverse order input
int viIndex = LengthDataBits + LengthECBits - 1;
for (int viWidth = 0; viWidth < VIRectangleWidth; viWidth++)
{
for (int viHeight = 0; viHeight < VIRectangleHeight; viHeight++)
{
bool bit = versionInfo[viIndex];
viIndex--;
// Bottom left
tsMatrix[viWidth, (matrixWidth - shiftLength + viHeight), MatrixStatus.NoMask] = bit;
// Top right
tsMatrix[(matrixWidth - shiftLength + viHeight), viWidth, MatrixStatus.NoMask] = bit;
}
}
}
private static BitList VersionInfoBitList(int version)
{
BitList result = new()
{
{ version, LengthDataBits },
{ BCHCalculator.CalculateBCH(version, VersionBCHPoly), LengthECBits }
};
if (result.Count != (LengthECBits + LengthDataBits))
{
throw new Exception("Version Info creation error. Result is not 18 bits");
}
return result;
}
}
@@ -0,0 +1,84 @@
using Gma.QrCodeNet.Encoding.ReedSolomon;
using System;
using System.Collections.Generic;
namespace Gma.QrCodeNet.Encoding.ErrorCorrection;
internal static class ECGenerator
{
internal static BitList FillECCodewords(BitList dataCodewords, VersionDetail vd)
{
List<byte> dataCodewordsByte = dataCodewords.List;
int ecBlockGroup1 = vd.ECBlockGroup1;
int numDataBytesGroup1 = vd.NumDataBytesGroup1;
int numDataBytesGroup2 = vd.NumDataBytesGroup2;
int ecBytesPerBlock = vd.NumECBytesPerBlock;
int dataBytesOffset = 0;
byte[][] dByteJArray = new byte[vd.NumECBlocks][];
byte[][] ecByteJArray = new byte[vd.NumECBlocks][];
GaloisField256 gf256 = GaloisField256.QRCodeGaloisField;
GeneratorPolynomial generator = new(gf256);
for (int blockId = 0; blockId < vd.NumECBlocks; blockId++)
{
if (blockId < ecBlockGroup1)
{
dByteJArray[blockId] = new byte[numDataBytesGroup1];
for (int index = 0; index < numDataBytesGroup1; index++)
{
dByteJArray[blockId][index] = dataCodewordsByte[dataBytesOffset + index];
}
dataBytesOffset += numDataBytesGroup1;
}
else
{
dByteJArray[blockId] = new byte[numDataBytesGroup2];
for (int index = 0; index < numDataBytesGroup2; index++)
{
dByteJArray[blockId][index] = dataCodewordsByte[dataBytesOffset + index];
}
dataBytesOffset += numDataBytesGroup2;
}
ecByteJArray[blockId] = ReedSolomonEncoder.Encode(dByteJArray[blockId], ecBytesPerBlock, generator);
}
if (vd.NumDataBytes != dataBytesOffset)
{
throw new ArgumentException("Data bytes do not match offset");
}
BitList codewords = new();
int maxDataLength = ecBlockGroup1 == vd.NumECBlocks ? numDataBytesGroup1 : numDataBytesGroup2;
for (int dataId = 0; dataId < maxDataLength; dataId++)
{
for (int blockId = 0; blockId < vd.NumECBlocks; blockId++)
{
if (!(dataId == numDataBytesGroup1 && blockId < ecBlockGroup1))
{
codewords.Add(dByteJArray[blockId][dataId], 8);
}
}
}
for (int ecId = 0; ecId < ecBytesPerBlock; ecId++)
{
for (int blockId = 0; blockId < vd.NumECBlocks; blockId++)
{
codewords.Add(ecByteJArray[blockId][ecId], 8);
}
}
if (vd.NumTotalBytes != codewords.Count >> 3)
{
throw new ArgumentException($"Total bytes: {vd.NumTotalBytes}. Actual bits: {codewords.Count}");
}
return codewords;
}
}
@@ -0,0 +1,9 @@
namespace Gma.QrCodeNet.Encoding;
public enum ErrorCorrectionLevel
{
L,
M,
Q,
H
}
@@ -0,0 +1,17 @@
using System;
namespace Gma.QrCodeNet.Encoding;
/// <summary>
/// Use this exception for null or empty input string or when input string is too large.
/// </summary>
public class InputOutOfBoundaryException : Exception
{
public InputOutOfBoundaryException() : base()
{
}
public InputOutOfBoundaryException(string message) : base(message)
{
}
}
@@ -0,0 +1,13 @@
namespace Gma.QrCodeNet.Encoding.Masking;
public enum MaskPatternType
{
Type0 = 0,
Type1 = 1,
Type2 = 2,
Type3 = 3,
Type4 = 4,
Type5 = 5,
Type6 = 6,
Type7 = 7
}
@@ -0,0 +1,44 @@
using System;
using Gma.QrCodeNet.Encoding.EncodingRegion;
namespace Gma.QrCodeNet.Encoding.Masking;
public static class MatrixExtensions
{
public static TriStateMatrix Xor(this TriStateMatrix first, Pattern second, ErrorCorrectionLevel errorLevel)
{
TriStateMatrix result = XorMatrix(first, second);
result.EmbedFormatInformation(errorLevel, second);
return result;
}
private static TriStateMatrix XorMatrix(TriStateMatrix first, BitMatrix second)
{
int width = first.Width;
TriStateMatrix maskedMatrix = new(width);
for (int x = 0; x < width; x++)
{
for (int y = 0; y < width; y++)
{
MatrixStatus states = first.MStatus(x, y);
switch (states)
{
case MatrixStatus.NoMask:
maskedMatrix[x, y, MatrixStatus.NoMask] = first[x, y];
break;
case MatrixStatus.Data:
maskedMatrix[x, y, MatrixStatus.Data] = first[x, y] ^ second[x, y];
break;
default:
throw new ArgumentException($"{nameof(TriStateMatrix)} has None value cell.", nameof(first));
}
}
}
return maskedMatrix;
}
public static TriStateMatrix Apply(this TriStateMatrix matrix, Pattern pattern, ErrorCorrectionLevel errorLevel) => matrix.Xor(pattern, errorLevel);
}
@@ -0,0 +1,13 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
public abstract class Pattern : BitMatrix
{
public override int Width => throw new NotSupportedException();
public override int Height => throw new NotSupportedException();
public override bool[,] InternalArray => throw new NotImplementedException();
public abstract MaskPatternType MaskPatternType { get; }
}
@@ -0,0 +1,14 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern0 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type0;
public override bool this[int i, int j]
{
get => (j + i) % 2 == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,14 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern1 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type1;
public override bool this[int i, int j]
{
get => j % 2 == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,14 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern2 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type2;
public override bool this[int i, int j]
{
get => i % 3 == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,14 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern3 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type3;
public override bool this[int i, int j]
{
get => (j + i) % 3 == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,14 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern4 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type4;
public override bool this[int i, int j]
{
get => ((j / 2) + (i / 3)) % 2 == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,14 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern5 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type5;
public override bool this[int i, int j]
{
get => (((i * j) % 2) + ((i * j) % 3)) == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,13 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern6 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type6;
public override bool this[int i, int j]
{
get => ((((i * j) % 2) + ((i * j) % 3)) % 2) == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,14 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class Pattern7 : Pattern
{
public override MaskPatternType MaskPatternType => MaskPatternType.Type7;
public override bool this[int i, int j]
{
get => (((i * j) % 3) + (((i + j) % 2) % 2)) == 0;
set => throw new NotSupportedException();
}
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace Gma.QrCodeNet.Encoding.Masking;
internal class PatternFactory
{
internal Pattern CreateByType(MaskPatternType maskPatternType)
{
return maskPatternType switch
{
MaskPatternType.Type0 => new Pattern0(),
MaskPatternType.Type1 => new Pattern1(),
MaskPatternType.Type2 => new Pattern2(),
MaskPatternType.Type3 => new Pattern3(),
MaskPatternType.Type4 => new Pattern4(),
MaskPatternType.Type5 => new Pattern5(),
MaskPatternType.Type6 => new Pattern6(),
MaskPatternType.Type7 => new Pattern7(),
_ => throw new NotSupportedException("This should never happen.")
};
}
internal IEnumerable<Pattern> AllPatterns()
{
foreach (MaskPatternType patternType in Enum.GetValues(typeof(MaskPatternType)))
{
yield return CreateByType(patternType);
}
}
}
@@ -0,0 +1,36 @@
using System.Linq;
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
internal static class MatrixScoreCalculator
{
internal static BitMatrix GetLowestPenaltyMatrix(this TriStateMatrix matrix, ErrorCorrectionLevel errorLevel)
{
PatternFactory patternFactory = new();
int score = int.MaxValue;
int tempScore;
TriStateMatrix result = new(matrix.Width);
TriStateMatrix triMatrix;
foreach (Pattern pattern in patternFactory.AllPatterns())
{
triMatrix = matrix.Apply(pattern, errorLevel);
tempScore = triMatrix.PenaltyScore();
if (tempScore < score)
{
score = tempScore;
result = triMatrix;
}
}
return result;
}
internal static int PenaltyScore(this BitMatrix matrix)
{
PenaltyFactory penaltyFactory = new();
return
penaltyFactory
.AllRules()
.Sum(penalty => penalty.PenaltyCalculate(matrix));
}
}
@@ -0,0 +1,6 @@
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
public abstract class Penalty
{
internal abstract int PenaltyCalculate(BitMatrix matrix);
}
@@ -0,0 +1,85 @@
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
/// <summary>
/// ISO/IEC 18004:2000 Chapter 8.8.2 Page 52
/// </summary>
internal class Penalty1 : Penalty
{
/// <summary>
/// Calculate penalty value for first rule.
/// </summary>
internal override int PenaltyCalculate(BitMatrix matrix)
{
int penaltyValue = PenaltyCalculation(matrix, true) + PenaltyCalculation(matrix, false);
return penaltyValue;
}
private int PenaltyCalculation(BitMatrix matrix, bool isHorizontal)
{
int penalty = 0;
int width = matrix.Width;
int i = 0;
int j = 0;
while (i < width)
{
while (j < width - 4)
{
bool preBit = isHorizontal
? matrix[j + 4, i]
: matrix[i, j + 4];
int numSameBitCell = 1;
for (int x = 1; x <= 4; x++)
{
bool bit = isHorizontal
? matrix[j + 4 - x, i]
: matrix[i, j + 4 - x];
if (bit == preBit)
{
numSameBitCell++;
}
else
{
break;
}
}
if (numSameBitCell == 1)
{
j += 4;
}
else
{
int x = 5;
while ((j + x) < width)
{
bool bit = isHorizontal
? matrix[j + x, i]
: matrix[i, j + x];
if (bit == preBit)
{
numSameBitCell++;
}
else
{
break;
}
x++;
}
if (numSameBitCell >= 5)
{
penalty += (3 + (numSameBitCell - 5));
}
j += x;
}
}
j = 0;
i++;
}
return penalty;
}
}
@@ -0,0 +1,51 @@
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
/// <summary>
/// ISO/IEC 18004:2000 Chapter 8.8.2 Page 52
/// </summary>
internal class Penalty2 : Penalty
{
internal override int PenaltyCalculate(BitMatrix matrix)
{
int width = matrix.Width;
int x = 0;
int y = 0;
int penalty = 0;
while (y < (width - 1))
{
while (x < (width - 1))
{
bool topR = matrix[x + 1, y];
if (topR == matrix[x + 1, y + 1]) // Bottom Right
{
if (topR == matrix[x, y + 1]) // Bottom Left
{
if (topR == matrix[x, y]) // Top Left
{
penalty += 3;
x += 1;
}
else
{
x += 1;
}
}
else
{
x += 1;
}
}
else
{
x += 2;
}
}
x = 0;
y++;
}
return penalty;
}
}
@@ -0,0 +1,141 @@
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
/// <summary>
/// ISO/IEC 18004:2000 Chapter 8.8.2 Page 52
/// </summary>
internal class Penalty3 : Penalty
{
/// <summary>
/// Calculate penalty value for Third rule.
/// </summary>
internal override int PenaltyCalculate(BitMatrix matrix) => PenaltyCalculation(matrix, true) + PenaltyCalculation(matrix, false);
private int PenaltyCalculation(BitMatrix matrix, bool isHorizontal)
{
int i = 0;
int j = 1;
int penalty = 0;
int width = matrix.Width;
bool bit;
while (i < width)
{
while (j < width - 5)
{
bit = isHorizontal
? matrix[j + 4, i]
: matrix[i, j + 4];
if (!bit)
{
bit = isHorizontal
? matrix[j, i]
: matrix[i, j];
if (!bit)
{
penalty += PatternCheck(matrix, i, j, isHorizontal);
j += 4;
}
else
{
j += 4;
}
}
else
{
for (int num = 4; num > 0; num--)
{
bit = isHorizontal
? matrix[j + num, i]
: matrix[i, j + num];
if (!bit)
{
j += num;
break;
}
if (num == 1)
{
j += 5;
}
}
}
}
j = 0;
i++;
}
return penalty;
}
private int PatternCheck(BitMatrix matrix, int i, int j, bool isHorizontal)
{
bool bit;
for (int num = 3; num >= 1; num--)
{
bit = isHorizontal
? matrix[j + num, i]
: matrix[i, j + num];
if (!bit)
{
return 0;
}
}
// Check for left side and right side x ( xoxxxox ).
if ((j - 1) < 0 || (j + 1) >= matrix.Width)
{
return 0;
}
bit = isHorizontal
? matrix[j + 5, i]
: matrix[i, j + 5];
if (!bit)
{
return 0;
}
bit = isHorizontal
? matrix[j - 1, i]
: matrix[i, j - 1];
if (!bit)
{
return 0;
}
if ((j - 5) >= 0)
{
for (int num = -2; num >= -5; num--)
{
bit = isHorizontal
? matrix[j + num, i]
: matrix[i, j + num];
if (bit)
{
break;
}
if (num == -5)
{
return 40;
}
}
}
if ((j + 9) < matrix.Width)
{
for (int num = 6; num <= 9; num++)
{
bit = isHorizontal
? matrix[j + num, i]
: matrix[i, j + num];
if (bit)
{
return 0;
}
}
return 40;
}
else
{
return 0;
}
}
}
@@ -0,0 +1,36 @@
using System;
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
/// <summary>
/// ISO/IEC 18004:2000 Chapter 8.8.2 Page 52
/// </summary>
internal class Penalty4 : Penalty
{
/// <summary>
/// Calculate penalty value for Fourth rule.
/// Perform O(n) search for available x modules
/// </summary>
internal override int PenaltyCalculate(BitMatrix matrix)
{
int width = matrix.Width;
int darkBitCount = 0;
for (int j = 0; j < width; j++)
{
for (int i = 0; i < width; i++)
{
if (matrix[i, j])
{
darkBitCount++;
}
}
}
int matrixCount = width * width;
double ratio = (double)darkBitCount / matrixCount;
return Math.Abs((int)((ratio * 100) - 50)) / 5 * 10;
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
/// <summary>
/// Description of PenaltyFactory.
/// </summary>
internal class PenaltyFactory
{
internal Penalty CreateByRule(PenaltyRules penaltyRule)
{
return penaltyRule switch
{
PenaltyRules.Rule01 => new Penalty1(),
PenaltyRules.Rule02 => new Penalty2(),
PenaltyRules.Rule03 => new Penalty3(),
PenaltyRules.Rule04 => new Penalty4(),
_ => throw new ArgumentException($"Unsupport penalty rule: {penaltyRule}", nameof(penaltyRule))
};
}
internal IEnumerable<Penalty> AllRules()
{
foreach (PenaltyRules penaltyRule in Enum.GetValues(typeof(PenaltyRules)))
{
yield return CreateByRule(penaltyRule);
}
}
}
@@ -0,0 +1,9 @@
namespace Gma.QrCodeNet.Encoding.Masking.Scoring;
public enum PenaltyRules
{
Rule01 = 1,
Rule02 = 2,
Rule03 = 3,
Rule04 = 4
}
@@ -0,0 +1,20 @@
namespace Gma.QrCodeNet.Encoding;
public struct MatrixPoint
{
internal MatrixPoint(int x, int y)
: this()
{
X = x;
Y = y;
}
public int X { get; private set; }
public int Y { get; private set; }
public MatrixPoint Offset(MatrixPoint offset) => new(offset.X + X, offset.Y + Y);
internal MatrixPoint Offset(int offsetX, int offsetY) => Offset(new MatrixPoint(offsetX, offsetY));
public override string ToString() => $"Point({X};{Y})";
}
@@ -0,0 +1,32 @@
using System.Collections;
using System.Collections.Generic;
namespace Gma.QrCodeNet.Encoding;
internal struct MatrixRectangle : IEnumerable<MatrixPoint>
{
internal MatrixRectangle(MatrixPoint location, MatrixSize size) :
this()
{
Location = location;
Size = size;
}
public MatrixPoint Location { get; private set; }
public MatrixSize Size { get; private set; }
public IEnumerator<MatrixPoint> GetEnumerator()
{
for (int j = Location.Y; j < Location.Y + Size.Height; j++)
{
for (int i = Location.X; i < Location.X + Size.Width; i++)
{
yield return new MatrixPoint(i, j);
}
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override string ToString() => $"Rectangle({Location.X};{Location.Y}):({Size.Width} x {Size.Height})";
}
@@ -0,0 +1,19 @@
namespace Gma.QrCodeNet.Encoding;
public struct MatrixSize
{
internal MatrixSize(int width, int height)
: this()
{
Width = width;
Height = height;
}
public int Width { get; private set; }
public int Height { get; private set; }
public override string ToString()
{
return $"Size({Width};{Height})";
}
}
@@ -0,0 +1,8 @@
namespace Gma.QrCodeNet.Encoding;
public enum MatrixStatus
{
None,
NoMask,
Data
}
@@ -0,0 +1,14 @@
using Gma.QrCodeNet.Encoding.Positioning.Stencils;
namespace Gma.QrCodeNet.Encoding.Positioning;
internal static class PositioningPatternBuilder
{
internal static void EmbedBasicPatterns(int version, TriStateMatrix matrix)
{
new PositionDetectionPattern(version).ApplyTo(matrix);
new DarkDotAtLeftBottom(version).ApplyTo(matrix);
new AlignmentPattern(version).ApplyTo(matrix);
new TimingPattern(version).ApplyTo(matrix);
}
}
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Gma.QrCodeNet.Encoding.Positioning.Stencils;
internal class AlignmentPattern : PatternStencilBase
{
public AlignmentPattern(int version)
: base(version)
{
}
private static bool[,] AlignmentPatternArray { get; } =
new[,]
{
{ X, X, X, X, X },
{ X, O, O, O, X },
{ X, O, X, O, X },
{ X, O, O, O, X },
{ X, X, X, X, X }
};
public override bool[,] Stencil => AlignmentPatternArray;
// Table E.1 — Row/column coordinates of center module of Alignment Patterns
private static byte[][] AlignmentPatternCoordinatesByVersion { get; } =
new[]
{
Array.Empty<byte>(),
Array.Empty<byte>(),
new byte[] { 6, 18 },
new byte[] { 6, 22 },
new byte[] { 6, 26 },
new byte[] { 6, 30 },
new byte[] { 6, 34 },
new byte[] { 6, 22, 38 },
new byte[] { 6, 24, 42 },
new byte[] { 6, 26, 46 },
new byte[] { 6, 28, 50 },
new byte[] { 6, 30, 54 },
new byte[] { 6, 32, 58 },
new byte[] { 6, 34, 62 },
new byte[] { 6, 26, 46, 66 },
new byte[] { 6, 26, 48, 70 },
new byte[] { 6, 26, 50, 74 },
new byte[] { 6, 30, 54, 78 },
new byte[] { 6, 30, 56, 82 },
new byte[] { 6, 30, 58, 86 },
new byte[] { 6, 34, 62, 90 },
new byte[] { 6, 28, 50, 72, 94 },
new byte[] { 6, 26, 50, 74, 98 },
new byte[] { 6, 30, 54, 78, 102 },
new byte[] { 6, 28, 54, 80, 106 },
new byte[] { 6, 32, 58, 84, 110 },
new byte[] { 6, 30, 58, 86, 114 },
new byte[] { 6, 34, 62, 90, 118 },
new byte[] { 6, 26, 50, 74, 98, 122 },
new byte[] { 6, 30, 54, 78, 102, 126 },
new byte[] { 6, 26, 52, 78, 104, 130 },
new byte[] { 6, 30, 56, 82, 108, 134 },
new byte[] { 6, 34, 60, 86, 112, 138 },
new byte[] { 6, 30, 58, 86, 114, 142 },
new byte[] { 6, 34, 62, 90, 118, 146 },
new byte[] { 6, 30, 54, 78, 102, 126, 150 },
new byte[] { 6, 24, 50, 76, 102, 128, 154 },
new byte[] { 6, 28, 54, 80, 106, 132, 158 },
new byte[] { 6, 32, 58, 84, 110, 136, 162 },
new byte[] { 6, 26, 54, 82, 110, 138, 166 },
new byte[] { 6, 30, 58, 86, 114, 142, 170 }
};
public override void ApplyTo(TriStateMatrix matrix)
{
foreach (MatrixPoint coordinatePair in GetNonColidingCoordinatePairs(matrix))
{
CopyTo(matrix, coordinatePair, MatrixStatus.NoMask);
}
}
public IEnumerable<MatrixPoint> GetNonColidingCoordinatePairs(TriStateMatrix matrix)
{
return
GetAllCoordinatePairs()
.Where(point => matrix.MStatus(point.Offset(2, 2)) == MatrixStatus.None);
}
private IEnumerable<MatrixPoint> GetAllCoordinatePairs()
{
IEnumerable<byte> coordinates = GetPatternCoordinatesByVersion(Version);
foreach (byte centerX in coordinates)
{
foreach (byte centerY in coordinates)
{
MatrixPoint location = new(centerX - 2, centerY - 2);
yield return location;
}
}
}
private static IEnumerable<byte> GetPatternCoordinatesByVersion(int version)
{
return AlignmentPatternCoordinatesByVersion[version];
}
}
@@ -0,0 +1,17 @@
using System;
namespace Gma.QrCodeNet.Encoding.Positioning.Stencils;
internal class DarkDotAtLeftBottom : PatternStencilBase
{
public DarkDotAtLeftBottom(int version) : base(version)
{
}
public override bool[,] Stencil => throw new NotImplementedException();
public override void ApplyTo(TriStateMatrix matrix)
{
matrix[8, matrix.Width - 8, MatrixStatus.NoMask] = true;
}
}
@@ -0,0 +1,32 @@
using System;
namespace Gma.QrCodeNet.Encoding.Positioning.Stencils;
internal abstract class PatternStencilBase : BitMatrix
{
protected const bool O = false;
protected const bool X = true;
internal PatternStencilBase(int version)
{
Version = version;
}
public int Version { get; private set; }
public abstract bool[,] Stencil { get; }
public override int Width => Stencil.GetLength(0);
public override int Height => Stencil.GetLength(1);
public override bool[,] InternalArray => throw new NotImplementedException();
public override bool this[int i, int j]
{
get => Stencil[i, j];
set => throw new NotSupportedException();
}
public abstract void ApplyTo(TriStateMatrix matrix);
}
@@ -0,0 +1,41 @@
namespace Gma.QrCodeNet.Encoding.Positioning.Stencils;
internal class PositionDetectionPattern : PatternStencilBase
{
public PositionDetectionPattern(int version)
: base(version)
{
}
private static bool[,] PositionDetection { get; } =
new[,]
{
{ O, O, O, O, O, O, O, O, O },
{ O, X, X, X, X, X, X, X, O },
{ O, X, O, O, O, O, O, X, O },
{ O, X, O, X, X, X, O, X, O },
{ O, X, O, X, X, X, O, X, O },
{ O, X, O, X, X, X, O, X, O },
{ O, X, O, O, O, O, O, X, O },
{ O, X, X, X, X, X, X, X, O },
{ O, O, O, O, O, O, O, O, O }
};
public override bool[,] Stencil => PositionDetection;
public override void ApplyTo(TriStateMatrix matrix)
{
MatrixSize size = GetSizeOfSquareWithSeparators();
MatrixPoint leftTopCorner = new(0, 0);
CopyTo(matrix, new MatrixRectangle(new MatrixPoint(1, 1), size), leftTopCorner, MatrixStatus.NoMask);
MatrixPoint rightTopCorner = new(matrix.Width - Width + 1, 0);
CopyTo(matrix, new MatrixRectangle(new MatrixPoint(0, 1), size), rightTopCorner, MatrixStatus.NoMask);
MatrixPoint leftBottomCorner = new(0, matrix.Width - Width + 1);
CopyTo(matrix, new MatrixRectangle(new MatrixPoint(1, 0), size), leftBottomCorner, MatrixStatus.NoMask);
}
private MatrixSize GetSizeOfSquareWithSeparators() => new(Width - 1, Height - 1);
}
@@ -0,0 +1,35 @@
using System;
namespace Gma.QrCodeNet.Encoding.Positioning.Stencils;
internal class TimingPattern : PatternStencilBase
{
public TimingPattern(int version)
: base(version)
{
}
public override bool[,] Stencil => throw new NotImplementedException();
public override void ApplyTo(TriStateMatrix matrix)
{
// -8 is for skipping position detection patterns (size 7), and two horizontal/vertical
// separation patterns (size 1). Thus, 8 = 7 + 1.
for (int i = 8; i < matrix.Width - 8; ++i)
{
bool value = (sbyte)((i + 1) % 2) == 1;
// Horizontal line.
if (matrix.MStatus(6, i) == MatrixStatus.None)
{
matrix[6, i, MatrixStatus.NoMask] = value;
}
// Vertical line.
if (matrix.MStatus(i, 6) == MatrixStatus.None)
{
matrix[i, 6, MatrixStatus.NoMask] = value;
}
}
}
}
@@ -0,0 +1,52 @@
namespace Gma.QrCodeNet.Encoding;
/// <summary>
/// Contain most of common constant variables. S
/// </summary>
public static class QRCodeConstantVariable
{
public const int MinVersion = 1;
public const int MaxVersion = 40;
public const string DefaultEncoding = "iso-8859-1";
public const string UTF8Encoding = "utf-8";
/// <summary>
/// ISO/IEC 18004:2006(E) Page 45 Chapter Generating the error correction codewords
/// Primative Polynomial = Bin 100011101 = Dec 285
/// </summary>
public const int QRCodePrimitive = 285;
internal const int TerminatorNPaddingBit = 0;
internal const int TerminatorLength = 4;
/// <summary>
/// 0xEC
/// </summary>
internal const int PadeCodewordsOdd = 0xec;
/// <summary>
/// 0x11
/// </summary>
internal const int PadeCodewordsEven = 0x11;
internal const int PositionStencilWidth = 7;
internal static bool[] PadeOdd = new bool[]
{
true, true, true, false,
true, true, false, false
};
internal static bool[] PadeEven = new bool[]
{
false, false, false, true,
false, false, false, true
};
/// <summary>
/// URL:http://en.wikipedia.org/wiki/Byte-order_mark
/// </summary>
public static byte[] UTF8ByteOrderMark => new byte[] { 0xEF, 0xBB, 0xBF };
}
@@ -0,0 +1,32 @@
using Gma.QrCodeNet.Encoding.DataEncodation;
using Gma.QrCodeNet.Encoding.EncodingRegion;
using Gma.QrCodeNet.Encoding.ErrorCorrection;
using Gma.QrCodeNet.Encoding.Masking;
using Gma.QrCodeNet.Encoding.Masking.Scoring;
using Gma.QrCodeNet.Encoding.Positioning;
namespace Gma.QrCodeNet.Encoding;
internal static class QRCodeEncode
{
internal static BitMatrix Encode(string content, ErrorCorrectionLevel errorLevel)
{
EncodationStruct encodeStruct = DataEncode.Encode(content, errorLevel);
return ProcessEncodationResult(encodeStruct, errorLevel);
}
private static BitMatrix ProcessEncodationResult(EncodationStruct encodeStruct, ErrorCorrectionLevel errorLevel)
{
BitList codewords = ECGenerator.FillECCodewords(encodeStruct.DataCodewords, encodeStruct.VersionDetail);
TriStateMatrix triMatrix = new(encodeStruct.VersionDetail.MatrixWidth);
PositioningPatternBuilder.EmbedBasicPatterns(encodeStruct.VersionDetail.Version, triMatrix);
triMatrix.EmbedVersionInformation(encodeStruct.VersionDetail.Version);
triMatrix.EmbedFormatInformation(errorLevel, new Pattern0());
triMatrix.TryEmbedCodewords(codewords);
return triMatrix.GetLowestPenaltyMatrix(errorLevel);
}
}
@@ -0,0 +1,28 @@
namespace Gma.QrCodeNet.Encoding;
/// <summary>
/// This class contain two variables.
/// BitMatrix for QrCode
/// isContainMatrix for indicate whether QrCode contains BitMatrix or not.
/// BitMatrix will be equal to null if isContainMatrix is false.
/// </summary>
public class QrCode
{
internal QrCode(BitMatrix matrix)
{
Matrix = matrix;
IsContainMatrix = true;
}
public bool IsContainMatrix
{
get;
private set;
}
public BitMatrix Matrix
{
get;
private set;
}
}
@@ -0,0 +1,39 @@
namespace Gma.QrCodeNet.Encoding;
public class QrEncoder
{
/// <summary>
/// Default QrEncoder will set ErrorCorrectionLevel as M
/// </summary>
public QrEncoder()
: this(ErrorCorrectionLevel.M)
{
}
/// <summary>
/// QrEncoder with parameter ErrorCorrectionLevel.
/// </summary>
public QrEncoder(ErrorCorrectionLevel errorCorrectionLevel)
{
ErrorCorrectionLevel = errorCorrectionLevel;
}
public ErrorCorrectionLevel ErrorCorrectionLevel { get; set; }
/// <summary>
/// Encode string content to QrCode matrix
/// </summary>
/// <exception cref="InputOutOfBoundaryException">
/// This exception for string content is null, empty or too large</exception>
public QrCode Encode(string content)
{
if (string.IsNullOrEmpty(content))
{
throw new InputOutOfBoundaryException("Input cannot be null or empty.");
}
else
{
return new QrCode(QRCodeEncode.Encode(content, ErrorCorrectionLevel));
}
}
}
@@ -0,0 +1,122 @@
using System;
namespace Gma.QrCodeNet.Encoding.ReedSolomon;
/// <summary>
/// Description of GaloisField256.
/// </summary>
internal sealed class GaloisField256
{
internal GaloisField256(int primitive)
{
AntiLogTable = new int[256];
LogTable = new int[256];
Primitive = primitive;
int gfx = 1;
// Power cycle is from 0 to 254. 2^255 = 1 = 2^0
// Value cycle is from 1 to 255. Thus there should not have Log(0).
for (int powers = 0; powers < 256; powers++)
{
AntiLogTable[powers] = gfx;
if (powers != 255)
{
LogTable[gfx] = powers;
}
gfx <<= 1; // gfx = gfx * 2 where alpha is 2.
if (gfx > 255)
{
gfx ^= primitive;
}
}
}
private int[] AntiLogTable { get; }
private int[] LogTable { get; }
internal int Primitive { get; }
internal static GaloisField256 QRCodeGaloisField => new(QRCodeConstantVariable.QRCodePrimitive);
/// <returns>
/// Powers of a in GF table. Where a = 2
/// </returns>
internal int Exponent(int powersOfa) => AntiLogTable[powersOfa];
/// <returns>
/// Log (power of a) in GF table. Where a = 2
/// </returns>
internal int Log(int gfValue)
{
if (gfValue == 0)
{
throw new ArgumentException("GaloisField value will not be equal to 0, Log method.");
}
return LogTable[gfValue];
}
internal int Inverse(int gfValue)
{
if (gfValue == 0)
{
throw new ArgumentException("GaloisField value will not be equal to 0, Inverse method.");
}
return Exponent(255 - Log(gfValue));
}
internal int Addition(int gfValueA, int gfValueB) => gfValueA ^ gfValueB;
internal int Subtraction(int gfValueA, int gfValueB) => Addition(gfValueA, gfValueB); // Subtraction is same as addition.
/// <returns>
/// Product of two values.
/// In other words. a multiply b
/// </returns>
internal int Product(int gfValueA, int gfValueB)
{
if (gfValueA == 0 || gfValueB == 0)
{
return 0;
}
if (gfValueA == 1)
{
return gfValueB;
}
if (gfValueB == 1)
{
return gfValueA;
}
return Exponent((Log(gfValueA) + Log(gfValueB)) % 255);
}
/// <returns>
/// Quotient of two values.
/// In other words. a divided b
/// </returns>
internal int Quotient(int gfValueA, int gfValueB)
{
if (gfValueA == 0)
{
return 0;
}
if (gfValueB == 0)
{
throw new ArgumentException($"{nameof(gfValueB)} cannot be zero.");
}
if (gfValueB == 1)
{
return gfValueA;
}
return Exponent(Math.Abs(Log(gfValueA) - Log(gfValueB)) % 255);
}
}
@@ -0,0 +1,62 @@
using System.Collections.Generic;
namespace Gma.QrCodeNet.Encoding.ReedSolomon;
/// <summary>
/// Description of GeneratorPolynomial.
/// </summary>
internal sealed class GeneratorPolynomial
{
/// <summary>
/// After create GeneratorPolynomial. Keep it as long as possible.
/// Unless QRCode encode is done or no more QRCode need to generate.
/// </summary>
internal GeneratorPolynomial(GaloisField256 gfield)
{
Gfield = gfield;
CacheGenerator = new List<Polynomial>(10)
{
new Polynomial(Gfield, new int[] { 1 })
};
}
private GaloisField256 Gfield { get; }
private List<Polynomial> CacheGenerator { get; }
/// <summary>
/// Get generator by degree. (Largest degree for that generator)
/// </summary>
/// <returns>Generator</returns>
internal Polynomial GetGenerator(int degree)
{
if (degree >= CacheGenerator.Count)
{
BuildGenerator(degree);
}
return CacheGenerator[degree];
}
/// <summary>
/// Build Generator if we cannot find specific degree of generator from cache
/// </summary>
private void BuildGenerator(int degree)
{
lock (CacheGenerator)
{
int currentCacheLength = CacheGenerator.Count;
if (degree >= currentCacheLength)
{
Polynomial lastGenerator = CacheGenerator[currentCacheLength - 1];
for (int d = currentCacheLength; d <= degree; d++)
{
Polynomial nextGenerator = lastGenerator.Multiply(new Polynomial(Gfield, new int[] { 1, Gfield.Exponent(d - 1) }));
CacheGenerator.Add(nextGenerator);
lastGenerator = nextGenerator;
}
}
}
}
}
@@ -0,0 +1,15 @@
namespace Gma.QrCodeNet.Encoding.ReedSolomon;
internal struct PolyDivideStruct
{
internal PolyDivideStruct(Polynomial quotient, Polynomial remainder)
: this()
{
Quotient = quotient;
Remainder = remainder;
}
internal Polynomial Quotient { get; private set; }
internal Polynomial Remainder { get; private set; }
}
@@ -0,0 +1,242 @@
using System;
namespace Gma.QrCodeNet.Encoding.ReedSolomon;
internal sealed class Polynomial
{
internal Polynomial(GaloisField256 gfield, int[] coefficients)
{
int coefficientsLength = coefficients.Length;
if (coefficientsLength == 0 || coefficients is null)
{
throw new ArithmeticException($"Cannot create empty {nameof(Polynomial)}.");
}
GField = gfield;
Primitive = gfield.Primitive;
if (coefficientsLength > 1 && coefficients[0] == 0)
{
int firstNonZeroIndex = 1;
while (firstNonZeroIndex < coefficientsLength && coefficients[firstNonZeroIndex] == 0)
{
firstNonZeroIndex++;
}
if (firstNonZeroIndex == coefficientsLength)
{
Coefficients = new int[] { 0 };
}
else
{
int newLength = coefficientsLength - firstNonZeroIndex;
Coefficients = new int[newLength];
Array.Copy(coefficients, firstNonZeroIndex, Coefficients, 0, newLength);
}
}
else
{
Coefficients = new int[coefficientsLength];
Array.Copy(coefficients, Coefficients, coefficientsLength);
}
}
internal int[] Coefficients { get; }
internal GaloisField256 GField { get; }
internal int Degree => Coefficients.Length - 1;
internal int Primitive { get; }
internal bool IsMonomialZero => Coefficients[0] == 0;
/// <returns>
/// Coefficient position. where (coefficient)x^degree
/// </returns>
internal int GetCoefficient(int degree)
{
// Eg: x^2 + x + 1. degree 1, reverse position = degree + 1 = 2.
// Pos = 3 - 2 = 1
return Coefficients[^(degree + 1)];
}
/// <summary>
/// Add another Polynomial to current one
/// </summary>
/// <param name="other">The polynomial need to add or subtract to current one</param>
/// <returns>Result polynomial after add or subtract</returns>
internal Polynomial AddOrSubtract(Polynomial other)
{
if (Primitive != other.Primitive)
{
throw new ArgumentException($"{nameof(Polynomial)} cannot perform {nameof(AddOrSubtract)} as they do not have the same {nameof(Primitive)}" +
$" for {nameof(GaloisField256)}.");
}
if (IsMonomialZero)
{
return other;
}
else if (other.IsMonomialZero)
{
return this;
}
int otherLength = other.Coefficients.Length;
int thisLength = Coefficients.Length;
if (otherLength > thisLength)
{
return CoefficientXor(Coefficients, other.Coefficients);
}
else
{
return CoefficientXor(other.Coefficients, Coefficients);
}
}
internal Polynomial CoefficientXor(int[] smallerCoefficients, int[] largerCoefficients)
{
if (smallerCoefficients.Length > largerCoefficients.Length)
{
throw new ArgumentException($"Cannot perform {nameof(CoefficientXor)} method as smaller {nameof(Coefficients)} length is greater than the larger one.");
}
int targetLength = largerCoefficients.Length;
int[] xorCoefficient = new int[targetLength];
int lengthDiff = largerCoefficients.Length - smallerCoefficients.Length;
Array.Copy(largerCoefficients, 0, xorCoefficient, 0, lengthDiff);
for (int index = lengthDiff; index < targetLength; index++)
{
xorCoefficient[index] = GField.Addition(largerCoefficients[index], smallerCoefficients[index - lengthDiff]);
}
return new Polynomial(GField, xorCoefficient);
}
/// <summary>
/// Multiply current Polynomial to another one.
/// </summary>
/// <returns>Result polynomial after multiply</returns>
internal Polynomial Multiply(Polynomial other)
{
if (Primitive != other.Primitive)
{
throw new ArgumentException($"{nameof(Polynomial)} cannot perform {nameof(Multiply)} as they do not have the same {nameof(Primitive)}" +
$" for {nameof(GaloisField256)}.");
}
if (IsMonomialZero || other.IsMonomialZero)
{
return new Polynomial(GField, new int[] { 0 });
}
int[] aCoefficients = Coefficients;
int aLength = aCoefficients.Length;
int[] bCoefficient = other.Coefficients;
int bLength = bCoefficient.Length;
int[] rCoefficients = new int[aLength + bLength - 1];
for (int aIndex = 0; aIndex < aLength; aIndex++)
{
int aCoeff = aCoefficients[aIndex];
for (int bIndex = 0; bIndex < bLength; bIndex++)
{
rCoefficients[aIndex + bIndex] =
GField.Addition(rCoefficients[aIndex + bIndex], GField.Product(aCoeff, bCoefficient[bIndex]));
}
}
return new Polynomial(GField, rCoefficients);
}
/// <summary>
/// Multiplay scalar to current polynomial
/// </summary>
/// <returns>Result of polynomial after multiply scalar</returns>
internal Polynomial MultiplyScalar(int scalar)
{
if (scalar == 0)
{
return new Polynomial(GField, new int[] { 0 });
}
else if (scalar == 1)
{
return this;
}
int length = Coefficients.Length;
int[] rCoefficient = new int[length];
for (int index = 0; index < length; index++)
{
rCoefficient[index] = GField.Product(Coefficients[index], scalar);
}
return new Polynomial(GField, rCoefficient);
}
/// <summary>
/// Divide current polynomial by "other"
/// </summary>
/// <returns>Result polynomial after divide</returns>
internal PolyDivideStruct Divide(Polynomial other)
{
if (Primitive != other.Primitive)
{
throw new ArgumentException($"{nameof(Polynomial)} cannot perform {nameof(Divide)} as they do not have the same {nameof(Primitive)}" +
$" for {nameof(GaloisField256)}.");
}
if (other.IsMonomialZero)
{
throw new ArgumentException($"Cannot divide by {nameof(Polynomial)} Zero.");
}
// This divide by other = a divide by b
int aLength = Coefficients.Length;
// We will make change to aCoefficient. It will return as remainder
int[] aCoefficients = new int[aLength];
Array.Copy(Coefficients, 0, aCoefficients, 0, aLength);
int bLength = other.Coefficients.Length;
if (aLength < bLength)
{
return new PolyDivideStruct(new Polynomial(GField, new int[] { 0 }), this);
}
else
{
// Quotient coefficients
// qLastIndex = alength - blength qlength = qLastIndex + 1
int[] qCoefficients = new int[(aLength - bLength) + 1];
// Denominator
int otherLeadingTerm = other.GetCoefficient(other.Degree);
int inverseOtherLeadingTerm = GField.Inverse(otherLeadingTerm);
for (int aIndex = 0; aIndex <= aLength - bLength; aIndex++)
{
if (aCoefficients[aIndex] != 0)
{
int aScalar = GField.Product(inverseOtherLeadingTerm, aCoefficients[aIndex]);
Polynomial term = other.MultiplyScalar(aScalar);
qCoefficients[aIndex] = aScalar;
int[] bCoefficient = term.Coefficients;
if (bCoefficient[0] != 0)
{
for (int bIndex = 0; bIndex < bLength; bIndex++)
{
aCoefficients[aIndex + bIndex] = GField.Subtraction(aCoefficients[aIndex + bIndex], bCoefficient[bIndex]);
}
}
}
}
return new PolyDivideStruct(new Polynomial(GField, qCoefficients), new Polynomial(GField, aCoefficients));
}
}
}
@@ -0,0 +1,91 @@
using System;
namespace Gma.QrCodeNet.Encoding.ReedSolomon;
internal sealed class ReedSolomonEncoder
{
/// <summary>
/// Encode an array of data codeword with GaloisField 256.
/// </summary>
/// <param name="dataBytes">Array of data codewords for a single block.</param>
/// <param name="numECBytes">Number of error correction codewords for data codewords</param>
/// <param name="generatorPoly">Cached or newly create GeneratorPolynomial</param>
/// <returns>Return error correction codewords array</returns>
internal static byte[] Encode(byte[] dataBytes, int numECBytes, GeneratorPolynomial generatorPoly)
{
int dataLength = dataBytes.Length;
if (generatorPoly == null)
throw new ArgumentNullException(nameof(generatorPoly));
if (dataLength == 0)
{
throw new ArgumentException("There is no data bytes to encode.");
}
if (numECBytes <= 0)
{
throw new ArgumentException("No Error Correction bytes.");
}
int[] toEncode = ConvertToIntArray(dataBytes, dataLength, numECBytes);
Polynomial generator = generatorPoly.GetGenerator(numECBytes);
Polynomial dataPoly = new(generator.GField, toEncode);
PolyDivideStruct divideResult = dataPoly.Divide(generator);
int[] remainderCoeffs = divideResult.Remainder.Coefficients;
return ConvertTosByteArray(remainderCoeffs, numECBytes);
}
/// <summary>
/// Convert data codewords to int array. And add error correction space at end of that array
/// </summary>
/// <param name="dataBytes">Data codewords array</param>
/// <param name="dataLength">Data codewords length</param>
/// <param name="numECBytes">Num of error correction bytes</param>
/// <returns>Int array for data codewords array follow by error correction space</returns>
private static int[] ConvertToIntArray(byte[] dataBytes, int dataLength, int numECBytes)
{
int[] resultArray = new int[dataLength + numECBytes];
for (int index = 0; index < dataLength; index++)
{
resultArray[index] = dataBytes[index] & 0xff;
}
return resultArray;
}
/// <summary>
/// Reassembly error correction codewords. As Polynomial class will eliminate zero monomial at front.
/// </summary>
/// <param name="remainder">Remainder byte array after divide. </param>
/// <param name="numECBytes">Error correction codewords length</param>
/// <returns>Error correction codewords</returns>
private static byte[] ConvertTosByteArray(int[] remainder, int numECBytes)
{
int remainderLength = remainder.Length;
if (remainderLength > numECBytes)
{
throw new ArgumentException($"Num of {nameof(remainder)} bytes cannot be larger than {nameof(numECBytes)}.");
}
int numZeroCoeffs = numECBytes - remainderLength;
byte[] resultArray = new byte[numECBytes];
for (int index = 0; index < numZeroCoeffs; index++)
{
resultArray[index] = 0;
}
for (int rIndex = 0; rIndex < remainderLength; rIndex++)
{
resultArray[numZeroCoeffs + rIndex] = (byte)remainder[rIndex];
}
return resultArray;
}
}
@@ -0,0 +1,28 @@
namespace Gma.QrCodeNet.Encoding;
public sealed class StateMatrix
{
public StateMatrix(int width)
{
Width = width;
MatrixStatus = new MatrixStatus[width, width];
}
private MatrixStatus[,] MatrixStatus { get; }
public MatrixStatus this[int x, int y]
{
get => MatrixStatus[x, y];
set => MatrixStatus[x, y] = value;
}
internal MatrixStatus this[MatrixPoint point]
{
get => this[point.X, point.Y];
set => this[point.X, point.Y] = value;
}
public int Width { get; }
public int Height => Width;
}
@@ -0,0 +1,73 @@
using System;
namespace Gma.QrCodeNet.Encoding.Terminate;
internal static class Terminator
{
private const int NumBitsForByte = 8;
/// <summary>
/// This method will create BitList that contains
/// terminator, padding and pad codewords for given datacodewords.
/// Use it to full fill the data codewords capacity. Thus avoid massive empty bits.
/// </summary>
/// <remarks>ISO/IEC 18004:2006 P. 32 33.
/// Terminator / Bit stream to codeword conversion</remarks>
/// <param name="baseList">Method will add terminator bits (Terminator, padding and padcodewords) at end of baseList</param>
/// <param name="dataCount">Num of bits for datacodewords without terminator</param>
/// <param name="numTotalDataCodewords">Total number of datacodewords for specific version.
/// Receive it under Version/VersionTable</param>
internal static void TerminateBites(this BitList baseList, int dataCount, int numTotalDataCodewords)
{
int numTotalDataBits = numTotalDataCodewords << 3;
int numDataBits = dataCount;
int numFillerBits = numTotalDataBits - numDataBits;
int numBitsNeedForLastByte = numFillerBits & 0x7;
int numFillerBytes = numFillerBits >> 3;
// BitList result = new BitList();
if (numBitsNeedForLastByte >= QRCodeConstantVariable.TerminatorLength)
{
baseList.TerminatorPadding(numBitsNeedForLastByte);
baseList.PadeCodewords(numFillerBytes);
}
else if (numFillerBytes == 0)
{
baseList.TerminatorPadding(numBitsNeedForLastByte);
}
else if (numFillerBytes > 0)
{
baseList.TerminatorPadding(numBitsNeedForLastByte + NumBitsForByte);
baseList.PadeCodewords(numFillerBytes - 1);
}
if (baseList.Count != numTotalDataBits)
{
throw new ArgumentException(
$"Generate terminator and Padding fail. Num of bits need: {numFillerBytes}. Actual length: {baseList.Count - numDataBits}");
}
}
private static void PadeCodewords(this BitList mainList, int numOfPadeCodewords)
{
if (numOfPadeCodewords < 0)
{
throw new ArgumentException("Num of pade codewords is less than Zero");
}
for (int numOfP = 1; numOfP <= numOfPadeCodewords; numOfP++)
{
if (numOfP % 2 == 1)
{
mainList.Add(QRCodeConstantVariable.PadeCodewordsOdd, NumBitsForByte);
}
else
{
mainList.Add(QRCodeConstantVariable.PadeCodewordsEven, NumBitsForByte);
}
}
}
private static void TerminatorPadding(this BitList mainList, int numBits) => mainList.Add(QRCodeConstantVariable.TerminatorNPaddingBit, numBits);
}
@@ -0,0 +1,48 @@
using System;
namespace Gma.QrCodeNet.Encoding;
public class TriStateMatrix : BitMatrixBase
{
public TriStateMatrix(int width) : base(width, new bool[width, width])
{
StateMatrix = new StateMatrix(width);
}
internal TriStateMatrix(bool[,] internalArray) : base(internalArray)
{
StateMatrix = new StateMatrix(internalArray.GetLength(0));
}
private StateMatrix StateMatrix { get; }
public override bool this[int i, int j]
{
get => InternalArray[i, j];
set
{
if (MStatus(i, j) is MatrixStatus.None or MatrixStatus.NoMask)
{
throw new InvalidOperationException($"The value of cell [{i}, {j}] is not set or is Stencil.");
}
InternalArray[i, j] = value;
}
}
public bool this[int i, int j, MatrixStatus mstatus]
{
set
{
StateMatrix[i, j] = mstatus;
InternalArray[i, j] = value;
}
}
public override int Height => Width;
public override int Width => base.Width;
internal MatrixStatus MStatus(int i, int j) => StateMatrix[i, j];
internal MatrixStatus MStatus(MatrixPoint point) => MStatus(point.X, point.Y);
}
@@ -0,0 +1,34 @@
namespace Gma.QrCodeNet.Encoding;
public struct VersionDetail
{
internal VersionDetail(int version, int numTotalBytes, int numDataBytes, int numECBlocks)
: this()
{
Version = version;
NumTotalBytes = numTotalBytes;
NumDataBytes = numDataBytes;
NumECBlocks = numECBlocks;
}
internal int Version { get; private set; }
internal int NumTotalBytes { get; private set; }
internal int NumDataBytes { get; private set; }
internal int NumECBlocks { get; private set; }
internal int MatrixWidth => Width(Version);
internal int ECBlockGroup1 => NumECBlocks - ECBlockGroup2;
internal int ECBlockGroup2 => NumTotalBytes % NumECBlocks;
internal int NumDataBytesGroup1 => NumDataBytes / NumECBlocks;
internal int NumDataBytesGroup2 => NumDataBytesGroup1 + 1;
internal int NumECBytesPerBlock => (NumTotalBytes - NumDataBytes) / NumECBlocks;
internal static int Width(int version) => 17 + (4 * version);
public override string ToString() => $"{Version};{NumTotalBytes};{NumDataBytes};{NumECBlocks}";
}
@@ -0,0 +1,15 @@
namespace Gma.QrCodeNet.Encoding.Versions;
internal struct ErrorCorrectionBlock
{
internal ErrorCorrectionBlock(int numErrorCorrectionBlock, int numDataCodewards)
: this()
{
NumErrorCorrectionBlock = numErrorCorrectionBlock;
NumDataCodewords = numDataCodewards;
}
internal int NumErrorCorrectionBlock { get; private set; }
internal int NumDataCodewords { get; private set; }
}
@@ -0,0 +1,55 @@
using System;
namespace Gma.QrCodeNet.Encoding.Versions;
internal struct ErrorCorrectionBlocks
{
internal ErrorCorrectionBlocks(int numErrorCorrectionCodewords, ErrorCorrectionBlock ecBlock)
: this()
{
NumErrorCorrectionCodewards = numErrorCorrectionCodewords;
ECBlock = new ErrorCorrectionBlock[] { ecBlock };
Initialize();
}
internal ErrorCorrectionBlocks(int numErrorCorrectionCodewords, ErrorCorrectionBlock ecBlock1, ErrorCorrectionBlock ecBlock2)
: this()
{
NumErrorCorrectionCodewards = numErrorCorrectionCodewords;
ECBlock = new ErrorCorrectionBlock[] { ecBlock1, ecBlock2 };
Initialize();
}
internal int NumErrorCorrectionCodewards { get; private set; }
internal int NumBlocks { get; private set; }
internal int ErrorCorrectionCodewordsPerBlock { get; private set; }
private ErrorCorrectionBlock[] ECBlock { get; }
/// <summary>
/// Get Error Correction Blocks
/// </summary>
internal ErrorCorrectionBlock[] GetECBlocks() => ECBlock;
/// <summary>
/// Initialize for NumBlocks and ErrorCorrectionCodewordsPerBlock
/// </summary>
private void Initialize()
{
if (ECBlock == null)
throw new ArgumentNullException(nameof(ECBlock));
NumBlocks = 0;
int blockLength = ECBlock.Length;
for (int i = 0; i < blockLength; i++)
{
NumBlocks += ECBlock[i].NumErrorCorrectionBlock;
}
ErrorCorrectionCodewordsPerBlock = NumErrorCorrectionCodewards / NumBlocks;
}
}
@@ -0,0 +1,35 @@
using System;
namespace Gma.QrCodeNet.Encoding.Versions;
internal struct QRCodeVersion
{
internal QRCodeVersion(int versionNum, int totalCodewords, ErrorCorrectionBlocks ecblocksL, ErrorCorrectionBlocks ecblocksM, ErrorCorrectionBlocks ecblocksQ, ErrorCorrectionBlocks ecblocksH)
: this()
{
VersionNum = versionNum;
TotalCodewords = totalCodewords;
ECBlocks = new ErrorCorrectionBlocks[] { ecblocksL, ecblocksM, ecblocksQ, ecblocksH };
DimensionForVersion = 17 + (versionNum * 4);
}
internal int VersionNum { get; private set; }
internal int TotalCodewords { get; private set; }
internal int DimensionForVersion { get; private set; }
private ErrorCorrectionBlocks[] ECBlocks { get; }
internal ErrorCorrectionBlocks GetECBlocksByLevel(ErrorCorrectionLevel eCLevel)
{
return eCLevel switch
{
ErrorCorrectionLevel.L => ECBlocks[0],
ErrorCorrectionLevel.M => ECBlocks[1],
ErrorCorrectionLevel.Q => ECBlocks[2],
ErrorCorrectionLevel.H => ECBlocks[3],
_ => throw new ArgumentOutOfRangeException(nameof(eCLevel))
};
}
}
@@ -0,0 +1,144 @@
using System;
using Gma.QrCodeNet.Encoding.DataEncodation;
namespace Gma.QrCodeNet.Encoding.Versions;
internal static class VersionControl
{
private const int NumBitsModeIndicator = 4;
private const string DefaultEncoding = QRCodeConstantVariable.DefaultEncoding;
private static readonly int[] VERSION_GROUP = new int[] { 9, 26, 40 };
/// <summary>
/// Determine which version to use
/// </summary>
/// <param name="dataBitsLength">Number of bits for encoded content</param>
/// <param name="encodingName">Encoding name for EightBitByte</param>
/// <returns>VersionDetail and ECI</returns>
internal static VersionControlStruct InitialSetup(int dataBitsLength, ErrorCorrectionLevel level, string encodingName)
{
int totalDataBits = dataBitsLength;
bool containECI = false;
BitList eciHeader = new();
if (encodingName is not DefaultEncoding and not QRCodeConstantVariable.UTF8Encoding)
{
ECISet eciSet = new(ECISet.AppendOption.NameToValue);
int eciValue = eciSet.GetECIValueByName(encodingName);
totalDataBits += ECISet.NumOfECIHeaderBits(eciValue);
eciHeader = eciSet.GetECIHeader(encodingName);
containECI = true;
}
// Determine which version group it belong to
int searchGroup = DynamicSearchIndicator(totalDataBits, level);
int[] charCountIndicator = CharCountIndicatorTable.GetCharCountIndicatorSet();
totalDataBits += (NumBitsModeIndicator + charCountIndicator[searchGroup]);
int lowerSearchBoundary = searchGroup == 0 ? 1 : (VERSION_GROUP[searchGroup - 1] + 1);
int higherSearchBoundary = VERSION_GROUP[searchGroup];
// Binary search to find proper version
int versionNum = BinarySearch(totalDataBits, level, lowerSearchBoundary, higherSearchBoundary);
VersionControlStruct vcStruct = FillVCStruct(versionNum, level);
vcStruct.IsContainECI = containECI;
vcStruct.ECIHeader = eciHeader;
return vcStruct;
}
private static VersionControlStruct FillVCStruct(int versionNum, ErrorCorrectionLevel level)
{
if (versionNum is < 1 or > 40)
{
throw new InvalidOperationException($"Unexpected version number: {versionNum}");
}
VersionControlStruct vcStruct = new();
int version = versionNum;
QRCodeVersion versionData = VersionTable.GetVersionByNum(versionNum);
int numTotalBytes = versionData.TotalCodewords;
ErrorCorrectionBlocks ecBlocks = versionData.GetECBlocksByLevel(level);
int numDataBytes = numTotalBytes - ecBlocks.NumErrorCorrectionCodewards;
int numECBlocks = ecBlocks.NumBlocks;
VersionDetail vcDetail = new(version, numTotalBytes, numDataBytes, numECBlocks);
vcStruct.VersionDetail = vcDetail;
return vcStruct;
}
/// <summary>
/// Decide which version group it belong to
/// </summary>
/// <param name="numBits">Number of bits for bitlist where it contain DataBits encode from input content and ECI header</param>
/// <param name="level">Error correction level</param>
/// <returns>Version group index for VERSION_GROUP</returns>
private static int DynamicSearchIndicator(int numBits, ErrorCorrectionLevel level)
{
int[] charCountIndicator = CharCountIndicatorTable.GetCharCountIndicatorSet();
int loopLength = VERSION_GROUP.Length;
for (int i = 0; i < loopLength; i++)
{
int totalBits = numBits + NumBitsModeIndicator + charCountIndicator[i];
QRCodeVersion version = VersionTable.GetVersionByNum(VERSION_GROUP[i]);
int numECCodewords = version.GetECBlocksByLevel(level).NumErrorCorrectionCodewards;
int dataCodewords = version.TotalCodewords - numECCodewords;
if (totalBits <= dataCodewords * 8)
{
return i;
}
}
throw new InputOutOfBoundaryException($"QRCode do not have enough space for {(numBits + NumBitsModeIndicator + charCountIndicator[2])} bits");
}
/// <summary>
/// Use number of data bits(header + eci header + data bits from EncoderBase) to search for proper version to use
/// between min and max boundary.
/// Boundary define by DynamicSearchIndicator method.
/// </summary>
private static int BinarySearch(int numDataBits, ErrorCorrectionLevel level, int lowerVersionNum, int higherVersionNum)
{
int middleVersionNumber;
while (lowerVersionNum <= higherVersionNum)
{
middleVersionNumber = (lowerVersionNum + higherVersionNum) / 2;
QRCodeVersion version = VersionTable.GetVersionByNum(middleVersionNumber);
int numECCodewords = version.GetECBlocksByLevel(level).NumErrorCorrectionCodewards;
int dataCodewords = version.TotalCodewords - numECCodewords;
if (dataCodewords << 3 == numDataBits)
{
return middleVersionNumber;
}
if (dataCodewords << 3 > numDataBits)
{
higherVersionNum = middleVersionNumber - 1;
}
else
{
lowerVersionNum = middleVersionNumber + 1;
}
}
return lowerVersionNum;
}
}
@@ -0,0 +1,8 @@
namespace Gma.QrCodeNet.Encoding.Versions;
internal struct VersionControlStruct
{
internal VersionDetail VersionDetail { get; set; }
internal bool IsContainECI { get; set; }
internal BitList ECIHeader { get; set; }
}
@@ -0,0 +1,317 @@
using System;
namespace Gma.QrCodeNet.Encoding.Versions;
public static class VersionTable
{
private static readonly QRCodeVersion[] Version = Initialize();
internal static QRCodeVersion GetVersionByNum(int versionNum)
{
if (versionNum is < QRCodeConstantVariable.MinVersion or > QRCodeConstantVariable.MaxVersion)
{
throw new InvalidOperationException($"Unexpected version number: {versionNum}.");
}
return Version[versionNum - 1];
}
internal static QRCodeVersion GetVersionByWidth(int matrixWidth)
{
if ((matrixWidth - 17) % 4 != 0)
{
throw new ArgumentException("Incorrect matrix width.");
}
else
{
return GetVersionByNum((matrixWidth - 17) / 4);
}
}
private static QRCodeVersion[] Initialize()
{
return new QRCodeVersion[]
{
new QRCodeVersion(
1,
26,
new ErrorCorrectionBlocks(7, new ErrorCorrectionBlock(1, 19)),
new ErrorCorrectionBlocks(10, new ErrorCorrectionBlock(1, 16)),
new ErrorCorrectionBlocks(13, new ErrorCorrectionBlock(1, 13)),
new ErrorCorrectionBlocks(17, new ErrorCorrectionBlock(1, 9))),
new QRCodeVersion(
2,
44,
new ErrorCorrectionBlocks(10, new ErrorCorrectionBlock(1, 34)),
new ErrorCorrectionBlocks(16, new ErrorCorrectionBlock(1, 28)),
new ErrorCorrectionBlocks(22, new ErrorCorrectionBlock(1, 22)),
new ErrorCorrectionBlocks(28, new ErrorCorrectionBlock(1, 16))),
new QRCodeVersion(
3,
70,
new ErrorCorrectionBlocks(15, new ErrorCorrectionBlock(1, 55)),
new ErrorCorrectionBlocks(26, new ErrorCorrectionBlock(1, 44)),
new ErrorCorrectionBlocks(36, new ErrorCorrectionBlock(2, 17)),
new ErrorCorrectionBlocks(44, new ErrorCorrectionBlock(2, 13))),
new QRCodeVersion(
4,
100,
new ErrorCorrectionBlocks(20, new ErrorCorrectionBlock(1, 80)),
new ErrorCorrectionBlocks(36, new ErrorCorrectionBlock(2, 32)),
new ErrorCorrectionBlocks(52, new ErrorCorrectionBlock(2, 24)),
new ErrorCorrectionBlocks(64, new ErrorCorrectionBlock(4, 9))),
new QRCodeVersion(
5,
134,
new ErrorCorrectionBlocks(26, new ErrorCorrectionBlock(1, 108)),
new ErrorCorrectionBlocks(48, new ErrorCorrectionBlock(2, 43)),
new ErrorCorrectionBlocks(72, new ErrorCorrectionBlock(2, 15), new ErrorCorrectionBlock(2, 16)),
new ErrorCorrectionBlocks(88, new ErrorCorrectionBlock(2, 11), new ErrorCorrectionBlock(2, 12))),
new QRCodeVersion(
6,
172,
new ErrorCorrectionBlocks(36, new ErrorCorrectionBlock(2, 68)),
new ErrorCorrectionBlocks(64, new ErrorCorrectionBlock(4, 27)),
new ErrorCorrectionBlocks(96, new ErrorCorrectionBlock(4, 19)),
new ErrorCorrectionBlocks(112, new ErrorCorrectionBlock(4, 15))),
new QRCodeVersion(
7,
196,
new ErrorCorrectionBlocks(40, new ErrorCorrectionBlock(2, 78)),
new ErrorCorrectionBlocks(72, new ErrorCorrectionBlock(4, 31)),
new ErrorCorrectionBlocks(108, new ErrorCorrectionBlock(2, 14), new ErrorCorrectionBlock(4, 15)),
new ErrorCorrectionBlocks(130, new ErrorCorrectionBlock(4, 13), new ErrorCorrectionBlock(1, 14))),
new QRCodeVersion(
8,
242,
new ErrorCorrectionBlocks(48, new ErrorCorrectionBlock(2, 97)),
new ErrorCorrectionBlocks(88, new ErrorCorrectionBlock(2, 38), new ErrorCorrectionBlock(2, 39)),
new ErrorCorrectionBlocks(132, new ErrorCorrectionBlock(4, 18), new ErrorCorrectionBlock(2, 19)),
new ErrorCorrectionBlocks(156, new ErrorCorrectionBlock(4, 14), new ErrorCorrectionBlock(2, 15))),
new QRCodeVersion(
9,
292,
new ErrorCorrectionBlocks(60, new ErrorCorrectionBlock(2, 116)),
new ErrorCorrectionBlocks(110, new ErrorCorrectionBlock(3, 36), new ErrorCorrectionBlock(2, 37)),
new ErrorCorrectionBlocks(160, new ErrorCorrectionBlock(4, 16), new ErrorCorrectionBlock(4, 17)),
new ErrorCorrectionBlocks(192, new ErrorCorrectionBlock(4, 12), new ErrorCorrectionBlock(4, 13))),
new QRCodeVersion(
10,
346,
new ErrorCorrectionBlocks(72, new ErrorCorrectionBlock(2, 68), new ErrorCorrectionBlock(2, 69)),
new ErrorCorrectionBlocks(130, new ErrorCorrectionBlock(4, 43), new ErrorCorrectionBlock(1, 44)),
new ErrorCorrectionBlocks(192, new ErrorCorrectionBlock(6, 19), new ErrorCorrectionBlock(2, 20)),
new ErrorCorrectionBlocks(224, new ErrorCorrectionBlock(6, 15), new ErrorCorrectionBlock(2, 16))),
new QRCodeVersion(
11,
404,
new ErrorCorrectionBlocks(80, new ErrorCorrectionBlock(4, 81)),
new ErrorCorrectionBlocks(150, new ErrorCorrectionBlock(1, 50), new ErrorCorrectionBlock(4, 51)),
new ErrorCorrectionBlocks(224, new ErrorCorrectionBlock(4, 22), new ErrorCorrectionBlock(4, 23)),
new ErrorCorrectionBlocks(264, new ErrorCorrectionBlock(3, 12), new ErrorCorrectionBlock(8, 13))),
new QRCodeVersion(
12,
466,
new ErrorCorrectionBlocks(96, new ErrorCorrectionBlock(2, 92), new ErrorCorrectionBlock(2, 93)),
new ErrorCorrectionBlocks(176, new ErrorCorrectionBlock(6, 36), new ErrorCorrectionBlock(2, 37)),
new ErrorCorrectionBlocks(260, new ErrorCorrectionBlock(4, 20), new ErrorCorrectionBlock(6, 21)),
new ErrorCorrectionBlocks(308, new ErrorCorrectionBlock(7, 14), new ErrorCorrectionBlock(4, 15))),
new QRCodeVersion(
13,
532,
new ErrorCorrectionBlocks(104, new ErrorCorrectionBlock(4, 107)),
new ErrorCorrectionBlocks(198, new ErrorCorrectionBlock(8, 37), new ErrorCorrectionBlock(1, 38)),
new ErrorCorrectionBlocks(288, new ErrorCorrectionBlock(8, 20), new ErrorCorrectionBlock(4, 21)),
new ErrorCorrectionBlocks(352, new ErrorCorrectionBlock(12, 11), new ErrorCorrectionBlock(4, 12))),
new QRCodeVersion(
14,
581,
new ErrorCorrectionBlocks(120, new ErrorCorrectionBlock(3, 115), new ErrorCorrectionBlock(1, 116)),
new ErrorCorrectionBlocks(216, new ErrorCorrectionBlock(4, 40), new ErrorCorrectionBlock(5, 41)),
new ErrorCorrectionBlocks(320, new ErrorCorrectionBlock(11, 16), new ErrorCorrectionBlock(5, 17)),
new ErrorCorrectionBlocks(384, new ErrorCorrectionBlock(11, 12), new ErrorCorrectionBlock(5, 13))),
new QRCodeVersion(
15,
655,
new ErrorCorrectionBlocks(132, new ErrorCorrectionBlock(5, 87), new ErrorCorrectionBlock(1, 88)),
new ErrorCorrectionBlocks(240, new ErrorCorrectionBlock(5, 41), new ErrorCorrectionBlock(5, 42)),
new ErrorCorrectionBlocks(360, new ErrorCorrectionBlock(5, 24), new ErrorCorrectionBlock(7, 25)),
new ErrorCorrectionBlocks(432, new ErrorCorrectionBlock(11, 12), new ErrorCorrectionBlock(7, 13))),
new QRCodeVersion(
16,
733,
new ErrorCorrectionBlocks(144, new ErrorCorrectionBlock(5, 98), new ErrorCorrectionBlock(1, 99)),
new ErrorCorrectionBlocks(280, new ErrorCorrectionBlock(7, 45), new ErrorCorrectionBlock(3, 46)),
new ErrorCorrectionBlocks(408, new ErrorCorrectionBlock(15, 19), new ErrorCorrectionBlock(2, 20)),
new ErrorCorrectionBlocks(480, new ErrorCorrectionBlock(3, 15), new ErrorCorrectionBlock(13, 16))),
new QRCodeVersion(
17,
815,
new ErrorCorrectionBlocks(168, new ErrorCorrectionBlock(1, 107), new ErrorCorrectionBlock(5, 108)),
new ErrorCorrectionBlocks(308, new ErrorCorrectionBlock(10, 46), new ErrorCorrectionBlock(1, 47)),
new ErrorCorrectionBlocks(448, new ErrorCorrectionBlock(1, 22), new ErrorCorrectionBlock(15, 23)),
new ErrorCorrectionBlocks(532, new ErrorCorrectionBlock(2, 14), new ErrorCorrectionBlock(17, 15))),
new QRCodeVersion(
18,
901,
new ErrorCorrectionBlocks(180, new ErrorCorrectionBlock(5, 120), new ErrorCorrectionBlock(1, 121)),
new ErrorCorrectionBlocks(338, new ErrorCorrectionBlock(9, 43), new ErrorCorrectionBlock(4, 44)),
new ErrorCorrectionBlocks(504, new ErrorCorrectionBlock(17, 22), new ErrorCorrectionBlock(1, 23)),
new ErrorCorrectionBlocks(588, new ErrorCorrectionBlock(2, 14), new ErrorCorrectionBlock(19, 15))),
new QRCodeVersion(
19,
991,
new ErrorCorrectionBlocks(196, new ErrorCorrectionBlock(3, 113), new ErrorCorrectionBlock(4, 114)),
new ErrorCorrectionBlocks(364, new ErrorCorrectionBlock(3, 44), new ErrorCorrectionBlock(11, 45)),
new ErrorCorrectionBlocks(546, new ErrorCorrectionBlock(17, 21), new ErrorCorrectionBlock(4, 22)),
new ErrorCorrectionBlocks(650, new ErrorCorrectionBlock(9, 13), new ErrorCorrectionBlock(16, 14))),
new QRCodeVersion(
20,
1085,
new ErrorCorrectionBlocks(224, new ErrorCorrectionBlock(3, 107), new ErrorCorrectionBlock(5, 108)),
new ErrorCorrectionBlocks(416, new ErrorCorrectionBlock(3, 41), new ErrorCorrectionBlock(13, 42)),
new ErrorCorrectionBlocks(600, new ErrorCorrectionBlock(15, 24), new ErrorCorrectionBlock(5, 25)),
new ErrorCorrectionBlocks(700, new ErrorCorrectionBlock(15, 15), new ErrorCorrectionBlock(10, 16))),
new QRCodeVersion(
21,
1156,
new ErrorCorrectionBlocks(224, new ErrorCorrectionBlock(4, 116), new ErrorCorrectionBlock(4, 117)),
new ErrorCorrectionBlocks(442, new ErrorCorrectionBlock(17, 42)),
new ErrorCorrectionBlocks(644, new ErrorCorrectionBlock(17, 22), new ErrorCorrectionBlock(6, 23)),
new ErrorCorrectionBlocks(750, new ErrorCorrectionBlock(19, 16), new ErrorCorrectionBlock(6, 17))),
new QRCodeVersion(
22,
1258,
new ErrorCorrectionBlocks(252, new ErrorCorrectionBlock(2, 111), new ErrorCorrectionBlock(7, 112)),
new ErrorCorrectionBlocks(476, new ErrorCorrectionBlock(17, 46)),
new ErrorCorrectionBlocks(690, new ErrorCorrectionBlock(7, 24), new ErrorCorrectionBlock(16, 25)),
new ErrorCorrectionBlocks(816, new ErrorCorrectionBlock(34, 13))),
new QRCodeVersion(
23,
1364,
new ErrorCorrectionBlocks(270, new ErrorCorrectionBlock(4, 121), new ErrorCorrectionBlock(5, 122)),
new ErrorCorrectionBlocks(504, new ErrorCorrectionBlock(4, 47), new ErrorCorrectionBlock(14, 48)),
new ErrorCorrectionBlocks(750, new ErrorCorrectionBlock(11, 24), new ErrorCorrectionBlock(14, 25)),
new ErrorCorrectionBlocks(900, new ErrorCorrectionBlock(16, 15), new ErrorCorrectionBlock(14, 16))),
new QRCodeVersion(
24,
1474,
new ErrorCorrectionBlocks(300, new ErrorCorrectionBlock(6, 117), new ErrorCorrectionBlock(4, 118)),
new ErrorCorrectionBlocks(560, new ErrorCorrectionBlock(6, 45), new ErrorCorrectionBlock(14, 46)),
new ErrorCorrectionBlocks(810, new ErrorCorrectionBlock(11, 24), new ErrorCorrectionBlock(16, 25)),
new ErrorCorrectionBlocks(960, new ErrorCorrectionBlock(30, 16), new ErrorCorrectionBlock(2, 17))),
new QRCodeVersion(
25,
1588,
new ErrorCorrectionBlocks(312, new ErrorCorrectionBlock(8, 106), new ErrorCorrectionBlock(4, 107)),
new ErrorCorrectionBlocks(588, new ErrorCorrectionBlock(8, 47), new ErrorCorrectionBlock(13, 48)),
new ErrorCorrectionBlocks(870, new ErrorCorrectionBlock(7, 24), new ErrorCorrectionBlock(22, 25)),
new ErrorCorrectionBlocks(1050, new ErrorCorrectionBlock(22, 15), new ErrorCorrectionBlock(13, 16))),
new QRCodeVersion(
26,
1706,
new ErrorCorrectionBlocks(336, new ErrorCorrectionBlock(10, 114), new ErrorCorrectionBlock(2, 115)),
new ErrorCorrectionBlocks(644, new ErrorCorrectionBlock(19, 46), new ErrorCorrectionBlock(4, 47)),
new ErrorCorrectionBlocks(952, new ErrorCorrectionBlock(28, 22), new ErrorCorrectionBlock(6, 23)),
new ErrorCorrectionBlocks(1110, new ErrorCorrectionBlock(33, 16), new ErrorCorrectionBlock(4, 17))),
new QRCodeVersion(
27,
1828,
new ErrorCorrectionBlocks(360, new ErrorCorrectionBlock(8, 122), new ErrorCorrectionBlock(4, 123)),
new ErrorCorrectionBlocks(700, new ErrorCorrectionBlock(22, 45), new ErrorCorrectionBlock(3, 46)),
new ErrorCorrectionBlocks(1020, new ErrorCorrectionBlock(8, 23), new ErrorCorrectionBlock(26, 24)),
new ErrorCorrectionBlocks(1200, new ErrorCorrectionBlock(12, 15), new ErrorCorrectionBlock(28, 16))),
new QRCodeVersion(
28,
1921,
new ErrorCorrectionBlocks(390, new ErrorCorrectionBlock(3, 117), new ErrorCorrectionBlock(10, 118)),
new ErrorCorrectionBlocks(728, new ErrorCorrectionBlock(3, 45), new ErrorCorrectionBlock(23, 46)),
new ErrorCorrectionBlocks(1050, new ErrorCorrectionBlock(4, 24), new ErrorCorrectionBlock(31, 25)),
new ErrorCorrectionBlocks(1260, new ErrorCorrectionBlock(11, 15), new ErrorCorrectionBlock(31, 16))),
new QRCodeVersion(
29,
2051,
new ErrorCorrectionBlocks(420, new ErrorCorrectionBlock(7, 116), new ErrorCorrectionBlock(7, 117)),
new ErrorCorrectionBlocks(784, new ErrorCorrectionBlock(21, 45), new ErrorCorrectionBlock(7, 46)),
new ErrorCorrectionBlocks(1140, new ErrorCorrectionBlock(1, 23), new ErrorCorrectionBlock(37, 24)),
new ErrorCorrectionBlocks(1350, new ErrorCorrectionBlock(19, 15), new ErrorCorrectionBlock(26, 16))),
new QRCodeVersion(
30,
2185,
new ErrorCorrectionBlocks(450, new ErrorCorrectionBlock(5, 115), new ErrorCorrectionBlock(10, 116)),
new ErrorCorrectionBlocks(812, new ErrorCorrectionBlock(19, 47), new ErrorCorrectionBlock(10, 48)),
new ErrorCorrectionBlocks(1200, new ErrorCorrectionBlock(15, 24), new ErrorCorrectionBlock(25, 25)),
new ErrorCorrectionBlocks(1440, new ErrorCorrectionBlock(23, 15), new ErrorCorrectionBlock(25, 16))),
new QRCodeVersion(
31,
2323,
new ErrorCorrectionBlocks(480, new ErrorCorrectionBlock(13, 115), new ErrorCorrectionBlock(3, 116)),
new ErrorCorrectionBlocks(868, new ErrorCorrectionBlock(2, 46), new ErrorCorrectionBlock(29, 47)),
new ErrorCorrectionBlocks(1290, new ErrorCorrectionBlock(42, 24), new ErrorCorrectionBlock(1, 25)),
new ErrorCorrectionBlocks(1530, new ErrorCorrectionBlock(23, 15), new ErrorCorrectionBlock(28, 16))),
new QRCodeVersion(
32,
2465,
new ErrorCorrectionBlocks(510, new ErrorCorrectionBlock(17, 115)),
new ErrorCorrectionBlocks(924, new ErrorCorrectionBlock(10, 46), new ErrorCorrectionBlock(23, 47)),
new ErrorCorrectionBlocks(1350, new ErrorCorrectionBlock(10, 24), new ErrorCorrectionBlock(35, 25)),
new ErrorCorrectionBlocks(1620, new ErrorCorrectionBlock(19, 15), new ErrorCorrectionBlock(35, 16))),
new QRCodeVersion(
33,
2611,
new ErrorCorrectionBlocks(540, new ErrorCorrectionBlock(17, 115), new ErrorCorrectionBlock(1, 116)),
new ErrorCorrectionBlocks(980, new ErrorCorrectionBlock(14, 46), new ErrorCorrectionBlock(21, 47)),
new ErrorCorrectionBlocks(1440, new ErrorCorrectionBlock(29, 24), new ErrorCorrectionBlock(19, 25)),
new ErrorCorrectionBlocks(1710, new ErrorCorrectionBlock(11, 15), new ErrorCorrectionBlock(46, 16))),
new QRCodeVersion(
34,
2761,
new ErrorCorrectionBlocks(570, new ErrorCorrectionBlock(13, 115), new ErrorCorrectionBlock(6, 116)),
new ErrorCorrectionBlocks(1036, new ErrorCorrectionBlock(14, 46), new ErrorCorrectionBlock(23, 47)),
new ErrorCorrectionBlocks(1530, new ErrorCorrectionBlock(44, 24), new ErrorCorrectionBlock(7, 25)),
new ErrorCorrectionBlocks(1800, new ErrorCorrectionBlock(59, 16), new ErrorCorrectionBlock(1, 17))),
new QRCodeVersion(
35,
2876,
new ErrorCorrectionBlocks(570, new ErrorCorrectionBlock(12, 121), new ErrorCorrectionBlock(7, 122)),
new ErrorCorrectionBlocks(1064, new ErrorCorrectionBlock(12, 47), new ErrorCorrectionBlock(26, 48)),
new ErrorCorrectionBlocks(1590, new ErrorCorrectionBlock(39, 24), new ErrorCorrectionBlock(14, 25)),
new ErrorCorrectionBlocks(1890, new ErrorCorrectionBlock(22, 15), new ErrorCorrectionBlock(41, 16))),
new QRCodeVersion(
36,
3034,
new ErrorCorrectionBlocks(600, new ErrorCorrectionBlock(6, 121), new ErrorCorrectionBlock(14, 122)),
new ErrorCorrectionBlocks(1120, new ErrorCorrectionBlock(6, 47), new ErrorCorrectionBlock(34, 48)),
new ErrorCorrectionBlocks(1680, new ErrorCorrectionBlock(46, 24), new ErrorCorrectionBlock(10, 25)),
new ErrorCorrectionBlocks(1980, new ErrorCorrectionBlock(2, 15), new ErrorCorrectionBlock(64, 16))),
new QRCodeVersion(
37,
3196,
new ErrorCorrectionBlocks(630, new ErrorCorrectionBlock(17, 122), new ErrorCorrectionBlock(4, 123)),
new ErrorCorrectionBlocks(1204, new ErrorCorrectionBlock(29, 46), new ErrorCorrectionBlock(14, 47)),
new ErrorCorrectionBlocks(1770, new ErrorCorrectionBlock(49, 24), new ErrorCorrectionBlock(10, 25)),
new ErrorCorrectionBlocks(2100, new ErrorCorrectionBlock(24, 15), new ErrorCorrectionBlock(46, 16))),
new QRCodeVersion(
38,
3362,
new ErrorCorrectionBlocks(660, new ErrorCorrectionBlock(4, 122), new ErrorCorrectionBlock(18, 123)),
new ErrorCorrectionBlocks(1260, new ErrorCorrectionBlock(13, 46), new ErrorCorrectionBlock(32, 47)),
new ErrorCorrectionBlocks(1860, new ErrorCorrectionBlock(48, 24), new ErrorCorrectionBlock(14, 25)),
new ErrorCorrectionBlocks(2220, new ErrorCorrectionBlock(42, 15), new ErrorCorrectionBlock(32, 16))),
new QRCodeVersion(
39,
3532,
new ErrorCorrectionBlocks(720, new ErrorCorrectionBlock(20, 117), new ErrorCorrectionBlock(4, 118)),
new ErrorCorrectionBlocks(1316, new ErrorCorrectionBlock(40, 47), new ErrorCorrectionBlock(7, 48)),
new ErrorCorrectionBlocks(1950, new ErrorCorrectionBlock(43, 24), new ErrorCorrectionBlock(22, 25)),
new ErrorCorrectionBlocks(2310, new ErrorCorrectionBlock(10, 15), new ErrorCorrectionBlock(67, 16))),
new QRCodeVersion(
40,
3706,
new ErrorCorrectionBlocks(750, new ErrorCorrectionBlock(19, 118), new ErrorCorrectionBlock(6, 119)),
new ErrorCorrectionBlocks(1372, new ErrorCorrectionBlock(18, 47), new ErrorCorrectionBlock(31, 48)),
new ErrorCorrectionBlocks(2040, new ErrorCorrectionBlock(34, 24), new ErrorCorrectionBlock(34, 25)),
new ErrorCorrectionBlocks(2430, new ErrorCorrectionBlock(20, 15), new ErrorCorrectionBlock(61, 16))),
};
}
}
@@ -0,0 +1,751 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
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;
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
/// <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
}
}
@@ -0,0 +1,52 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Toolkit.UI.Controls.Avalonia"
xmlns:ui="using:FluentAvalonia.UI.Controls">
<Design.PreviewWith>
<Border Padding="20" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type controls:SettingsExpander}" TargetType="controls:SettingsExpander">
<Setter Property="Background" Value="{DynamicResource ExpanderBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource ExpanderHeaderBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource ExpanderHeaderBorderThickness}" />
<Setter Property="Padding" Value="{DynamicResource SettingsExpanderPadding}" />
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="MinHeight" Value="{DynamicResource SettingsExpanderMinHeight}" />
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<StackPanel Spacing="1" />
</ItemsPanelTemplate>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Expander
Name="Expander"
IsExpanded="{TemplateBinding IsExpanded,
Mode=TwoWay}"
Theme="{StaticResource SettingsExpanderExpanderStyle}">
<Expander.Header>
<controls:SettingsExpanderItem
Name="ContentHost"
Padding="{DynamicResource SettingsExpanderPadding}"
ActionIconSource="{TemplateBinding ActionIconSource}"
Background="Transparent"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Description="{TemplateBinding Description}"
Footer="{TemplateBinding Footer}"
FooterTemplate="{TemplateBinding FooterTemplate}"
IconSource="{TemplateBinding IconSource}"
IsClickEnabled="{TemplateBinding IsClickEnabled}" />
</Expander.Header>
<ItemsPresenter ItemsPanel="{TemplateBinding ItemsPanel}" />
</Expander>
</ControlTemplate>
</Setter>
<Style Selector="^:empty /template/ ItemsPresenter#ItemsHost">
<Setter Property="IsVisible" Value="False" />
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -0,0 +1,33 @@
using Avalonia;
namespace Toolkit.UI.Controls.Avalonia;
public class SettingsExpander : FluentAvalonia.UI.Controls.SettingsExpander
{
protected override Type StyleKeyOverride =>
typeof(SettingsExpander);
public new static readonly StyledProperty<object> DescriptionProperty =
AvaloniaProperty.Register<SettingsExpander, object>(nameof(Description));
public new object Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
}
public class SettingsExpanderItem : FluentAvalonia.UI.Controls.SettingsExpanderItem
{
protected override Type StyleKeyOverride =>
typeof(SettingsExpanderItem);
public new static readonly StyledProperty<object> DescriptionProperty =
AvaloniaProperty.Register<SettingsExpanderItem, object>(nameof(Description));
public new object Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
}
@@ -0,0 +1,144 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Toolkit.UI.Controls.Avalonia"
xmlns:ui="using:FluentAvalonia.UI.Controls">
<Design.PreviewWith>
<Border Padding="20" />
</Design.PreviewWith>
<Thickness x:Key="SettingsExpanderItemPadding">16 10</Thickness>
<x:Double x:Key="SettingsExpanderItemMinHeight">48</x:Double>
<Thickness x:Key="SettingsExpanderItemContentMargin">42 0 0 0</Thickness>
<Thickness x:Key="SettingsExpanderItemFooterMargin">16 0 0 0</Thickness>
<Thickness x:Key="SettingsExpanderItemBottomFooterMargin">42 16 0 0</Thickness>
<x:Double x:Key="SettingsExpanderItemIconSize">24</x:Double>
<x:Double x:Key="SettingsExpanderItemActionIconSize">18</x:Double>
<x:Double x:Key="SettingsExpanderItemAdaptiveWidthTrigger">460</x:Double>
<ControlTheme x:Key="{x:Type controls:SettingsExpanderItem}" TargetType="controls:SettingsExpanderItem">
<Setter Property="Background" Value="{DynamicResource ExpanderHeaderBackground}" />
<Setter Property="Padding" Value="{DynamicResource SettingsExpanderItemPadding}" />
<Setter Property="MinHeight" Value="{DynamicResource SettingsExpanderItemMinHeight}" />
<Setter Property="Template">
<ControlTemplate>
<ui:FABorder
Name="Root"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ui:FABorder.Transitions>
<Transitions>
<BrushTransition Property="Background" Duration="00:00:00.083" />
<BrushTransition Property="BorderBrush" Duration="00:00:00.083" />
</Transitions>
</ui:FABorder.Transitions>
<Grid ColumnDefinitions="*,Auto,Auto" RowDefinitions="*,Auto">
<Viewbox
Name="IconHost"
Width="{DynamicResource SettingsExpanderItemIconSize}"
Height="{DynamicResource SettingsExpanderItemIconSize}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
IsVisible="False">
<ContentPresenter Name="IconPresenter" Content="{Binding TemplateSettings.Icon, RelativeSource={RelativeSource TemplatedParent}}" />
</Viewbox>
<StackPanel
Name="HeaderRegion"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center">
<ContentPresenter
Name="ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
IsVisible="False" />
<ContentPresenter
Name="DescriptionText"
Content="{TemplateBinding Description}"
FontSize="{DynamicResource CaptionTextBlockFontSize}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Theme="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
<ContentPresenter
Name="FooterPresenter"
Grid.Row="0"
Grid.Column="1"
Grid.ColumnSpan="1"
Margin="{DynamicResource SettingsExpanderItemFooterMargin}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding Footer}"
ContentTemplate="{TemplateBinding FooterTemplate}"
IsVisible="False" />
<Viewbox
Name="ActionIconHost"
Grid.Column="2"
Width="{DynamicResource SettingsExpanderItemActionIconSize}"
Height="{DynamicResource SettingsExpanderItemActionIconSize}"
IsVisible="False">
<ContentPresenter Name="ExpandChevronActionIconContainer" Content="{Binding TemplateSettings.ActionIcon, RelativeSource={RelativeSource TemplatedParent}}" />
</Viewbox>
</Grid>
</ui:FABorder>
</ControlTemplate>
</Setter>
<Style Selector="^:empty /template/ ItemsPresenter#ItemsHost">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:nth-last-child(1) /template/ ui|FABorder#Root">
<Setter Property="CornerRadius" Value="{Binding Source={StaticResource ControlCornerRadius}, Converter={StaticResource BottomCornerRadiusFilterConverter}}" />
</Style>
<Style Selector="^:footer /template/ ContentPresenter#FooterPresenter">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:footerBottom /template/ ContentPresenter#FooterPresenter">
<Setter Property="Grid.Row" Value="1" />
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Grid.ColumnSpan" Value="3" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Margin" Value="{DynamicResource SettingsExpanderItemBottomFooterMargin}" />
</Style>
<Style Selector="^:actionIcon /template/ Viewbox#ActionIconHost">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:content /template/ ContentPresenter#ContentPresenter">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:icon">
<Style Selector="^ /template/ Viewbox#IconHost">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^ /template/ StackPanel#HeaderRegion">
<Setter Property="Margin" Value="{DynamicResource SettingsExpanderItemContentMargin}" />
</Style>
</Style>
<Style Selector="^:iconPlaceholder">
<Style Selector="^ /template/ StackPanel#HeaderRegion">
<Setter Property="Margin" Value="{DynamicResource SettingsExpanderItemContentMargin}" />
</Style>
</Style>
<Style Selector="^:allowClick">
<Style Selector="^:pointerover /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
</Style>
<Style Selector="^:pressed /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorTertiaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Style>
</Style>
<Style Selector="^:disabled">
<Style Selector="^ /template/ ui|FABorder#Root">
<Setter Property="Background" Value="{DynamicResource ControlFillColorDisabledBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</Style>
<Style Selector="^ /template/ ContentPresenter#DescriptionText">
<Setter Property="TextElement.Foreground" Value="{DynamicResource TextFillColorDisabledBrush}" />
</Style>
</Style>
</ControlTheme>
</ResourceDictionary>
@@ -0,0 +1,6 @@
namespace Toolkit.UI.Controls.Avalonia;
public interface ISpacingDefinition
{
double Spacing { get; set; }
}
@@ -0,0 +1,166 @@
using Avalonia;
using Avalonia.Controls;
using System.Collections.Specialized;
namespace Toolkit.UI.Controls.Avalonia;
public class SpacedGrid : Grid
{
public static readonly StyledProperty<double> ColumnSpacingProperty =
AvaloniaProperty.Register<SpacedGrid, double>(nameof(ColumnSpacing), 3);
public static readonly StyledProperty<double> RowSpacingProperty =
AvaloniaProperty.Register<SpacedGrid, double>(nameof(RowSpacing), 3);
public SpacedGrid() => Children.CollectionChanged += OnCollectionChanged;
public double ColumnSpacing
{
get => GetValue(ColumnSpacingProperty);
set => SetValue(ColumnSpacingProperty, value);
}
public double RowSpacing
{
get => GetValue(RowSpacingProperty);
set => SetValue(RowSpacingProperty, value);
}
public IEnumerable<ColumnDefinition> UserDefinedColumnDefinitions =>
ColumnDefinitions.Where(definition => definition is not ISpacingDefinition);
public IEnumerable<RowDefinition> UserDefinedRowDefinitions =>
RowDefinitions.Where(definition => definition is not ISpacingDefinition);
protected override void OnInitialized()
{
base.OnInitialized();
RowDefinitions.CollectionChanged += delegate { UpdateSpacedRows(); };
ColumnDefinitions.CollectionChanged += delegate { UpdateSpacedColumns(); };
UpdateSpacedRows();
UpdateSpacedColumns();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(RowSpacing):
RecalculateRowSpacing();
break;
case nameof(ColumnSpacing):
RecalculateColumnSpacing();
break;
}
}
private void OnCollectionChanged(object? sender,
NotifyCollectionChangedEventArgs args)
{
if (args.Action == NotifyCollectionChangedAction.Add || args.Action == NotifyCollectionChangedAction.Replace)
{
if (args.NewItems is not null)
{
foreach (Control item in args.NewItems)
{
SetRow(item, GetRow(item) * 2);
SetRowSpan(item, GetRowSpan(item) * 2 - 1);
SetColumn(item, GetColumn(item) * 2);
SetColumnSpan(item, GetColumnSpan(item) * 2 - 1);
}
}
}
}
private void OnInitialized(object? sender, EventArgs args)
{
if (sender is Control item)
{
item.Initialized -= OnInitialized;
SetRow(item, GetRow(item) * 2);
SetRowSpan(item, GetRowSpan(item) * 2 - 1);
var d = GetColumn(item);
SetColumn(item, GetColumn(item) * 2);
SetColumnSpan(item, GetColumnSpan(item) * 2 - 1);
}
}
private void RecalculateColumnSpacing()
{
foreach (ISpacingDefinition spacingColumn in ColumnDefinitions.OfType<ISpacingDefinition>())
{
spacingColumn.Spacing = ColumnSpacing;
}
}
private void RecalculateRowSpacing()
{
foreach (ISpacingDefinition spacingRow in RowDefinitions.OfType<ISpacingDefinition>())
{
spacingRow.Spacing = RowSpacing;
}
}
private void UpdateSpacedColumns()
{
List<ColumnDefinition> userColumnDefinitions = UserDefinedColumnDefinitions.ToList();
ColumnDefinitions actualColumnDefinitions = [];
int currentUserDefinition = 0;
int currentActualDefinition = 0;
while (currentUserDefinition < userColumnDefinitions.Count)
{
if (currentActualDefinition % 2 == 0)
{
actualColumnDefinitions.Add(userColumnDefinitions[currentUserDefinition]);
currentUserDefinition++;
}
else
{
actualColumnDefinitions.Add(new SpacingColumnDefinition(ColumnSpacing));
}
currentActualDefinition++;
}
ColumnDefinitions = actualColumnDefinitions;
ColumnDefinitions.CollectionChanged += delegate { UpdateSpacedColumns(); };
}
private void UpdateSpacedRows()
{
List<RowDefinition> userRowDefinitions = UserDefinedRowDefinitions.ToList();
RowDefinitions actualRowDefinitions = [];
int currentUserDefinition = 0;
int currentActualDefinition = 0;
while (currentUserDefinition < userRowDefinitions.Count)
{
if (currentActualDefinition % 2 == 0)
{
actualRowDefinitions.Add(userRowDefinitions[currentUserDefinition]);
currentUserDefinition++;
}
else
{
actualRowDefinitions.Add(new SpacingRowDefinition(RowSpacing));
}
currentActualDefinition++;
}
RowDefinitions = actualRowDefinitions;
RowDefinitions.CollectionChanged += delegate { UpdateSpacedRows(); };
}
}
@@ -0,0 +1,14 @@
using Avalonia.Controls;
namespace Toolkit.UI.Controls.Avalonia;
public class SpacingColumnDefinition(double width) :
ColumnDefinition(width, GridUnitType.Pixel),
ISpacingDefinition
{
public double Spacing
{
get => Width.Value;
set => Width = new GridLength(value, GridUnitType.Pixel);
}
}
@@ -0,0 +1,14 @@
using Avalonia.Controls;
namespace Toolkit.UI.Controls.Avalonia;
public class SpacingRowDefinition(double height) :
RowDefinition(height, GridUnitType.Pixel),
ISpacingDefinition
{
public double Spacing
{
get => Height.Value;
set => Height = new GridLength(value, GridUnitType.Pixel);
}
}
@@ -0,0 +1,12 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="../CarouselView/CarouselView.axaml" />
<MergeResourceInclude Source="../ContentDialog/ContentDialog.axaml" />
<MergeResourceInclude Source="../SettingsExpander/SettingsExpander.axaml" />
<MergeResourceInclude Source="../SettingsExpander/SettingsExpanderItem.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>
</Styles>
@@ -0,0 +1,10 @@
<Styles
x:Class="Toolkit.UI.Controls.Avalonia.ThemeResources"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fluent="using:FluentAvalonia.Styling"
xmlns:labs="using:Avalonia.Labs.Controls">
<fluent:FluentAvaloniaTheme />
<StyleInclude Source="ControlResources.axaml" />
<labs:ControlThemes />
</Styles>
@@ -0,0 +1,13 @@
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
namespace Toolkit.UI.Controls.Avalonia;
public class ThemeResources :
Styles
{
public ThemeResources(IServiceProvider? serviceProvider = null)
{
AvaloniaXamlLoader.Load(serviceProvider, this);
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.1.0-beta1" />
<PackageReference Include="Avalonia.Labs.Controls" Version="11.0.3" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.5" />
</ItemGroup>
</Project>
+6
View File
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.Foundation", "Toolk
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.UI.Avalonia", "Toolkit.UI.Avalonia\Toolkit.UI.Avalonia.csproj", "{E091FA94-2F15-403A-98D1-4557C2FF9A02}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.UI.Avalonia", "Toolkit.UI.Avalonia\Toolkit.UI.Avalonia.csproj", "{E091FA94-2F15-403A-98D1-4557C2FF9A02}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.UI.Controls.Avalonia", "Toolkit.UI.Controls.Avalonia\Toolkit.UI.Controls.Avalonia.csproj", "{8841990D-A246-495D-9A40-C39BA4347505}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
{E091FA94-2F15-403A-98D1-4557C2FF9A02}.Debug|Any CPU.Build.0 = Debug|Any CPU {E091FA94-2F15-403A-98D1-4557C2FF9A02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E091FA94-2F15-403A-98D1-4557C2FF9A02}.Release|Any CPU.ActiveCfg = Release|Any CPU {E091FA94-2F15-403A-98D1-4557C2FF9A02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E091FA94-2F15-403A-98D1-4557C2FF9A02}.Release|Any CPU.Build.0 = Release|Any CPU {E091FA94-2F15-403A-98D1-4557C2FF9A02}.Release|Any CPU.Build.0 = Release|Any CPU
{8841990D-A246-495D-9A40-C39BA4347505}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8841990D-A246-495D-9A40-C39BA4347505}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8841990D-A246-495D-9A40-C39BA4347505}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8841990D-A246-495D-9A40-C39BA4347505}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE