ImageEditor.axaml.cs 29 KB

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