ImageEditor.axaml.cs 29 KB

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