ImageEditor.axaml.cs 29 KB

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