ImageEditor.axaml.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  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. [RelayCommand]
  320. private void SetMode(ImageEditingMode mode)
  321. {
  322. foreach(var button in ModeButtons)
  323. {
  324. button.Active = button.Mode == mode;
  325. }
  326. Mode = mode;
  327. GestureRecognizer.IsEnabled = Mode == ImageEditingMode.Select;
  328. // ShapeButton.Content = CreateModeButtonContent(mode);
  329. SecondaryColour.IsVisible = HasSecondaryColour();
  330. LineThicknessButton.IsVisible = HasLineThickness();
  331. FontSizeButton.IsVisible = Mode == ImageEditingMode.Text;
  332. }
  333. private bool HasSecondaryColour()
  334. {
  335. return Mode == ImageEditingMode.Rectangle || Mode == ImageEditingMode.Ellipse;
  336. }
  337. private bool HasLineThickness()
  338. {
  339. return Mode == ImageEditingMode.Rectangle
  340. || Mode == ImageEditingMode.Ellipse
  341. || Mode == ImageEditingMode.Polyline
  342. || Mode == ImageEditingMode.Dimension;
  343. }
  344. #endregion
  345. #region Mode Buttons
  346. private void AddModeButtons()
  347. {
  348. AddModeButton(ImageEditingMode.Select);
  349. AddModeButton(ImageEditingMode.Polyline);
  350. AddModeButton(ImageEditingMode.Rectangle);
  351. AddModeButton(ImageEditingMode.Ellipse);
  352. AddModeButton(ImageEditingMode.Text);
  353. AddModeButton(ImageEditingMode.Dimension);
  354. }
  355. private void AddModeButton(ImageEditingMode mode)
  356. {
  357. ModeButtons.Add(new(mode, CreateModeButtonContent(mode), mode == Mode));
  358. }
  359. private Control? CreateModeButtonContent(ImageEditingMode mode, bool bindColour = false)
  360. {
  361. switch (mode)
  362. {
  363. case ImageEditingMode.Select:
  364. var selectImage = new Image
  365. {
  366. Source = Images.move,
  367. Width = 25,
  368. Height = 25
  369. };
  370. return selectImage;
  371. case ImageEditingMode.Polyline:
  372. var canvas = new Canvas();
  373. {
  374. var points = new Point[] { new(0, 0), new(20, 8), new(5, 16), new(25, 25) };
  375. var line1 = new Polyline { Points = points, Width = 25, Height = 25 };
  376. var line2 = new Polyline { Points = points, Width = 25, Height = 25 };
  377. line1.StrokeThickness = 4;
  378. line1.StrokeLineCap = PenLineCap.Round;
  379. line1.StrokeJoin = PenLineJoin.Round;
  380. line1.Stroke = new SolidColorBrush(Colors.Black);
  381. canvas.Children.Add(line1);
  382. if (bindColour)
  383. {
  384. line1.StrokeThickness = 5;
  385. line2.StrokeThickness = 4;
  386. line2.StrokeLineCap = PenLineCap.Round;
  387. line2.StrokeJoin = PenLineJoin.Round;
  388. line2.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  389. {
  390. Source = this,
  391. Converter = ImageEditorRemoveOpacityConverter.Instance
  392. });
  393. canvas.Children.Add(line2);
  394. }
  395. }
  396. return canvas;
  397. case ImageEditingMode.Rectangle:
  398. canvas = new Canvas();
  399. canvas.Width = 25;
  400. canvas.Height = 25;
  401. var rectangle = new Rectangle();
  402. if (bindColour)
  403. {
  404. rectangle.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  405. {
  406. Source = this,
  407. Converter = ImageEditorRemoveOpacityConverter.Instance
  408. });
  409. rectangle.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  410. {
  411. Source = this,
  412. Converter = ImageEditorTransparentImageBrushConverter.Instance
  413. });
  414. }
  415. else
  416. {
  417. rectangle.Stroke = new SolidColorBrush(Colors.Black);
  418. rectangle.Fill = new SolidColorBrush(Colors.White);
  419. }
  420. rectangle.StrokeThickness = 1.0;
  421. rectangle.Width = 25;
  422. rectangle.Height = 25;
  423. canvas.Children.Add(rectangle);
  424. return canvas;
  425. case ImageEditingMode.Ellipse:
  426. canvas = new Canvas();
  427. canvas.Width = 25;
  428. canvas.Height = 25;
  429. var ellipse = new Ellipse();
  430. if (bindColour)
  431. {
  432. ellipse.Bind(Rectangle.StrokeProperty, new Binding(nameof(PrimaryBrush))
  433. {
  434. Source = this,
  435. Converter = ImageEditorRemoveOpacityConverter.Instance
  436. });
  437. ellipse.Bind(Rectangle.FillProperty, new Binding(nameof(SecondaryBrush))
  438. {
  439. Source = this,
  440. Converter = ImageEditorTransparentImageBrushConverter.Instance
  441. });
  442. }
  443. else
  444. {
  445. ellipse.Stroke = new SolidColorBrush(Colors.Black);
  446. ellipse.Fill = new SolidColorBrush(Colors.White);
  447. }
  448. ellipse.StrokeThickness = 1.0;
  449. ellipse.Width = 25;
  450. ellipse.Height = 25;
  451. canvas.Children.Add(ellipse);
  452. return canvas;
  453. case ImageEditingMode.Text:
  454. var textBox = new TextBlock();
  455. textBox.Text = "T";
  456. textBox.FontSize = 25;
  457. textBox.TextAlignment = TextAlignment.Center;
  458. textBox.HorizontalAlignment = HorizontalAlignment.Center;
  459. textBox.VerticalAlignment = VerticalAlignment.Center;
  460. if (bindColour)
  461. {
  462. textBox.Bind(TextBlock.ForegroundProperty, new Binding(nameof(PrimaryBrush))
  463. {
  464. Source = this,
  465. Converter = ImageEditorRemoveOpacityConverter.Instance
  466. });
  467. }
  468. return textBox;
  469. case ImageEditingMode.Dimension:
  470. canvas = new Canvas();
  471. canvas.Width = 25;
  472. canvas.Height = 25;
  473. {
  474. var dimLines = new List<Line>();
  475. dimLines.Add(new Line
  476. {
  477. StartPoint = new(2, 10),
  478. EndPoint = new(23, 10),
  479. StrokeLineCap = PenLineCap.Round
  480. });
  481. dimLines.Add(new Line
  482. {
  483. StartPoint = new(2, 10),
  484. EndPoint = new(5, 7),
  485. StrokeLineCap = PenLineCap.Square
  486. });
  487. dimLines.Add(new Line
  488. {
  489. StartPoint = new(2, 10),
  490. EndPoint = new(5, 13),
  491. StrokeLineCap = PenLineCap.Square
  492. });
  493. dimLines.Add(new Line
  494. {
  495. StartPoint = new(23, 10),
  496. EndPoint = new(20, 7),
  497. StrokeLineCap = PenLineCap.Square
  498. });
  499. dimLines.Add(new Line
  500. {
  501. StartPoint = new(23, 10),
  502. EndPoint = new(20, 13),
  503. StrokeLineCap = PenLineCap.Square
  504. });
  505. var dotLines = new List<Line>();
  506. dotLines.Add(new Line
  507. {
  508. StartPoint = new(2, 10),
  509. EndPoint = new(2, 24),
  510. StrokeDashArray = [2, 2]
  511. });
  512. dotLines.Add(new Line
  513. {
  514. StartPoint = new(23, 10),
  515. EndPoint = new(23, 24),
  516. StrokeDashArray = [2, 2]
  517. });
  518. var number = new TextBlock
  519. {
  520. Text = "10",
  521. FontSize = 9,
  522. TextAlignment = TextAlignment.Center,
  523. Width = 25
  524. };
  525. Canvas.SetLeft(number, 0);
  526. Canvas.SetTop(number, -1);
  527. foreach (var line in dimLines)
  528. {
  529. line.StrokeThickness = 2;
  530. line.Stroke = new SolidColorBrush(Colors.Black);
  531. }
  532. foreach (var line in dotLines)
  533. {
  534. line.StrokeThickness = 1;
  535. line.Stroke = new SolidColorBrush(Colors.Black);
  536. }
  537. if (bindColour)
  538. {
  539. foreach (var line in dimLines)
  540. {
  541. line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  542. {
  543. Source = this,
  544. Converter = ImageEditorRemoveOpacityConverter.Instance
  545. });
  546. }
  547. foreach (var line in dotLines)
  548. {
  549. line.Bind(Polyline.StrokeProperty, new Binding(nameof(PrimaryBrush))
  550. {
  551. Source = this,
  552. Converter = ImageEditorRemoveOpacityConverter.Instance
  553. });
  554. }
  555. }
  556. foreach (var line in dimLines)
  557. {
  558. canvas.Children.Add(line);
  559. }
  560. foreach (var line in dotLines)
  561. {
  562. canvas.Children.Add(line);
  563. }
  564. canvas.Children.Add(number);
  565. }
  566. return canvas;
  567. default:
  568. return null;
  569. }
  570. }
  571. #endregion
  572. #region Public Interface
  573. public void Reset()
  574. {
  575. Objects.Clear();
  576. RedoStack.Clear();
  577. UpdateUndoRedoButtons();
  578. Changed?.Invoke(this, new EventArgs());
  579. }
  580. public Bitmap GetImage()
  581. {
  582. var renderBitmap = new RenderTargetBitmap(new PixelSize(ImageWidth, ImageHeight));
  583. renderBitmap.Render(Image);
  584. using var context = renderBitmap.CreateDrawingContext();
  585. if(Source is not null)
  586. {
  587. context.DrawImage(Source, new(0, 0, ImageWidth, ImageHeight));
  588. }
  589. CurrentObject = null;
  590. var state = new ImageEditorState(1);
  591. foreach (var obj in Objects)
  592. {
  593. var control = obj.GetControl();
  594. Render(context, control);
  595. }
  596. return renderBitmap;
  597. }
  598. private void Render(DrawingContext context, Control control)
  599. {
  600. var left = Canvas.GetLeft(control);
  601. var top = Canvas.GetTop(control);
  602. if (double.IsNaN(left)) left = 0;
  603. if (double.IsNaN(top)) top = 0;
  604. var matrix = Matrix.CreateTranslation(new(left, top));
  605. if(control.RenderTransform is not null)
  606. {
  607. Vector offset;
  608. if(control.RenderTransformOrigin.Unit == RelativeUnit.Relative)
  609. {
  610. offset = new Vector(
  611. control.Bounds.Width * control.RenderTransformOrigin.Point.X,
  612. control.Bounds.Height * control.RenderTransformOrigin.Point.Y);
  613. }
  614. else
  615. {
  616. offset = new Vector(control.RenderTransformOrigin.Point.X, control.RenderTransformOrigin.Point.Y);
  617. }
  618. matrix = (Matrix.CreateTranslation(-offset) * control.RenderTransform.Value * Matrix.CreateTranslation(offset)) * matrix;
  619. }
  620. using (context.PushTransform(matrix))
  621. {
  622. control.Render(context);
  623. if(control is Panel panel)
  624. {
  625. foreach(var child in panel.Children)
  626. {
  627. Render(context, child);
  628. }
  629. }
  630. }
  631. }
  632. public byte[] SaveImage()
  633. {
  634. var bitmap = GetImage();
  635. var stream = new MemoryStream();
  636. bitmap.Save(stream);
  637. return stream.ToArray();
  638. }
  639. #endregion
  640. #region Editing
  641. private void RefreshObjects()
  642. {
  643. Canvas.Children.Clear();
  644. foreach(var item in Objects)
  645. {
  646. item.Update();
  647. Canvas.Children.Add(item.GetControl());
  648. }
  649. }
  650. private void Objects_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  651. {
  652. RefreshObjects();
  653. }
  654. Point ConvertToImageCoordinates(Point canvasCoordinates)
  655. {
  656. return canvasCoordinates;// new(canvasCoordinates.X / ScaleFactor, canvasCoordinates.Y / ScaleFactor);
  657. }
  658. private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
  659. {
  660. CurrentObject = null;
  661. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  662. switch (Mode)
  663. {
  664. case ImageEditingMode.Polyline:
  665. CurrentObject = new PolylineObject
  666. {
  667. Points = [position],
  668. PrimaryBrush = PrimaryBrush,
  669. Thickness = LineThickness
  670. };
  671. AddObject(CurrentObject);
  672. break;
  673. case ImageEditingMode.Rectangle:
  674. CurrentObject = new RectangleObject
  675. {
  676. Point1 = position,
  677. Point2 = position,
  678. PrimaryBrush = PrimaryBrush,
  679. SecondaryBrush = SecondaryBrush,
  680. Thickness = LineThickness
  681. };
  682. AddObject(CurrentObject);
  683. break;
  684. case ImageEditingMode.Ellipse:
  685. CurrentObject = new EllipseObject
  686. {
  687. Point1 = position,
  688. Point2 = position,
  689. PrimaryBrush = PrimaryBrush,
  690. SecondaryBrush = SecondaryBrush,
  691. Thickness = LineThickness
  692. };
  693. AddObject(CurrentObject);
  694. break;
  695. case ImageEditingMode.Dimension:
  696. CurrentObject = new DimensionObject
  697. {
  698. Point1 = position,
  699. Point2 = position,
  700. PrimaryBrush = PrimaryBrush,
  701. Text = "",
  702. Offset = 30,
  703. LineThickness = LineThickness
  704. };
  705. AddObject(CurrentObject);
  706. break;
  707. }
  708. }
  709. private void Canvas_PointerMoved(object? sender, PointerEventArgs e)
  710. {
  711. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  712. switch (CurrentObject)
  713. {
  714. case PolylineObject polyline:
  715. polyline.Points.Add(position);
  716. polyline.Update();
  717. Changed?.Invoke(this, new EventArgs());
  718. break;
  719. case RectangleObject rectangle:
  720. rectangle.Point2 = position;
  721. rectangle.Update();
  722. Changed?.Invoke(this, new EventArgs());
  723. break;
  724. case EllipseObject ellipse:
  725. ellipse.Point2 = position;
  726. ellipse.Update();
  727. Changed?.Invoke(this, new EventArgs());
  728. break;
  729. case SelectionObject textSelection:
  730. textSelection.Point2 = position;
  731. textSelection.Update();
  732. Changed?.Invoke(this, new EventArgs());
  733. break;
  734. case DimensionObject dimension:
  735. if (!dimension.Complete)
  736. {
  737. dimension.Point2 = position;
  738. dimension.Update();
  739. Changed?.Invoke(this, new EventArgs());
  740. }
  741. break;
  742. }
  743. }
  744. private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
  745. {
  746. var position = ConvertToImageCoordinates(e.GetPosition(Canvas));
  747. switch (CurrentObject)
  748. {
  749. case PolylineObject polyline:
  750. polyline.Points.Add(position);
  751. polyline.Update();
  752. CurrentObject = null;
  753. Changed?.Invoke(this, new EventArgs());
  754. break;
  755. case RectangleObject rectangle:
  756. rectangle.Point2 = position;
  757. rectangle.Update();
  758. CurrentObject = null;
  759. Changed?.Invoke(this, new EventArgs());
  760. break;
  761. case EllipseObject ellipse:
  762. ellipse.Point2 = position;
  763. ellipse.Update();
  764. CurrentObject = null;
  765. Changed?.Invoke(this, new EventArgs());
  766. break;
  767. case DimensionObject dimension:
  768. dimension.Point2 = position;
  769. if(dimension.Point1 == dimension.Point2)
  770. {
  771. Objects.Remove(dimension);
  772. CurrentObject = null;
  773. return;
  774. }
  775. dimension.Complete = true;
  776. Navigation.Popup<TextDialogViewModel, string?>(x =>
  777. {
  778. x.Title = "Enter Dimension:";
  779. }).ContinueWith(task =>
  780. {
  781. dimension.Text = task.Result ?? "";
  782. dimension.Update();
  783. }, TaskScheduler.FromCurrentSynchronizationContext());
  784. Changed?.Invoke(this, new EventArgs());
  785. break;
  786. default:
  787. switch (Mode)
  788. {
  789. case ImageEditingMode.Text:
  790. Navigation.Popup<TextDialogViewModel, string?>(x =>
  791. {
  792. x.Title = "Enter Text:";
  793. }).ContinueWith(task =>
  794. {
  795. var text = new TextObject
  796. {
  797. Text = task.Result ?? "",
  798. FontSize = FontSize,
  799. PrimaryBrush = PrimaryBrush,
  800. Point = position
  801. };
  802. AddObject(text);
  803. Changed?.Invoke(this, new EventArgs());
  804. }, TaskScheduler.FromCurrentSynchronizationContext());
  805. break;
  806. }
  807. break;
  808. }
  809. }
  810. #endregion
  811. }