/*
* 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
}