ImageEditor.axaml.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  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. using Avalonia.LogicalTree;
  21. using CommunityToolkit.Mvvm.ComponentModel;
  22. namespace InABox.Avalonia.Components;
  23. public enum ImageEditingMode
  24. {
  25. Polyline,
  26. Rectangle,
  27. Ellipse,
  28. Text,
  29. Dimension
  30. }
  31. public partial class ImageEditorModeButton(ImageEditingMode mode, Control? content, bool active) : ObservableObject
  32. {
  33. public ImageEditingMode Mode { get; set; } = mode;
  34. public Control? Content { get; set; } = content;
  35. [ObservableProperty]
  36. private bool _active = active;
  37. }
  38. public class ImageEditorTransparentImageBrushConverter : AbstractConverter<IBrush?, IBrush?>
  39. {
  40. public static readonly ImageEditorTransparentImageBrushConverter Instance = new ImageEditorTransparentImageBrushConverter();
  41. protected override IBrush? Convert(IBrush? value, object? parameter = null)
  42. {
  43. if (value is SolidColorBrush solid && solid.Color.A == 255) return solid;
  44. var brush = new VisualBrush
  45. {
  46. TileMode = TileMode.Tile,
  47. DestinationRect = new(0, 0, 10, 10, RelativeUnit.Absolute)
  48. };
  49. var canvas = new Canvas
  50. {
  51. Width = 10,
  52. Height = 10
  53. };
  54. var rect1 = new Rectangle { Width = 5, Height = 5 };
  55. var rect2 = new Rectangle { Width = 5, Height = 5 };
  56. Canvas.SetLeft(rect2, 5);
  57. Canvas.SetTop(rect2, 5);
  58. rect1.Fill = new SolidColorBrush(Colors.LightGray);
  59. rect2.Fill = new SolidColorBrush(Colors.LightGray);
  60. var rect3 = new Rectangle { Width = 10, Height = 10 };
  61. rect3.Fill = value;
  62. canvas.Children.Add(rect1);
  63. canvas.Children.Add(rect2);
  64. canvas.Children.Add(rect3);
  65. brush.Visual = canvas;
  66. return brush;
  67. }
  68. }
  69. public class ImageEditorRemoveOpacityConverter : AbstractConverter<IBrush?, IBrush?>
  70. {
  71. public static readonly ImageEditorRemoveOpacityConverter Instance = new();
  72. protected override IBrush? Convert(IBrush? value, object? parameter = null)
  73. {
  74. if (value is SolidColorBrush solid)
  75. {
  76. return new SolidColorBrush(new Color(255, solid.Color.R, solid.Color.G, solid.Color.B));
  77. }
  78. return value;
  79. }
  80. }
  81. // TODO: Make it so we don't re-render everything everytime 'Objects' changes.
  82. public partial class ImageEditor : UserControl
  83. {
  84. public static readonly StyledProperty<IImage?> SourceProperty =
  85. AvaloniaProperty.Register<ImageEditor, IImage?>(nameof(Source));
  86. public static readonly StyledProperty<IBrush?> PrimaryBrushProperty =
  87. AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(PrimaryBrush), new SolidColorBrush(Colors.Black));
  88. public static readonly StyledProperty<IBrush?> SecondaryBrushProperty =
  89. AvaloniaProperty.Register<ImageEditor, IBrush?>(nameof(SecondaryBrush), new SolidColorBrush(Colors.White));
  90. public static readonly StyledProperty<double> LineThicknessProperty =
  91. AvaloniaProperty.Register<ImageEditor, double>(nameof(LineThickness), 3.0);
  92. public static readonly StyledProperty<int> ImageWidthProperty =
  93. AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageWidth), 100);
  94. public static readonly StyledProperty<int> ImageHeightProperty =
  95. AvaloniaProperty.Register<ImageEditor, int>(nameof(ImageHeight), 100);
  96. public static readonly StyledProperty<ImageEditingMode> ModeProperty =
  97. AvaloniaProperty.Register<ImageEditor, ImageEditingMode>(nameof(Mode), ImageEditingMode.Polyline);
  98. public static readonly StyledProperty<bool> ShowButtonsProperty =
  99. AvaloniaProperty.Register<ImageEditor, bool>(nameof(ShowButtons), true);
  100. public IImage? Source
  101. {
  102. get => GetValue(SourceProperty);
  103. set => SetValue(SourceProperty, value);
  104. }
  105. public int ImageWidth
  106. {
  107. get => GetValue(ImageWidthProperty);
  108. set => SetValue(ImageWidthProperty, value);
  109. }
  110. public int ImageHeight
  111. {
  112. get => GetValue(ImageHeightProperty);
  113. set => SetValue(ImageHeightProperty, value);
  114. }
  115. public bool ShowButtons
  116. {
  117. get => GetValue(ShowButtonsProperty);
  118. set => SetValue(ShowButtonsProperty, value);
  119. }
  120. #region Editing Properties
  121. public ImageEditingMode Mode
  122. {
  123. get => GetValue(ModeProperty);
  124. set => SetValue(ModeProperty, value);
  125. }
  126. public IBrush? PrimaryBrush
  127. {
  128. get => GetValue(PrimaryBrushProperty);
  129. set => SetValue(PrimaryBrushProperty, value);
  130. }
  131. public IBrush? SecondaryBrush
  132. {
  133. get => GetValue(SecondaryBrushProperty);
  134. set => SetValue(SecondaryBrushProperty, value);
  135. }
  136. public double LineThickness
  137. {
  138. get => GetValue(LineThicknessProperty);
  139. set => SetValue(LineThicknessProperty, value);
  140. }
  141. #endregion
  142. #region Events
  143. public event EventHandler? Changed;
  144. #endregion
  145. #region Private Properties
  146. public ObservableCollection<ImageEditorModeButton> ModeButtons { get; set; } = new();
  147. private ObservableCollection<IImageEditorObject> Objects = new();
  148. private IImageEditorObject? _currentObject;
  149. private IImageEditorObject? CurrentObject
  150. {
  151. get => _currentObject;
  152. set
  153. {
  154. _currentObject?.SetActive(false);
  155. _currentObject = value;
  156. }
  157. }
  158. private Stack<IImageEditorObject> RedoStack = new();
  159. private double ScaleFactor = 1.0;
  160. private double _originalScaleFactor = 1.0;
  161. // Center of the image.
  162. private Point ImageCenter = new();
  163. #endregion
  164. static ImageEditor()
  165. {
  166. SourceProperty.Changed.AddClassHandler<ImageEditor>(Source_Changed);
  167. }
  168. private static void Source_Changed(ImageEditor editor, AvaloniaPropertyChangedEventArgs args)
  169. {
  170. if(editor.Source is not null)
  171. {
  172. editor.ImageWidth = (int)Math.Floor(editor.Source.Size.Width);
  173. editor.ImageHeight = (int)Math.Floor(editor.Source.Size.Height);
  174. editor.PositionImage();
  175. }
  176. }
  177. public ImageEditor()
  178. {
  179. InitializeComponent();
  180. Objects.CollectionChanged += Objects_CollectionChanged;
  181. AddModeButtons();
  182. SetMode(Mode);
  183. OuterCanvas.LayoutUpdated += OuterCanvas_LayoutUpdated;
  184. OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEndedEvent, OuterCanvas_PinchEnded);
  185. OuterCanvas.AddHandler(PanAndZoomGestureRecognizer.PanAndZoomEvent, OuterCanvas_Pinch);
  186. }
  187. private void OuterCanvas_PinchEnded(object? sender, PanAndZoomEndedEventArgs e)
  188. {
  189. _originalScaleFactor = ScaleFactor;
  190. }
  191. private void OuterCanvas_Pinch(object? sender, PanAndZoomEventArgs e)
  192. {
  193. // Convert Scale Origin to image coordinates (relative to center).
  194. // Work out where this position will move to under the new scaling.
  195. // Adjust so that these are the same.
  196. var pos = (e.ScaleOrigin - e.Pan) - ImageCenter;
  197. var contentMPos = pos / ScaleFactor;
  198. ScaleFactor = _originalScaleFactor * e.Scale;
  199. var scaledPos = ImageCenter + contentMPos * ScaleFactor;
  200. var offset = scaledPos - e.ScaleOrigin;
  201. ImageCenter -= offset;
  202. UpdateCanvasPosition();
  203. }
  204. #region Layout
  205. private void OuterCanvas_LayoutUpdated(object? sender, EventArgs e)
  206. {
  207. // PositionImage();
  208. }
  209. protected override void OnLoaded(RoutedEventArgs e)
  210. {
  211. base.OnLoaded(e);
  212. PositionImage();
  213. }
  214. private void PositionImage()
  215. {
  216. var canvasWidth = OuterCanvas.Bounds.Width;
  217. var canvasHeight = OuterCanvas.Bounds.Height;
  218. var scaleFactor = Math.Min(canvasWidth / ImageWidth, canvasHeight / ImageHeight);
  219. ScaleFactor = scaleFactor;
  220. _originalScaleFactor = ScaleFactor;
  221. ImageCenter = new Point(
  222. OuterCanvas.Bounds.Width / 2,
  223. OuterCanvas.Bounds.Height / 2);
  224. UpdateCanvasPosition();
  225. }
  226. private void UpdateCanvasPosition()
  227. {
  228. var imageWidth = ImageWidth * ScaleFactor;
  229. var imageHeight = ImageHeight * ScaleFactor;
  230. ImageBorder.Width = imageWidth;
  231. ImageBorder.Height = imageHeight;
  232. Canvas.SetLeft(ImageBorder, ImageCenter.X - imageWidth / 2);
  233. Canvas.SetTop(ImageBorder, ImageCenter.Y - imageHeight / 2);
  234. Canvas.RenderTransform = new ScaleTransform(ScaleFactor, ScaleFactor);
  235. Canvas.Width = ImageWidth;
  236. Canvas.Height = ImageHeight;
  237. }
  238. #endregion
  239. #region Editing Commands
  240. private void UpdateUndoRedoButtons()
  241. {
  242. UndoButton.IsEnabled = Objects.Count > 0;
  243. RedoButton.IsEnabled = RedoStack.Count > 0;
  244. }
  245. [RelayCommand]
  246. private void Undo()
  247. {
  248. if (Objects.Count == 0) return;
  249. RedoStack.Push(Objects[^1]);
  250. Objects.RemoveAt(Objects.Count - 1);
  251. UpdateUndoRedoButtons();
  252. Changed?.Invoke(this, new EventArgs());
  253. }
  254. [RelayCommand]
  255. private void Redo()
  256. {
  257. if (!RedoStack.TryPop(out var top)) return;
  258. Objects.Add(top);
  259. UpdateUndoRedoButtons();
  260. Changed?.Invoke(this, new EventArgs());
  261. }
  262. private void AddObject(IImageEditorObject obj)
  263. {
  264. Objects.Add(obj);
  265. RedoStack.Clear();
  266. UpdateUndoRedoButtons();
  267. Changed?.Invoke(this, new EventArgs());
  268. }
  269. [RelayCommand]
  270. private void SetMode(ImageEditingMode mode)
  271. {
  272. foreach(var button in ModeButtons)
  273. {
  274. button.Active = button.Mode == mode;
  275. }
  276. Mode = mode;
  277. // ShapeButton.Content = CreateModeButtonContent(mode);
  278. SecondaryColour.IsVisible = HasSecondaryColour();
  279. LineThicknessButton.IsVisible = HasLineThickness();
  280. }
  281. private bool HasSecondaryColour()
  282. {
  283. return Mode == ImageEditingMode.Rectangle || Mode == ImageEditingMode.Ellipse;
  284. }
  285. private bool HasLineThickness()
  286. {
  287. return Mode == ImageEditingMode.Rectangle
  288. || Mode == ImageEditingMode.Ellipse
  289. || Mode == ImageEditingMode.Polyline
  290. || Mode == ImageEditingMode.Dimension;
  291. }
  292. #endregion
  293. #region Mode Buttons
  294. private void AddModeButtons()
  295. {
  296. AddModeButton(ImageEditingMode.Polyline);
  297. AddModeButton(ImageEditingMode.Rectangle);
  298. AddModeButton(ImageEditingMode.Ellipse);
  299. AddModeButton(ImageEditingMode.Text);
  300. AddModeButton(ImageEditingMode.Dimension);
  301. }
  302. private void AddModeButton(ImageEditingMode mode)
  303. {
  304. ModeButtons.Add(new(mode, CreateModeButtonContent(mode), mode == Mode));
  305. }
  306. private Control? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
  307. {
  308. switch (mode)
  309. {
  310. case ImageEditingMode.Polyline:
  311. var canvas = new Canvas();
  312. {
  313. var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
  314. var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
  315. var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
  316. line1.StrokeThickness = 4;
  317. line1.StrokeLineCap = PenLineCap.Round;
  318. line1.StrokeJoin = PenLineJoin.Round;
  319. line1.Stroke = new SolidColorBrush(Colors.Black);
  320. canvas.Children.Add(line1);
  321. if (bindColour)
  322. {
  323. line1.StrokeThickness = 5;
  324. line2.StrokeThickness = 4;
  325. line2.StrokeLineCap = PenLineCap.Round;
  326. line2.StrokeJoin = PenLineJoin.Round;
  327. line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  328. {
  329. Source = this,
  330. Converter = ImageEditorRemoveOpacityConverter.Instance
  331. });
  332. canvas.Children.Add(line2);
  333. }
  334. }
  335. return canvas;
  336. case ImageEditingMode.Rectangle:
  337. canvas = new Canvas();
  338. canvas.Width = 25;
  339. canvas.Height = 25;
  340. var rectangle = new Rectangle();
  341. if (bindColour)
  342. {
  343. rectangle.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  344. {
  345. Source = this,
  346. Converter = ImageEditorRemoveOpacityConverter.Instance
  347. });
  348. rectangle.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  349. {
  350. Source = this,
  351. Converter = ImageEditorTransparentImageBrushConverter.Instance
  352. });
  353. }
  354. else
  355. {
  356. rectangle.Stroke = new SolidColorBrush(Colors.Black);
  357. rectangle.Fill = new SolidColorBrush(Colors.White);
  358. }
  359. rectangle.StrokeThickness = 1.0;
  360. rectangle.Width = 25;
  361. rectangle.Height = 25;
  362. canvas.Children.Add(rectangle);
  363. return canvas;
  364. case ImageEditingMode.Ellipse:
  365. canvas = new Canvas();
  366. canvas.Width = 25;
  367. canvas.Height = 25;
  368. var ellipse = new Ellipse();
  369. if (bindColour)
  370. {
  371. ellipse.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  372. {
  373. Source = this,
  374. Converter = ImageEditorRemoveOpacityConverter.Instance
  375. });
  376. ellipse.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  377. {
  378. Source = this,
  379. Converter = ImageEditorTransparentImageBrushConverter.Instance
  380. });
  381. }
  382. else
  383. {
  384. ellipse.Stroke = new SolidColorBrush(Colors.Black);
  385. ellipse.Fill = new SolidColorBrush(Colors.White);
  386. }
  387. ellipse.StrokeThickness = 1.0;
  388. ellipse.Width = 25;
  389. ellipse.Height = 25;
  390. canvas.Children.Add(ellipse);
  391. return canvas;
  392. case ImageEditingMode.Text:
  393. var textBox = new TextBlock();
  394. textBox.Text = "T";
  395. textBox.FontSize = 25;
  396. textBox.TextAlignment = TextAlignment.Center;
  397. textBox.HorizontalAlignment = HorizontalAlignment.Center;
  398. textBox.VerticalAlignment = VerticalAlignment.Center;
  399. if (bindColour)
  400. {
  401. textBox.Bind(TextBlock.ForegroundProperty, new Binding(nameof(PrimaryBrush))
  402. {
  403. Source = this,
  404. Converter = ImageEditorRemoveOpacityConverter.Instance
  405. });
  406. }
  407. return textBox;
  408. case ImageEditingMode.Dimension:
  409. canvas = new Canvas();
  410. canvas.Width = 25;
  411. canvas.Height = 25;
  412. {
  413. var dimLines = new List<Line>();
  414. dimLines.Add(new Line
  415. {
  416. StartPoint = new(2, 10),
  417. EndPoint = new(23, 10),
  418. StrokeLineCap = PenLineCap.Round
  419. });
  420. dimLines.Add(new Line
  421. {
  422. StartPoint = new(2, 10),
  423. EndPoint = new(5, 7),
  424. StrokeLineCap = PenLineCap.Square
  425. });
  426. dimLines.Add(new Line
  427. {
  428. StartPoint = new(2, 10),
  429. EndPoint = new(5, 13),
  430. StrokeLineCap = PenLineCap.Square
  431. });
  432. dimLines.Add(new Line
  433. {
  434. StartPoint = new(23, 10),
  435. EndPoint = new(20, 7),
  436. StrokeLineCap = PenLineCap.Square
  437. });
  438. dimLines.Add(new Line
  439. {
  440. StartPoint = new(23, 10),
  441. EndPoint = new(20, 13),
  442. StrokeLineCap = PenLineCap.Square
  443. });
  444. var dotLines = new List<Line>();
  445. dotLines.Add(new Line
  446. {
  447. StartPoint = new(2, 10),
  448. EndPoint = new(2, 24),
  449. StrokeDashArray = [2, 2]
  450. });
  451. dotLines.Add(new Line
  452. {
  453. StartPoint = new(23, 10),
  454. EndPoint = new(23, 24),
  455. StrokeDashArray = [2, 2]
  456. });
  457. var number = new TextBlock
  458. {
  459. Text = "10",
  460. FontSize = 9,
  461. TextAlignment = TextAlignment.Center,
  462. Width = 25
  463. };
  464. Canvas.SetLeft(number, 0);
  465. Canvas.SetTop(number, -1);
  466. foreach (var line in dimLines)
  467. {
  468. line.StrokeThickness = 2;
  469. line.Stroke = new SolidColorBrush(Colors.Black);
  470. }
  471. foreach (var line in dotLines)
  472. {
  473. line.StrokeThickness = 1;
  474. line.Stroke = new SolidColorBrush(Colors.Black);
  475. }
  476. if (bindColour)
  477. {
  478. foreach (var line in dimLines)
  479. {
  480. line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  481. {
  482. Source = this,
  483. Converter = ImageEditorRemoveOpacityConverter.Instance
  484. });
  485. }
  486. foreach (var line in dotLines)
  487. {
  488. line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  489. {
  490. Source = this,
  491. Converter = ImageEditorRemoveOpacityConverter.Instance
  492. });
  493. }
  494. }
  495. foreach (var line in dimLines)
  496. {
  497. canvas.Children.Add(line);
  498. }
  499. foreach (var line in dotLines)
  500. {
  501. canvas.Children.Add(line);
  502. }
  503. canvas.Children.Add(number);
  504. }
  505. return canvas;
  506. default:
  507. return null;
  508. }
  509. }
  510. #endregion
  511. #region Public Interface
  512. public void Reset()
  513. {
  514. Objects.Clear();
  515. RedoStack.Clear();
  516. UpdateUndoRedoButtons();
  517. Changed?.Invoke(this, new EventArgs());
  518. }
  519. public Bitmap GetImage()
  520. {
  521. var renderBitmap = new RenderTargetBitmap(new PixelSize(ImageWidth, ImageHeight));
  522. renderBitmap.Render(Image);
  523. using var context = renderBitmap.CreateDrawingContext();
  524. if(Source is not null)
  525. {
  526. context.DrawImage(Source, new(0, 0, ImageWidth, ImageHeight));
  527. }
  528. CurrentObject = null;
  529. foreach (var obj in Objects)
  530. {
  531. var control = obj.GetControl();
  532. Render(context, control);
  533. }
  534. return renderBitmap;
  535. }
  536. private void Render(DrawingContext context, Control control)
  537. {
  538. var left = Canvas.GetLeft(control);
  539. var top = Canvas.GetTop(control);
  540. if (double.IsNaN(left)) left = 0;
  541. if (double.IsNaN(top)) top = 0;
  542. var matrix = Matrix.CreateTranslation(new(left, top));
  543. if(control.RenderTransform is not null)
  544. {
  545. Vector offset;
  546. if(control.RenderTransformOrigin.Unit == RelativeUnit.Relative)
  547. {
  548. offset = new Vector(
  549. control.Bounds.Width * control.RenderTransformOrigin.Point.X,
  550. control.Bounds.Height * control.RenderTransformOrigin.Point.Y);
  551. }
  552. else
  553. {
  554. offset = new Vector(control.RenderTransformOrigin.Point.X, control.RenderTransformOrigin.Point.Y);
  555. }
  556. matrix = (Matrix.CreateTranslation(-offset) * control.RenderTransform.Value * Matrix.CreateTranslation(offset)) * matrix;
  557. }
  558. using (context.PushTransform(matrix))
  559. {
  560. control.Render(context);
  561. if(control is Panel panel)
  562. {
  563. foreach(var child in panel.Children)
  564. {
  565. Render(context, child);
  566. }
  567. }
  568. }
  569. }
  570. public byte[] SaveImage()
  571. {
  572. var bitmap = GetImage();
  573. var stream = new MemoryStream();
  574. bitmap.Save(stream);
  575. return stream.ToArray();
  576. }
  577. #endregion
  578. #region Editing
  579. private void RefreshObjects()
  580. {
  581. Canvas.Children.Clear();
  582. foreach(var item in Objects)
  583. {
  584. item.Update();
  585. Canvas.Children.Add(item.GetControl());
  586. }
  587. }
  588. private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  589. {
  590. RefreshObjects();
  591. }
  592. Point ConvertToImageCoordinates(Point canvasCoordinates)
  593. {
  594. return canvasCoordinates;// new(canvasCoordinates.X / ScaleFactor, canvasCoordinates.Y / ScaleFactor);
  595. }
  596. private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
  597. {
  598. CurrentObject = null;
  599. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  600. switch (Mode)
  601. {
  602. case ImageEditingMode.Polyline:
  603. CurrentObject = new PolylineObject
  604. {
  605. Points = [position],
  606. PrimaryBrush = PrimaryBrush,
  607. Thickness = LineThickness
  608. };
  609. AddObject(CurrentObject);
  610. break;
  611. case ImageEditingMode.Rectangle:
  612. CurrentObject = new RectangleObject
  613. {
  614. Point1 = position,
  615. Point2 = position,
  616. PrimaryBrush = PrimaryBrush,
  617. SecondaryBrush = SecondaryBrush,
  618. Thickness = LineThickness
  619. };
  620. AddObject(CurrentObject);
  621. break;
  622. case ImageEditingMode.Ellipse:
  623. CurrentObject = new EllipseObject
  624. {
  625. Point1 = position,
  626. Point2 = position,
  627. PrimaryBrush = PrimaryBrush,
  628. SecondaryBrush = SecondaryBrush,
  629. Thickness = LineThickness
  630. };
  631. AddObject(CurrentObject);
  632. break;
  633. case ImageEditingMode.Text:
  634. CurrentObject = new SelectionObject
  635. {
  636. Point1 = position,
  637. Point2 = position,
  638. PrimaryBrush = PrimaryBrush
  639. };
  640. AddObject(CurrentObject);
  641. break;
  642. case ImageEditingMode.Dimension:
  643. CurrentObject = new DimensionObject
  644. {
  645. Point1 = position,
  646. Point2 = position,
  647. PrimaryBrush = PrimaryBrush,
  648. Text = "",
  649. Offset = 30,
  650. LineThickness = LineThickness
  651. };
  652. AddObject(CurrentObject);
  653. break;
  654. }
  655. }
  656. private void Canvas_PointerMoved(object? sender, PointerEventArgs e)
  657. {
  658. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  659. switch (CurrentObject)
  660. {
  661. case PolylineObject polyline:
  662. polyline.Points.Add(position);
  663. polyline.Update();
  664. Changed?.Invoke(this, new EventArgs());
  665. break;
  666. case RectangleObject rectangle:
  667. rectangle.Point2 = position;
  668. rectangle.Update();
  669. Changed?.Invoke(this, new EventArgs());
  670. break;
  671. case EllipseObject ellipse:
  672. ellipse.Point2 = position;
  673. ellipse.Update();
  674. Changed?.Invoke(this, new EventArgs());
  675. break;
  676. case SelectionObject textSelection:
  677. textSelection.Point2 = position;
  678. textSelection.Update();
  679. Changed?.Invoke(this, new EventArgs());
  680. break;
  681. case DimensionObject dimension:
  682. if (!dimension.Complete)
  683. {
  684. dimension.Point2 = position;
  685. dimension.Update();
  686. Changed?.Invoke(this, new EventArgs());
  687. }
  688. break;
  689. }
  690. }
  691. private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
  692. {
  693. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  694. switch (CurrentObject)
  695. {
  696. case PolylineObject polyline:
  697. polyline.Points.Add(position);
  698. polyline.Update();
  699. CurrentObject = null;
  700. Changed?.Invoke(this, new EventArgs());
  701. break;
  702. case RectangleObject rectangle:
  703. rectangle.Point2 = position;
  704. rectangle.Update();
  705. CurrentObject = null;
  706. Changed?.Invoke(this, new EventArgs());
  707. break;
  708. case EllipseObject ellipse:
  709. ellipse.Point2 = position;
  710. ellipse.Update();
  711. CurrentObject = null;
  712. Changed?.Invoke(this, new EventArgs());
  713. break;
  714. case SelectionObject selection:
  715. selection.Point2 = position;
  716. Objects.Remove(selection);
  717. CurrentObject = null;
  718. CreateObjectFromSelection(selection).ContinueWith(task =>
  719. {
  720. if(task.Exception != null)
  721. {
  722. MobileLogging.LogExceptionMessage(task.Exception);
  723. }
  724. });
  725. break;
  726. case DimensionObject dimension:
  727. dimension.Point2 = position;
  728. if(dimension.Point1 == dimension.Point2)
  729. {
  730. Objects.Remove(dimension);
  731. CurrentObject = null;
  732. return;
  733. }
  734. dimension.Complete = true;
  735. Navigation.Popup<TextEditViewModel, string?>(x => { }).ContinueWith(task =>
  736. {
  737. dimension.Text = task.Result ?? "";
  738. dimension.Update();
  739. }, TaskScheduler.FromCurrentSynchronizationContext());
  740. Changed?.Invoke(this, new EventArgs());
  741. break;
  742. }
  743. }
  744. private async Task CreateObjectFromSelection(SelectionObject selection)
  745. {
  746. switch (Mode)
  747. {
  748. case ImageEditingMode.Text:
  749. var text = await Navigation.Popup<TextEditViewModel, string?>(x => { });
  750. if(text is null)
  751. {
  752. return;
  753. }
  754. CurrentObject = new TextObject
  755. {
  756. Point = selection.GetTopLeft(),
  757. Size = selection.GetSize(),
  758. Text = text,
  759. PrimaryBrush = selection.PrimaryBrush
  760. };
  761. AddObject(CurrentObject);
  762. break;
  763. }
  764. }
  765. #endregion
  766. }