Toolkit.UI.Controls.Avalonia
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
+57
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
@@ -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
|
||||
}
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+105
@@ -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];
|
||||
}
|
||||
}
|
||||
+17
@@ -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;
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
+41
@@ -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>
|
||||
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.Foundation", "Toolk
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Toolkit.UI.Avalonia", "Toolkit.UI.Avalonia\Toolkit.UI.Avalonia.csproj", "{E091FA94-2F15-403A-98D1-4557C2FF9A02}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user