ImageEditor.axaml.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. using Avalonia;
  2. using Avalonia.Controls;
  3. using Avalonia.Controls.Shapes;
  4. using Avalonia.Data;
  5. using Avalonia.Input;
  6. using Avalonia.Interactivity;
  7. using Avalonia.Markup.Xaml;
  8. using Avalonia.Media;
  9. using Avalonia.Media.Imaging;
  10. using Avalonia.Layout;
  11. using Avalonia.Skia.Helpers;
  12. using CommunityToolkit.Mvvm.Input;
  13. using FluentResults;
  14. using InABox.Avalonia.Components.ImageEditing;
  15. using InABox.Avalonia.Converters;
  16. using InABox.Core;
  17. using SkiaSharp;
  18. using System.Collections.ObjectModel;
  19. using System.Threading.Tasks;
  20. namespace InABox.Avalonia.Components;
  21. public enum ImageEditingMode
  22. {
  23. Polyline,
  24. Rectangle,
  25. Ellipse,
  26. Text,
  27. Dimension
  28. }
  29. public class ImageEditorModeButton(ImageEditingMode mode, Control? content)
  30. {
  31. public ImageEditingMode Mode { get; set; } = mode;
  32. public Control? Content { get; set; } = content;
  33. }
  34. public class ImageEditorTransparentImageBrushConverter : AbstractConverter<IBrush?, IBrush?>
  35. {
  36. public static readonly ImageEditorTransparentImageBrushConverter Instance = new ImageEditorTransparentImageBrushConverter();
  37. protected override IBrush? Convert(IBrush? value, object? parameter = null)
  38. {
  39. if (value is SolidColorBrush solid && solid.Color.A == 255) return solid;
  40. var brush = new VisualBrush
  41. {
  42. TileMode = TileMode.Tile,
  43. DestinationRect = new(0, 0, 10, 10, RelativeUnit.Absolute)
  44. };
  45. var canvas = new Canvas
  46. {
  47. Width = 10,
  48. Height = 10
  49. };
  50. var rect1 = new Rectangle { Width = 5, Height = 5 };
  51. var rect2 = new Rectangle { Width = 5, Height = 5 };
  52. Canvas.SetLeft(rect2, 5);
  53. Canvas.SetTop(rect2, 5);
  54. rect1.Fill = new SolidColorBrush(Colors.LightGray);
  55. rect2.Fill = new SolidColorBrush(Colors.LightGray);
  56. var rect3 = new Rectangle { Width = 10, Height = 10 };
  57. rect3.Fill = value;
  58. canvas.Children.Add(rect1);
  59. canvas.Children.Add(rect2);
  60. canvas.Children.Add(rect3);
  61. brush.Visual = canvas;
  62. return brush;
  63. }
  64. }
  65. public class ImageEditorRemoveOpacityConverter : AbstractConverter<IBrush?, IBrush?>
  66. {
  67. public static readonly ImageEditorRemoveOpacityConverter Instance = new();
  68. protected override IBrush? Convert(IBrush? value, object? parameter = null)
  69. {
  70. if (value is SolidColorBrush solid)
  71. {
  72. return new SolidColorBrush(new Color(255, solid.Color.R, solid.Color.G, solid.Color.B));
  73. }
  74. return value;
  75. }
  76. }
  77. // TODO: Make it so we don't re-render everything everytime 'Objects' changes.
  78. public partial class ImageEditor : UserControl
  79. {
  80. public static readonly StyledProperty<IImage?> SourceProperty =
  81. AvaloniaProperty.Register<ImageEditor, IImage?>(nameof(Source));
  82. public static readonly StyledProperty<IBrush?> PrimaryBrushProperty =
  83. AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(PrimaryBrush), new SolidColorBrush(Colors.Black));
  84. public static readonly StyledProperty<IBrush?> SecondaryBrushProperty =
  85. AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(SecondaryBrush), new SolidColorBrush(Colors.White));
  86. public static readonly StyledProperty<double> LineThicknessProperty =
  87. AvaloniaProperty.Register<ImageEditor, double>(nameof(LineThickness), 3.0);
  88. public static readonly StyledProperty<int> ImageWidthProperty =
  89. AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageWidth), 100);
  90. public static readonly StyledProperty<int> ImageHeightProperty =
  91. AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageHeight), 100);
  92. public static readonly StyledProperty<ImageEditingMode> ModeProperty =
  93. AvaloniaProperty.Register<ImageEditor, ImageEditingMode>(nameof(Mode), ImageEditingMode.Polyline);
  94. public static readonly StyledProperty<bool> ShowButtonsProperty =
  95. AvaloniaProperty.Register<ImageEditor, bool>(nameof(ShowButtons), true);
  96. public IImage? Source
  97. {
  98. get => GetValue(SourceProperty);
  99. set => SetValue(SourceProperty, value);
  100. }
  101. public int ImageWidth
  102. {
  103. get => GetValue(ImageWidthProperty);
  104. set => SetValue(ImageWidthProperty, value);
  105. }
  106. public int ImageHeight
  107. {
  108. get => GetValue(ImageHeightProperty);
  109. set => SetValue(ImageHeightProperty, value);
  110. }
  111. public bool ShowButtons
  112. {
  113. get => GetValue(ShowButtonsProperty);
  114. set => SetValue(ShowButtonsProperty, value);
  115. }
  116. #region Editing Properties
  117. public ImageEditingMode Mode
  118. {
  119. get => GetValue(ModeProperty);
  120. set => SetValue(ModeProperty, value);
  121. }
  122. public IBrush? PrimaryBrush
  123. {
  124. get => GetValue(PrimaryBrushProperty);
  125. set => SetValue(PrimaryBrushProperty, value);
  126. }
  127. public IBrush? SecondaryBrush
  128. {
  129. get => GetValue(SecondaryBrushProperty);
  130. set => SetValue(SecondaryBrushProperty, value);
  131. }
  132. public double LineThickness
  133. {
  134. get => GetValue(LineThicknessProperty);
  135. set => SetValue(LineThicknessProperty, value);
  136. }
  137. #endregion
  138. #region Events
  139. public event EventHandler? Changed;
  140. #endregion
  141. #region Private Properties
  142. public ObservableCollection<ImageEditorModeButton> ModeButtons { get; set; } = new();
  143. private ObservableCollection<IImageEditorObject> Objects = new();
  144. private IImageEditorObject? CurrentObject;
  145. private Stack<IImageEditorObject> RedoStack = new();
  146. private double ScaleFactor = 1.0;
  147. #endregion
  148. static ImageEditor()
  149. {
  150. SourceProperty.Changed.AddClassHandler<ImageEditor>(Source_Changed);
  151. }
  152. private static void Source_Changed(ImageEditor editor, AvaloniaPropertyChangedEventArgs args)
  153. {
  154. if(editor.Source is not null)
  155. {
  156. editor.ImageWidth = (int)Math.Floor(editor.Source.Size.Width);
  157. editor.ImageHeight = (int)Math.Floor(editor.Source.Size.Height);
  158. editor.PositionImage();
  159. }
  160. }
  161. public ImageEditor()
  162. {
  163. InitializeComponent();
  164. Objects.CollectionChanged += Objects_CollectionChanged;
  165. AddModeButtons();
  166. SetMode(Mode);
  167. OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
  168. }
  169. #region Layout
  170. private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
  171. {
  172. PositionImage();
  173. }
  174. protected override void OnLoaded(RoutedEventArgs e)
  175. {
  176. base.OnLoaded(e);
  177. PositionImage();
  178. }
  179. private void PositionImage()
  180. {
  181. var canvasWidth = OuterCanvas.Bounds.Width;
  182. var canvasHeight = OuterCanvas.Bounds.Height;
  183. var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
  184. ScaleFactor = scaleFactor;
  185. var imageWidth = ImageWidth * scaleFactor;
  186. var imageHeight = ImageHeight * scaleFactor;
  187. ImageBorder.Width = imageWidth;
  188. ImageBorder.Height = imageHeight;
  189. Canvas.SetLeft(ImageBorder, OuterCanvas.Bounds.Width / 2 - imageWidth / 2);
  190. Canvas.SetTop(ImageBorder, OuterCanvas.Bounds.Height / 2 - imageHeight / 2);
  191. Canvas.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
  192. Canvas.Width = ImageWidth;
  193. Canvas.Height = ImageHeight;
  194. }
  195. #endregion
  196. #region Editing Commands
  197. private void UpdateUndoRedoButtons()
  198. {
  199. UndoButton.IsEnabled = Objects.Count > 0;
  200. RedoButton.IsEnabled = RedoStack.Count > 0;
  201. }
  202. [RelayCommand]
  203. private void Undo()
  204. {
  205. if (Objects.Count == 0) return;
  206. RedoStack.Push(Objects[^1]);
  207. Objects.RemoveAt(Objects.Count - 1);
  208. UpdateUndoRedoButtons();
  209. Changed?.Invoke(this, new EventArgs());
  210. }
  211. [RelayCommand]
  212. private void Redo()
  213. {
  214. if (!RedoStack.TryPop(out var top)) return;
  215. Objects.Add(top);
  216. UpdateUndoRedoButtons();
  217. Changed?.Invoke(this, new EventArgs());
  218. }
  219. private void AddObject(IImageEditorObject obj)
  220. {
  221. Objects.Add(obj);
  222. RedoStack.Clear();
  223. UpdateUndoRedoButtons();
  224. Changed?.Invoke(this, new EventArgs());
  225. }
  226. [RelayCommand]
  227. private void SetMode(ImageEditingMode mode)
  228. {
  229. Mode = mode;
  230. ShapeButton.Content = CreateModeButtonContent(mode);
  231. SecondaryColour.IsVisible = HasSecondaryColour();
  232. }
  233. private bool HasSecondaryColour()
  234. {
  235. return Mode == ImageEditingMode.Rectangle || Mode == ImageEditingMode.Ellipse;
  236. }
  237. #endregion
  238. #region Mode Buttons
  239. private void AddModeButtons()
  240. {
  241. AddModeButton(ImageEditingMode.Polyline);
  242. AddModeButton(ImageEditingMode.Rectangle);
  243. AddModeButton(ImageEditingMode.Ellipse);
  244. AddModeButton(ImageEditingMode.Text);
  245. }
  246. private void AddModeButton(ImageEditingMode mode)
  247. {
  248. ModeButtons.Add(new(mode, CreateModeButtonContent(mode)));
  249. }
  250. private Control? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
  251. {
  252. switch (mode)
  253. {
  254. case ImageEditingMode.Polyline:
  255. var canvas = new Canvas();
  256. var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
  257. var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
  258. var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
  259. line1.StrokeThickness = 4;
  260. line1.StrokeLineCap = PenLineCap.Round;
  261. line1.StrokeJoin = PenLineJoin.Round;
  262. line1.Stroke = new SolidColorBrush(Colors.Black);
  263. canvas.Children.Add(line1);
  264. if (bindColour)
  265. {
  266. line1.StrokeThickness = 5;
  267. line2.StrokeThickness = 4;
  268. line2.StrokeLineCap = PenLineCap.Round;
  269. line2.StrokeJoin = PenLineJoin.Round;
  270. line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  271. {
  272. Source = this,
  273. Converter = ImageEditorRemoveOpacityConverter.Instance
  274. });
  275. canvas.Children.Add(line2);
  276. }
  277. return canvas;
  278. case ImageEditingMode.Rectangle:
  279. canvas = new Canvas();
  280. canvas.Width = 25;
  281. canvas.Height = 25;
  282. var rectangle = new Rectangle();
  283. if (bindColour)
  284. {
  285. rectangle.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  286. {
  287. Source = this,
  288. Converter = ImageEditorRemoveOpacityConverter.Instance
  289. });
  290. rectangle.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  291. {
  292. Source = this,
  293. Converter = ImageEditorTransparentImageBrushConverter.Instance
  294. });
  295. }
  296. else
  297. {
  298. rectangle.Stroke = new SolidColorBrush(Colors.Black);
  299. rectangle.Fill = new SolidColorBrush(Colors.White);
  300. }
  301. rectangle.StrokeThickness = 1.0;
  302. rectangle.Width = 25;
  303. rectangle.Height = 25;
  304. canvas.Children.Add(rectangle);
  305. return canvas;
  306. case ImageEditingMode.Ellipse:
  307. canvas = new Canvas();
  308. canvas.Width = 25;
  309. canvas.Height = 25;
  310. var ellipse = new Ellipse();
  311. if (bindColour)
  312. {
  313. ellipse.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  314. {
  315. Source = this,
  316. Converter = ImageEditorRemoveOpacityConverter.Instance
  317. });
  318. ellipse.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  319. {
  320. Source = this,
  321. Converter = ImageEditorTransparentImageBrushConverter.Instance
  322. });
  323. }
  324. else
  325. {
  326. ellipse.Stroke = new SolidColorBrush(Colors.Black);
  327. ellipse.Fill = new SolidColorBrush(Colors.White);
  328. }
  329. ellipse.StrokeThickness = 1.0;
  330. ellipse.Width = 25;
  331. ellipse.Height = 25;
  332. canvas.Children.Add(ellipse);
  333. return canvas;
  334. case ImageEditingMode.Text:
  335. var textBox = new TextBlock();
  336. textBox.Text = "T";
  337. textBox.FontSize = 25;
  338. textBox.TextAlignment = TextAlignment.Center;
  339. textBox.HorizontalAlignment = HorizontalAlignment.Center;
  340. textBox.VerticalAlignment = VerticalAlignment.Center;
  341. return textBox;
  342. default:
  343. return null;
  344. }
  345. }
  346. #endregion
  347. #region Public Interface
  348. public void Reset()
  349. {
  350. Objects.Clear();
  351. RedoStack.Clear();
  352. UpdateUndoRedoButtons();
  353. Changed?.Invoke(this, new EventArgs());
  354. }
  355. public Bitmap GetImage()
  356. {
  357. var renderBitmap = new RenderTargetBitmap(new PixelSize(ImageWidth, ImageHeight));
  358. renderBitmap.Render(Image);
  359. using var context = renderBitmap.CreateDrawingContext();
  360. if(Source is not null)
  361. {
  362. context.DrawImage(Source, new(0, 0, ImageWidth, ImageHeight));
  363. }
  364. foreach (var obj in Objects)
  365. {
  366. var control = obj.GetControl();
  367. var left = Canvas.GetLeft(control);
  368. var top = Canvas.GetTop(control);
  369. if (double.IsNaN(left)) left = 0;
  370. if (double.IsNaN(top)) top = 0;
  371. using (context.PushTransform(Matrix.CreateTranslation(new(left, top))))
  372. {
  373. control.Render(context);
  374. }
  375. }
  376. return renderBitmap;
  377. }
  378. public byte[] SaveImage()
  379. {
  380. var bitmap = GetImage();
  381. var stream = new MemoryStream();
  382. bitmap.Save(stream);
  383. return stream.ToArray();
  384. }
  385. #endregion
  386. #region Editing
  387. private void RefreshObjects()
  388. {
  389. Canvas.Children.Clear();
  390. foreach(var item in Objects)
  391. {
  392. item.Update();
  393. Canvas.Children.Add(item.GetControl());
  394. }
  395. }
  396. private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  397. {
  398. RefreshObjects();
  399. }
  400. Point ConvertToImageCoordinates(Point canvasCoordinates)
  401. {
  402. return canvasCoordinates;// new(canvasCoordinates.X / ScaleFactor, canvasCoordinates.Y / ScaleFactor);
  403. }
  404. private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
  405. {
  406. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  407. switch (Mode)
  408. {
  409. case ImageEditingMode.Polyline:
  410. CurrentObject = new PolylineObject
  411. {
  412. Points = [position],
  413. PrimaryBrush = PrimaryBrush,
  414. Thickness = LineThickness
  415. };
  416. AddObject(CurrentObject);
  417. break;
  418. case ImageEditingMode.Rectangle:
  419. CurrentObject = new RectangleObject
  420. {
  421. Point1 = position,
  422. Point2 = position,
  423. PrimaryBrush = PrimaryBrush,
  424. SecondaryBrush = SecondaryBrush,
  425. Thickness = LineThickness
  426. };
  427. AddObject(CurrentObject);
  428. break;
  429. case ImageEditingMode.Ellipse:
  430. CurrentObject = new EllipseObject
  431. {
  432. Point1 = position,
  433. Point2 = position,
  434. PrimaryBrush = PrimaryBrush,
  435. SecondaryBrush = SecondaryBrush,
  436. Thickness = LineThickness
  437. };
  438. AddObject(CurrentObject);
  439. break;
  440. case ImageEditingMode.Text:
  441. CurrentObject = new SelectionObject
  442. {
  443. Point1 = position,
  444. Point2 = position,
  445. PrimaryBrush = PrimaryBrush
  446. };
  447. AddObject(CurrentObject);
  448. break;
  449. }
  450. }
  451. private void Canvas_PointerMoved(object? sender, PointerEventArgs e)
  452. {
  453. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  454. switch (CurrentObject)
  455. {
  456. case PolylineObject polyline:
  457. polyline.Points.Add(position);
  458. polyline.Update();
  459. Changed?.Invoke(this, new EventArgs());
  460. break;
  461. case RectangleObject rectangle:
  462. rectangle.Point2 = position;
  463. rectangle.Update();
  464. Changed?.Invoke(this, new EventArgs());
  465. break;
  466. case EllipseObject ellipse:
  467. ellipse.Point2 = position;
  468. ellipse.Update();
  469. Changed?.Invoke(this, new EventArgs());
  470. break;
  471. case SelectionObject textSelection:
  472. textSelection.Point2 = position;
  473. textSelection.Update();
  474. Changed?.Invoke(this, new EventArgs());
  475. break;
  476. }
  477. }
  478. private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
  479. {
  480. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  481. switch (CurrentObject)
  482. {
  483. case PolylineObject polyline:
  484. polyline.Points.Add(position);
  485. polyline.Update();
  486. CurrentObject = null;
  487. Changed?.Invoke(this, new EventArgs());
  488. break;
  489. case RectangleObject rectangle:
  490. rectangle.Point2 = position;
  491. rectangle.Update();
  492. CurrentObject = null;
  493. Changed?.Invoke(this, new EventArgs());
  494. break;
  495. case EllipseObject ellipse:
  496. ellipse.Point2 = position;
  497. ellipse.Update();
  498. CurrentObject = null;
  499. Changed?.Invoke(this, new EventArgs());
  500. break;
  501. case SelectionObject selection:
  502. selection.Point2 = position;
  503. Objects.Remove(selection);
  504. CurrentObject = null;
  505. CreateObjectFromSelection(selection).ContinueWith(task =>
  506. {
  507. if(task.Exception != null)
  508. {
  509. MobileLogging.LogExceptionMessage(task.Exception);
  510. }
  511. });
  512. break;
  513. }
  514. }
  515. private async Task CreateObjectFromSelection(SelectionObject selection)
  516. {
  517. switch (Mode)
  518. {
  519. case ImageEditingMode.Text:
  520. var text = await Navigation.Popup<TextEditViewModel, string?>(x => { });
  521. if(text is null)
  522. {
  523. return;
  524. }
  525. CurrentObject = new TextObject
  526. {
  527. Point = selection.GetTopLeft(),
  528. Size = selection.GetSize(),
  529. Text = text,
  530. PrimaryBrush = selection.PrimaryBrush
  531. };
  532. AddObject(CurrentObject);
  533. break;
  534. }
  535. }
  536. #endregion
  537. }