/* * The MIT License (MIT) * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so. */ // Port from: https://github.com/cyotek/Cyotek.Windows.Forms.ImageBox to AvaloniaUI // Modified from: https://github.com/sn4k3/UVtools/blob/master/UVtools.AvaloniaControls/AdvancedImageBox.axaml.cs using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; using System.Runtime.CompilerServices; using AsyncAwaitBestPractices; using AsyncImageLoader; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; using Bitmap = Avalonia.Media.Imaging.Bitmap; using Brushes = Avalonia.Media.Brushes; using Color = Avalonia.Media.Color; using Pen = Avalonia.Media.Pen; using Point = Avalonia.Point; using Size = Avalonia.Size; namespace StabilityMatrix.Avalonia.Controls; [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class AdvancedImageBox : TemplatedControl { #region Bindable Base /// /// Multicast event for property change notifications. /// private PropertyChangedEventHandler? _propertyChanged; public new event PropertyChangedEventHandler PropertyChanged { add => _propertyChanged += value; remove => _propertyChanged -= value; } protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, value)) return false; field = value; RaisePropertyChanged(propertyName); return true; } protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { } /// /// Notifies listeners that a property value has changed. /// /// /// Name of the property used to notify listeners. This /// value is optional and can be provided automatically when invoked from compilers /// that support . /// protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) { var e = new PropertyChangedEventArgs(propertyName); OnPropertyChanged(e); _propertyChanged?.Invoke(this, e); } #endregion #region Sub Classes /// /// Represents available levels of zoom in an control /// public class ZoomLevelCollection : IList { #region Public Constructors /// /// Initializes a new instance of the class. /// public ZoomLevelCollection() { List = new SortedList(); } /// /// Initializes a new instance of the class. /// /// The default values to populate the collection with. /// Thrown if the collection parameter is null public ZoomLevelCollection(IEnumerable collection) : this() { if (collection == null) { throw new ArgumentNullException(nameof(collection)); } AddRange(collection); } #endregion #region Public Class Properties /// /// Returns the default zoom levels /// public static ZoomLevelCollection Default => new( new[] { 7, 10, 13, 16, 20, 24, // 10 increments 30, 40, 50, 60, 70, // 100 increments 100, 200, 300, 400, 500, 600, 700, 800, // 400 increments 1200, 1600, 2000, 2400, // 800 increments 3200, 4000, 4800, // 1000 increments 5800, 6800, 7800, 8800 } ); #endregion #region Public Properties /// /// Gets the number of elements contained in the . /// /// /// The number of elements contained in the . /// public int Count => List.Count; /// /// Gets a value indicating whether the is read-only. /// /// true if this instance is read only; otherwise, false. /// true if the is read-only; otherwise, false. /// public bool IsReadOnly => false; /// /// Gets or sets the zoom level at the specified index. /// /// The index. public int this[int index] { get => List.Values[index]; set { List.RemoveAt(index); Add(value); } } #endregion #region Protected Properties /// /// Gets or sets the backing list. /// protected SortedList List { get; set; } #endregion #region Public Members /// /// Adds an item to the . /// /// The object to add to the . public void Add(int item) { List.Add(item, item); } /// /// Adds a range of items to the . /// /// The items to add to the collection. /// Thrown if the collection parameter is null. public void AddRange(IEnumerable collection) { if (collection == null) { throw new ArgumentNullException(nameof(collection)); } foreach (var value in collection) { Add(value); } } /// /// Removes all items from the . /// public void Clear() { List.Clear(); } /// /// Determines whether the contains a specific value. /// /// The object to locate in the . /// true if is found in the ; otherwise, false. public bool Contains(int item) { return List.ContainsKey(item); } /// /// Copies a range of elements this collection into a destination . /// /// The that receives the data. /// A 64-bit integer that represents the index in the at which storing begins. public void CopyTo(int[] array, int arrayIndex) { for (var i = 0; i < Count; i++) { array[arrayIndex + i] = List.Values[i]; } } /// /// Finds the index of a zoom level matching or nearest to the specified value. /// /// The zoom level. public int FindNearest(int zoomLevel) { var nearestValue = List.Values[0]; var nearestDifference = Math.Abs(nearestValue - zoomLevel); for (var i = 1; i < Count; i++) { var value = List.Values[i]; var difference = Math.Abs(value - zoomLevel); if (difference < nearestDifference) { nearestValue = value; nearestDifference = difference; } } return nearestValue; } /// /// Returns an enumerator that iterates through the collection. /// /// A that can be used to iterate through the collection. public IEnumerator GetEnumerator() { return List.Values.GetEnumerator(); } /// /// Determines the index of a specific item in the . /// /// The object to locate in the . /// The index of if found in the list; otherwise, -1. public int IndexOf(int item) { return List.IndexOfKey(item); } /// /// Not implemented. /// /// The index. /// The item. /// Not implemented public void Insert(int index, int item) { throw new NotImplementedException(); } /// /// Returns the next increased zoom level for the given current zoom. /// /// The current zoom level. /// When positive, constrain maximum zoom to this value /// The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom. public int NextZoom(int zoomLevel, int constrainZoomLevel = 0) { var index = IndexOf(FindNearest(zoomLevel)); if (index < Count - 1) index++; return constrainZoomLevel > 0 && this[index] >= constrainZoomLevel ? constrainZoomLevel : this[index]; } /// /// Returns the next decreased zoom level for the given current zoom. /// /// The current zoom level. /// When positive, constrain minimum zoom to this value /// The next matching decreased zoom level for the given current zoom if applicable, otherwise the nearest zoom. public int PreviousZoom(int zoomLevel, int constrainZoomLevel = 0) { var index = IndexOf(FindNearest(zoomLevel)); if (index > 0) index--; return constrainZoomLevel > 0 && this[index] <= constrainZoomLevel ? constrainZoomLevel : this[index]; } /// /// Removes the first occurrence of a specific object from the . /// /// The object to remove from the . /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . public bool Remove(int item) { return List.Remove(item); } /// /// Removes the element at the specified index of the . /// /// The zero-based index of the element to remove. public void RemoveAt(int index) { List.RemoveAt(index); } /// /// Copies the elements of the to a new array. /// /// An array containing copies of the elements of the . public int[] ToArray() { var results = new int[Count]; CopyTo(results, 0); return results; } #endregion #region IList Members /// /// Returns an enumerator that iterates through a collection. /// /// An object that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } #endregion #region Enums /// /// Determines the sizing mode of an image hosted in an control. /// public enum SizeModes : byte { /// /// The image is displayed according to current zoom and scroll properties. /// Normal, /// /// The image is stretched to fill the client area of the control. /// Stretch, /// /// The image is stretched to fill as much of the client area of the control as possible, whilst retaining the same aspect ratio for the width and height. /// Fit } [Flags] public enum MouseButtons : byte { None = 0, LeftButton = 1, MiddleButton = 2, RightButton = 4 } /// /// Describes the zoom action occurring /// [Flags] public enum ZoomActions : byte { /// /// No action. /// None = 0, /// /// The control is increasing the zoom. /// ZoomIn = 1, /// /// The control is decreasing the zoom. /// ZoomOut = 2, /// /// The control zoom was reset. /// ActualSize = 4 } public enum SelectionModes { /// /// No selection. /// None, /// /// Rectangle selection. /// Rectangle, /// /// Zoom selection. /// Zoom } #endregion #region UI Controls [NotNull] public ScrollBar? HorizontalScrollBar { get; private set; } [NotNull] public ScrollBar? VerticalScrollBar { get; private set; } [NotNull] public ContentPresenter? ViewPort { get; private set; } // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract public bool IsViewPortLoaded => ViewPort is not null; public Vector Offset { get => new(HorizontalScrollBar.Value, VerticalScrollBar.Value); set { HorizontalScrollBar.Value = value.X; VerticalScrollBar.Value = value.Y; RaisePropertyChanged(); TriggerRender(); } } public Size ViewPortSize => ViewPort.Bounds.Size; #endregion #region Private Members private Point _startMousePosition; private Vector _startScrollPosition; private bool _isPanning; private bool _isSelecting; private Bitmap? _trackerImage; private bool _canRender = true; private Point _pointerPosition; #endregion #region Properties public static readonly DirectProperty CanRenderProperty = AvaloniaProperty.RegisterDirect< AdvancedImageBox, bool >(nameof(CanRender), o => o.CanRender); /// /// Gets or sets if control can render the image /// public bool CanRender { get => _canRender; set { if (!SetAndRaise(CanRenderProperty, ref _canRender, value)) return; if (_canRender) TriggerRender(); } } public static readonly StyledProperty GridCellSizeProperty = AvaloniaProperty.Register< AdvancedImageBox, byte >(nameof(GridCellSize), 15); /// /// Gets or sets the grid cell size /// public byte GridCellSize { get => GetValue(GridCellSizeProperty); set => SetValue(GridCellSizeProperty, value); } public static readonly StyledProperty GridColorProperty = AvaloniaProperty.Register< AdvancedImageBox, ISolidColorBrush >(nameof(GridColor), SolidColorBrush.Parse("#181818")); /// /// Gets or sets the color used to create the checkerboard style background /// public ISolidColorBrush GridColor { get => GetValue(GridColorProperty); set => SetValue(GridColorProperty, value); } public static readonly StyledProperty GridColorAlternateProperty = AvaloniaProperty.Register< AdvancedImageBox, ISolidColorBrush >(nameof(GridColorAlternate), SolidColorBrush.Parse("#252525")); /// /// Gets or sets the color used to create the checkerboard style background /// public ISolidColorBrush GridColorAlternate { get => GetValue(GridColorAlternateProperty); set => SetValue(GridColorAlternateProperty, value); } public static readonly StyledProperty SourceProperty = AvaloniaProperty.Register< AdvancedImageBox, string? >("Source"); public string? Source { get => GetValue(SourceProperty); set { SetValue(SourceProperty, value); // Also set the image if (value is not null) { var loader = ImageLoader.AsyncImageLoader; Dispatcher .UIThread .InvokeAsync(async () => { Image = await loader.ProvideImageAsync(value); }) .SafeFireAndForget(); } } } public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register( nameof(Image) ); /// /// Gets or sets the image to be displayed /// public Bitmap? Image { get => GetValue(ImageProperty); set => SetValue(ImageProperty, value); } /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); if (change.Property == ImageProperty) { var (oldImage, newImage) = change.GetOldAndNewValue(); Vector? offsetBackup = null; if (newImage is null) { SelectNone(); } else if (IsViewPortLoaded) { if (oldImage is null) { ZoomToFit(); RestoreSizeMode(); } else if (newImage.Size != oldImage.Size) { offsetBackup = Offset; var zoomFactorScale = (float)GetZoomLevelToFit(newImage) / GetZoomLevelToFit(oldImage); var imageScale = newImage.Size / oldImage.Size; Debug.WriteLine($"Image scale: {imageScale}"); /*var oldScaledSize = oldImage.Size * ZoomFactor; var newScaledSize = newImage.Size * ZoomFactor; Debug.WriteLine($"Old scaled {oldScaledSize} -> new scaled {newScaledSize}");*/ var currentZoom = Zoom; var currentFactor = ZoomFactor; // var currentOffset = Offset; // Scale zoom and offset to new size Zoom = (int)Math.Floor(Zoom * zoomFactorScale); /*Offset = new Vector( Offset.X * imageScale.X, Offset.Y * imageScale.Y );*/ Debug.WriteLine($"Zoom changed from {currentZoom} to {Zoom}"); Debug.WriteLine($"Zoom factor changed from {currentFactor} to {ZoomFactor}"); } if (offsetBackup is not null) { Offset = offsetBackup.Value; } UpdateViewPort(); TriggerRender(); } RaisePropertyChanged(nameof(IsImageLoaded)); } } /*public WriteableBitmap? ImageAsWriteableBitmap { get { if (Image is null) return null; return (WriteableBitmap)Image; } }*/ public bool IsImageLoaded => Image is not null; public static readonly DirectProperty TrackerImageProperty = AvaloniaProperty.RegisterDirect( nameof(TrackerImage), o => o.TrackerImage, (o, v) => o.TrackerImage = v ); /// /// Gets or sets an image to follow the mouse pointer /// public Bitmap? TrackerImage { get => _trackerImage; set { if (!SetAndRaise(TrackerImageProperty, ref _trackerImage, value)) return; TriggerRender(); RaisePropertyChanged(nameof(HaveTrackerImage)); } } public bool HaveTrackerImage => _trackerImage is not null; public static readonly StyledProperty TrackerImageAutoZoomProperty = AvaloniaProperty.Register< AdvancedImageBox, bool >(nameof(TrackerImageAutoZoom), true); /// /// Gets or sets if the tracker image will be scaled to the current zoom /// public bool TrackerImageAutoZoom { get => GetValue(TrackerImageAutoZoomProperty); set => SetValue(TrackerImageAutoZoomProperty, value); } public static readonly StyledProperty IsTrackerImageEnabledProperty = AvaloniaProperty.Register< AdvancedImageBox, bool >("IsTrackerImageEnabled"); public bool IsTrackerImageEnabled { get => GetValue(IsTrackerImageEnabledProperty); set => SetValue(IsTrackerImageEnabledProperty, value); } public bool IsHorizontalBarVisible { get { if (Image is null) return false; if (SizeMode != SizeModes.Normal) return false; return ScaledImageWidth > ViewPortSize.Width; } } public bool IsVerticalBarVisible { get { if (Image is null) return false; if (SizeMode != SizeModes.Normal) return false; return ScaledImageHeight > ViewPortSize.Height; } } public static readonly StyledProperty ShowGridProperty = AvaloniaProperty.Register( nameof(ShowGrid), true ); /// /// Gets or sets the grid visibility when reach high zoom levels /// public bool ShowGrid { get => GetValue(ShowGridProperty); set => SetValue(ShowGridProperty, value); } public static readonly DirectProperty PointerPositionProperty = AvaloniaProperty.RegisterDirect(nameof(PointerPosition), o => o.PointerPosition); /// /// Gets the current pointer position /// public Point PointerPosition { get => _pointerPosition; private set => SetAndRaise(PointerPositionProperty, ref _pointerPosition, value); } public static readonly DirectProperty IsPanningProperty = AvaloniaProperty.RegisterDirect< AdvancedImageBox, bool >(nameof(IsPanning), o => o.IsPanning); /// /// Gets if control is currently panning /// public bool IsPanning { get => _isPanning; protected set { if (!SetAndRaise(IsPanningProperty, ref _isPanning, value)) return; _startScrollPosition = Offset; if (value) { Cursor = new Cursor(StandardCursorType.SizeAll); //this.OnPanStart(EventArgs.Empty); } else { Cursor = Cursor.Default; //this.OnPanEnd(EventArgs.Empty); } } } public static readonly DirectProperty IsSelectingProperty = AvaloniaProperty.RegisterDirect< AdvancedImageBox, bool >(nameof(IsSelecting), o => o.IsSelecting); /// /// Gets if control is currently selecting a ROI /// public bool IsSelecting { get => _isSelecting; protected set => SetAndRaise(IsSelectingProperty, ref _isSelecting, value); } /// /// Gets the center point of the viewport /// public Point CenterPoint { get { var viewport = GetImageViewPort(); return new(viewport.Width / 2, viewport.Height / 2); } } public static readonly StyledProperty AutoPanProperty = AvaloniaProperty.Register( nameof(AutoPan), true ); /// /// Gets or sets if the control can pan with the mouse /// public bool AutoPan { get => GetValue(AutoPanProperty); set => SetValue(AutoPanProperty, value); } public static readonly StyledProperty PanWithMouseButtonsProperty = AvaloniaProperty.Register< AdvancedImageBox, MouseButtons >(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton); /// /// Gets or sets the mouse buttons to pan the image /// public MouseButtons PanWithMouseButtons { get => GetValue(PanWithMouseButtonsProperty); set => SetValue(PanWithMouseButtonsProperty, value); } public static readonly StyledProperty PanWithArrowsProperty = AvaloniaProperty.Register< AdvancedImageBox, bool >(nameof(PanWithArrows), true); /// /// Gets or sets if the control can pan with the keyboard arrows /// public bool PanWithArrows { get => GetValue(PanWithArrowsProperty); set => SetValue(PanWithArrowsProperty, value); } public static readonly StyledProperty SelectWithMouseButtonsProperty = AvaloniaProperty.Register< AdvancedImageBox, MouseButtons >(nameof(SelectWithMouseButtons), MouseButtons.LeftButton); /// /// Gets or sets the mouse buttons to select a region on image /// public MouseButtons SelectWithMouseButtons { get => GetValue(SelectWithMouseButtonsProperty); set => SetValue(SelectWithMouseButtonsProperty, value); } public static readonly StyledProperty InvertMousePanProperty = AvaloniaProperty.Register< AdvancedImageBox, bool >(nameof(InvertMousePan)); /// /// Gets or sets if mouse pan is inverted /// public bool InvertMousePan { get => GetValue(InvertMousePanProperty); set => SetValue(InvertMousePanProperty, value); } public static readonly StyledProperty AutoCenterProperty = AvaloniaProperty.Register( nameof(AutoCenter), true ); /// /// Gets or sets if image is auto centered /// public bool AutoCenter { get => GetValue(AutoCenterProperty); set => SetValue(AutoCenterProperty, value); } public static readonly StyledProperty SizeModeProperty = AvaloniaProperty.Register< AdvancedImageBox, SizeModes >(nameof(SizeMode)); /// /// Gets or sets the image size mode /// public SizeModes SizeMode { get => GetValue(SizeModeProperty); set { SetValue(SizeModeProperty, value); // Run changed if loaded if (IsViewPortLoaded) { SizeModeChanged(); } RaisePropertyChanged(nameof(IsHorizontalBarVisible)); RaisePropertyChanged(nameof(IsVerticalBarVisible)); } } private void SizeModeChanged() { switch (SizeMode) { case SizeModes.Normal: HorizontalScrollBar.Visibility = ScrollBarVisibility.Auto; VerticalScrollBar.Visibility = ScrollBarVisibility.Auto; break; case SizeModes.Stretch: case SizeModes.Fit: HorizontalScrollBar.Visibility = ScrollBarVisibility.Hidden; VerticalScrollBar.Visibility = ScrollBarVisibility.Hidden; break; default: throw new ArgumentOutOfRangeException(nameof(SizeMode), SizeMode, null); } } public static readonly StyledProperty AllowZoomProperty = AvaloniaProperty.Register( nameof(AllowZoom), true ); /// /// Gets or sets if zoom is allowed /// public bool AllowZoom { get => GetValue(AllowZoomProperty); set => SetValue(AllowZoomProperty, value); } public static readonly DirectProperty ZoomLevelsProperty = AvaloniaProperty.RegisterDirect( nameof(ZoomLevels), o => o.ZoomLevels, (o, v) => o.ZoomLevels = v ); ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default; /// /// Gets or sets the zoom levels. /// /// The zoom levels. public ZoomLevelCollection ZoomLevels { get => _zoomLevels; set => SetAndRaise(ZoomLevelsProperty, ref _zoomLevels, value); } public static readonly StyledProperty MinZoomProperty = AvaloniaProperty.Register( nameof(MinZoom), 10 ); /// /// Gets or sets the minimum possible zoom. /// /// The zoom. public int MinZoom { get => GetValue(MinZoomProperty); set => SetValue(MinZoomProperty, value); } public static readonly StyledProperty MaxZoomProperty = AvaloniaProperty.Register( nameof(MaxZoom), 6400 ); /// /// Gets or sets the maximum possible zoom. /// /// The zoom. public int MaxZoom { get => GetValue(MaxZoomProperty); set => SetValue(MaxZoomProperty, value); } public static readonly StyledProperty ConstrainZoomOutToFitLevelProperty = AvaloniaProperty.Register< AdvancedImageBox, bool >(nameof(ConstrainZoomOutToFitLevel), true); /// /// Gets or sets if the zoom out should constrain to fit image as the lowest zoom level. /// public bool ConstrainZoomOutToFitLevel { get => GetValue(ConstrainZoomOutToFitLevelProperty); set => SetValue(ConstrainZoomOutToFitLevelProperty, value); } public static readonly DirectProperty OldZoomProperty = AvaloniaProperty.RegisterDirect< AdvancedImageBox, int >(nameof(OldZoom), o => o.OldZoom); private int _oldZoom = 100; /// /// Gets the previous zoom value /// /// The zoom. public int OldZoom { get => _oldZoom; private set => SetAndRaise(OldZoomProperty, ref _oldZoom, value); } public static readonly StyledProperty ZoomProperty = AvaloniaProperty.Register( nameof(Zoom), 100 ); /// /// Gets or sets the zoom. /// /// The zoom. public int Zoom { get => GetValue(ZoomProperty); set { var minZoom = MinZoom; if (ConstrainZoomOutToFitLevel) minZoom = Math.Max(ZoomLevelToFit, minZoom); var newZoom = Math.Clamp(value, minZoom, MaxZoom); var previousZoom = Zoom; if (previousZoom == newZoom) return; OldZoom = previousZoom; SetValue(ZoomProperty, newZoom); UpdateViewPort(); TriggerRender(); RaisePropertyChanged(nameof(IsHorizontalBarVisible)); RaisePropertyChanged(nameof(IsVerticalBarVisible)); } } /// /// Gets if the image have zoom. /// True if zoomed in or out /// False if no zoom applied /// public bool IsActualSize => Zoom == 100; /// /// Gets the zoom factor, the zoom / 100.0 /// public double ZoomFactor => Zoom / 100.0; /// /// Gets the zoom to fit level which shows all the image /// public int ZoomLevelToFit => Image is null ? 100 : GetZoomLevelToFit(Image); private int GetZoomLevelToFit(IImage image) { double zoom; double aspectRatio; if (image.Size.Width > image.Size.Height) { aspectRatio = ViewPortSize.Width / image.Size.Width; zoom = aspectRatio * 100.0; if (ViewPortSize.Height < image.Size.Height * zoom / 100.0) { aspectRatio = ViewPortSize.Height / image.Size.Height; zoom = aspectRatio * 100.0; } } else { aspectRatio = ViewPortSize.Height / image.Size.Height; zoom = aspectRatio * 100.0; if (ViewPortSize.Width < image.Size.Width * zoom / 100.0) { aspectRatio = ViewPortSize.Width / image.Size.Width; zoom = aspectRatio * 100.0; } } return (int)zoom; } /// /// Gets the width of the scaled image. /// /// The width of the scaled image. public double ScaledImageWidth => Image?.Size.Width * ZoomFactor ?? 0; /// /// Gets the height of the scaled image. /// /// The height of the scaled image. public double ScaledImageHeight => Image?.Size.Height * ZoomFactor ?? 0; public static readonly StyledProperty PixelGridColorProperty = AvaloniaProperty.Register< AdvancedImageBox, ISolidColorBrush >(nameof(PixelGridColor), Brushes.DimGray); /// /// Gets or sets the color of the pixel grid. /// /// The color of the pixel grid. public ISolidColorBrush PixelGridColor { get => GetValue(PixelGridColorProperty); set => SetValue(PixelGridColorProperty, value); } public static readonly StyledProperty PixelGridZoomThresholdProperty = AvaloniaProperty.Register< AdvancedImageBox, int >(nameof(PixelGridZoomThreshold), 13); /// /// Minimum size of zoomed pixel's before the pixel grid will be drawn /// public int PixelGridZoomThreshold { get => GetValue(PixelGridZoomThresholdProperty); set => SetValue(PixelGridZoomThresholdProperty, value); } public static readonly StyledProperty SelectionModeProperty = AvaloniaProperty.Register< AdvancedImageBox, SelectionModes >(nameof(SelectionMode)); public static readonly StyledProperty IsPixelGridEnabledProperty = AvaloniaProperty.Register< AdvancedImageBox, bool >("IsPixelGridEnabled", true); /// /// Whether or not to draw the pixel grid at the /// public bool IsPixelGridEnabled { get => GetValue(IsPixelGridEnabledProperty); set => SetValue(IsPixelGridEnabledProperty, value); } public SelectionModes SelectionMode { get => GetValue(SelectionModeProperty); set => SetValue(SelectionModeProperty, value); } public static readonly StyledProperty SelectionColorProperty = AvaloniaProperty.Register< AdvancedImageBox, ISolidColorBrush >(nameof(SelectionColor), new SolidColorBrush(new Color(127, 0, 128, 255))); public ISolidColorBrush SelectionColor { get => GetValue(SelectionColorProperty); set => SetValue(SelectionColorProperty, value); } public static readonly StyledProperty SelectionRegionProperty = AvaloniaProperty.Register< AdvancedImageBox, Rect >(nameof(SelectionRegion), EmptyRect); public Rect SelectionRegion { get => GetValue(SelectionRegionProperty); set { SetValue(SelectionRegionProperty, value); //if (!RaiseAndSetIfChanged(ref _selectionRegion, value)) return; TriggerRender(); RaisePropertyChanged(nameof(HaveSelection)); RaisePropertyChanged(nameof(SelectionRegionNet)); RaisePropertyChanged(nameof(SelectionPixelSize)); } } public Rectangle SelectionRegionNet { get { var rect = SelectionRegion; return new Rectangle( (int)Math.Ceiling(rect.X), (int)Math.Ceiling(rect.Y), (int)rect.Width, (int)rect.Height ); } } public PixelSize SelectionPixelSize { get { var rect = SelectionRegion; return new PixelSize((int)rect.Width, (int)rect.Height); } } public bool HaveSelection => !IsRectEmpty(SelectionRegion); private BitmapInterpolationMode? _bitmapInterpolationMode; /// /// Gets or sets the current Bitmap Interpolation Mode /// public BitmapInterpolationMode BitmapInterpolationMode { get => _bitmapInterpolationMode ??= RenderOptions.GetBitmapInterpolationMode(this); set { if (_bitmapInterpolationMode == value) return; _bitmapInterpolationMode = value; RenderOptions.SetBitmapInterpolationMode(this, value); } } #endregion #region Constructor /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); // FocusableProperty.OverrideDefaultValue(typeof(AdvancedImageBox), true); AffectsRender(ShowGridProperty); HorizontalScrollBar = e.NameScope.Find("PART_HorizontalScrollBar") ?? throw new NullReferenceException(); VerticalScrollBar = e.NameScope.Find("PART_VerticalScrollBar") ?? throw new NullReferenceException(); ViewPort = e.NameScope.Find("PART_ViewPort") ?? throw new NullReferenceException(); SizeModeChanged(); HorizontalScrollBar.Scroll += ScrollBarOnScroll; VerticalScrollBar.Scroll += ScrollBarOnScroll; // ViewPort.PointerPressed += ViewPortOnPointerPressed; // ViewPort.PointerExited += ViewPortOnPointerExited; // ViewPort.PointerMoved += ViewPortOnPointerMoved; // ViewPort!.PointerWheelChanged += ViewPort_OnPointerWheelChanged; } #endregion #region Render methods public void TriggerRender(bool renderOnlyCursorTracker = false) { if (!_canRender) return; if (renderOnlyCursorTracker && _trackerImage is null) return; var isHighZoom = ZoomFactor > PixelGridZoomThreshold; // If we're in high zoom, switch off bitmap interpolation mode // Otherwise use high quality BitmapInterpolationMode = isHighZoom ? BitmapInterpolationMode.None : BitmapInterpolationMode.HighQuality; InvalidateVisual(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RenderBackgroundGrid(DrawingContext context) { var size = GridCellSize; var square1Drawing = new GeometryDrawing { Brush = GridColorAlternate, Geometry = new RectangleGeometry(new Rect(0.0, 0.0, size, size)) }; var square2Drawing = new GeometryDrawing { Brush = GridColorAlternate, Geometry = new RectangleGeometry(new Rect(size, size, size, size)) }; var drawingGroup = new DrawingGroup { Children = { square1Drawing, square2Drawing } }; var tileBrush = new DrawingBrush(drawingGroup) { AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top, DestinationRect = new RelativeRect(new Size(2 * size, 2 * size), RelativeUnit.Absolute), Stretch = Stretch.None, TileMode = TileMode.Tile, }; context.FillRectangle(GridColor, Bounds); context.FillRectangle(tileBrush, Bounds); } public override void Render(DrawingContext context) { var gridCellSize = GridCellSize; if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) { RenderBackgroundGrid(context); } var zoomFactor = ZoomFactor; var shouldDrawPixelGrid = IsPixelGridEnabled && SizeMode == SizeModes.Normal && zoomFactor > PixelGridZoomThreshold; // Draw Grid /*var viewPortSize = ViewPortSize; if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) { // draw the background var gridColor = GridColor; var altColor = GridColorAlternate; var currentColor = gridColor; for (var y = 0; y < viewPortSize.Height; y += gridCellSize) { var firstRowColor = currentColor; for (var x = 0; x < viewPortSize.Width; x += gridCellSize) { context.FillRectangle(currentColor, new Rect(x, y, gridCellSize, gridCellSize)); currentColor = ReferenceEquals(currentColor, gridColor) ? altColor : gridColor; } if (Equals(firstRowColor, currentColor)) currentColor = ReferenceEquals(currentColor, gridColor) ? altColor : gridColor; } }*/ /*else { context.FillRectangle(Background, new Rect(0, 0, Viewport.Width, Viewport.Height)); }*/ var image = Image; if (image is null) return; var imageViewPort = GetImageViewPort(); // Draw iamge context.DrawImage(image, GetSourceImageRegion(), imageViewPort); if (HaveTrackerImage && _pointerPosition is { X: >= 0, Y: >= 0 }) { var destSize = TrackerImageAutoZoom ? new Size(_trackerImage!.Size.Width * zoomFactor, _trackerImage.Size.Height * zoomFactor) : image.Size; var destPos = new Point(_pointerPosition.X - destSize.Width / 2, _pointerPosition.Y - destSize.Height / 2); context.DrawImage(_trackerImage!, new Rect(destPos, destSize)); } //SkiaContext.SkCanvas.dr // Draw pixel grid if (shouldDrawPixelGrid) { var offsetX = Offset.X % zoomFactor; var offsetY = Offset.Y % zoomFactor; Pen pen = new(PixelGridColor); for (var x = imageViewPort.X + zoomFactor - offsetX; x < imageViewPort.Right; x += zoomFactor) { context.DrawLine(pen, new Point(x, imageViewPort.X), new Point(x, imageViewPort.Bottom)); } for (var y = imageViewPort.Y + zoomFactor - offsetY; y < imageViewPort.Bottom; y += zoomFactor) { context.DrawLine(pen, new Point(imageViewPort.Y, y), new Point(imageViewPort.Right, y)); } context.DrawRectangle(pen, imageViewPort); } if (!IsRectEmpty(SelectionRegion)) { var rect = GetOffsetRectangle(SelectionRegion); var selectionColor = SelectionColor; context.FillRectangle(selectionColor, rect); var color = Color.FromArgb(255, selectionColor.Color.R, selectionColor.Color.G, selectionColor.Color.B); context.DrawRectangle(new Pen(color.ToUInt32()), rect); } } private bool UpdateViewPort() { if (Image is null) { HorizontalScrollBar.Maximum = 0; VerticalScrollBar.Maximum = 0; return true; } var scaledImageWidth = ScaledImageWidth; var scaledImageHeight = ScaledImageHeight; var width = scaledImageWidth - HorizontalScrollBar.ViewportSize; var height = scaledImageHeight - VerticalScrollBar.ViewportSize; //var width = scaledImageWidth <= Viewport.Width ? Viewport.Width : scaledImageWidth; //var height = scaledImageHeight <= Viewport.Height ? Viewport.Height : scaledImageHeight; var changed = false; if (Math.Abs(HorizontalScrollBar.Maximum - width) > 0.01) { HorizontalScrollBar.Maximum = width; changed = true; } if (Math.Abs(VerticalScrollBar.Maximum - scaledImageHeight) > 0.01) { VerticalScrollBar.Maximum = height; changed = true; } /*if (changed) { var newContainer = new ContentControl { Width = width, Height = height }; FillContainer.Content = SizedContainer = newContainer; Debug.WriteLine($"Updated ViewPort: {DateTime.Now.Ticks}"); //TriggerRender(); }*/ return changed; } #endregion #region Events and Overrides private void ScrollBarOnScroll(object? sender, ScrollEventArgs e) { TriggerRender(); } /*protected override void OnScrollChanged(ScrollChangedEventArgs e) { Debug.WriteLine($"ViewportDelta: {e.ViewportDelta} | OffsetDelta: {e.OffsetDelta} | ExtentDelta: {e.ExtentDelta}"); if (!e.ViewportDelta.IsDefault) { UpdateViewPort(); } TriggerRender(); base.OnScrollChanged(e); }*/ /// protected override void OnPointerWheelChanged(PointerWheelEventArgs e) { base.OnPointerWheelChanged(e); e.PreventGestureRecognition(); e.Handled = true; if (Image is null) return; if (AllowZoom && SizeMode == SizeModes.Normal) { // The MouseWheel event can contain multiple "spins" of the wheel so we need to adjust accordingly //double spins = Math.Abs(e.Delta.Y); //Debug.WriteLine(e.GetPosition(this)); // TODO: Really should update the source method to handle multiple increments rather than calling it multiple times /*for (int i = 0; i < spins; i++) {*/ ProcessMouseZoom(e.Delta.Y > 0, e.GetPosition(ViewPort)); //} } } /// protected override void OnPointerPressed(PointerPressedEventArgs e) { base.OnPointerPressed(e); if (e.Handled || _isPanning || _isSelecting || Image is null) return; var pointer = e.GetCurrentPoint(this); if (SelectionMode != SelectionModes.None) { if ( !( pointer.Properties.IsLeftButtonPressed && (SelectWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (SelectWithMouseButtons & MouseButtons.MiddleButton) != 0 || pointer.Properties.IsRightButtonPressed && (SelectWithMouseButtons & MouseButtons.RightButton) != 0 ) ) return; IsSelecting = true; } else { if ( !( pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & MouseButtons.LeftButton) != 0 || pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & MouseButtons.MiddleButton) != 0 || pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & MouseButtons.RightButton) != 0 ) || !AutoPan || SizeMode != SizeModes.Normal ) return; IsPanning = true; } var location = pointer.Position; if (location.X > ViewPortSize.Width) return; if (location.Y > ViewPortSize.Height) return; _startMousePosition = location; } protected override void OnPointerReleased(PointerReleasedEventArgs e) { base.OnPointerReleased(e); if (e.Handled) return; IsPanning = false; IsSelecting = false; } /// protected override void OnPointerExited(PointerEventArgs e) { base.OnPointerExited(e); PointerPosition = new Point(-1, -1); TriggerRender(true); e.Handled = true; } /*private void ViewPortOnPointerExited(object? sender, PointerEventArgs e) { PointerPosition = new Point(-1, -1); TriggerRender(true); e.Handled = true; }*/ /*protected override void OnPointerLeave(PointerEventArgs e) { base.OnPointerLeave(e); PointerPosition = new Point(-1, -1); TriggerRender(true); e.Handled = true; }*/ /// protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); if (e.Handled) return; var pointer = e.GetCurrentPoint(ViewPort); PointerPosition = pointer.Position; if (!_isPanning && !_isSelecting) { TriggerRender(true); return; } if (_isPanning) { double x; double y; if (!InvertMousePan) { x = _startScrollPosition.X + (_startMousePosition.X - _pointerPosition.X); y = _startScrollPosition.Y + (_startMousePosition.Y - _pointerPosition.Y); } else { x = (_startScrollPosition.X - (_startMousePosition.X - _pointerPosition.X)); y = (_startScrollPosition.Y - (_startMousePosition.Y - _pointerPosition.Y)); } Offset = new Vector(x, y); } else if (_isSelecting) { var viewPortPoint = new Point( Math.Min(_pointerPosition.X, ViewPort.Bounds.Right), Math.Min(_pointerPosition.Y, ViewPort.Bounds.Bottom) ); double x; double y; double w; double h; var imageOffset = GetImageViewPort().Position; if (viewPortPoint.X < _startMousePosition.X) { x = viewPortPoint.X; w = _startMousePosition.X - viewPortPoint.X; } else { x = _startMousePosition.X; w = viewPortPoint.X - _startMousePosition.X; } if (viewPortPoint.Y < _startMousePosition.Y) { y = viewPortPoint.Y; h = _startMousePosition.Y - viewPortPoint.Y; } else { y = _startMousePosition.Y; h = viewPortPoint.Y - _startMousePosition.Y; } x -= imageOffset.X - Offset.X; y -= imageOffset.Y - Offset.Y; var zoomFactor = ZoomFactor; x /= zoomFactor; y /= zoomFactor; w /= zoomFactor; h /= zoomFactor; if (w > 0 && h > 0) { SelectionRegion = FitRectangle(new Rect(x, y, w, h)); } } e.Handled = true; } /*protected override void OnPointerMoved(PointerEventArgs e) { base.OnPointerMoved(e); if (e.Handled || !ViewPort.IsPointerOver) return; var pointer = e.GetCurrentPoint(ViewPort); PointerPosition = pointer.Position; if (!_isPanning && !_isSelecting) { TriggerRender(true); return; } if (_isPanning) { double x; double y; if (!InvertMousePan) { x = _startScrollPosition.X + (_startMousePosition.X - _pointerPosition.X); y = _startScrollPosition.Y + (_startMousePosition.Y - _pointerPosition.Y); } else { x = (_startScrollPosition.X - (_startMousePosition.X - _pointerPosition.X)); y = (_startScrollPosition.Y - (_startMousePosition.Y - _pointerPosition.Y)); } Offset = new Vector(x, y); } else if (_isSelecting) { double x; double y; double w; double h; var imageOffset = GetImageViewPort().Position; if (_pointerPosition.X < _startMousePosition.X) { x = _pointerPosition.X; w = _startMousePosition.X - _pointerPosition.X; } else { x = _startMousePosition.X; w = _pointerPosition.X - _startMousePosition.X; } if (_pointerPosition.Y < _startMousePosition.Y) { y = _pointerPosition.Y; h = _startMousePosition.Y - _pointerPosition.Y; } else { y = _startMousePosition.Y; h = _pointerPosition.Y - _startMousePosition.Y; } x -= imageOffset.X - Offset.X; y -= imageOffset.Y - Offset.Y; var zoomFactor = ZoomFactor; x /= zoomFactor; y /= zoomFactor; w /= zoomFactor; h /= zoomFactor; if (w > 0 && h > 0) { SelectionRegion = FitRectangle(new Rect(x, y, w, h)); } } e.Handled = true; }*/ /// protected override void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); if (SizeMode == SizeModes.Fit) { try { ZoomToFit(); } catch (Exception exception) { Debug.WriteLine(exception); } try { RestoreSizeMode(); } catch (Exception exception) { Debug.WriteLine(exception); } } } #endregion #region Zoom and Size modes private void ProcessMouseZoom(bool isZoomIn, Point cursorPosition) => PerformZoom(isZoomIn ? ZoomActions.ZoomIn : ZoomActions.ZoomOut, true, cursorPosition); /// /// Returns an appropriate zoom level based on the specified action, relative to the current zoom level. /// /// The action to determine the zoom level. /// Thrown if an unsupported action is specified. private int GetZoomLevel(ZoomActions action) { var result = action switch { ZoomActions.None => Zoom, ZoomActions.ZoomIn => _zoomLevels.NextZoom(Zoom), ZoomActions.ZoomOut => _zoomLevels.PreviousZoom(Zoom), ZoomActions.ActualSize => 100, _ => throw new ArgumentOutOfRangeException(nameof(action), action, null), }; return result; } /// /// Resets the property whilsts retaining the original . /// protected void RestoreSizeMode() { if (SizeMode != SizeModes.Normal) { var previousZoom = Zoom; SizeMode = SizeModes.Normal; Zoom = previousZoom; // Stop the zoom getting reset to 100% before calculating the new zoom } } private void PerformZoom(ZoomActions action, bool preservePosition) => PerformZoom(action, preservePosition, CenterPoint); private void PerformZoom(ZoomActions action, bool preservePosition, Point relativePoint) { var currentPixel = PointToImage(relativePoint); var currentZoom = Zoom; var newZoom = GetZoomLevel(action); /*if (preservePosition && Zoom != currentZoom) CanRender = false;*/ RestoreSizeMode(); Zoom = newZoom; if (preservePosition && Zoom != currentZoom) { ScrollTo(currentPixel, relativePoint); } } /// /// Zooms into the image /// public void ZoomIn() => ZoomIn(true); /// /// Zooms into the image /// /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. public void ZoomIn(bool preservePosition) { PerformZoom(ZoomActions.ZoomIn, preservePosition); } /// /// Zooms out of the image /// public void ZoomOut() => ZoomOut(true); /// /// Zooms out of the image /// /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. public void ZoomOut(bool preservePosition) { PerformZoom(ZoomActions.ZoomOut, preservePosition); } /// /// Zooms to the maximum size for displaying the entire image within the bounds of the control. /// public void ZoomToFit() { if (!IsImageLoaded) return; Zoom = ZoomLevelToFit; } /// /// Adjusts the view port to fit the given region /// /// The X co-ordinate of the selection region. /// The Y co-ordinate of the selection region. /// The width of the selection region. /// The height of the selection region. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(double x, double y, double width, double height, double margin = 0) { ZoomToRegion(new Rect(x, y, width, height), margin); } /// /// Adjusts the view port to fit the given region /// /// The X co-ordinate of the selection region. /// The Y co-ordinate of the selection region. /// The width of the selection region. /// The height of the selection region. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(int x, int y, int width, int height, double margin = 0) { ZoomToRegion(new Rect(x, y, width, height), margin); } /// /// Adjusts the view port to fit the given region /// /// The rectangle to fit the view port to. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(Rectangle rectangle, double margin = 0) => ZoomToRegion(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height, margin); /// /// Adjusts the view port to fit the given region /// /// The rectangle to fit the view port to. /// Give a margin to rectangle by a value to zoom-out that pixel value public void ZoomToRegion(Rect rectangle, double margin = 0) { if (margin > 0) rectangle = rectangle.Inflate(margin); var ratioX = ViewPortSize.Width / rectangle.Width; var ratioY = ViewPortSize.Height / rectangle.Height; var zoomFactor = Math.Min(ratioX, ratioY); var cx = rectangle.X + rectangle.Width / 2; var cy = rectangle.Y + rectangle.Height / 2; CanRender = false; Zoom = (int)(zoomFactor * 100); // This function sets the zoom so viewport will change CenterAt(new Point(cx, cy)); // If i call this here, it will move to the wrong position due wrong viewport } /// /// Zooms to current selection region /// public void ZoomToSelectionRegion(double margin = 0) { if (!HaveSelection) return; ZoomToRegion(SelectionRegion, margin); } /// /// Resets the zoom to 100%. /// public void PerformActualSize() { SizeMode = SizeModes.Normal; //SetZoom(100, ImageZoomActions.ActualSize | (Zoom < 100 ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut)); Zoom = 100; } #endregion #region Utility methods /// /// Determines whether the specified rectangle is empty /// private static bool IsRectEmpty(Rect rect) { return rect == EmptyRect; } /// /// Static empty rectangle /// private static readonly Rect EmptyRect = new(); /// /// Determines whether the specified point is located within the image view port /// /// The point. /// /// true if the specified point is located within the image view port; otherwise, false. /// public bool IsPointInImage(Point point) => GetImageViewPort().Contains(point); /// /// Determines whether the specified point is located within the image view port /// /// The X co-ordinate of the point to check. /// The Y co-ordinate of the point to check. /// /// true if the specified point is located within the image view port; otherwise, false. /// public bool IsPointInImage(int x, int y) => IsPointInImage(new Point(x, y)); /// /// Determines whether the specified point is located within the image view port /// /// The X co-ordinate of the point to check. /// The Y co-ordinate of the point to check. /// /// true if the specified point is located within the image view port; otherwise, false. /// public bool IsPointInImage(double x, double y) => IsPointInImage(new Point(x, y)); /// /// Converts the given client size point to represent a coordinate on the source image. /// /// The X co-ordinate of the point to convert. /// The Y co-ordinate of the point to convert. /// /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. /// /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point public Point PointToImage(double x, double y, bool fitToBounds = true) => PointToImage(new Point(x, y), fitToBounds); /// /// Converts the given client size point to represent a coordinate on the source image. /// /// The X co-ordinate of the point to convert. /// The Y co-ordinate of the point to convert. /// /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. /// /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point public Point PointToImage(int x, int y, bool fitToBounds = true) { return PointToImage(new Point(x, y), fitToBounds); } /// /// Converts the given client size point to represent a coordinate on the source image. /// /// The source point. /// /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. /// /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point public Point PointToImage(Point point, bool fitToBounds = true) { double x; double y; var viewport = GetImageViewPort(); if (!fitToBounds || viewport.Contains(point)) { x = (point.X + Offset.X - viewport.X) / ZoomFactor; y = (point.Y + Offset.Y - viewport.Y) / ZoomFactor; var image = Image; if (fitToBounds) { x = Math.Clamp(x, 0, image!.Size.Width - 1); y = Math.Clamp(y, 0, image.Size.Height - 1); } } else { x = 0; // Return Point.Empty if we couldn't match y = 0; } return new(x, y); } /// /// Returns the source repositioned to include the current image offset and scaled by the current zoom level /// /// The source to offset. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(System.Drawing.Point source) { var offset = GetOffsetPoint(new Point(source.X, source.Y)); return new((int)offset.X, (int)offset.Y); } /// /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level /// /// The source X co-ordinate. /// The source Y co-ordinate. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(int x, int y) { return GetOffsetPoint(new System.Drawing.Point(x, y)); } /// /// Returns the source co-ordinates repositioned to include the current image offset and scaled by the current zoom level /// /// The source X co-ordinate. /// The source Y co-ordinate. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(double x, double y) { return GetOffsetPoint(new Point(x, y)); } /// /// Returns the source repositioned to include the current image offset and scaled by the current zoom level /// /// The source to offset. /// A which has been repositioned to match the current zoom level and image offset public Point GetOffsetPoint(Point source) { var viewport = GetImageViewPort(); var scaled = GetScaledPoint(source); var offsetX = viewport.Left + Offset.X; var offsetY = viewport.Top + Offset.Y; return new(scaled.X + offsetX, scaled.Y + offsetY); } /// /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset /// /// The source to offset. /// A which has been resized and repositioned to match the current zoom level and image offset public Rect GetOffsetRectangle(Rect source) { var viewport = GetImageViewPort(); var scaled = GetScaledRectangle(source); var offsetX = viewport.Left - Offset.X; var offsetY = viewport.Top - Offset.Y; return new(new Point(scaled.Left + offsetX, scaled.Top + offsetY), scaled.Size); } /// /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been resized and repositioned to match the current zoom level and image offset public Rectangle GetOffsetRectangle(int x, int y, int width, int height) { return GetOffsetRectangle(new Rectangle(x, y, width, height)); } /// /// Returns the source rectangle scaled according to the current zoom level and repositioned to include the current image offset /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been resized and repositioned to match the current zoom level and image offset public Rect GetOffsetRectangle(double x, double y, double width, double height) { return GetOffsetRectangle(new Rect(x, y, width, height)); } /// /// Returns the source scaled according to the current zoom level and repositioned to include the current image offset /// /// The source to offset. /// A which has been resized and repositioned to match the current zoom level and image offset public Rectangle GetOffsetRectangle(Rectangle source) { var viewport = GetImageViewPort(); var scaled = GetScaledRectangle(source); var offsetX = viewport.Left + Offset.X; var offsetY = viewport.Top + Offset.Y; return new( new System.Drawing.Point((int)(scaled.Left + offsetX), (int)(scaled.Top + offsetY)), new System.Drawing.Size((int)scaled.Size.Width, (int)scaled.Size.Height) ); } /// /// Fits a given to match image boundaries /// /// The rectangle. /// /// A structure remapped to fit the image boundaries /// public Rectangle FitRectangle(Rectangle rectangle) { var image = Image; if (image is null) return Rectangle.Empty; var x = rectangle.X; var y = rectangle.Y; var w = rectangle.Width; var h = rectangle.Height; if (x < 0) { x = 0; } if (y < 0) { y = 0; } if (x + w > image.Size.Width) { w = (int)(image.Size.Width - x); } if (y + h > image.Size.Height) { h = (int)(image.Size.Height - y); } return new(x, y, w, h); } /// /// Fits a given to match image boundaries /// /// The rectangle. /// /// A structure remapped to fit the image boundaries /// public Rect FitRectangle(Rect rectangle) { var image = Image; if (image is null) return EmptyRect; var x = rectangle.X; var y = rectangle.Y; var w = rectangle.Width; var h = rectangle.Height; if (x < 0) { w -= -x; x = 0; } if (y < 0) { h -= -y; y = 0; } if (x + w > image.Size.Width) { w = image.Size.Width - x; } if (y + h > image.Size.Height) { h = image.Size.Height - y; } return new(x, y, w, h); } #endregion #region Navigate / Scroll methods /// /// Scrolls the control to the given point in the image, offset at the specified display point /// /// The X co-ordinate of the point to scroll to. /// The Y co-ordinate of the point to scroll to. /// The X co-ordinate relative to the x parameter. /// The Y co-ordinate relative to the y parameter. public void ScrollTo(double x, double y, double relativeX, double relativeY) => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); /// /// Scrolls the control to the given point in the image, offset at the specified display point /// /// The X co-ordinate of the point to scroll to. /// The Y co-ordinate of the point to scroll to. /// The X co-ordinate relative to the x parameter. /// The Y co-ordinate relative to the y parameter. public void ScrollTo(int x, int y, int relativeX, int relativeY) => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); /// /// Scrolls the control to the given point in the image, offset at the specified display point /// /// The point of the image to attempt to scroll to. /// The relative display point to offset scrolling by. public void ScrollTo(Point imageLocation, Point relativeDisplayPoint) { //CanRender = false; var zoomFactor = ZoomFactor; var x = imageLocation.X * zoomFactor - relativeDisplayPoint.X; var y = imageLocation.Y * zoomFactor - relativeDisplayPoint.Y; _canRender = true; Offset = new Vector(x, y); /*Debug.WriteLine( $"X/Y: {x},{y} | \n" + $"Offset: {Offset} | \n" + $"ZoomFactor: {ZoomFactor} | \n" + $"Image Location: {imageLocation}\n" + $"MAX: {HorizontalScrollBar.Maximum},{VerticalScrollBar.Maximum} \n" + $"ViewPort: {Viewport.Width},{Viewport.Height} \n" + $"Container: {HorizontalScrollBar.ViewportSize},{VerticalScrollBar.ViewportSize} \n" + $"Relative: {relativeDisplayPoint}");*/ } /// /// Centers the given point in the image in the center of the control /// /// The point of the image to attempt to center. public void CenterAt(System.Drawing.Point imageLocation) => ScrollTo( new Point(imageLocation.X, imageLocation.Y), new Point(ViewPortSize.Width / 2, ViewPortSize.Height / 2) ); /// /// Centers the given point in the image in the center of the control /// /// The point of the image to attempt to center. public void CenterAt(Point imageLocation) => ScrollTo(imageLocation, new Point(ViewPortSize.Width / 2, ViewPortSize.Height / 2)); /// /// Centers the given point in the image in the center of the control /// /// The X co-ordinate of the point to center. /// The Y co-ordinate of the point to center. public void CenterAt(int x, int y) => CenterAt(new Point(x, y)); /// /// Centers the given point in the image in the center of the control /// /// The X co-ordinate of the point to center. /// The Y co-ordinate of the point to center. public void CenterAt(double x, double y) => CenterAt(new Point(x, y)); /// /// Resets the viewport to show the center of the image. /// public void CenterToImage() { Offset = new Vector(HorizontalScrollBar.Maximum / 2, VerticalScrollBar.Maximum / 2); } #endregion #region Selection / ROI methods /// /// Returns the source scaled according to the current zoom level /// /// The X co-ordinate of the point to scale. /// The Y co-ordinate of the point to scale. /// A which has been scaled to match the current zoom level public Point GetScaledPoint(int x, int y) { return GetScaledPoint(new Point(x, y)); } /// /// Returns the source scaled according to the current zoom level /// /// The X co-ordinate of the point to scale. /// The Y co-ordinate of the point to scale. /// A which has been scaled to match the current zoom level public PointF GetScaledPoint(float x, float y) { return GetScaledPoint(new PointF(x, y)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public Point GetScaledPoint(Point source) { return new(source.X * ZoomFactor, source.Y * ZoomFactor); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public PointF GetScaledPoint(PointF source) { return new((float)(source.X * ZoomFactor), (float)(source.Y * ZoomFactor)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been scaled to match the current zoom level public Rect GetScaledRectangle(int x, int y, int width, int height) { return GetScaledRectangle(new Rect(x, y, width, height)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The X co-ordinate of the source rectangle. /// The Y co-ordinate of the source rectangle. /// The width of the rectangle. /// The height of the rectangle. /// A which has been scaled to match the current zoom level public RectangleF GetScaledRectangle(float x, float y, float width, float height) { return GetScaledRectangle(new RectangleF(x, y, width, height)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The location of the source rectangle. /// The size of the source rectangle. /// A which has been scaled to match the current zoom level public Rect GetScaledRectangle(Point location, Size size) { return GetScaledRectangle(new Rect(location, size)); } /// /// Returns the source rectangle scaled according to the current zoom level /// /// The location of the source rectangle. /// The size of the source rectangle. /// A which has been scaled to match the current zoom level public RectangleF GetScaledRectangle(PointF location, SizeF size) { return GetScaledRectangle(new RectangleF(location, size)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public Rect GetScaledRectangle(Rect source) { return new( source.Left * ZoomFactor, source.Top * ZoomFactor, source.Width * ZoomFactor, source.Height * ZoomFactor ); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been scaled to match the current zoom level public RectangleF GetScaledRectangle(RectangleF source) { return new( (float)(source.Left * ZoomFactor), (float)(source.Top * ZoomFactor), (float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor) ); } /// /// Returns the source size scaled according to the current zoom level /// /// The width of the size to scale. /// The height of the size to scale. /// A which has been resized to match the current zoom level public SizeF GetScaledSize(float width, float height) { return GetScaledSize(new SizeF(width, height)); } /// /// Returns the source size scaled according to the current zoom level /// /// The width of the size to scale. /// The height of the size to scale. /// A which has been resized to match the current zoom level public Size GetScaledSize(int width, int height) { return GetScaledSize(new Size(width, height)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been resized to match the current zoom level public SizeF GetScaledSize(SizeF source) { return new((float)(source.Width * ZoomFactor), (float)(source.Height * ZoomFactor)); } /// /// Returns the source scaled according to the current zoom level /// /// The source to scale. /// A which has been resized to match the current zoom level public Size GetScaledSize(Size source) { return new(source.Width * ZoomFactor, source.Height * ZoomFactor); } /// /// Creates a selection region which encompasses the entire image /// /// Thrown if no image is currently set public void SelectAll() { var image = Image; if (image is null) return; SelectionRegion = new Rect(0, 0, image.Size.Width, image.Size.Height); } /// /// Clears any existing selection region /// public void SelectNone() { SelectionRegion = EmptyRect; } #endregion #region Viewport and image region methods /// /// Gets the source image region. /// /// public Rect GetSourceImageRegion() { var image = Image; if (image is null) return EmptyRect; switch (SizeMode) { case SizeModes.Normal: var offset = Offset; var viewPort = GetImageViewPort(); var zoomFactor = ZoomFactor; var sourceLeft = offset.X / zoomFactor; var sourceTop = offset.Y / zoomFactor; var sourceWidth = viewPort.Width / zoomFactor; var sourceHeight = viewPort.Height / zoomFactor; return new Rect(sourceLeft, sourceTop, sourceWidth, sourceHeight); } return new Rect(0, 0, image.Size.Width, image.Size.Height); } /// /// Gets the image view port. /// /// public Rect GetImageViewPort() { var viewPortSize = ViewPortSize; if (!IsImageLoaded || viewPortSize is { Width: 0, Height: 0 }) return EmptyRect; double xOffset = 0; double yOffset = 0; double width; double height; switch (SizeMode) { case SizeModes.Normal: if (AutoCenter) { xOffset = (!IsHorizontalBarVisible ? (viewPortSize.Width - ScaledImageWidth) / 2 : 0); yOffset = (!IsVerticalBarVisible ? (viewPortSize.Height - ScaledImageHeight) / 2 : 0); } width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), viewPortSize.Width); height = Math.Min(ScaledImageHeight - Math.Abs(Offset.Y), viewPortSize.Height); break; case SizeModes.Stretch: width = viewPortSize.Width; height = viewPortSize.Height; break; case SizeModes.Fit: var image = Image; var scaleFactor = Math.Min( viewPortSize.Width / image!.Size.Width, viewPortSize.Height / image.Size.Height ); width = Math.Floor(image.Size.Width * scaleFactor); height = Math.Floor(image.Size.Height * scaleFactor); if (AutoCenter) { xOffset = (viewPortSize.Width - width) / 2; yOffset = (viewPortSize.Height - height) / 2; } break; default: throw new ArgumentOutOfRangeException(nameof(SizeMode), SizeMode, null); } return new(xOffset, yOffset, width, height); } #endregion #region Image methods public void LoadImage(string path) { Image = new Bitmap(path); } public Bitmap? GetSelectedBitmap() { if (!HaveSelection || Image is null) return null; using var stream = new MemoryStream(); Image.Save(stream); var image = WriteableBitmap.Decode(stream); stream.Dispose(); var selection = SelectionRegionNet; var pixelSize = SelectionPixelSize; using var frameBuffer = image.Lock(); var newBitmap = new WriteableBitmap(pixelSize, image.Dpi, frameBuffer.Format, AlphaFormat.Unpremul); using var newFrameBuffer = newBitmap.Lock(); var i = 0; unsafe { var inputPixels = (uint*)(void*)frameBuffer.Address; var targetPixels = (uint*)(void*)newFrameBuffer.Address; for (var y = selection.Y; y < selection.Bottom; y++) { var thisY = y * frameBuffer.Size.Width; for (var x = selection.X; x < selection.Right; x++) { targetPixels![i++] = inputPixels![thisY + x]; } } } return newBitmap; } #endregion }