ImageEditor.axaml.cs 31 KB

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