123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568 |
- using Avalonia;
- using Avalonia.Controls;
- using Avalonia.Controls.Shapes;
- using Avalonia.Data;
- using Avalonia.Input;
- using Avalonia.Interactivity;
- using Avalonia.Markup.Xaml;
- using Avalonia.Media;
- using Avalonia.Media.Imaging;
- using Avalonia.Skia.Helpers;
- using CommunityToolkit.Mvvm.Input;
- using FluentResults;
- using InABox.Avalonia.Components.ImageEditing;
- using InABox.Avalonia.Converters;
- using InABox.Core;
- using SkiaSharp;
- using System.Collections.ObjectModel;
- namespace InABox.Avalonia.Components;
- public enum ImageEditingMode
- {
- Polyline,
- Rectangle,
- Ellipse
- }
- public class ImageEditorModeButton(ImageEditingMode mode, Canvas? canvas)
- {
- public ImageEditingMode Mode { get; set; } = mode;
- public Canvas? Canvas { get; set; } = canvas;
- }
- public class ImageEditorTransparentImageBrushConverter : AbstractConverter<IBrush?, IBrush?>
- {
- public static readonly ImageEditorTransparentImageBrushConverter Instance = new ImageEditorTransparentImageBrushConverter();
- protected override IBrush? Convert(IBrush? value, object? parameter = null)
- {
- if (value is SolidColorBrush solid && solid.Color.A == 255) return solid;
- var brush = new VisualBrush
- {
- TileMode = TileMode.Tile,
- DestinationRect = new(0, 0, 10, 10, RelativeUnit.Absolute)
- };
- var canvas = new Canvas
- {
- Width = 10,
- Height = 10
- };
- var rect1 = new Rectangle { Width = 5, Height = 5 };
- var rect2 = new Rectangle { Width = 5, Height = 5 };
- Canvas.SetLeft(rect2, 5);
- Canvas.SetTop(rect2, 5);
- rect1.Fill = new SolidColorBrush(Colors.LightGray);
- rect2.Fill = new SolidColorBrush(Colors.LightGray);
- var rect3 = new Rectangle { Width = 10, Height = 10 };
- rect3.Fill = value;
- canvas.Children.Add(rect1);
- canvas.Children.Add(rect2);
- canvas.Children.Add(rect3);
- brush.Visual = canvas;
- return brush;
- }
- }
- public class ImageEditorRemoveOpacityConverter : AbstractConverter<IBrush?, IBrush?>
- {
- public static readonly ImageEditorRemoveOpacityConverter Instance = new();
-
- protected override IBrush? Convert(IBrush? value, object? parameter = null)
- {
- if (value is SolidColorBrush solid)
- {
- return new SolidColorBrush(new Color(255, solid.Color.R, solid.Color.G, solid.Color.B));
- }
- return value;
- }
- }
- // TODO: Make it so we don't re-render everything everytime 'Objects' changes.
- public partial class ImageEditor : UserControl
- {
- public static readonly StyledProperty<IImage?> SourceProperty =
- AvaloniaProperty.Register<ImageEditor, IImage?>(nameof(Source));
- public static readonly StyledProperty<IBrush?> PrimaryBrushProperty =
- AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(PrimaryBrush), new SolidColorBrush(Colors.Black));
- public static readonly StyledProperty<IBrush?> SecondaryBrushProperty =
- AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(SecondaryBrush), new SolidColorBrush(Colors.White));
- public static readonly StyledProperty<double> LineThicknessProperty =
- AvaloniaProperty.Register<ImageEditor, double>(nameof(LineThickness), 3.0);
- public static readonly StyledProperty<int> ImageWidthProperty =
- AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageWidth), 100);
- public static readonly StyledProperty<int> ImageHeightProperty =
- AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageHeight), 100);
- public static readonly StyledProperty<ImageEditingMode> ModeProperty =
- AvaloniaProperty.Register<ImageEditor, ImageEditingMode>(nameof(Mode), ImageEditingMode.Polyline);
- public static readonly StyledProperty<bool> ShowButtonsProperty =
- AvaloniaProperty.Register<ImageEditor, bool>(nameof(ShowButtons), true);
- public IImage? Source
- {
- get => GetValue(SourceProperty);
- set => SetValue(SourceProperty, value);
- }
- public int ImageWidth
- {
- get => GetValue(ImageWidthProperty);
- set => SetValue(ImageWidthProperty, value);
- }
- public int ImageHeight
- {
- get => GetValue(ImageHeightProperty);
- set => SetValue(ImageHeightProperty, value);
- }
- public bool ShowButtons
- {
- get => GetValue(ShowButtonsProperty);
- set => SetValue(ShowButtonsProperty, value);
- }
- #region Editing Properties
- public ImageEditingMode Mode
- {
- get => GetValue(ModeProperty);
- set => SetValue(ModeProperty, value);
- }
- public IBrush? PrimaryBrush
- {
- get => GetValue(PrimaryBrushProperty);
- set => SetValue(PrimaryBrushProperty, value);
- }
- public IBrush? SecondaryBrush
- {
- get => GetValue(SecondaryBrushProperty);
- set => SetValue(SecondaryBrushProperty, value);
- }
- public double LineThickness
- {
- get => GetValue(LineThicknessProperty);
- set => SetValue(LineThicknessProperty, value);
- }
- #endregion
- #region Events
- public event EventHandler? Changed;
- #endregion
- #region Private Properties
- public ObservableCollection<ImageEditorModeButton> ModeButtons { get; set; } = new();
- private ObservableCollection<IImageEditorObject> Objects = new();
- private IImageEditorObject? CurrentObject;
- private Stack<IImageEditorObject> RedoStack = new();
- private double ScaleFactor = 1.0;
- #endregion
- static ImageEditor()
- {
- SourceProperty.Changed.AddClassHandler<ImageEditor>(Source_Changed);
- }
- private static void Source_Changed(ImageEditor editor, AvaloniaPropertyChangedEventArgs args)
- {
- if(editor.Source is not null)
- {
- editor.ImageWidth = (int)Math.Floor(editor.Source.Size.Width);
- editor.ImageHeight = (int)Math.Floor(editor.Source.Size.Height);
- editor.PositionImage();
- }
- }
- public ImageEditor()
- {
- InitializeComponent();
- Objects.CollectionChanged += Objects_CollectionChanged;
- AddModeButtons();
- SetMode(Mode);
- OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
- }
- #region Layout
- private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
- {
- PositionImage();
- }
- protected override void OnLoaded(RoutedEventArgs e)
- {
- base.OnLoaded(e);
- PositionImage();
- }
- private void PositionImage()
- {
- var canvasWidth = OuterCanvas.Bounds.Width;
- var canvasHeight = OuterCanvas.Bounds.Height;
- var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
- ScaleFactor = scaleFactor;
- var imageWidth = ImageWidth * scaleFactor;
- var imageHeight = ImageHeight * scaleFactor;
- ImageBorder.Width = imageWidth;
- ImageBorder.Height = imageHeight;
- Canvas.SetLeft(ImageBorder, OuterCanvas.Bounds.Width / 2 - imageWidth / 2);
- Canvas.SetTop(ImageBorder, OuterCanvas.Bounds.Height / 2 - imageHeight / 2);
- Canvas.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
- Canvas.Width = ImageWidth;
- Canvas.Height = ImageHeight;
- }
- #endregion
- #region Editing Commands
- private void UpdateUndoRedoButtons()
- {
- UndoButton.IsEnabled = Objects.Count > 0;
- RedoButton.IsEnabled = RedoStack.Count > 0;
- }
- [RelayCommand]
- private void Undo()
- {
- if (Objects.Count == 0) return;
- RedoStack.Push(Objects[^1]);
- Objects.RemoveAt(Objects.Count - 1);
- UpdateUndoRedoButtons();
- Changed?.Invoke(this, new EventArgs());
- }
- [RelayCommand]
- private void Redo()
- {
- if (!RedoStack.TryPop(out var top)) return;
- Objects.Add(top);
- UpdateUndoRedoButtons();
- Changed?.Invoke(this, new EventArgs());
- }
- private void AddObject(IImageEditorObject obj)
- {
- Objects.Add(obj);
- RedoStack.Clear();
- UpdateUndoRedoButtons();
- Changed?.Invoke(this, new EventArgs());
- }
- [RelayCommand]
- private void SetMode(ImageEditingMode mode)
- {
- Mode = mode;
- ShapeButton.Content = CreateModeButtonContent(mode);
- SecondaryColour.IsVisible = HasSecondaryColour();
- }
- private bool HasSecondaryColour()
- {
- return Mode == ImageEditingMode.Rectangle || Mode == ImageEditingMode.Ellipse;
- }
- #endregion
- #region Mode Buttons
- private void AddModeButtons()
- {
- AddModeButton(ImageEditingMode.Polyline);
- AddModeButton(ImageEditingMode.Rectangle);
- AddModeButton(ImageEditingMode.Ellipse);
- }
- private void AddModeButton(ImageEditingMode mode)
- {
- ModeButtons.Add(new(mode, CreateModeButtonContent(mode)));
- }
- private Canvas? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
- {
- switch (mode)
- {
- case ImageEditingMode.Polyline:
- var canvas = new Canvas();
- var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
- var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
- var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
- line1.StrokeThickness = 4;
- line1.StrokeLineCap = PenLineCap.Round;
- line1.StrokeJoin = PenLineJoin.Round;
- line1.Stroke = new SolidColorBrush(Colors.Black);
- canvas.Children.Add(line1);
- if (bindColour)
- {
- line1.StrokeThickness = 5;
- line2.StrokeThickness = 4;
- line2.StrokeLineCap = PenLineCap.Round;
- line2.StrokeJoin = PenLineJoin.Round;
- line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
- {
- Source = this,
- Converter = ImageEditorRemoveOpacityConverter.Instance
- });
- canvas.Children.Add(line2);
- }
- return canvas;
- case ImageEditingMode.Rectangle:
- canvas = new Canvas();
- canvas.Width = 25;
- canvas.Height = 25;
- var rectangle = new Rectangle();
- if (bindColour)
- {
- rectangle.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
- {
- Source = this,
- Converter = ImageEditorRemoveOpacityConverter.Instance
- });
- rectangle.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
- {
- Source = this,
- Converter = ImageEditorTransparentImageBrushConverter.Instance
- });
- }
- else
- {
- rectangle.Stroke = new SolidColorBrush(Colors.Black);
- rectangle.Fill = new SolidColorBrush(Colors.White);
- }
- rectangle.StrokeThickness = 1.0;
- rectangle.Width = 25;
- rectangle.Height = 25;
- canvas.Children.Add(rectangle);
- return canvas;
- case ImageEditingMode.Ellipse:
- canvas = new Canvas();
- canvas.Width = 25;
- canvas.Height = 25;
- var ellipse = new Ellipse();
- if (bindColour)
- {
- ellipse.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
- {
- Source = this,
- Converter = ImageEditorRemoveOpacityConverter.Instance
- });
- ellipse.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
- {
- Source = this,
- Converter = ImageEditorTransparentImageBrushConverter.Instance
- });
- }
- else
- {
- ellipse.Stroke = new SolidColorBrush(Colors.Black);
- ellipse.Fill = new SolidColorBrush(Colors.White);
- }
- ellipse.StrokeThickness = 1.0;
- ellipse.Width = 25;
- ellipse.Height = 25;
- canvas.Children.Add(ellipse);
- return canvas;
- default:
- return null;
- }
- }
- #endregion
- #region Public Interface
- public void Reset()
- {
- Objects.Clear();
- RedoStack.Clear();
- UpdateUndoRedoButtons();
- Changed?.Invoke(this, new EventArgs());
- }
- public Bitmap GetImage()
- {
- var renderBitmap = new RenderTargetBitmap(new PixelSize(ImageWidth, ImageHeight));
- renderBitmap.Render(Image);
- using var context = renderBitmap.CreateDrawingContext();
- if(Source is not null)
- {
- context.DrawImage(Source, new(0, 0, ImageWidth, ImageHeight));
- }
- foreach (var obj in Objects)
- {
- var control = obj.GetControl();
- var left = Canvas.GetLeft(control);
- var top = Canvas.GetTop(control);
- if (double.IsNaN(left)) left = 0;
- if (double.IsNaN(top)) top = 0;
- using (context.PushTransform(Matrix.CreateTranslation(new(left, top))))
- {
- control.Render(context);
- }
- }
- return renderBitmap;
- }
- public byte[] SaveImage()
- {
- var bitmap = GetImage();
- var stream = new MemoryStream();
- bitmap.Save(stream);
- return stream.ToArray();
- }
- #endregion
- #region Editing
- private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
- {
- Canvas.Children.Clear();
- foreach(var item in Objects)
- {
- item.Update();
- Canvas.Children.Add(item.GetControl());
- }
- }
- Point ConvertToImageCoordinates(Point canvasCoordinates)
- {
- return canvasCoordinates;// new(canvasCoordinates.X / ScaleFactor, canvasCoordinates.Y / ScaleFactor);
- }
- private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
- {
- var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
- switch (Mode)
- {
- case ImageEditingMode.Polyline:
- CurrentObject = new PolylineObject
- {
- Points = [position],
- PrimaryBrush = PrimaryBrush,
- Thickness = LineThickness
- };
- AddObject(CurrentObject);
- break;
- case ImageEditingMode.Rectangle:
- CurrentObject = new RectangleObject
- {
- Point1 = position,
- Point2 = position,
- PrimaryBrush = PrimaryBrush,
- SecondaryBrush = SecondaryBrush,
- Thickness = LineThickness
- };
- AddObject(CurrentObject);
- break;
- case ImageEditingMode.Ellipse:
- CurrentObject = new EllipseObject
- {
- Point1 = position,
- Point2 = position,
- PrimaryBrush = PrimaryBrush,
- SecondaryBrush = SecondaryBrush,
- Thickness = LineThickness
- };
- AddObject(CurrentObject);
- break;
- }
- }
- private void Canvas_PointerMoved(object? sender, PointerEventArgs e)
- {
- var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
- switch (CurrentObject)
- {
- case PolylineObject polyline:
- polyline.Points.Add(position);
- polyline.Update();
- Changed?.Invoke(this, new EventArgs());
- break;
- case RectangleObject rectangle:
- rectangle.Point2 = position;
- rectangle.Update();
- Changed?.Invoke(this, new EventArgs());
- break;
- case EllipseObject ellipse:
- ellipse.Point2 = position;
- ellipse.Update();
- Changed?.Invoke(this, new EventArgs());
- break;
- }
- }
- private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
- {
- var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
- switch (CurrentObject)
- {
- case PolylineObject polyline:
- polyline.Points.Add(position);
- polyline.Update();
- CurrentObject = null;
- Changed?.Invoke(this, new EventArgs());
- break;
- case RectangleObject rectangle:
- rectangle.Point2 = position;
- rectangle.Update();
- CurrentObject = null;
- Changed?.Invoke(this, new EventArgs());
- break;
- case EllipseObject ellipse:
- ellipse.Point2 = position;
- ellipse.Update();
- CurrentObject = null;
- Changed?.Invoke(this, new EventArgs());
- break;
- }
- }
- #endregion
- }
|