diff --git a/CHANGELOG.md b/CHANGELOG.md index 66823e9..29f0e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,67 @@ # Changelog +## 12/03/2022 - v3.0.0 + +- **(Add) Suggestions:** + - A new module that detect bad or parameters out of a defined range and suggest a change on the file, those can be auto applied if configured to do so + - **Avaliable suggestions:** + - **Bottom layer count:** Bottom layers should be kept to a minimum, usually from 2 to 3, it function is to provide a good adhesion to the first layer on the build plate, using a high count have disadvantages. + - **Wait time before cure:** Rest some time before cure the layer is crucial to let the resin settle after the lift sequence and allow some time for the arm settle at the correct Z position as the resin will offer some resistance and push the structure. + This lead to better quality with more successful prints, less lamination problems, better first layers with more success of stick to the build plate and less elephant foot effect. + - **Wait time after cure:** Rest some time after cure the layer and before the lift sequence can be important to allow the layer to cooldown a bit and detach better from the FEP. + - **Layer height:** Using the right layer height is important to get successful prints: + Thin layers may cause problems on adhesion, lamination, will print much slower and have no real visual benefits. + Thick layers may not fully cure no matter the exposure time you use, causing lamination and other hazards. Read your resin dtasheet to know the limits. + Using layer height with too many decimal digits may produce a wrong positioning due stepper step loss and/or Z axis quality. +- **Core:** + - Convert the project to Nullable aware and "null-safe" +- **File Formats:** + - (Add) `Volume` property to get the total model volume + - (Add) `SanitizeLayers` method to reassign indexes and force attribute parent file + - (Improvement) Merge `LayerManager` into `FileFormat` and cleanup: This affects the whole project and external scripts. + If using scripts please update them, search for `.LayerManager.` and replace by `.` + - (Change) Chitubox encrypted format can now be saved as normal + - (Fix) Converted files layers was pointing to the source file and related to it +- **Layers:** + - (Add) Methods: `ResetParameters`, `CopyParametersTo`, `CopyExposureTo`, `CopyWaitTimesTo` + - (Improvement) `IsBottomLayer` property will also return true when the index is inside bottom layer count +- **Scripting:** + - (Add) Configuration variable: `MinimumVersionToRun` - Sets the minimum version able to run the script + - (Improvement) Allow run scripts written in C# 10 with the new namespace; style as well as nullables methods + - (Improvement) Convert scripts to use Nullable code +- **UI:** + - (Add) Fluent Dark theme + - (Add) Default Light theme + - (Add) Default Dark theme + - (Change) Use fontawesome and material design to render the icons instead of static png images + - (Change) Some icons + - (Change) Move log tab to clipboard tab + - (Change) Tooltip overlay default color + - (Improvement) Windows position for tool windows, sometimes framework can return negative values affecting positions, now limits to 0 (#387) + - (Fix) Center image icon for layer action button + - (Fix) Center image icon for save layer image button +- **Tools:** + - (Add) Layer re-height: Offset mode, change layers position by a defined offset (#423) + - (Improvement) Rotate: Unable to use an angle of 0 + - (Improvement) Remove layers: Will not recalcualte and reset properties of layers anymore, allowing removing layers on dynamic layer height models and others + - (Improvement) Clone layers: Will not recalcualte and reset properties of layers anymore, allowing cloning layers on dynamic layer height models and others + - (Fix) Exposure time finder: Very small printers may not print the stock object as it is configured, lead to a unknown error while generating the test. It will now show a better error message and advice a solution (#426) +- **Terminal:** + - (Add) More default namespaces + - (Improvement) Set a MinHeight for the rows to prevent spliter from eat the elements + - (Change) Set working space to the MainWindow instead of TerminalWindow +- **(Upgrade) .NET from 5.0.14 to 6.0.3** + - This brings big performance improvements, better JIT, faster I/O operations and others + - Read more: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6 + - Due this macOS requirement starts at 10.15 (Catalina) + - Read more: https://github.com/dotnet/core/blob/main/release-notes/6.0/supported-os.md +- (Add) Native support for MacOS ARM64 architecture (Mac M1 and upcomming Mac's) (#187) +- (Exchange) Dependency Newtonsoft Json by System.Text.Json to parse the json documents +- (Remove) Dependency: MoreLinq +- (Remove) "Automations - Light-off delay" in favor of new suggestion "wait time before cure" module +- (Fix) File - Send to: Winrar or 7zip have a wrong extension on the list (uvt) when should be (uvj) +- (Upgrade) AvaloniaUI from 0.10.12 to 0.10.13 + ## 21/02/2022 - v2.29.0 - **File formats:** @@ -9,7 +71,7 @@ - (Add) SL1: Keyword `TransitionLayerCount_xxx` - Sets the number of transition layers - (Improvement) CTB, PHZ, FDG: Implement the `ModifiedTimestampMinutes` field, it was the MysteriousId before as an unknown field - (Fix) CWS: Open in partial mode will cause an exception and prevent file from load -- **CCode:** +- **GCode:** - (Add) Allow inverse lifts to work as an retract - (Fix) Parsing of WaitTimeAfterLift was incorrect when lacking a lift sequence - (Fix) Layers lacking an exposure time was defaulting to global time, now defaults to 0 @@ -19,7 +81,7 @@ - (Add) Information: Raise Layer count equivalence - (Add) Information: Additional lifts to be generated - (Add) Option: Ensure the last layer - If enabled, it will generate an obligatory layer to cover the last layer - - (Improvement) Optimize lift for virtual layer mode, allowing set a slow and fast lift / retract by using another virtual layyer to emulate a lift + - (Improvement) Optimize lift for virtual layer mode, allowing set a slow and fast lift / retract by using another virtual layer to emulate a lift - (Improvement) Allow to define slow and fast speed for virtual layer mode even if TSMC isn't supported - (Add) Fade exposure time: Setting 'Disable firmware transition layers' - Attempt to disable firmware strict transition layers in favor of this tool - (Add) Calibration tests: Attempt to auto disable the firmware transifiton layers @@ -769,6 +831,7 @@ - (Fix) macOS: Include libusb-1.0.0.dylib - Note: `brew install libusb` still required - **UI:** + - (Add) Shorcuts: Arrow up and down to navigate layers while layer image is on focus - (Fix) Refresh gcode does not update text on UI for ZIP, CWS, ZCODEX files - (Fix) Operations: Import a .uvtop file by drag and drop into the UI would not load the layer range - (Change) When convert a file, the result dialog will have Yes, No and Cancel actions, where No will open the converted file on current window, while Cancel will not perform any action (The old No behaviour) diff --git a/CREDITS.md b/CREDITS.md index bcdabe4..7f9976e 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -68,4 +68,6 @@ - Riccardo Kocmann - Joshua Pitts - Tim Anderson -- Sakari Toivonen \ No newline at end of file +- Sakari Toivonen +- Ed Wagaman +- Marcin Chomiczuk \ No newline at end of file diff --git a/README.md b/README.md index 119719b..3c94132 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ The UVtools executable allow to set some arguments to do special functions: - Type: appwiz.cpl (and press Enter key) - Click on: Turn Windows features on or off - Check the "Media Extensions" and click Ok - + 1. 4GB RAM or higher 1. 1920 x 1080 @ 100% scale as minimum resolution @@ -215,7 +215,7 @@ sudo dpkg -i packages-microsoft-prod.deb rm packages-microsoft-prod.deb sudo apt-get update sudo apt-get install -y apt-transport-https -dotnet-runtime-5.0 +dotnet-runtime-6.0 !--> ```bash @@ -348,7 +348,7 @@ this process is very slow but only need to run once. Open a terminal on any fold ```bash sudo yum update -y sudo yum groupinstall -y "Development Tools" "Development Libraries" -sudo yum install -y cmake gcc-c++ dotnet-sdk-5.0 gtk3-devel gstreamer1-devel ffmpeg ffmpeg-devel libdc1394 libv4l-devel cmake-gui ocl-icd-devel freeglut libgeotiff libusb +sudo yum install -y cmake gcc-c++ dotnet-sdk-6.0 gtk3-devel gstreamer1-devel ffmpeg ffmpeg-devel libdc1394 libv4l-devel cmake-gui ocl-icd-devel freeglut libgeotiff libusb git clone https://github.com/emgucv/emgucv emgucv cd emgucv git submodule update --init --recursive @@ -370,11 +370,11 @@ anyone with same system version can make use of it without the need of the compi ## Mac -1. macOS 10.13 High Sierra +1. macOS 10.15 Catalina or higher 1. 4GB RAM or higher diff --git a/UVtools.AvaloniaControls/AdvancedImageBox.axaml.cs b/UVtools.AvaloniaControls/AdvancedImageBox.axaml.cs index 6ff899d..f8c88c2 100644 --- a/UVtools.AvaloniaControls/AdvancedImageBox.axaml.cs +++ b/UVtools.AvaloniaControls/AdvancedImageBox.axaml.cs @@ -27,2178 +27,2185 @@ using Pen = Avalonia.Media.Pen; using Point = Avalonia.Point; using Size = Avalonia.Size; -namespace UVtools.AvaloniaControls -{ - public class AdvancedImageBox : UserControl - { - #region Bindable Base - /// - /// Multicast event for property change notifications. - /// - private PropertyChangedEventHandler _propertyChanged; +namespace UVtools.AvaloniaControls; - public new event PropertyChangedEventHandler PropertyChanged +public class AdvancedImageBox : UserControl +{ + #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() { - add => _propertyChanged += value; - remove => _propertyChanged -= value; + List = new SortedList(); } - protected bool RaiseAndSetIfChanged(ref T field, T value, [CallerMemberName] string propertyName = null) + + /// + /// 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 (EqualityComparer.Default.Equals(field, value)) return false; - field = value; - RaisePropertyChanged(propertyName); + 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, 15, 20, 25, 30, 50, 70, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600, 3200 + }); + + #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 (int 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 (int 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) + { + int nearestValue = List.Values[0]; + int nearestDifference = Math.Abs(nearestValue - zoomLevel); + for (int i = 1; i < Count; i++) + { + int value = List.Values[i]; + int 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. + /// The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom. + public int NextZoom(int zoomLevel) + { + var index = IndexOf(FindNearest(zoomLevel)); + if (index < Count - 1) + { + index++; + } + + return this[index]; + } + + /// + /// Returns the next decreased zoom level for the given current zoom. + /// + /// The current zoom level. + /// The next matching decreased zoom level for the given current zoom if applicable, otherwise the nearest zoom. + public int PreviousZoom(int zoomLevel) + { + var index = IndexOf(FindNearest(zoomLevel)); + if (index > 0) + { + index--; + } + + return 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() + { + int[] results; + + 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 + public ScrollBar HorizontalScrollBar { get; } + public ScrollBar VerticalScrollBar { get; } + public ContentPresenter ViewPort { get; } + + 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( + 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(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(nameof(GridColor), Brushes.Gainsboro); + + /// + /// 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(nameof(GridColorAlternate), Brushes.White); + + /// + /// 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 ImageProperty = + AvaloniaProperty.Register(nameof(Image)); + + /// + /// Gets or sets the image to be displayed + /// + public Bitmap? Image + { + get => GetValue(ImageProperty); + set + { + SetValue(ImageProperty, value); + + if (value is null) + { + SelectNone(); + } + + 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(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 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( + 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( + 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(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton | MouseButtons.RightButton); + + /// + /// 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(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(nameof(SelectWithMouseButtons), MouseButtons.LeftButton | MouseButtons.RightButton); + + + /// + /// 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(nameof(InvertMousePan), false); + + /// + /// 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(nameof(SizeMode), SizeModes.Normal); + + /// + /// Gets or sets the image size mode + /// + public SizeModes SizeMode + { + get => GetValue(SizeModeProperty); + set + { + SetValue(SizeModeProperty, value); + 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), 3500); + + /// + /// Gets or sets the maximum possible zoom. + /// + /// The zoom. + public int MaxZoom + { + get => GetValue(MaxZoomProperty); + set => SetValue(MaxZoomProperty, value); + } + + + public static readonly DirectProperty OldZoomProperty = + AvaloniaProperty.RegisterDirect( + 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 newZoom = Math.Clamp(value, MinZoom, MaxZoom); + + var previousZoom = Zoom; + if (previousZoom == newZoom) return; + OldZoom = previousZoom; + SetValue(ZoomProperty, value); + + UpdateViewPort(); + TriggerRender(); + + RaisePropertyChanged(nameof(IsHorizontalBarVisible)); + RaisePropertyChanged(nameof(IsVerticalBarVisible)); + } + } + + public bool IsActualSize => Zoom == 100; + + /// + /// Gets the zoom factor, the zoom / 100 + /// + public double ZoomFactor => Zoom / 100.0; + + /// + /// 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(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(nameof(PixelGridZoomThreshold), 5); + + /// + /// Gets or sets the minimum size of zoomed pixel's before the pixel grid will be drawn + /// + /// The pixel grid threshold. + + public int PixelGridZoomThreshold + { + get => GetValue(PixelGridZoomThresholdProperty); + set => SetValue(PixelGridZoomThresholdProperty, value); + } + + public static readonly StyledProperty SelectionModeProperty = + AvaloniaProperty.Register(nameof(SelectionMode), SelectionModes.None); + + public SelectionModes SelectionMode + { + get => GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); + } + + public static readonly StyledProperty SelectionColorProperty = + AvaloniaProperty.Register(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(nameof(SelectionRegion), Rect.Empty); + + + 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 => !SelectionRegion.IsEmpty; + #endregion + + #region Constructor + public AdvancedImageBox() + { + InitializeComponent(); + + FocusableProperty.OverrideDefaultValue(typeof(AdvancedImageBox), true); + AffectsRender(ShowGridProperty); + + HorizontalScrollBar = this.FindControl("HorizontalScrollBar"); + VerticalScrollBar = this.FindControl("VerticalScrollBar"); + ViewPort = this.FindControl("ViewPort"); + + SizeModeChanged(); + + HorizontalScrollBar.Scroll += ScrollBarOnScroll; + VerticalScrollBar.Scroll += ScrollBarOnScroll; + ViewPort.PointerWheelChanged += FillContainerOnPointerWheelChanged; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + #endregion + + #region Render methods + public void TriggerRender(bool renderOnlyCursorTracker = false) + { + if (!_canRender) return; + if (renderOnlyCursorTracker && _trackerImage is null) return; + + InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + //Debug.WriteLine($"Render: {DateTime.Now.Ticks}"); + base.Render(context); + + // Draw Grid + var gridCellSize = GridCellSize; + if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) + { + // draw the background + var gridColor = GridColor; + var altColor = GridColorAlternate; + var currentColor = gridColor; + for (int y = 0; y < ViewPortSize.Height; y += gridCellSize) + { + var firstRowColor = currentColor; + + for (int 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; + // Draw iamge + context.DrawImage(image, + GetSourceImageRegion(), + GetImageViewPort() + ); + + var zoomFactor = ZoomFactor; + + + if (HaveTrackerImage && _pointerPosition.X >= 0 && _pointerPosition.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 (zoomFactor > PixelGridZoomThreshold && SizeMode == SizeModes.Normal) + { + var viewport = GetImageViewPort(); + var offsetX = Offset.X % zoomFactor; + var offsetY = Offset.Y % zoomFactor; + + Pen pen = new(PixelGridColor); + for (double x = viewport.X + zoomFactor - offsetX; x < viewport.Right; x += zoomFactor) + { + context.DrawLine(pen, new Point(x, viewport.X), new Point(x, viewport.Bottom)); + } + + for (double y = viewport.Y + zoomFactor - offsetY; y < viewport.Bottom; y += zoomFactor) + { + context.DrawLine(pen, new Point(viewport.Y, y), new Point(viewport.Right, y)); + } + + context.DrawRectangle(pen, viewport); + } + + if (!SelectionRegion.IsEmpty) + { + var rect = GetOffsetRectangle(SelectionRegion); + var selectionColor = SelectionColor; + context.FillRectangle(selectionColor, rect); + Color 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; - protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + bool changed = false; + if (Math.Abs(HorizontalScrollBar.Maximum - width) > 0.01) { + HorizontalScrollBar.Maximum = width; + changed = true; } - /// - /// 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) + if (Math.Abs(VerticalScrollBar.Maximum - scaledImageHeight) > 0.01) { - var e = new PropertyChangedEventArgs(propertyName); - OnPropertyChanged(e); - _propertyChanged?.Invoke(this, e); + VerticalScrollBar.Maximum = height; + changed = true; } - #endregion - #region Sub Classes - - /// - /// Represents available levels of zoom in an control - /// - public class ZoomLevelCollection : IList + /*if (changed) { - #region Public Constructors - - /// - /// Initializes a new instance of the class. - /// - public ZoomLevelCollection() + var newContainer = new ContentControl { - 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, 15, 20, 25, 30, 50, 70, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600, 3200 - }); - - #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 (int 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 (int 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) - { - int nearestValue = List.Values[0]; - int nearestDifference = Math.Abs(nearestValue - zoomLevel); - for (int i = 1; i < Count; i++) - { - int value = List.Values[i]; - int 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. - /// The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom. - public int NextZoom(int zoomLevel) - { - var index = IndexOf(FindNearest(zoomLevel)); - if (index < Count - 1) - { - index++; - } - - return this[index]; - } - - /// - /// Returns the next decreased zoom level for the given current zoom. - /// - /// The current zoom level. - /// The next matching decreased zoom level for the given current zoom if applicable, otherwise the nearest zoom. - public int PreviousZoom(int zoomLevel) - { - var index = IndexOf(FindNearest(zoomLevel)); - if (index > 0) - { - index--; - } - - return 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() - { - int[] results; - - 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 occuring - /// - [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 - public ScrollBar HorizontalScrollBar { get; } - public ScrollBar VerticalScrollBar { get; } - public ContentPresenter ViewPort { get; } - - 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( - 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(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(nameof(GridColor), Brushes.Gainsboro); - - /// - /// 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(nameof(GridColorAlternate), Brushes.White); - - /// - /// 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 ImageProperty = - AvaloniaProperty.Register(nameof(Image)); - - /// - /// Gets or sets the image to be displayed - /// - public Bitmap Image - { - get => GetValue(ImageProperty); - set - { - SetValue(ImageProperty, value); - - if (value is null) - { - SelectNone(); - } - - UpdateViewPort(); - TriggerRender(); - - RaisePropertyChanged(nameof(IsImageLoaded)); - } - } - - public WriteableBitmap ImageAsWriteableBitmap => (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(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 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( - 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( - 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(nameof(PanWithMouseButtons), MouseButtons.LeftButton | MouseButtons.MiddleButton | MouseButtons.RightButton); - - /// - /// 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(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(nameof(SelectWithMouseButtons), MouseButtons.LeftButton | MouseButtons.RightButton); - - - /// - /// 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(nameof(InvertMousePan), false); - - /// - /// 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(nameof(SizeMode), SizeModes.Normal); - - /// - /// Gets or sets the image size mode - /// - public SizeModes SizeMode - { - get => GetValue(SizeModeProperty); - set - { - SetValue(SizeModeProperty, value); - 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), 3500); - - /// - /// Gets or sets the maximum possible zoom. - /// - /// The zoom. - public int MaxZoom - { - get => GetValue(MaxZoomProperty); - set => SetValue(MaxZoomProperty, value); - } - - - public static readonly DirectProperty OldZoomProperty = - AvaloniaProperty.RegisterDirect( - 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 newZoom = Math.Clamp(value, MinZoom, MaxZoom); - - var previousZoom = Zoom; - if (previousZoom == newZoom) return; - OldZoom = previousZoom; - SetValue(ZoomProperty, value); - - UpdateViewPort(); - TriggerRender(); - - RaisePropertyChanged(nameof(IsHorizontalBarVisible)); - RaisePropertyChanged(nameof(IsVerticalBarVisible)); - } - } - - public bool IsActualSize => Zoom == 100; - - /// - /// Gets the zoom factor, the zoom / 100 - /// - public double ZoomFactor => Zoom / 100.0; - - /// - /// Gets the width of the scaled image. - /// - /// The width of the scaled image. - public double ScaledImageWidth => Image.Size.Width * ZoomFactor; - - /// - /// Gets the height of the scaled image. - /// - /// The height of the scaled image. - public double ScaledImageHeight => Image.Size.Height * ZoomFactor; - - public static readonly StyledProperty PixelGridColorProperty = - AvaloniaProperty.Register(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(nameof(PixelGridZoomThreshold), 5); - - /// - /// Gets or sets the minimum size of zoomed pixel's before the pixel grid will be drawn - /// - /// The pixel grid threshold. - - public int PixelGridZoomThreshold - { - get => GetValue(PixelGridZoomThresholdProperty); - set => SetValue(PixelGridZoomThresholdProperty, value); - } - - public static readonly StyledProperty SelectionModeProperty = - AvaloniaProperty.Register(nameof(SelectionMode), SelectionModes.None); - - public SelectionModes SelectionMode - { - get => GetValue(SelectionModeProperty); - set => SetValue(SelectionModeProperty, value); - } - - public static readonly StyledProperty SelectionColorProperty = - AvaloniaProperty.Register(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(nameof(SelectionRegion), Rect.Empty); - - - 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 => !SelectionRegion.IsEmpty; - #endregion - - #region Constructor - public AdvancedImageBox() - { - InitializeComponent(); - - FocusableProperty.OverrideDefaultValue(typeof(AdvancedImageBox), true); - AffectsRender(ShowGridProperty); - - HorizontalScrollBar = this.FindControl("HorizontalScrollBar"); - VerticalScrollBar = this.FindControl("VerticalScrollBar"); - ViewPort = this.FindControl("ViewPort"); - - SizeModeChanged(); - - HorizontalScrollBar.Scroll += ScrollBarOnScroll; - VerticalScrollBar.Scroll += ScrollBarOnScroll; - ViewPort.PointerWheelChanged += FillContainerOnPointerWheelChanged; - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - #endregion - - #region Render methods - public void TriggerRender(bool renderOnlyCursorTracker = false) - { - if (!_canRender) return; - if (renderOnlyCursorTracker && _trackerImage is null) return; - - InvalidateVisual(); - } - - public override void Render(DrawingContext context) - { - //Debug.WriteLine($"Render: {DateTime.Now.Ticks}"); - base.Render(context); - - // Draw Grid - var gridCellSize = GridCellSize; - if (ShowGrid & gridCellSize > 0 && (!IsHorizontalBarVisible || !IsVerticalBarVisible)) - { - // draw the background - var gridColor = GridColor; - var altColor = GridColorAlternate; - var currentColor = gridColor; - for (int y = 0; y < ViewPortSize.Height; y += gridCellSize) - { - var firstRowColor = currentColor; - - for (int 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; - // Draw iamge - context.DrawImage(image, - GetSourceImageRegion(), - GetImageViewPort() - ); - - var zoomFactor = ZoomFactor; - - - if (HaveTrackerImage && _pointerPosition.X >= 0 && _pointerPosition.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 (zoomFactor > PixelGridZoomThreshold && SizeMode == SizeModes.Normal) - { - var viewport = GetImageViewPort(); - var offsetX = Offset.X % zoomFactor; - var offsetY = Offset.Y % zoomFactor; - - Pen pen = new(PixelGridColor); - for (double x = viewport.X + zoomFactor - offsetX; x < viewport.Right; x += zoomFactor) - { - context.DrawLine(pen, new Point(x, viewport.X), new Point(x, viewport.Bottom)); - } - - for (double y = viewport.Y + zoomFactor - offsetY; y < viewport.Bottom; y += zoomFactor) - { - context.DrawLine(pen, new Point(viewport.Y, y), new Point(viewport.Right, y)); - } - - context.DrawRectangle(pen, viewport); - } - - if (!SelectionRegion.IsEmpty) - { - var rect = GetOffsetRectangle(SelectionRegion); - var selectionColor = SelectionColor; - context.FillRectangle(selectionColor, rect); - Color 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; - - bool 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); + Width = width, + Height = height + }; + FillContainer.Content = SizedContainer = newContainer; + Debug.WriteLine($"Updated ViewPort: {DateTime.Now.Ticks}"); + //TriggerRender(); }*/ - private void FillContainerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) + 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) { - 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)); - //} - } + UpdateViewPort(); } - protected override void OnPointerPressed(PointerPressedEventArgs e) + TriggerRender(); + + base.OnScrollChanged(e); + }*/ + + private void FillContainerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) + { + e.Handled = true; + if (Image is null) return; + if (AllowZoom && SizeMode == SizeModes.Normal) { - base.OnPointerPressed(e); - if (e.Handled - || _isPanning - || _isSelecting - || Image is null) return; + // 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)); + //} + } + } - var pointer = e.GetCurrentPoint(this); + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (e.Handled + || _isPanning + || _isSelecting + || Image is null) return; - 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 + var pointer = e.GetCurrentPoint(this); - ) return; + 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 - IsPanning = true; - } + ) return; - var location = pointer.Position; - - if (location.X > ViewPortSize.Width) return; - if (location.Y > ViewPortSize.Height) return; - _startMousePosition = location; + IsPanning = true; } - protected override void OnPointerReleased(PointerReleasedEventArgs e) - { - base.OnPointerReleased(e); - if (e.Handled) return; + var location = pointer.Position; - IsPanning = false; - IsSelecting = false; - } + if (location.X > ViewPortSize.Width) return; + if (location.Y > ViewPortSize.Height) return; + _startMousePosition = location; + } - protected override void OnPointerLeave(PointerEventArgs e) + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + if (e.Handled) return; + + IsPanning = false; + IsSelecting = false; + } + + 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(this); + PointerPosition = pointer.Position; + + if (!_isPanning && !_isSelecting) { - base.OnPointerLeave(e); - PointerPosition = new Point(-1, -1); TriggerRender(true); - e.Handled = true; + return; } - protected override void OnPointerMoved(PointerEventArgs e) - { - base.OnPointerMoved(e); - if (e.Handled) return; - - var pointer = e.GetCurrentPoint(this); - 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; - } - #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) - { - Point currentPixel = PointToImage(relativePoint); - int currentZoom = Zoom; - int 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() - { - var image = Image; - if (image is null) return; - - 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; - } - } - - Zoom = (int)zoom; - } - - /// - /// 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 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) + if (_isPanning) { double x; double y; - var viewport = GetImageViewPort(); - - if (!fitToBounds || viewport.Contains(point)) + if (!InvertMousePan) { - 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); - } + x = _startScrollPosition.X + (_startMousePosition.X - _pointerPosition.X); + y = _startScrollPosition.Y + (_startMousePosition.Y - _pointerPosition.Y); } else { - x = 0; // Return Point.Empty if we couldn't match - y = 0; + x = (_startScrollPosition.X - (_startMousePosition.X - _pointerPosition.X)); + y = (_startScrollPosition.Y - (_startMousePosition.Y - _pointerPosition.Y)); } - 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) - { - Rect 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 Rect.Empty; - 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); + } + else if (_isSelecting) + { + double x; + double y; + double w; + double h; - /*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}");*/ + 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)); + } } - /// - /// 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)); + e.Handled = true; + } + #endregion - /// - /// 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)); + #region Zoom and Size modes + private void ProcessMouseZoom(bool isZoomIn, Point cursorPosition) + => PerformZoom(isZoomIn ? ZoomActions.ZoomIn : ZoomActions.ZoomOut, true, cursorPosition); - /// - /// 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() + /// + /// 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 { - Offset = new Vector(HorizontalScrollBar.Maximum / 2, VerticalScrollBar.Maximum / 2); + 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 } - #endregion + } - #region Selection / ROI methods + private void PerformZoom(ZoomActions action, bool preservePosition) + => PerformZoom(action, preservePosition, CenterPoint); - /// - /// 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) + private void PerformZoom(ZoomActions action, bool preservePosition, Point relativePoint) + { + Point currentPixel = PointToImage(relativePoint); + int currentZoom = Zoom; + int newZoom = GetZoomLevel(action); + + /*if (preservePosition && Zoom != currentZoom) + CanRender = false;*/ + + RestoreSizeMode(); + Zoom = newZoom; + + if (preservePosition && Zoom != currentZoom) { - return GetScaledPoint(new Point(x, y)); + 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() + { + var image = Image; + if (image is null) return; + + 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; + } } - /// - /// 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)); - } + Zoom = (int)zoom; + } - /// - /// 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); - } + /// + /// 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); + } - /// - /// 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)); - } + /// + /// 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); + } - /// - /// 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)); - } + /// + /// 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); - /// - /// 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)); - } + /// + /// 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; - /// - /// 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)); - } + 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 + } - /// - /// 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)); - } + /// + /// Zooms to current selection region + /// + public void ZoomToSelectionRegion(double margin = 0) + { + if (!HaveSelection) return; + ZoomToRegion(SelectionRegion, margin); + } - /// - /// 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); - } + /// + /// Resets the zoom to 100%. + /// + public void PerformActualSize() + { + SizeMode = SizeModes.Normal; + //SetZoom(100, ImageZoomActions.ActualSize | (Zoom < 100 ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut)); + Zoom = 100; + } + #endregion - /// - /// 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)); - } + #region Utility methods + /// + /// 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); - /// - /// 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)); - } + /// + /// 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)); - /// - /// 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)); - } + /// + /// 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)); - /// - /// 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)); - } + /// + /// 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); - /// - /// 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); - } + /// + /// 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); + } - /// - /// Creates a selection region which encompasses the entire image - /// - /// Thrown if no image is currently set - public void SelectAll() + /// + /// 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 (image is null) return; - SelectionRegion = new Rect(0, 0, image.Size.Width, image.Size.Height); + if (fitToBounds) + { + x = Math.Clamp(x, 0, image!.Size.Width-1); + y = Math.Clamp(y, 0, image.Size.Height-1); + } } - - /// - /// Clears any existing selection region - /// - public void SelectNone() + else { - SelectionRegion = Rect.Empty; + x = 0; // Return Point.Empty if we couldn't match + y = 0; } - #endregion + 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) + { + Rect 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 Rect.Empty; + 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 = Rect.Empty; + } + + #endregion - #region Viewport and image region methods - /// - /// Gets the source image region. - /// - /// - public Rect GetSourceImageRegion() + #region Viewport and image region methods + /// + /// Gets the source image region. + /// + /// + public Rect GetSourceImageRegion() + { + var image = Image; + if (image is null) return Rect.Empty; + + switch (SizeMode) { - var image = Image; - if (image is null) return Rect.Empty; - - switch (SizeMode) - { - case SizeModes.Normal: - var offset = Offset; - var viewPort = GetImageViewPort(); - var zoomFactor = ZoomFactor; - double sourceLeft = (offset.X / zoomFactor); - double sourceTop = (offset.Y / zoomFactor); - double sourceWidth = (viewPort.Width / zoomFactor); - double sourceHeight = (viewPort.Height / zoomFactor); - - return new(sourceLeft, sourceTop, sourceWidth, sourceHeight); - } - - return new(0, 0, image.Size.Width, image.Size.Height); + case SizeModes.Normal: + var offset = Offset; + var viewPort = GetImageViewPort(); + var zoomFactor = ZoomFactor; + double sourceLeft = (offset.X / zoomFactor); + double sourceTop = (offset.Y / zoomFactor); + double sourceWidth = (viewPort.Width / zoomFactor); + double sourceHeight = (viewPort.Height / zoomFactor); + return new(sourceLeft, sourceTop, sourceWidth, sourceHeight); } - /// - /// Gets the image view port. - /// - /// - public Rect GetImageViewPort() - { - if (!IsImageLoaded || (ViewPortSize.Width == 0 && ViewPortSize.Height == 0)) return Rect.Empty; - - double xOffset = 0; - double yOffset = 0; - double width = 0; - double height = 0; - - 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; - double 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() - { - var image = ImageAsWriteableBitmap; - if (image is null || !HaveSelection) return null; - 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(); - - int i = 0; - - unsafe - { - var inputPixels = (uint*) (void*) frameBuffer.Address; - var targetPixels = (uint*) (void*) newFrameBuffer.Address; - - for (int y = selection.Y; y < selection.Bottom; y++) - { - var thisY = y * frameBuffer.Size.Width; - for (int x = selection.X; x < selection.Right; x++) - { - targetPixels[i++] = inputPixels[thisY + x]; - } - } - } - - return newBitmap; - } - #endregion + return new(0, 0, image.Size.Width, image.Size.Height); } -} + + /// + /// Gets the image view port. + /// + /// + public Rect GetImageViewPort() + { + if (!IsImageLoaded || (ViewPortSize.Width == 0 && ViewPortSize.Height == 0)) return Rect.Empty; + + double xOffset = 0; + double yOffset = 0; + double width = 0; + double height = 0; + + 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; + double 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() + { + var image = ImageAsWriteableBitmap; + if (image is null || !HaveSelection) return null; + + 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(); + + int i = 0; + + unsafe + { + var inputPixels = (uint*) (void*) frameBuffer.Address; + var targetPixels = (uint*) (void*) newFrameBuffer.Address; + + for (int y = selection.Y; y < selection.Bottom; y++) + { + var thisY = y * frameBuffer.Size.Width; + for (int x = selection.X; x < selection.Right; x++) + { + targetPixels![i++] = inputPixels![thisY + x]; + } + } + } + + return newBitmap; + } + #endregion + +} \ No newline at end of file diff --git a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj index 7ed8446..5974565 100644 --- a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj +++ b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 Tiago Conceição PTRTECH Copyright © 2020 PTRTECH @@ -14,6 +14,7 @@ true AvaloniaUI Controls 1.0.1 + enable @@ -34,7 +35,7 @@ - + diff --git a/UVtools.Core/About.cs b/UVtools.Core/About.cs index 1bc41d5..afde01e 100644 --- a/UVtools.Core/About.cs +++ b/UVtools.Core/About.cs @@ -9,24 +9,23 @@ using System; using System.Reflection; -namespace UVtools.Core +namespace UVtools.Core; + +public static class About { - public static class About - { - public const string Software = "UVtools"; - public static string SoftwareWithVersion => $"{Software} v{VersionStr}"; - public const string Author = "Tiago Conceição"; - public const string Company = "PTRTECH"; - public const string License = "GNU Affero General Public License v3.0 (AGPL)"; - public const string LicenseUrl = "https://github.com/sn4k3/UVtools/blob/master/LICENSE"; - public const string Website = "https://github.com/sn4k3/UVtools"; - public const string Donate = "https://paypal.me/SkillTournament"; - public const string Sponsor = "https://github.com/sponsors/sn4k3"; + public const string Software = "UVtools"; + public static string SoftwareWithVersion => $"{Software} v{VersionStr}"; + public const string Author = "Tiago Conceição"; + public const string Company = "PTRTECH"; + public const string License = "GNU Affero General Public License v3.0 (AGPL)"; + public const string LicenseUrl = "https://github.com/sn4k3/UVtools/blob/master/LICENSE"; + public const string Website = "https://github.com/sn4k3/UVtools"; + public const string Donate = "https://paypal.me/SkillTournament"; + public const string Sponsor = "https://github.com/sponsors/sn4k3"; - public const string DemoFile = "UVtools_demo_file.sl1"; + public const string DemoFile = "UVtools_demo_file.sl1"; - public static Version Version => Assembly.GetExecutingAssembly().GetName().Version; - public static string VersionStr => Assembly.GetExecutingAssembly().GetName().Version.ToString(3); - public static string Arch => Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; - } -} + public static Version Version => Assembly.GetExecutingAssembly().GetName().Version!; + public static string VersionStr => Version.ToString(3); + public static string Arch => Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; +} \ No newline at end of file diff --git a/UVtools.Core/Converters/NullTerminatedConverter.cs b/UVtools.Core/Converters/NullTerminatedConverter.cs index 077e87f..e456bd6 100644 --- a/UVtools.Core/Converters/NullTerminatedConverter.cs +++ b/UVtools.Core/Converters/NullTerminatedConverter.cs @@ -7,18 +7,17 @@ */ using BinarySerialization; -namespace UVtools.Core.Converters -{ - public class NullTerminatedConverter : IValueConverter - { - public object Convert(object value, object converterParameter, BinarySerializationContext context) - { - return value.ToString()?.TrimEnd(char.MinValue); - } +namespace UVtools.Core.Converters; - public object ConvertBack(object value, object converterParameter, BinarySerializationContext context) - { - return value is null ? null : $"{value}{char.MinValue}"; - } +public class NullTerminatedConverter : IValueConverter +{ + public object Convert(object value, object converterParameter, BinarySerializationContext context) + { + return value.ToString()!.TrimEnd(char.MinValue); } -} + + public object ConvertBack(object value, object converterParameter, BinarySerializationContext context) + { + return $"{value}{char.MinValue}"; + } +} \ No newline at end of file diff --git a/UVtools.Core/Converters/NullTerminatedLengthConverter.cs b/UVtools.Core/Converters/NullTerminatedLengthConverter.cs index d097e00..1397e54 100644 --- a/UVtools.Core/Converters/NullTerminatedLengthConverter.cs +++ b/UVtools.Core/Converters/NullTerminatedLengthConverter.cs @@ -7,25 +7,24 @@ */ using BinarySerialization; -namespace UVtools.Core.Converters -{ - public class NullTerminatedLengthConverter : IValueConverter - { - // Read - public object Convert(object value, object converterParameter, BinarySerializationContext context) - { - //var uintValue = System.Convert.ToUInt32(value); - //if (uintValue == 0) return 0; - //return uintValue - 1; - return value; - } +namespace UVtools.Core.Converters; - // Write - public object ConvertBack(object value, object converterParameter, BinarySerializationContext context) - { - var uintValue = System.Convert.ToUInt32(value); - if (uintValue == 0) return 0; - return uintValue + 1; - } +public class NullTerminatedLengthConverter : IValueConverter +{ + // Read + public object Convert(object value, object converterParameter, BinarySerializationContext context) + { + //var uintValue = System.Convert.ToUInt32(value); + //if (uintValue == 0) return 0; + //return uintValue - 1; + return value; } -} + + // Write + public object ConvertBack(object value, object converterParameter, BinarySerializationContext context) + { + var uintValue = System.Convert.ToUInt32(value); + if (uintValue == 0) return 0; + return uintValue + 1; + } +} \ No newline at end of file diff --git a/UVtools.Core/CoreSettings.cs b/UVtools.Core/CoreSettings.cs index 1301579..e7b54fb 100644 --- a/UVtools.Core/CoreSettings.cs +++ b/UVtools.Core/CoreSettings.cs @@ -6,48 +6,86 @@ * of this license document, but changing it is not allowed. */ -using System; -using System.Threading.Tasks; using Emgu.CV.Cuda; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; -namespace UVtools.Core +namespace UVtools.Core; + +public static class CoreSettings { - public static class CoreSettings + #region Members + private static int _maxDegreeOfParallelism = -1; + + #endregion + + #region Properties + + /// + /// Gets or sets the maximum number of concurrent tasks enabled by this ParallelOptions instance. + /// Less or equal to 0 will set to auto number + /// 1 = Single thread + /// n = Multi threads + /// + public static int MaxDegreeOfParallelism { - #region Members - private static int _maxDegreeOfParallelism = -1; - - #endregion - - #region Properties - - /// - /// Gets or sets the maximum number of concurrent tasks enabled by this ParallelOptions instance. - /// Less or equal to 0 will set to auto number - /// 1 = Single thread - /// n = Multi threads - /// - public static int MaxDegreeOfParallelism - { - get => _maxDegreeOfParallelism; - set => _maxDegreeOfParallelism = value > 0 ? Math.Min(value, Environment.ProcessorCount) : -1; - } - - /// - /// Gets the ParallelOptions with set - /// - public static ParallelOptions ParallelOptions => new() {MaxDegreeOfParallelism = _maxDegreeOfParallelism}; - - /// - /// Gets or sets if operations run via CUDA when possible - /// - public static bool EnableCuda { get; set; } - - /// - /// Gets if we can use cuda on operations - /// - public static bool CanUseCuda => EnableCuda && CudaInvoke.HasCuda; - - #endregion + get => _maxDegreeOfParallelism; + set => _maxDegreeOfParallelism = value > 0 ? Math.Min(value, Environment.ProcessorCount) : -1; } -} + + /// + /// Gets the ParallelOptions with set + /// + public static ParallelOptions ParallelOptions => new() {MaxDegreeOfParallelism = _maxDegreeOfParallelism}; + + /// + /// Gets or sets if operations run via CUDA when possible + /// + public static bool EnableCuda { get; set; } + + /// + /// Gets if we can use cuda on operations + /// + public static bool CanUseCuda => EnableCuda && CudaInvoke.HasCuda; + + /// + /// Gets the default folder to save the settings + /// + public static string DefaultSettingsFolder + { + get + { + var folder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + if (string.IsNullOrWhiteSpace(folder)) folder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + if (string.IsNullOrWhiteSpace(folder)) folder = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + if (string.IsNullOrWhiteSpace(folder)) folder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var path = Path.Combine(folder, About.Software); + return path; + } + } + + /// + /// Gets the default folder to save the settings + /// + public static string DefaultSettingsFolderAndEnsureCreation + { + get + { + var path = DefaultSettingsFolder; + try + { + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + return path; + } + } + + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/EmguCV/EmguContour.cs b/UVtools.Core/EmguCV/EmguContour.cs index 9b43995..4ecfecb 100644 --- a/UVtools.Core/EmguCV/EmguContour.cs +++ b/UVtools.Core/EmguCV/EmguContour.cs @@ -6,256 +6,255 @@ * of this license document, but changing it is not allowed. */ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Drawing; using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; using UVtools.Core.Extensions; -namespace UVtools.Core.EmguCV +namespace UVtools.Core.EmguCV; + +/// +/// A contour cache for OpenCV +/// +public class EmguContour : IReadOnlyCollection, IDisposable { + #region Constants + + public const byte HierarchyNextSameLevel = 0; + public const byte HierarchyPreviousSameLevel = 1; + public const byte HierarchyFirstChild = 2; + public const byte HierarchyParent = 3; + + #endregion + + #region Members + + private VectorOfPoint _points = null!; + private Rectangle? _bounds; + private RotatedRect? _boundsBestFit; + private CircleF? _minEnclosingCircle; + private bool? _isConvex; + private double _area = double.NaN; + private double _perimeter = double.NaN; + private Moments? _moments; + private Point? _centroid; + + #endregion + + #region Properties + public int XMin => Bounds.X; + + public int YMin => Bounds.Y; + + public int XMax => Bounds.Right; + + public int YMax => Bounds.Bottom; + + public Rectangle Bounds => _bounds ??= CvInvoke.BoundingRectangle(_points); + + public RotatedRect BoundsBestFit => _boundsBestFit ??= CvInvoke.MinAreaRect(_points); + + public CircleF MinEnclosingCircle => _minEnclosingCircle ??= CvInvoke.MinEnclosingCircle(_points); + + public bool IsConvex => _isConvex ??= CvInvoke.IsContourConvex(_points); + /// - /// A contour cache for OpenCV + /// Gets the area of the contour /// - public class EmguContour : IReadOnlyCollection, IDisposable + public double Area { - #region Constants - - public const byte HierarchyNextSameLevel = 0; - public const byte HierarchyPreviousSameLevel = 1; - public const byte HierarchyFirstChild = 2; - public const byte HierarchyParent = 3; - - #endregion - - #region Members - - private VectorOfPoint _points; - private Rectangle? _bounds; - private RotatedRect? _boundsBestFit; - private CircleF? _minEnclosingCircle; - private bool? _isConvex; - private double _area = double.NaN; - private double _perimeter = double.NaN; - private Moments _moments; - private Point? _centroid; - - #endregion - - #region Properties - public int XMin => Bounds.X; - - public int YMin => Bounds.Y; - - public int XMax => Bounds.Right; - - public int YMax => Bounds.Bottom; - - public Rectangle Bounds => _bounds ??= CvInvoke.BoundingRectangle(_points); - - public RotatedRect BoundsBestFit => _boundsBestFit ??= CvInvoke.MinAreaRect(_points); - - public CircleF MinEnclosingCircle => _minEnclosingCircle ??= CvInvoke.MinEnclosingCircle(_points); - - public bool IsConvex => _isConvex ??= CvInvoke.IsContourConvex(_points); - - /// - /// Gets the area of the contour - /// - public double Area + get { - get + if (double.IsNaN(_area)) { - if (double.IsNaN(_area)) - { - _area = CvInvoke.ContourArea(_points); - } + _area = CvInvoke.ContourArea(_points); + } - return _area; - } + return _area; } - - /// - /// Gets the perimeter of the contours - /// - public double Perimeter - { - get - { - if (double.IsNaN(_perimeter)) - { - _perimeter = CvInvoke.ArcLength(_points, true); - } - return _perimeter; - } - } - - public Moments Moments => _moments ??= CvInvoke.Moments(_points); - - /// - /// Gets the centroid of the contour - /// - public Point Centroid => _centroid ??= Moments.M00 == 0 ? new Point(-1,-1) : - new Point( - (int)Math.Round(_moments.M10 / _moments.M00), - (int)Math.Round(_moments.M01 / _moments.M00)); - - /// - /// Gets or sets the contour - /// - public VectorOfPoint Points - { - get => _points; - set - { - Dispose(); - _points = value ?? throw new ArgumentNullException(nameof(Points)); - } - } - - - /// - /// Gets if this contour have any point - /// - public bool IsEmpty => _points.Size == 0; - #endregion - - #region Constructor - public EmguContour(VectorOfPoint points) : this(points.ToArray()) - { } - - public EmguContour(Point[] points) - { - Points = new VectorOfPoint(points); - } - #endregion - - #region Methods - - /// - /// Checks if a given is inside the contour rectangle bounds - /// - /// - /// - public bool IsInsideBounds(Point point) => Bounds.Contains(point); - - /// - /// Gets if a given is inside the contour - /// - /// - /// - public bool IsInside(Point point) - { - if (!IsInsideBounds(point)) return false; - return CvInvoke.PointPolygonTest(_points, point, false) >= 0; - } - - public double MeasureDist(Point point) - { - if (!IsInsideBounds(point)) return -1; - return CvInvoke.PointPolygonTest(_points, point, true); - } - - public IOutputArray ContourApproximation(double epsilon = 0.1) - { - var mat = new Mat(); - CvInvoke.ApproxPolyDP(_points, mat, epsilon*Perimeter, true); - return mat; - } - - /* - /// - /// Calculate the X/Y min/max boundary - /// - private void CalculateMinMax() - { - Bounds = Rectangle.Empty; - - if (_contourPoints.Length == 0) - { - _xMin = -1; - _yMin = -1; - _xMax = -1; - _yMax = -1; - return; - } - - _xMin = int.MaxValue; - _yMin = int.MaxValue; - _xMax = int.MinValue; - _yMax = int.MinValue; - - for (int i = 0; i < _contourPoints.Length; i++) - { - _xMin = Math.Min(_xMin, _contourPoints[i].X); - _yMin = Math.Min(_yMin, _contourPoints[i].Y); - - _xMax = Math.Max(_xMax, _contourPoints[i].X); - _yMax = Math.Max(_yMax, _contourPoints[i].Y); - } - - Bounds = new Rectangle(_xMin, _yMin, _xMax - _xMin, _yMax - _yMin); - } - */ - - public void FitCircle(Mat src, MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, int shift = 0) - { - CvInvoke.Circle(src, - MinEnclosingCircle.Center.ToPoint(), - (int) Math.Round(MinEnclosingCircle.Radius), - color, - thickness, - lineType, - shift); - } - - /*public void FitEllipse(Mat src, MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, int shift = 0) - { - var ellipse = CvInvoke.FitEllipse(_points); - CvInvoke.Ellipse(src, ellipse.Center.ToPoint(), ellipse.Size.ToSize(), ellipse.Angle, 0, 0); - }*/ - #endregion - - #region Static methods - public static Point GetCentroid(VectorOfPoint points) - { - if (points is null || points.Length == 0) return new Point(-1, -1); - using var moments = CvInvoke.Moments(points); - return moments.M00 == 0 ? new Point(-1, -1) : - new Point( - (int)Math.Round(moments.M10 / moments.M00), - (int)Math.Round(moments.M01 / moments.M00)); - } - #endregion - - #region Implementations - - public IEnumerator GetEnumerator() - { - return (IEnumerator) _points.ToArray().GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public int Count => _points.Size; - - public Point this[int index] => _points[index]; - public Point this[uint index] => _points[(int) index]; - public Point this[long index] => _points[(int) index]; - public Point this[ulong index] => _points[(int) index]; - - public void Dispose() - { - _points?.Dispose(); - _moments?.Dispose(); - } - #endregion } -} + + /// + /// Gets the perimeter of the contours + /// + public double Perimeter + { + get + { + if (double.IsNaN(_perimeter)) + { + _perimeter = CvInvoke.ArcLength(_points, true); + } + return _perimeter; + } + } + + public Moments Moments => _moments ??= CvInvoke.Moments(_points); + + /// + /// Gets the centroid of the contour + /// + public Point Centroid => _centroid ??= Moments.M00 == 0 ? new Point(-1,-1) : + new Point( + (int)Math.Round(Moments.M10 / Moments.M00), + (int)Math.Round(Moments.M01 / Moments.M00)); + + /// + /// Gets or sets the contour + /// + public VectorOfPoint Points + { + get => _points; + set + { + Dispose(); + _points = value ?? throw new ArgumentNullException(nameof(Points)); + } + } + + + /// + /// Gets if this contour have any point + /// + public bool IsEmpty => _points.Size == 0; + #endregion + + #region Constructor + public EmguContour(VectorOfPoint points) : this(points.ToArray()) + { } + + public EmguContour(Point[] points) + { + Points = new VectorOfPoint(points); + } + #endregion + + #region Methods + + /// + /// Checks if a given is inside the contour rectangle bounds + /// + /// + /// + public bool IsInsideBounds(Point point) => Bounds.Contains(point); + + /// + /// Gets if a given is inside the contour + /// + /// + /// + public bool IsInside(Point point) + { + if (!IsInsideBounds(point)) return false; + return CvInvoke.PointPolygonTest(_points, point, false) >= 0; + } + + public double MeasureDist(Point point) + { + if (!IsInsideBounds(point)) return -1; + return CvInvoke.PointPolygonTest(_points, point, true); + } + + public IOutputArray ContourApproximation(double epsilon = 0.1) + { + var mat = new Mat(); + CvInvoke.ApproxPolyDP(_points, mat, epsilon*Perimeter, true); + return mat; + } + + /* + /// + /// Calculate the X/Y min/max boundary + /// + private void CalculateMinMax() + { + Bounds = Rectangle.Empty; + + if (_contourPoints.Length == 0) + { + _xMin = -1; + _yMin = -1; + _xMax = -1; + _yMax = -1; + return; + } + + _xMin = int.MaxValue; + _yMin = int.MaxValue; + _xMax = int.MinValue; + _yMax = int.MinValue; + + for (int i = 0; i < _contourPoints.Length; i++) + { + _xMin = Math.Min(_xMin, _contourPoints[i].X); + _yMin = Math.Min(_yMin, _contourPoints[i].Y); + + _xMax = Math.Max(_xMax, _contourPoints[i].X); + _yMax = Math.Max(_yMax, _contourPoints[i].Y); + } + + Bounds = new Rectangle(_xMin, _yMin, _xMax - _xMin, _yMax - _yMin); + } + */ + + public void FitCircle(Mat src, MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, int shift = 0) + { + CvInvoke.Circle(src, + MinEnclosingCircle.Center.ToPoint(), + (int) Math.Round(MinEnclosingCircle.Radius), + color, + thickness, + lineType, + shift); + } + + /*public void FitEllipse(Mat src, MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, int shift = 0) + { + var ellipse = CvInvoke.FitEllipse(_points); + CvInvoke.Ellipse(src, ellipse.Center.ToPoint(), ellipse.Size.ToSize(), ellipse.Angle, 0, 0); + }*/ + #endregion + + #region Static methods + public static Point GetCentroid(VectorOfPoint points) + { + if (points is null || points.Length == 0) return new Point(-1, -1); + using var moments = CvInvoke.Moments(points); + return moments.M00 == 0 ? new Point(-1, -1) : + new Point( + (int)Math.Round(moments.M10 / moments.M00), + (int)Math.Round(moments.M01 / moments.M00)); + } + #endregion + + #region Implementations + + public IEnumerator GetEnumerator() + { + return (IEnumerator) _points.ToArray().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int Count => _points.Size; + + public Point this[int index] => _points[index]; + public Point this[uint index] => _points[(int) index]; + public Point this[long index] => _points[(int) index]; + public Point this[ulong index] => _points[(int) index]; + + public void Dispose() + { + _points?.Dispose(); + _moments?.Dispose(); + } + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/EmguCV/EmguContours.cs b/UVtools.Core/EmguCV/EmguContours.cs index 3c15b56..958cfd8 100644 --- a/UVtools.Core/EmguCV/EmguContours.cs +++ b/UVtools.Core/EmguCV/EmguContours.cs @@ -6,246 +6,245 @@ * of this license document, but changing it is not allowed. */ +using Emgu.CV; +using Emgu.CV.Util; using System.Collections.Generic; using System.Drawing; using System.Threading.Tasks; -using Emgu.CV; -using Emgu.CV.Util; using UVtools.Core.Extensions; -namespace UVtools.Core.EmguCV +namespace UVtools.Core.EmguCV; + +/// +/// Utility methods for contour handling. +/// Use only with Tree type +/// +public static class EmguContours { /// - /// Utility methods for contour handling. - /// Use only with Tree type + /// Gets contours inside a point /// - public static class EmguContours + /// + /// + /// + /// If true it will include all limiting area, otherwise only outer contour will be returned + /// + public static VectorOfVectorOfPoint GetContoursInside(VectorOfVectorOfPoint contours, int[,] hierarchy, Point location, bool includeLimitingArea = true) { - /// - /// Gets contours inside a point - /// - /// - /// - /// - /// If true it will include all limiting area, otherwise only outer contour will be returned - /// - public static VectorOfVectorOfPoint GetContoursInside(VectorOfVectorOfPoint contours, int[,] hierarchy, Point location, bool includeLimitingArea = true) + var vector = new VectorOfVectorOfPoint(); + var vectorSize = contours.Size; + for (var i = vectorSize - 1; i >= 0; i--) { - var vector = new VectorOfVectorOfPoint(); - var vectorSize = contours.Size; - for (var i = vectorSize - 1; i >= 0; i--) + if (CvInvoke.PointPolygonTest(contours[i], location, false) < 0) continue; + vector.Push(contours[i]); + if (!includeLimitingArea) break; + for (int n = i + 1; n < vectorSize; n++) { - if (CvInvoke.PointPolygonTest(contours[i], location, false) < 0) continue; - vector.Push(contours[i]); - if (!includeLimitingArea) break; - for (int n = i + 1; n < vectorSize; n++) - { - if (hierarchy[n, EmguContour.HierarchyParent] != i) continue; - vector.Push(contours[n]); - } - break; + if (hierarchy[n, EmguContour.HierarchyParent] != i) continue; + vector.Push(contours[n]); } - - return vector; + break; } - /// - /// Gets a contour given a location. - /// - /// - /// - /// Contour index, -1 if not exists - /// null if not exists - public static VectorOfPoint GetContourInside(VectorOfVectorOfPoint contours, Point location, out int index) - { - index = -1; - var vectorSize = contours.Size; - for (int i = vectorSize - 1; i >= 0; i--) - { - if (CvInvoke.PointPolygonTest(contours[i], location, false) < 0) continue; - index = i; - return contours[i]; - } - - return null; - } - - /// - /// Gets only the outer most external contours - /// Only compatible with Tree type of contour detection - /// - /// - /// - /// - public static VectorOfVectorOfPoint GetExternalContours(VectorOfVectorOfPoint contours, int[,] hierarchy) - { - var result = new VectorOfVectorOfPoint(); - var vectorSize = contours.Size; - for (var i = 0; i < vectorSize; i++) - { - if (hierarchy[i, EmguContour.HierarchyParent] != -1) continue; - result.Push(contours[i]); - } - - return result; - } - - /// - /// Gets contours inside contours that are black pixels - /// - /// - /// - /// - public static VectorOfVectorOfPoint GetNegativeContours(VectorOfVectorOfPoint contours, int[,] hierarchy) - { - var result = new VectorOfVectorOfPoint(); - var vectorSize = contours.Size; - for (var i = 0; i < vectorSize; i++) - { - if (hierarchy[i, EmguContour.HierarchyParent] == -1) continue; - result.Push(contours[i]); - } - - return result; - } - - /// - /// Gets contours that are positive and negative pixels and group them by areas - /// Only compatible with Tree type of contour detection - /// - /// - public static List[] GetContoursInGroups(VectorOfVectorOfPoint contours, int[,] hierarchy) - { - return new []{GetPositiveContoursInGroups(contours, hierarchy), GetNegativeContoursInGroups(contours, hierarchy)}; - } - - /// - /// Gets contours that are positive pixels and group them by areas - /// Only compatible with Tree type of contour detection - /// - /// - public static List GetPositiveContoursInGroups(VectorOfVectorOfPoint contours, int[,] hierarchy) - { - var result = new List(); - var vectorSize = contours.Size; - var processedContours = new bool[vectorSize]; - for (int i = 0; i < vectorSize; i++) - { - if (processedContours[i]) continue; - processedContours[i] = true; - var index = result.Count; - result.Add(new VectorOfVectorOfPoint(contours[i])); - for (int n = i + 1; n < vectorSize; n++) - { - if (processedContours[n] || hierarchy[n, EmguContour.HierarchyParent] != i) continue; - processedContours[n] = true; - result[index].Push(contours[n]); - } - } - - return result; - } - - /// - /// Gets contours inside contours that are black pixels and group them by areas - /// Only compatible with Tree type of contour detection - /// - /// - public static List GetNegativeContoursInGroups(VectorOfVectorOfPoint contours, int[,] hierarchy) - { - var result = new List(); - var vectorSize = contours.Size; - var processedContours = new bool[vectorSize]; - for (int i = 0; i < vectorSize; i++) - { - if (processedContours[i]) continue; - processedContours[i] = true; - if (hierarchy[i, EmguContour.HierarchyParent] == -1) continue; - var index = result.Count; - result.Add(new VectorOfVectorOfPoint(contours[i])); - for (int n = i + 1; n < vectorSize; n++) - { - if (processedContours[n] || hierarchy[n, EmguContour.HierarchyParent] != i) continue; - processedContours[n] = true; - result[index].Push(contours[n]); - } - } - - return result; - } - - /// - /// Gets contour real area for a limited area - /// - /// - /// - public static double GetContourArea(VectorOfVectorOfPoint contours) - { - var vectorSize = contours.Size; - if (vectorSize == 0) return 0; - - double result = CvInvoke.ContourArea(contours[0]); - for (var i = 1; i < vectorSize; i++) - { - result -= CvInvoke.ContourArea(contours[i]); - } - return result; - } - - /// - /// Gets contours real area for a group of contours - /// - /// Grouped contours - /// True to run in parallel - /// Array with same size with contours area - public static double[] GetContoursArea(List contours, bool useParallel = false) - { - var result = new double[contours.Count]; - - if (useParallel) - { - Parallel.For(0, contours.Count, CoreSettings.ParallelOptions, i => - { - result[i] = GetContourArea(contours[i]); - }); - } - else - { - for (var i = 0; i < contours.Count; i++) - { - result[i] = GetContourArea(contours[i]); - } - } - - return result; - } - - /// - /// Checks if two contours intersects - /// - /// Contour 1 - /// Contour 2 - /// - public static bool ContoursIntersect(VectorOfVectorOfPoint contour1, VectorOfVectorOfPoint contour2) - { - var contour1Rect = CvInvoke.BoundingRectangle(contour1[0]); - var contour2Rect = CvInvoke.BoundingRectangle(contour2[0]); - - /* early exit if the bounding rectangles don't intersect */ - if (!contour1Rect.IntersectsWith(contour2Rect)) return false; - var totalRect = Rectangle.Union(contour1Rect, contour2Rect); - - using var contour1Mat = EmguExtensions.InitMat(totalRect.Size); - using var contour2Mat = EmguExtensions.InitMat(totalRect.Size); - - var inverseOffset = new Point(-totalRect.X, -totalRect.Y); - CvInvoke.DrawContours(contour1Mat, contour1, -1, EmguExtensions.WhiteColor, -1, Emgu.CV.CvEnum.LineType.EightConnected, null, int.MaxValue, inverseOffset); - CvInvoke.DrawContours(contour2Mat, contour2, -1, EmguExtensions.WhiteColor, -1, Emgu.CV.CvEnum.LineType.EightConnected, null, int.MaxValue, inverseOffset); - - CvInvoke.BitwiseAnd(contour1Mat, contour2Mat, contour1Mat); - - //return !contour1Mat.IsZeroed(); - return CvInvoke.CountNonZero(contour1Mat) > 0; - } + return vector; } -} + + /// + /// Gets a contour given a location. + /// + /// + /// + /// Contour index, -1 if not exists + /// null if not exists + public static VectorOfPoint? GetContourInside(VectorOfVectorOfPoint contours, Point location, out int index) + { + index = -1; + var vectorSize = contours.Size; + for (int i = vectorSize - 1; i >= 0; i--) + { + if (CvInvoke.PointPolygonTest(contours[i], location, false) < 0) continue; + index = i; + return contours[i]; + } + + return null; + } + + /// + /// Gets only the outer most external contours + /// Only compatible with Tree type of contour detection + /// + /// + /// + /// + public static VectorOfVectorOfPoint GetExternalContours(VectorOfVectorOfPoint contours, int[,] hierarchy) + { + var result = new VectorOfVectorOfPoint(); + var vectorSize = contours.Size; + for (var i = 0; i < vectorSize; i++) + { + if (hierarchy[i, EmguContour.HierarchyParent] != -1) continue; + result.Push(contours[i]); + } + + return result; + } + + /// + /// Gets contours inside contours that are black pixels + /// + /// + /// + /// + public static VectorOfVectorOfPoint GetNegativeContours(VectorOfVectorOfPoint contours, int[,] hierarchy) + { + var result = new VectorOfVectorOfPoint(); + var vectorSize = contours.Size; + for (var i = 0; i < vectorSize; i++) + { + if (hierarchy[i, EmguContour.HierarchyParent] == -1) continue; + result.Push(contours[i]); + } + + return result; + } + + /// + /// Gets contours that are positive and negative pixels and group them by areas + /// Only compatible with Tree type of contour detection + /// + /// + public static List[] GetContoursInGroups(VectorOfVectorOfPoint contours, int[,] hierarchy) + { + return new []{GetPositiveContoursInGroups(contours, hierarchy), GetNegativeContoursInGroups(contours, hierarchy)}; + } + + /// + /// Gets contours that are positive pixels and group them by areas + /// Only compatible with Tree type of contour detection + /// + /// + public static List GetPositiveContoursInGroups(VectorOfVectorOfPoint contours, int[,] hierarchy) + { + var result = new List(); + var vectorSize = contours.Size; + var processedContours = new bool[vectorSize]; + for (int i = 0; i < vectorSize; i++) + { + if (processedContours[i]) continue; + processedContours[i] = true; + var index = result.Count; + result.Add(new VectorOfVectorOfPoint(contours[i])); + for (int n = i + 1; n < vectorSize; n++) + { + if (processedContours[n] || hierarchy[n, EmguContour.HierarchyParent] != i) continue; + processedContours[n] = true; + result[index].Push(contours[n]); + } + } + + return result; + } + + /// + /// Gets contours inside contours that are black pixels and group them by areas + /// Only compatible with Tree type of contour detection + /// + /// + public static List GetNegativeContoursInGroups(VectorOfVectorOfPoint contours, int[,] hierarchy) + { + var result = new List(); + var vectorSize = contours.Size; + var processedContours = new bool[vectorSize]; + for (int i = 0; i < vectorSize; i++) + { + if (processedContours[i]) continue; + processedContours[i] = true; + if (hierarchy[i, EmguContour.HierarchyParent] == -1) continue; + var index = result.Count; + result.Add(new VectorOfVectorOfPoint(contours[i])); + for (int n = i + 1; n < vectorSize; n++) + { + if (processedContours[n] || hierarchy[n, EmguContour.HierarchyParent] != i) continue; + processedContours[n] = true; + result[index].Push(contours[n]); + } + } + + return result; + } + + /// + /// Gets contour real area for a limited area + /// + /// + /// + public static double GetContourArea(VectorOfVectorOfPoint contours) + { + var vectorSize = contours.Size; + if (vectorSize == 0) return 0; + + double result = CvInvoke.ContourArea(contours[0]); + for (var i = 1; i < vectorSize; i++) + { + result -= CvInvoke.ContourArea(contours[i]); + } + return result; + } + + /// + /// Gets contours real area for a group of contours + /// + /// Grouped contours + /// True to run in parallel + /// Array with same size with contours area + public static double[] GetContoursArea(List contours, bool useParallel = false) + { + var result = new double[contours.Count]; + + if (useParallel) + { + Parallel.For(0, contours.Count, CoreSettings.ParallelOptions, i => + { + result[i] = GetContourArea(contours[i]); + }); + } + else + { + for (var i = 0; i < contours.Count; i++) + { + result[i] = GetContourArea(contours[i]); + } + } + + return result; + } + + /// + /// Checks if two contours intersects + /// + /// Contour 1 + /// Contour 2 + /// + public static bool ContoursIntersect(VectorOfVectorOfPoint contour1, VectorOfVectorOfPoint contour2) + { + var contour1Rect = CvInvoke.BoundingRectangle(contour1[0]); + var contour2Rect = CvInvoke.BoundingRectangle(contour2[0]); + + /* early exit if the bounding rectangles don't intersect */ + if (!contour1Rect.IntersectsWith(contour2Rect)) return false; + var totalRect = Rectangle.Union(contour1Rect, contour2Rect); + + using var contour1Mat = EmguExtensions.InitMat(totalRect.Size); + using var contour2Mat = EmguExtensions.InitMat(totalRect.Size); + + var inverseOffset = new Point(-totalRect.X, -totalRect.Y); + CvInvoke.DrawContours(contour1Mat, contour1, -1, EmguExtensions.WhiteColor, -1, Emgu.CV.CvEnum.LineType.EightConnected, null, int.MaxValue, inverseOffset); + CvInvoke.DrawContours(contour2Mat, contour2, -1, EmguExtensions.WhiteColor, -1, Emgu.CV.CvEnum.LineType.EightConnected, null, int.MaxValue, inverseOffset); + + CvInvoke.BitwiseAnd(contour1Mat, contour2Mat, contour1Mat); + + //return !contour1Mat.IsZeroed(); + return CvInvoke.CountNonZero(contour1Mat) > 0; + } +} \ No newline at end of file diff --git a/UVtools.Core/Enumerations.cs b/UVtools.Core/Enumerations.cs index 98be0a5..c7b9a96 100644 --- a/UVtools.Core/Enumerations.cs +++ b/UVtools.Core/Enumerations.cs @@ -6,93 +6,92 @@ * of this license document, but changing it is not allowed. */ +using Emgu.CV.CvEnum; using System; using System.ComponentModel; -using Emgu.CV.CvEnum; -namespace UVtools.Core +namespace UVtools.Core; + +public class Enumerations { - public class Enumerations + public enum LayerRangeSelection : byte { - public enum LayerRangeSelection : byte - { - None, - All, - Current, - Bottom, - Normal, - First, - Last - } - - public enum FlipDirection : byte - { - None, - Horizontally, - Vertically, - Both, - } - - public enum RotateDirection : sbyte - { - [Description("None")] - None = -1, - /// Rotate 90 degrees clockwise (0) - [Description("Rotate 90º CW")] - Rotate90Clockwise = 0, - /// Rotate 180 degrees clockwise (1) - [Description("Rotate 180º")] - Rotate180 = 1, - /// Rotate 270 degrees clockwise (2) - [Description("Rotate 90º CCW")] - Rotate90CounterClockwise = 2, - } - - public enum Anchor : byte - { - TopLeft, TopCenter, TopRight, - MiddleLeft, MiddleCenter, MiddleRight, - BottomLeft, BottomCenter, BottomRight, - None - } - - public enum LightOffDelaySetMode : byte - { - [Description("Set the light-off with an extra delay")] - UpdateWithExtraDelay, - - [Description("Set the light-off without an extra delay")] - UpdateWithoutExtraDelay, - - [Description("Set the light-off to zero")] - SetToZero, - - [Description("Disabled")] - NoAction - } - - public static FlipType ToOpenCVFlipType(FlipDirection flip) - { - return flip switch - { - FlipDirection.None => throw new NotSupportedException($"Flip type: {flip} is not supported by OpenCV."), - FlipDirection.Horizontally => FlipType.Horizontal, - FlipDirection.Vertically => FlipType.Vertical, - FlipDirection.Both => FlipType.Both, - _ => throw new ArgumentOutOfRangeException(nameof(flip), flip, null) - }; - } - - public static RotateFlags ToOpenCVRotateFlags(RotateDirection rotate) - { - return rotate switch - { - RotateDirection.None => throw new NotSupportedException($"Rotate direction: {rotate} is not supported by OpenCV."), - RotateDirection.Rotate90Clockwise => RotateFlags.Rotate90Clockwise, - RotateDirection.Rotate90CounterClockwise => RotateFlags.Rotate90CounterClockwise, - RotateDirection.Rotate180 => RotateFlags.Rotate180, - _ => throw new ArgumentOutOfRangeException(nameof(rotate), rotate, null) - }; - } + None, + All, + Current, + Bottom, + Normal, + First, + Last } -} + + public enum FlipDirection : byte + { + None, + Horizontally, + Vertically, + Both, + } + + public enum RotateDirection : sbyte + { + [Description("None")] + None = -1, + /// Rotate 90 degrees clockwise (0) + [Description("Rotate 90º CW")] + Rotate90Clockwise = 0, + /// Rotate 180 degrees clockwise (1) + [Description("Rotate 180º")] + Rotate180 = 1, + /// Rotate 270 degrees clockwise (2) + [Description("Rotate 90º CCW")] + Rotate90CounterClockwise = 2, + } + + public enum Anchor : byte + { + TopLeft, TopCenter, TopRight, + MiddleLeft, MiddleCenter, MiddleRight, + BottomLeft, BottomCenter, BottomRight, + None + } + + public enum LightOffDelaySetMode : byte + { + [Description("Set the light-off with an extra delay")] + UpdateWithExtraDelay, + + [Description("Set the light-off without an extra delay")] + UpdateWithoutExtraDelay, + + [Description("Set the light-off to zero")] + SetToZero, + + [Description("Disabled")] + NoAction + } + + public static FlipType ToOpenCVFlipType(FlipDirection flip) + { + return flip switch + { + FlipDirection.None => throw new NotSupportedException($"Flip type: {flip} is not supported by OpenCV."), + FlipDirection.Horizontally => FlipType.Horizontal, + FlipDirection.Vertically => FlipType.Vertical, + FlipDirection.Both => FlipType.Both, + _ => throw new ArgumentOutOfRangeException(nameof(flip), flip, null) + }; + } + + public static RotateFlags ToOpenCVRotateFlags(RotateDirection rotate) + { + return rotate switch + { + RotateDirection.None => throw new NotSupportedException($"Rotate direction: {rotate} is not supported by OpenCV."), + RotateDirection.Rotate90Clockwise => RotateFlags.Rotate90Clockwise, + RotateDirection.Rotate90CounterClockwise => RotateFlags.Rotate90CounterClockwise, + RotateDirection.Rotate180 => RotateFlags.Rotate180, + _ => throw new ArgumentOutOfRangeException(nameof(rotate), rotate, null) + }; + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/BitExtensions.cs b/UVtools.Core/Extensions/BitExtensions.cs index 8efe29c..fdbfbb4 100644 --- a/UVtools.Core/Extensions/BitExtensions.cs +++ b/UVtools.Core/Extensions/BitExtensions.cs @@ -5,154 +5,153 @@ * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class BitExtensions { - public static class BitExtensions + public static ushort ToUShortLittleEndian(byte byte1, byte byte2) => (ushort)(byte1 + (byte2 << 8)); + public static ushort ToUShortBigEndian(byte byte1, byte byte2) => (ushort)((byte1 << 8) + byte2); + + public static ushort ToUShortLittleEndian(byte[] buffer, int offset = 0) + => (ushort)(buffer[offset] + (buffer[offset+1] << 8)); + public static ushort ToUShortBigEndian(byte[] buffer, int offset = 0) + => (ushort)((buffer[offset] << 8) + buffer[offset+1]); + + public static uint ToUIntLittleEndian(byte byte1, byte byte2, byte byte3, byte byte4) + => (uint)(byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24)); + public static uint ToUIntBigEndian(byte byte1, byte byte2, byte byte3, byte byte4) + => (uint)((byte1 << 24) + (byte2 << 16) + (byte3 << 8) + byte4); + + public static uint ToUIntLittleEndian(byte[] buffer, int offset = 0) + => (uint)(buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + (buffer[offset + 3] << 24)); + public static uint ToUIntBigEndian(byte[] buffer, int offset = 0) + => (uint)((buffer[offset] << 24) + (buffer[offset+1] << 16) + (buffer[offset+2] << 8) + buffer[offset+3]); + + public static byte[] ToBytesLittleEndian(ushort value) { - public static ushort ToUShortLittleEndian(byte byte1, byte byte2) => (ushort)(byte1 + (byte2 << 8)); - public static ushort ToUShortBigEndian(byte byte1, byte byte2) => (ushort)((byte1 << 8) + byte2); - - public static ushort ToUShortLittleEndian(byte[] buffer, int offset = 0) - => (ushort)(buffer[offset] + (buffer[offset+1] << 8)); - public static ushort ToUShortBigEndian(byte[] buffer, int offset = 0) - => (ushort)((buffer[offset] << 8) + buffer[offset+1]); - - public static uint ToUIntLittleEndian(byte byte1, byte byte2, byte byte3, byte byte4) - => (uint)(byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24)); - public static uint ToUIntBigEndian(byte byte1, byte byte2, byte byte3, byte byte4) - => (uint)((byte1 << 24) + (byte2 << 16) + (byte3 << 8) + byte4); - - public static uint ToUIntLittleEndian(byte[] buffer, int offset = 0) - => (uint)(buffer[offset] + (buffer[offset + 1] << 8) + (buffer[offset + 2] << 16) + (buffer[offset + 3] << 24)); - public static uint ToUIntBigEndian(byte[] buffer, int offset = 0) - => (uint)((buffer[offset] << 24) + (buffer[offset+1] << 16) + (buffer[offset+2] << 8) + buffer[offset+3]); - - public static byte[] ToBytesLittleEndian(ushort value) - { - var bytes = new byte[2]; - ToBytesLittleEndian(value, bytes); - return bytes; - } - - public static void ToBytesLittleEndian(ushort value, byte[] buffer, uint offset = 0) - { - buffer[offset] = (byte)value; - buffer[offset + 1] = (byte)(value >> 8); - } - - public static byte[] ToBytesBigEndian(ushort value) - { - var bytes = new byte[2]; - ToBytesBigEndian(value, bytes); - return bytes; - } - - public static void ToBytesBigEndian(ushort value, byte[] buffer, uint offset = 0) - { - buffer[offset] = (byte)(value >> 8); - buffer[offset + 1] = (byte)value; - } - - public static byte[] ToBytesLittleEndian(uint value) - { - var bytes = new byte[4]; - ToBytesLittleEndian(value, bytes); - return bytes; - } - - public static void ToBytesLittleEndian(uint value, byte[] buffer, uint offset = 0) - { - buffer[offset] = (byte)value; - buffer[offset + 1] = (byte)(value >> 8); - buffer[offset + 2] = (byte)(value >> 16); - buffer[offset + 3] = (byte)(value >> 24); - } - - public static byte[] ToBytesBigEndian(uint value) - { - var bytes = new byte[4]; - ToBytesBigEndian(value, bytes); - return bytes; - } - - public static void ToBytesBigEndian(uint value, byte[] buffer, uint offset = 0) - { - buffer[offset] = (byte)(value >> 24); - buffer[offset + 1] = (byte)(value >> 16); - buffer[offset + 2] = (byte)(value >> 8); - buffer[offset + 3] = (byte)value; - } - - public static byte[] ToBytesLittleEndian(int value) - { - var bytes = new byte[4]; - ToBytesLittleEndian(value, bytes); - return bytes; - } - - public static void ToBytesLittleEndian(int value, byte[] buffer, uint offset = 0) - { - buffer[offset] = (byte)value; - buffer[offset + 1] = (byte)(value >> 8); - buffer[offset + 2] = (byte)(value >> 16); - buffer[offset + 3] = (byte)(value >> 24); - } - - public static byte[] ToBytesBigEndian(int value) - { - var bytes = new byte[4]; - ToBytesBigEndian(value, bytes); - return bytes; - } - - public static void ToBytesBigEndian(int value, byte[] buffer, uint offset = 0) - { - buffer[offset] = (byte)(value >> 24); - buffer[offset + 1] = (byte)(value >> 16); - buffer[offset + 2] = (byte)(value >> 8); - buffer[offset + 3] = (byte)value; - } - - - public static byte[] ToBytesLittleEndian(ulong value) - { - var bytes = new byte[8]; - ToBytesLittleEndian(value, bytes); - return bytes; - } - - public static void ToBytesLittleEndian(ulong value, byte[] buffer, ulong offset = 0) - { - buffer[offset] = (byte)value; - buffer[offset + 1] = (byte)(value >> 8); - buffer[offset + 2] = (byte)(value >> 16); - buffer[offset + 3] = (byte)(value >> 24); - - buffer[offset + 4] = (byte)(value >> 32); - buffer[offset + 5] = (byte)(value >> 40); - buffer[offset + 6] = (byte)(value >> 48); - buffer[offset + 7] = (byte)(value >> 56); - } - - public static byte[] ToBytesBigEndian(ulong value) - { - var bytes = new byte[8]; - ToBytesBigEndian(value, bytes); - return bytes; - } - - public static void ToBytesBigEndian(ulong value, byte[] buffer, ulong offset = 0) - { - buffer[offset] = (byte)(value >> 56); - buffer[offset + 1] = (byte)(value >> 48); - buffer[offset + 2] = (byte)(value >> 40); - buffer[offset + 3] = (byte)(value >> 32); - buffer[offset + 4] = (byte)(value >> 24); - buffer[offset + 5] = (byte)(value >> 16); - buffer[offset + 6] = (byte)(value >> 8); - buffer[offset + 7] = (byte)value; - } - - + var bytes = new byte[2]; + ToBytesLittleEndian(value, bytes); + return bytes; } -} + + public static void ToBytesLittleEndian(ushort value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + } + + public static byte[] ToBytesBigEndian(ushort value) + { + var bytes = new byte[2]; + ToBytesBigEndian(value, bytes); + return bytes; + } + + public static void ToBytesBigEndian(ushort value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)(value >> 8); + buffer[offset + 1] = (byte)value; + } + + public static byte[] ToBytesLittleEndian(uint value) + { + var bytes = new byte[4]; + ToBytesLittleEndian(value, bytes); + return bytes; + } + + public static void ToBytesLittleEndian(uint value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + buffer[offset + 2] = (byte)(value >> 16); + buffer[offset + 3] = (byte)(value >> 24); + } + + public static byte[] ToBytesBigEndian(uint value) + { + var bytes = new byte[4]; + ToBytesBigEndian(value, bytes); + return bytes; + } + + public static void ToBytesBigEndian(uint value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)value; + } + + public static byte[] ToBytesLittleEndian(int value) + { + var bytes = new byte[4]; + ToBytesLittleEndian(value, bytes); + return bytes; + } + + public static void ToBytesLittleEndian(int value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + buffer[offset + 2] = (byte)(value >> 16); + buffer[offset + 3] = (byte)(value >> 24); + } + + public static byte[] ToBytesBigEndian(int value) + { + var bytes = new byte[4]; + ToBytesBigEndian(value, bytes); + return bytes; + } + + public static void ToBytesBigEndian(int value, byte[] buffer, uint offset = 0) + { + buffer[offset] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)value; + } + + + public static byte[] ToBytesLittleEndian(ulong value) + { + var bytes = new byte[8]; + ToBytesLittleEndian(value, bytes); + return bytes; + } + + public static void ToBytesLittleEndian(ulong value, byte[] buffer, ulong offset = 0) + { + buffer[offset] = (byte)value; + buffer[offset + 1] = (byte)(value >> 8); + buffer[offset + 2] = (byte)(value >> 16); + buffer[offset + 3] = (byte)(value >> 24); + + buffer[offset + 4] = (byte)(value >> 32); + buffer[offset + 5] = (byte)(value >> 40); + buffer[offset + 6] = (byte)(value >> 48); + buffer[offset + 7] = (byte)(value >> 56); + } + + public static byte[] ToBytesBigEndian(ulong value) + { + var bytes = new byte[8]; + ToBytesBigEndian(value, bytes); + return bytes; + } + + public static void ToBytesBigEndian(ulong value, byte[] buffer, ulong offset = 0) + { + buffer[offset] = (byte)(value >> 56); + buffer[offset + 1] = (byte)(value >> 48); + buffer[offset + 2] = (byte)(value >> 40); + buffer[offset + 3] = (byte)(value >> 32); + buffer[offset + 4] = (byte)(value >> 24); + buffer[offset + 5] = (byte)(value >> 16); + buffer[offset + 6] = (byte)(value >> 8); + buffer[offset + 7] = (byte)value; + } + + +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/ClassExtensions.cs b/UVtools.Core/Extensions/ClassExtensions.cs index 2f21c1b..222c76f 100644 --- a/UVtools.Core/Extensions/ClassExtensions.cs +++ b/UVtools.Core/Extensions/ClassExtensions.cs @@ -6,22 +6,21 @@ * of this license document, but changing it is not allowed. */ -using Newtonsoft.Json; +using System.Text.Json; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class ClassExtensions { - public static class ClassExtensions + public static T? CloneByJsonSerialization(this T classToClone) where T : class { - public static T CloneByJsonSerialization(this T classToClone) where T : class - { - var clone = JsonConvert.SerializeObject (classToClone); - return JsonConvert.DeserializeObject(clone); - } - - public static T CloneByXmlSerialization(this T classToClone) where T : class - { - var clone = XmlExtensions.SerializeObject(classToClone); - return XmlExtensions.DeserializeObject(clone); - } + var clone = JsonSerializer.SerializeToUtf8Bytes(classToClone); + return JsonSerializer.Deserialize(clone); } -} + + public static T CloneByXmlSerialization(this T classToClone) where T : class + { + var clone = XmlExtensions.SerializeObject(classToClone); + return XmlExtensions.DeserializeFromText(clone); + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/CryptExtensions.cs b/UVtools.Core/Extensions/CryptExtensions.cs index 268d8e1..3962901 100644 --- a/UVtools.Core/Extensions/CryptExtensions.cs +++ b/UVtools.Core/Extensions/CryptExtensions.cs @@ -10,115 +10,112 @@ using System; using System.IO; using System.Security.Cryptography; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class CryptExtensions { - public static class CryptExtensions + public static readonly SHA1 SHA1Instance = SHA1.Create(); + public static string ComputeSHA1Hash(byte[] input) { - public static readonly SHA1CryptoServiceProvider SHA1 = new(); - public static string ComputeSHA1Hash(byte[] input) - { - return Convert.ToBase64String(SHA1.ComputeHash(input)); - } - - public static readonly SHA256 SHA256 = SHA256.Create(); - public static byte[] ComputeSHA256Hash(byte[] input) - { - return SHA256.ComputeHash(input); - } - - public static byte[] AesCryptBytes(byte[] data, byte[] key, CipherMode mode, PaddingMode paddingMode, bool encrypt, byte[] iv = null) - { - if (data.Length % 16 != 0) - { - var temp = new byte[((data.Length / 16) + 1) * 16]; - Array.Copy(data, 0, temp, 0, data.Length); - data = temp; - } - - var aes = new AesManaged - { - KeySize = key.Length * 8, - Key = key, - Padding = paddingMode, - Mode = mode, - }; - - if (iv != null) - { - aes.IV = iv; - } - - var cryptor = encrypt ? aes.CreateEncryptor() : aes.CreateDecryptor(); - - using var msDecrypt = new MemoryStream(data); - using var csDecrypt = new CryptoStream(msDecrypt, cryptor, CryptoStreamMode.Read); - var outputBuffer = new byte[data.Length]; - csDecrypt.Read(outputBuffer, 0, data.Length); - - return outputBuffer; - } - - public static MemoryStream AesCryptMemoryStream(byte[] data, byte[] key, CipherMode mode, PaddingMode paddingMode, bool encrypt, byte[] iv = null) - => new(AesCryptBytes(data, key, mode, paddingMode, encrypt, iv)); - - public static string Base64EncodeString(string plainText) - { - var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); - return Convert.ToBase64String(plainTextBytes); - } - - public static string Base64DecodeString(string base64EncodedData) - { - var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); - return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); - } - - public static string XORCipherString(string text, string key) - { - var output = new char[text.Length]; - - for (int i = 0; i < text.Length; i++) - { - output[i] = (char)(text[i] ^ key[i % key.Length]); - } - - return new string(output); - } - - public static string XORCipherString(byte[] bytes, string key) - { - var output = new char[bytes.Length]; - - for (int i = 0; i < bytes.Length; i++) - { - output[i] = (char)(bytes[i] ^ key[i % key.Length]); - } - - return new string(output); - } - - public static byte[] XORCipher(string text, string key) - { - var output = new byte[text.Length]; - - for (int i = 0; i < text.Length; i++) - { - output[i] = (byte)(text[i] ^ key[i % key.Length]); - } - - return output; - } - - public static byte[] XORCipher(byte[] bytes, string key) - { - var output = new byte[bytes.Length]; - - for (int i = 0; i < bytes.Length; i++) - { - output[i] = (byte)(bytes[i] ^ key[i % key.Length]); - } - - return output; - } + return Convert.ToBase64String(SHA1Instance.ComputeHash(input)); } -} + + public static readonly SHA256 SHA256Instance = SHA256.Create(); + public static byte[] ComputeSHA256Hash(byte[] input) + { + return SHA256Instance.ComputeHash(input); + } + + public static byte[] AesCryptBytes(byte[] data, byte[] key, CipherMode mode, PaddingMode paddingMode, bool encrypt, byte[]? iv = null) + { + if (data.Length % 16 != 0) + { + var temp = new byte[((data.Length / 16) + 1) * 16]; + Array.Copy(data, 0, temp, 0, data.Length); + data = temp; + } + + var aes = Aes.Create(); + aes.KeySize = key.Length * 8; + aes.Key = key; + aes.Padding = paddingMode; + aes.Mode = mode; + + if (iv != null) + { + aes.IV = iv; + } + + var cryptor = encrypt ? aes.CreateEncryptor() : aes.CreateDecryptor(); + + using var msDecrypt = new MemoryStream(data); + using var csDecrypt = new CryptoStream(msDecrypt, cryptor, CryptoStreamMode.Read); + var outputBuffer = new byte[data.Length]; + csDecrypt.Read(outputBuffer, 0, data.Length); + + return outputBuffer; + } + + public static MemoryStream AesCryptMemoryStream(byte[] data, byte[] key, CipherMode mode, PaddingMode paddingMode, bool encrypt, byte[]? iv = null) + => new(AesCryptBytes(data, key, mode, paddingMode, encrypt, iv)); + + public static string Base64EncodeString(string plainText) + { + var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } + + public static string Base64DecodeString(string base64EncodedData) + { + var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); + return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); + } + + public static string XORCipherString(string text, string key) + { + var output = new char[text.Length]; + + for (int i = 0; i < text.Length; i++) + { + output[i] = (char)(text[i] ^ key[i % key.Length]); + } + + return new string(output); + } + + public static string XORCipherString(byte[] bytes, string key) + { + var output = new char[bytes.Length]; + + for (int i = 0; i < bytes.Length; i++) + { + output[i] = (char)(bytes[i] ^ key[i % key.Length]); + } + + return new string(output); + } + + public static byte[] XORCipher(string text, string key) + { + var output = new byte[text.Length]; + + for (int i = 0; i < text.Length; i++) + { + output[i] = (byte)(text[i] ^ key[i % key.Length]); + } + + return output; + } + + public static byte[] XORCipher(byte[] bytes, string key) + { + var output = new byte[bytes.Length]; + + for (int i = 0; i < bytes.Length; i++) + { + output[i] = (byte)(bytes[i] ^ key[i % key.Length]); + } + + return output; + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/DateTimeExtensions.cs b/UVtools.Core/Extensions/DateTimeExtensions.cs index f2c4b11..ebb132e 100644 --- a/UVtools.Core/Extensions/DateTimeExtensions.cs +++ b/UVtools.Core/Extensions/DateTimeExtensions.cs @@ -9,43 +9,42 @@ using System; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class DateTimeExtensions { - public static class DateTimeExtensions + /// + /// Gets the Unix timestamp since Jan 1, 1970 UTC + /// + public static TimeSpan Timestamp => DateTime.UtcNow.Subtract(DateTime.UnixEpoch); + + /// + /// Gets the Unix timestamp in seconds since Jan 1, 1970 UTC + /// + public static double TimestampSeconds => Timestamp.TotalSeconds; + + /// + /// Gets the Unix minutes in seconds since Jan 1, 1970 UTC + /// + public static double TimestampMinutes => Timestamp.TotalMinutes; + + /// + /// Gets the from a unix timestamp in seconds + /// + /// + /// + public static DateTime GetDateTimeFromTimestampSeconds(double seconds) { - /// - /// Gets the Unix timestamp since Jan 1, 1970 UTC - /// - public static TimeSpan Timestamp => DateTime.UtcNow.Subtract(DateTime.UnixEpoch); - - /// - /// Gets the Unix timestamp in seconds since Jan 1, 1970 UTC - /// - public static double TimestampSeconds => Timestamp.TotalSeconds; - - /// - /// Gets the Unix minutes in seconds since Jan 1, 1970 UTC - /// - public static double TimestampMinutes => Timestamp.TotalMinutes; - - /// - /// Gets the from a unix timestamp in seconds - /// - /// - /// - public static DateTime GetDateTimeFromTimestampSeconds(double seconds) - { - return DateTime.UnixEpoch.AddSeconds(seconds); - } - - /// - /// Gets the from a unix timestamp in minutes - /// - /// - /// - public static DateTime GetDateTimeFromTimestampMinutes(double minutes) - { - return DateTime.UnixEpoch.AddMinutes(minutes); - } + return DateTime.UnixEpoch.AddSeconds(seconds); } -} + + /// + /// Gets the from a unix timestamp in minutes + /// + /// + /// + public static DateTime GetDateTimeFromTimestampMinutes(double minutes) + { + return DateTime.UnixEpoch.AddMinutes(minutes); + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/DrawingExtensions.cs b/UVtools.Core/Extensions/DrawingExtensions.cs index d8f4385..f0f25bc 100644 --- a/UVtools.Core/Extensions/DrawingExtensions.cs +++ b/UVtools.Core/Extensions/DrawingExtensions.cs @@ -1,100 +1,99 @@ using System; using System.Drawing; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class DrawingExtensions { - public static class DrawingExtensions + public static Color FactorColor(this Color color, byte pixelColor, byte min = 0, byte max = byte.MaxValue) => + FactorColor(color, pixelColor / 255f, min, max); + + public static Color FactorColor(this Color color, float factor, byte min = 0, byte max = byte.MaxValue) { - public static Color FactorColor(this Color color, byte pixelColor, byte min = 0, byte max = byte.MaxValue) => - FactorColor(color, pixelColor / 255f, min, max); + byte r = (byte)(color.R == 0 ? 0 : + Math.Min(Math.Max(min, color.R * factor), max)); - public static Color FactorColor(this Color color, float factor, byte min = 0, byte max = byte.MaxValue) - { - byte r = (byte)(color.R == 0 ? 0 : - Math.Min(Math.Max(min, color.R * factor), max)); - - byte g = (byte)(color.G == 0 ? 0 : - Math.Min(Math.Max(min, color.G * factor), max)); + byte g = (byte)(color.G == 0 ? 0 : + Math.Min(Math.Max(min, color.G * factor), max)); - byte b = (byte)(color.B == 0 ? 0 : - Math.Min(Math.Max(min, color.B * factor), max)); - return Color.FromArgb(r, g, b); - } - - public static double CalculatePolygonSideLengthFromRadius(double radius, int sides) - { - return 2 * radius * Math.Sin(Math.PI / sides); - } - - public static double CalculatePolygonVerticalLengthFromRadius(double radius, int sides) - { - return radius * Math.Cos(Math.PI / sides); - } - - public static double CalculatePolygonRadiusFromSideLength(double length, int sides) - { - var theta = 360.0 / sides; - return length / (2 * Math.Cos((90 - theta / 2) * Math.PI / 180.0)); - } - - public static Point[] GetPolygonVertices(int sides, int radius, Point center, double startingAngle = 0, bool flipHorizontally = false, bool flipVertically = false) - { - if (sides < 3) - throw new ArgumentException("Polygons can't have less than 3 sides...", nameof(sides)); - - var vertices = new Point[sides]; - - double deg = 360.0 / sides;//calculate the rotation angle - var rad = Math.PI / 180.0; - - var x0 = center.X + radius * Math.Cos(-(((180 - deg) / 2) + startingAngle) * rad); - var y0 = center.Y - radius * Math.Sin(-(((180 - deg) / 2) + startingAngle) * rad); - - var x1 = center.X + radius * Math.Cos(-(((180 - deg) / 2) + deg + startingAngle) * rad); - var y1 = center.Y - radius * Math.Sin(-(((180 - deg) / 2) + deg + startingAngle) * rad); - - vertices[0] = new( - (int) Math.Round(x0), - (int) Math.Round(y0) - ); - - vertices[1] = new( - (int) Math.Round(x1), - (int) Math.Round(y1) - ); - - for (int i = 0; i < sides - 2; i++) - { - double dsinrot = Math.Sin((deg * (i + 1)) * rad); - double dcosrot = Math.Cos((deg * (i + 1)) * rad); - - vertices[i + 2] = new( - (int)Math.Round(center.X + dcosrot * (x1 - center.X) - dsinrot * (y1 - center.Y)), - (int)Math.Round(center.Y + dsinrot * (x1 - center.X) + dcosrot * (y1 - center.Y)) - ); - } - - if (flipHorizontally) - { - var startX = center.X - radius; - var endX = center.X + radius; - for (int i = 0; i < sides; i++) - { - vertices[i].X = endX - (vertices[i].X - startX); - } - } - - if (flipVertically) - { - var startY = center.Y - radius; - var endY = center.Y + radius; - for (int i = 0; i < sides; i++) - { - vertices[i].Y = endY - (vertices[i].Y - startY); - } - } - - return vertices; - } + byte b = (byte)(color.B == 0 ? 0 : + Math.Min(Math.Max(min, color.B * factor), max)); + return Color.FromArgb(r, g, b); } -} + + public static double CalculatePolygonSideLengthFromRadius(double radius, int sides) + { + return 2 * radius * Math.Sin(Math.PI / sides); + } + + public static double CalculatePolygonVerticalLengthFromRadius(double radius, int sides) + { + return radius * Math.Cos(Math.PI / sides); + } + + public static double CalculatePolygonRadiusFromSideLength(double length, int sides) + { + var theta = 360.0 / sides; + return length / (2 * Math.Cos((90 - theta / 2) * Math.PI / 180.0)); + } + + public static Point[] GetPolygonVertices(int sides, int radius, Point center, double startingAngle = 0, bool flipHorizontally = false, bool flipVertically = false) + { + if (sides < 3) + throw new ArgumentException("Polygons can't have less than 3 sides...", nameof(sides)); + + var vertices = new Point[sides]; + + double deg = 360.0 / sides;//calculate the rotation angle + var rad = Math.PI / 180.0; + + var x0 = center.X + radius * Math.Cos(-(((180 - deg) / 2) + startingAngle) * rad); + var y0 = center.Y - radius * Math.Sin(-(((180 - deg) / 2) + startingAngle) * rad); + + var x1 = center.X + radius * Math.Cos(-(((180 - deg) / 2) + deg + startingAngle) * rad); + var y1 = center.Y - radius * Math.Sin(-(((180 - deg) / 2) + deg + startingAngle) * rad); + + vertices[0] = new( + (int) Math.Round(x0), + (int) Math.Round(y0) + ); + + vertices[1] = new( + (int) Math.Round(x1), + (int) Math.Round(y1) + ); + + for (int i = 0; i < sides - 2; i++) + { + double dsinrot = Math.Sin((deg * (i + 1)) * rad); + double dcosrot = Math.Cos((deg * (i + 1)) * rad); + + vertices[i + 2] = new( + (int)Math.Round(center.X + dcosrot * (x1 - center.X) - dsinrot * (y1 - center.Y)), + (int)Math.Round(center.Y + dsinrot * (x1 - center.X) + dcosrot * (y1 - center.Y)) + ); + } + + if (flipHorizontally) + { + var startX = center.X - radius; + var endX = center.X + radius; + for (int i = 0; i < sides; i++) + { + vertices[i].X = endX - (vertices[i].X - startX); + } + } + + if (flipVertically) + { + var startY = center.Y - radius; + var endY = center.Y + radius; + for (int i = 0; i < sides; i++) + { + vertices[i].Y = endY - (vertices[i].Y - startY); + } + } + + return vertices; + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs index 730ec12..aae8bd6 100644 --- a/UVtools.Core/Extensions/EmguExtensions.cs +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -6,1350 +6,1349 @@ * of this license document, but changing it is not allowed. */ -using System; -using System.Drawing; -using System.Runtime.InteropServices; using Emgu.CV; using Emgu.CV.Cuda; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using Emgu.CV.Util; +using System; +using System.Drawing; +using System.Runtime.InteropServices; using UVtools.Core.EmguCV; using UVtools.Core.Objects; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class EmguExtensions { - public static class EmguExtensions + #region Constants + /// + /// White color: 255, 255, 255, 255 + /// + public static readonly MCvScalar WhiteColor = new(255, 255, 255, 255); + + /// + /// Black color: 0, 0, 0, 255 + /// + public static readonly MCvScalar BlackColor = new(0, 0, 0, 255); + //public static readonly MCvScalar TransparentColor = new(); + + public static readonly Mat Kernel3x3Rectangle = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), new Point(-1, -1)); + #endregion + + #region Initializers methods + /// + /// Create a byte array of size of this + /// + /// + /// Blank byte array + public static byte[] CreateBlankByteArray(this Mat mat) + => new byte[mat.GetLength()]; + + /// + /// Creates a new empty with same size and type of the source + /// + /// + /// + public static Mat New(this Mat mat) + => new(mat.Size, mat.Depth, mat.NumberOfChannels); + + /// + /// Creates a new empty with same size and type of the source + /// + /// + /// + /// + public static Mat New(this Mat src, MCvScalar color) + => InitMat(src.Size, color, src.NumberOfChannels, src.Depth); + + /// + /// Creates a new blanked (All zeros) with same size and type of the source + /// + /// + /// Blanked + public static Mat NewBlank(this Mat mat) + => InitMat(mat.Size, mat.NumberOfChannels, mat.Depth); + + /// + /// Creates a new blanked (All zeros) with same size and type of the source + /// + /// + /// Blanked + public static UMat NewBlank(this UMat mat) + => InitUMat(mat.Size, mat.NumberOfChannels, mat.Depth); + + /// + /// Creates a with same size and type of the source and set it to a color + /// + /// + /// + /// + public static Mat NewSetTo(this Mat mat, MCvScalar color) + => InitMat(mat.Size, color, mat.NumberOfChannels, mat.Depth); + + + /// + /// Creates a new and zero it + /// + /// + /// + /// + /// + public static Mat InitMat(Size size, int channels = 1, DepthType depthType = DepthType.Cv8U) + => size.IsEmpty ? new() : Mat.Zeros(size.Height, size.Width, depthType, channels); + + /// + /// Creates a new and zero it + /// + /// + /// + /// + /// + public static UMat InitUMat(Size size, int channels = 1, DepthType depthType = DepthType.Cv8U) { - #region Constants - /// - /// White color: 255, 255, 255, 255 - /// - public static readonly MCvScalar WhiteColor = new(255, 255, 255, 255); + if (size.IsEmpty) return new(); + var umat = new UMat(size.Height, size.Width, depthType, channels); + umat.SetTo(BlackColor); + return umat; + } - /// - /// Black color: 0, 0, 0, 255 - /// - public static readonly MCvScalar BlackColor = new(0, 0, 0, 255); - //public static readonly MCvScalar TransparentColor = new(); + /// + /// Creates a new and set it to a + /// + /// + /// + /// + /// + /// + public static Mat InitMat(Size size, MCvScalar color, int channels = 1, DepthType depthType = DepthType.Cv8U) + { + if (size.IsEmpty) return new(); + var mat = new Mat(size, depthType, channels); + mat.SetTo(color); + return mat; + } - public static readonly Mat Kernel3x3Rectangle = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(3, 3), new Point(-1, -1)); - #endregion + /// + /// Allocates a new array of mat's + /// + /// Array size + /// + public static Mat[] InitMats(uint count) => InitMats(count, Size.Empty); - #region Initializers methods - /// - /// Create a byte array of size of this - /// - /// - /// Blank byte array - public static byte[] CreateBlankByteArray(this Mat mat) - => new byte[mat.GetLength()]; - - /// - /// Creates a new empty with same size and type of the source - /// - /// - /// - public static Mat New(this Mat mat) - => new(mat.Size, mat.Depth, mat.NumberOfChannels); - - /// - /// Creates a new empty with same size and type of the source - /// - /// - /// - /// - public static Mat New(this Mat src, MCvScalar color) - => InitMat(src.Size, color, src.NumberOfChannels, src.Depth); - - /// - /// Creates a new blanked (All zeros) with same size and type of the source - /// - /// - /// Blanked - public static Mat NewBlank(this Mat mat) - => InitMat(mat.Size, mat.NumberOfChannels, mat.Depth); - - /// - /// Creates a new blanked (All zeros) with same size and type of the source - /// - /// - /// Blanked - public static UMat NewBlank(this UMat mat) - => InitUMat(mat.Size, mat.NumberOfChannels, mat.Depth); - - /// - /// Creates a with same size and type of the source and set it to a color - /// - /// - /// - /// - public static Mat NewSetTo(this Mat mat, MCvScalar color) - => InitMat(mat.Size, color, mat.NumberOfChannels, mat.Depth); - - - /// - /// Creates a new and zero it - /// - /// - /// - /// - /// - public static Mat InitMat(Size size, int channels = 1, DepthType depthType = DepthType.Cv8U) - => size.IsEmpty ? new() : Mat.Zeros(size.Height, size.Width, depthType, channels); - - /// - /// Creates a new and zero it - /// - /// - /// - /// - /// - public static UMat InitUMat(Size size, int channels = 1, DepthType depthType = DepthType.Cv8U) + /// + /// Allocates a new array of mat 's + /// + /// Array size + /// Image size to create, use to create a empty Mat + /// New mat array + public static Mat[] InitMats(uint count, Size size) + { + var layers = new Mat[count]; + for (var i = 0; i < layers.Length; i++) { - if (size.IsEmpty) return new(); - var umat = new UMat(size.Height, size.Width, depthType, channels); - umat.SetTo(BlackColor); - return umat; + layers[i] = InitMat(size); } - /// - /// Creates a new and set it to a - /// - /// - /// - /// - /// - /// - public static Mat InitMat(Size size, MCvScalar color, int channels = 1, DepthType depthType = DepthType.Cv8U) + return layers; + } + + /// + /// Create a new from + /// + /// + /// + public static GpuMat ToGpuMat(this Mat mat) + { + var gpuMat = new GpuMat(mat.Rows, mat.Cols, mat.Depth, mat.NumberOfChannels); + gpuMat.Upload(mat); + return gpuMat; + } + #endregion + + #region Memory accessors + + /// + /// Gets the byte pointer of this + /// + /// + /// + public static unsafe byte* GetBytePointer(this Mat mat) + { + return (byte*)mat.DataPointer.ToPointer(); + } + + /// + /// Gets the whole data span to manipulate or read pixels + /// + /// + /// + public static unsafe Span GetDataByteSpan(this Mat mat) + { + return new(mat.DataPointer.ToPointer(), mat.GetLength()); + } + + public static unsafe Span GetDataByteSpan(this Mat mat, int length, int offset = 0) + { + return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length <= 0 ? mat.GetLength() : length); + } + + /// + /// Gets the data span to manipulate or read pixels given a length and offset + /// + /// + /// + /// + /// + /// + public static unsafe Span GetDataSpan(this Mat mat, int length = 0, int offset = 0) + { + return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length <= 0 ? mat.GetLength() : length); + } + + /// + /// Gets a single pixel span to manipulate or read pixels + /// + /// + /// + /// + /// + /// + public static Span GetPixelSpan(this Mat mat, int x, int y) + { + return mat.GetDataSpan(mat.NumberOfChannels, mat.GetPixelPos(x, y)); + } + + /// + /// Gets a single pixel span to manipulate or read pixels + /// + /// + /// + /// + /// + public static Span GetPixelSpan(this Mat mat, int pos) + { + return mat.GetDataSpan(mat.NumberOfChannels, pos); + } + + /// + /// Gets a row span to manipulate or read pixels + /// + /// + /// + /// + /// + /// + /// + public static unsafe Span GetRowSpan(this Mat mat, int y, int length = 0, int offset = 0) + { + return new(IntPtr.Add(mat.DataPointer, y * mat.GetRealStep() + offset).ToPointer(), length <= 0 ? mat.GetRealStep() : length); + } + + /// + /// Gets a col span to manipulate or read pixels + /// + /// + /// + /// + /// + /// + /// + public static unsafe Span GetColSpan(this Mat mat, int x, int length = 0, int offset = 0) + { + var colMat = mat.Col(x); + return new(IntPtr.Add(colMat.DataPointer, offset).ToPointer(), length <= 0 ? mat.Height : length); + } + #endregion + + #region Memory Fill + + /// + /// Fill a mat span with a color + /// + /// Mat to fill + /// Start position, this reference will increment by the + /// Length to fill + /// Color to fill with + /// Ignore and sum to if is less than the threshold value + public static void FillSpan(this Mat mat, ref int startPosition, int length, byte color, byte colorFillMinThreshold = 1) + { + if (length <= 0) return; + if (color < colorFillMinThreshold) // Ignore threshold (mostly if blacks), spare cycles { - if (size.IsEmpty) return new(); - var mat = new Mat(size, depthType, channels); - mat.SetTo(color); - return mat; - } - - /// - /// Allocates a new array of mat's - /// - /// Array size - /// - public static Mat[] InitMats(uint count) => InitMats(count, Size.Empty); - - /// - /// Allocates a new array of mat 's - /// - /// Array size - /// Image size to create, use to create a empty Mat - /// New mat array - public static Mat[] InitMats(uint count, Size size) - { - var layers = new Mat[count]; - for (var i = 0; i < layers.Length; i++) - { - layers[i] = InitMat(size); - } - - return layers; - } - - /// - /// Create a new from - /// - /// - /// - public static GpuMat ToGpuMat(this Mat mat) - { - var gpuMat = new GpuMat(mat.Rows, mat.Cols, mat.Depth, mat.NumberOfChannels); - gpuMat.Upload(mat); - return gpuMat; - } - #endregion - - #region Memory accessors - - /// - /// Gets the byte pointer of this - /// - /// - /// - public static unsafe byte* GetBytePointer(this Mat mat) - { - return (byte*)mat.DataPointer.ToPointer(); - } - - /// - /// Gets the whole data span to manipulate or read pixels - /// - /// - /// - public static unsafe Span GetDataByteSpan(this Mat mat) - { - return new(mat.DataPointer.ToPointer(), mat.GetLength()); - } - - public static unsafe Span GetDataByteSpan(this Mat mat, int length, int offset = 0) - { - return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length <= 0 ? mat.GetLength() : length); - } - - /// - /// Gets the data span to manipulate or read pixels given a length and offset - /// - /// - /// - /// - /// - /// - public static unsafe Span GetDataSpan(this Mat mat, int length = 0, int offset = 0) - { - return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length <= 0 ? mat.GetLength() : length); - } - - /// - /// Gets a single pixel span to manipulate or read pixels - /// - /// - /// - /// - /// - /// - public static Span GetPixelSpan(this Mat mat, int x, int y) - { - return mat.GetDataSpan(mat.NumberOfChannels, mat.GetPixelPos(x, y)); - } - - /// - /// Gets a single pixel span to manipulate or read pixels - /// - /// - /// - /// - /// - public static Span GetPixelSpan(this Mat mat, int pos) - { - return mat.GetDataSpan(mat.NumberOfChannels, pos); - } - - /// - /// Gets a row span to manipulate or read pixels - /// - /// - /// - /// - /// - /// - /// - public static unsafe Span GetRowSpan(this Mat mat, int y, int length = 0, int offset = 0) - { - return new(IntPtr.Add(mat.DataPointer, y * mat.GetRealStep() + offset).ToPointer(), length <= 0 ? mat.GetRealStep() : length); - } - - /// - /// Gets a col span to manipulate or read pixels - /// - /// - /// - /// - /// - /// - /// - public static unsafe Span GetColSpan(this Mat mat, int x, int length = 0, int offset = 0) - { - var colMat = mat.Col(x); - return new(IntPtr.Add(colMat.DataPointer, offset).ToPointer(), length <= 0 ? mat.Height : length); - } - #endregion - - #region Memory Fill - - /// - /// Fill a mat span with a color - /// - /// Mat to fill - /// Start position, this reference will increment by the - /// Length to fill - /// Color to fill with - /// Ignore and sum to if is less than the threshold value - public static void FillSpan(this Mat mat, ref int startPosition, int length, byte color, byte colorFillMinThreshold = 1) - { - if (length <= 0) return; - if (color < colorFillMinThreshold) // Ignore threshold (mostly if blacks), spare cycles - { - startPosition += length; - return; - } - - mat.GetDataByteSpan(length, startPosition).Fill(color); startPosition += length; + return; } - /// - /// Fill a mat span with a color - /// - /// Mat to fill - /// - /// - /// Length to fill - /// Color to fill with - /// Ignore and sum to if is less than the threshold value - public static void FillSpan(this Mat mat, int x, int y, int length, byte color, byte colorFillMinThreshold = 1) + mat.GetDataByteSpan(length, startPosition).Fill(color); + startPosition += length; + } + + /// + /// Fill a mat span with a color + /// + /// Mat to fill + /// + /// + /// Length to fill + /// Color to fill with + /// Ignore and sum to if is less than the threshold value + public static void FillSpan(this Mat mat, int x, int y, int length, byte color, byte colorFillMinThreshold = 1) + { + if (length <= 0 || color < colorFillMinThreshold) return; // Ignore threshold (mostly if blacks), spare cycles + mat.GetDataByteSpan(length, mat.GetPixelPos(x, y)).Fill(color); + } + + /// + /// Fill a mat span with a color + /// + /// Mat to fill + /// + /// Length to fill + /// Color to fill with + /// Ignore and sum to if is less than the threshold value + public static void FillSpan(this Mat mat, Point position, int length, byte color, byte colorFillMinThreshold = 1) + => mat.FillSpan(position.X, position.Y, length, color, colorFillMinThreshold); + #endregion + + #region Get/Set methods + + /// + /// .Step return the original Mat step, if ROI step still from original matrix which lead to errors. + /// Use this to get the real step size + /// + /// + /// + public static int GetRealStep(this Mat mat) + { + return mat.Width * mat.NumberOfChannels; + } + + /// + /// Gets the total length of this + /// + /// + /// The total length of this + public static int GetLength(this Mat mat) + { + return mat.Total.ToInt32() * mat.NumberOfChannels; + } + + /// + /// Gets a pixel index position on a span given X and Y + /// + /// + /// X coordinate + /// Y coordinate + /// The pixel index position + public static int GetPixelPos(this Mat mat, int x, int y) + { + return y * mat.GetRealStep() + x * mat.NumberOfChannels; + } + + /// + /// Gets a pixel index position on a span given X and Y + /// + /// + /// X and Y Location + /// The pixel index position + public static int GetPixelPos(this Mat mat, Point point) + { + return mat.GetPixelPos(point.X, point.Y); + } + + /// + /// Gets a byte array copy of this + /// + /// + /// Byte array + public static byte[] GetBytes(this Mat mat) + { + var data = new byte[mat.GetLength()]; + Marshal.Copy(mat.DataPointer, data, 0, data.Length); + return data; + } + + /// + /// Gets a byte pixel at a position + /// + /// + /// + /// + public static byte GetByte(this Mat mat, int pos) + { + //return new Span(IntPtr.Add(mat.DataPointer, pos).ToPointer(), mat.Step)[0]; + var value = new byte[1]; + Marshal.Copy(mat.DataPointer + pos, value, 0, value.Length); + return value[0]; + } + + /// + /// Gets a byte pixel at a position + /// + /// + /// + /// + /// + public static byte GetByte(this Mat mat, int x, int y) => GetByte(mat, mat.GetPixelPos(x, y)); + + /// + /// Gets a byte pixel at a position + /// + /// + /// + /// + public static byte GetByte(this Mat mat, Point pos) => GetByte(mat, mat.GetPixelPos(pos.X, pos.Y)); + + /// + /// Sets a byte pixel at a position + /// + /// + /// + /// + public static void SetByte(this Mat mat, int pixel, byte value) => SetByte(mat, pixel, new[] { value }); + + /// + /// Sets a byte pixel at a position + /// + /// + /// + /// + public static void SetByte(this Mat mat, int pixel, byte[] value) => + Marshal.Copy(value, 0, mat.DataPointer + pixel, value.Length); + + /// + /// Sets a byte pixel at a position + /// + /// + /// + /// + /// + public static void SetByte(this Mat mat, int x, int y, byte value) => + SetByte(mat, x, y, new[] { value }); + + /// + /// Sets a byte pixel at a position + /// + /// + /// + /// + /// + public static void SetByte(this Mat mat, int x, int y, byte[] value) => + SetByte(mat, y * mat.GetRealStep() + x * mat.NumberOfChannels, value); + + /// + /// Sets bytes + /// + /// + /// + public static void SetBytes(this Mat mat, byte[] value) => + Marshal.Copy(value, 0, mat.DataPointer, value.Length); + + /// + /// Gets PNG byte array + /// + /// + /// + public static byte[] GetPngByes(this IInputArray mat) + { + return CvInvoke.Imencode(".png", mat); + } + #endregion + + #region Create methods + + public static Mat CreateMask(this Mat src, VectorOfVectorOfPoint contours) + { + var mask = src.NewBlank(); + CvInvoke.DrawContours(mask, contours, -1, WhiteColor, -1); + return mask; + } + + public static Mat CreateMask(this Mat src, Point[][] contours) + { + using var vec = new VectorOfVectorOfPoint(contours); + return src.CreateMask(vec); + } + + #endregion + + #region Copy methods + /// + /// Copy a region from to center of other + /// + /// Source to be copied to + /// Size of the center offset + /// Target to paste the + public static void CopyCenterToCenter(this Mat src, Size size, Mat dst) + { + var srcRoi = src.RoiFromCenter(size); + CopyToCenter(srcRoi, dst); + } + + /// + /// Copy a region from to center of other + /// + /// Source to be copied to + /// Region to copy + /// Target to paste the + public static void CopyRegionToCenter(this Mat src, Rectangle region, Mat dst) + { + var srcRoi = src.Roi(region); + CopyToCenter(srcRoi, dst); + } + + /// + /// Copy a to center of other + /// + /// Source to be copied to + /// Target to paste the + public static void CopyToCenter(this Mat src, Mat dst) + { + var srcStep = src.GetRealStep(); + var dstStep = dst.GetRealStep(); + var dx = Math.Abs(dstStep - srcStep) / 2; + var dy = Math.Abs(dst.Height - src.Height) / 2; + + if (src.Size == dst.Size) { - if (length <= 0 || color < colorFillMinThreshold) return; // Ignore threshold (mostly if blacks), spare cycles - mat.GetDataByteSpan(length, mat.GetPixelPos(x, y)).Fill(color); + src.CopyTo(dst); + return; } - /// - /// Fill a mat span with a color - /// - /// Mat to fill - /// - /// Length to fill - /// Color to fill with - /// Ignore and sum to if is less than the threshold value - public static void FillSpan(this Mat mat, Point position, int length, byte color, byte colorFillMinThreshold = 1) - => mat.FillSpan(position.X, position.Y, length, color, colorFillMinThreshold); - #endregion - - #region Get/Set methods - - /// - /// .Step return the original Mat step, if ROI step still from original matrix which lead to errors. - /// Use this to get the real step size - /// - /// - /// - public static int GetRealStep(this Mat mat) + if (dstStep > srcStep && dst.Height > src.Height) { - return mat.Width * mat.NumberOfChannels; + using var dstRoi = dst.Roi(new Rectangle(dx, dy, src.Width, src.Height)); + src.CopyTo(dstRoi); + return; } - - /// - /// Gets the total length of this - /// - /// - /// The total length of this - public static int GetLength(this Mat mat) - { - return mat.Total.ToInt32() * mat.NumberOfChannels; - } - - /// - /// Gets a pixel index position on a span given X and Y - /// - /// - /// X coordinate - /// Y coordinate - /// The pixel index position - public static int GetPixelPos(this Mat mat, int x, int y) - { - return y * mat.GetRealStep() + x * mat.NumberOfChannels; - } - - /// - /// Gets a pixel index position on a span given X and Y - /// - /// - /// X and Y Location - /// The pixel index position - public static int GetPixelPos(this Mat mat, Point point) - { - return mat.GetPixelPos(point.X, point.Y); - } - - /// - /// Gets a byte array copy of this - /// - /// - /// Byte array - public static byte[] GetBytes(this Mat mat) - { - var data = new byte[mat.GetLength()]; - Marshal.Copy(mat.DataPointer, data, 0, data.Length); - return data; - } - - /// - /// Gets a byte pixel at a position - /// - /// - /// - /// - public static byte GetByte(this Mat mat, int pos) - { - //return new Span(IntPtr.Add(mat.DataPointer, pos).ToPointer(), mat.Step)[0]; - var value = new byte[1]; - Marshal.Copy(mat.DataPointer + pos, value, 0, value.Length); - return value[0]; - } - - /// - /// Gets a byte pixel at a position - /// - /// - /// - /// - /// - public static byte GetByte(this Mat mat, int x, int y) => GetByte(mat, mat.GetPixelPos(x, y)); - - /// - /// Gets a byte pixel at a position - /// - /// - /// - /// - public static byte GetByte(this Mat mat, Point pos) => GetByte(mat, mat.GetPixelPos(pos.X, pos.Y)); - - /// - /// Sets a byte pixel at a position - /// - /// - /// - /// - public static void SetByte(this Mat mat, int pixel, byte value) => SetByte(mat, pixel, new[] { value }); - - /// - /// Sets a byte pixel at a position - /// - /// - /// - /// - public static void SetByte(this Mat mat, int pixel, byte[] value) => - Marshal.Copy(value, 0, mat.DataPointer + pixel, value.Length); - - /// - /// Sets a byte pixel at a position - /// - /// - /// - /// - /// - public static void SetByte(this Mat mat, int x, int y, byte value) => - SetByte(mat, x, y, new[] { value }); - - /// - /// Sets a byte pixel at a position - /// - /// - /// - /// - /// - public static void SetByte(this Mat mat, int x, int y, byte[] value) => - SetByte(mat, y * mat.GetRealStep() + x * mat.NumberOfChannels, value); - - /// - /// Sets bytes - /// - /// - /// - public static void SetBytes(this Mat mat, byte[] value) => - Marshal.Copy(value, 0, mat.DataPointer, value.Length); - - /// - /// Gets PNG byte array - /// - /// - /// - public static byte[] GetPngByes(this Mat mat) - { - return CvInvoke.Imencode(".png", mat); - } - #endregion - - #region Create methods - - public static Mat CreateMask(this Mat src, VectorOfVectorOfPoint contours) - { - var mask = src.NewBlank(); - CvInvoke.DrawContours(mask, contours, -1, WhiteColor, -1); - return mask; - } - - public static Mat CreateMask(this Mat src, Point[][] contours) - { - using var vec = new VectorOfVectorOfPoint(contours); - return src.CreateMask(vec); - } - - #endregion - - #region Copy methods - /// - /// Copy a region from to center of other - /// - /// Source to be copied to - /// Size of the center offset - /// Target to paste the - public static void CopyCenterToCenter(this Mat src, Size size, Mat dst) - { - var srcRoi = src.RoiFromCenter(size); - CopyToCenter(srcRoi, dst); - } - - /// - /// Copy a region from to center of other - /// - /// Source to be copied to - /// Region to copy - /// Target to paste the - public static void CopyRegionToCenter(this Mat src, Rectangle region, Mat dst) - { - var srcRoi = src.Roi(region); - CopyToCenter(srcRoi, dst); - } - - /// - /// Copy a to center of other - /// - /// Source to be copied to - /// Target to paste the - public static void CopyToCenter(this Mat src, Mat dst) - { - var srcStep = src.GetRealStep(); - var dstStep = dst.GetRealStep(); - var dx = Math.Abs(dstStep - srcStep) / 2; - var dy = Math.Abs(dst.Height - src.Height) / 2; - - if (src.Size == dst.Size) - { - src.CopyTo(dst); - return; - } - - if (dstStep > srcStep && dst.Height > src.Height) - { - using var dstRoi = dst.Roi(new Rectangle(dx, dy, src.Width, src.Height)); - src.CopyTo(dstRoi); - return; - } - if (dstStep < srcStep && dst.Height < src.Height) - { - using var srcRoi = src.Roi(new Rectangle(dx, dy, dst.Width, dst.Height)); - srcRoi.CopyTo(dst); - return; - } - - throw new InvalidOperationException("Unable to copy, out of bounds"); + if (dstStep < srcStep && dst.Height < src.Height) + { + using var srcRoi = src.Roi(new Rectangle(dx, dy, dst.Width, dst.Height)); + srcRoi.CopyTo(dst); + return; } - public static void CopyAreasSmallerThan(this Mat src, double threshold, Mat dst) + throw new InvalidOperationException("Unable to copy, out of bounds"); + } + + public static void CopyAreasSmallerThan(this Mat src, double threshold, Mat dst) + { + if (threshold <= 1) return; + using var contours = src.FindContours(out var hierarchy, RetrType.Tree); + var contourGroups = EmguContours.GetContoursInGroups(contours, hierarchy); + + var mask = src.NewBlank(); + uint drawContours = 0; + foreach (var contourGroup in contourGroups) { - if (threshold <= 1) return; - using var contours = src.FindContours(out var hierarchy, RetrType.Tree); - var contourGroups = EmguContours.GetContoursInGroups(contours, hierarchy); - - var mask = src.NewBlank(); - uint drawContours = 0; - foreach (var contourGroup in contourGroups) + using var selectedContours = new VectorOfVectorOfPoint(); + foreach (var group in contourGroup) { - using var selectedContours = new VectorOfVectorOfPoint(); - foreach (var group in contourGroup) - { - var area = EmguContours.GetContourArea(group); - if (area >= threshold) continue; - drawContours++; - selectedContours.Push(group); - } - - if (selectedContours.Size == 0) continue; - CvInvoke.DrawContours(mask, selectedContours, -1, WhiteColor, -1); - + var area = EmguContours.GetContourArea(group); + if (area >= threshold) continue; + drawContours++; + selectedContours.Push(group); } - if (drawContours > 0) src.CopyTo(dst, mask); + + if (selectedContours.Size == 0) continue; + CvInvoke.DrawContours(mask, selectedContours, -1, WhiteColor, -1); + } + if (drawContours > 0) src.CopyTo(dst, mask); + } - public static void CopyAreasLargerThan(this Mat src, double threshold, Mat dst) + public static void CopyAreasLargerThan(this Mat src, double threshold, Mat dst) + { + if (threshold <= 0) return; + using var contours = src.FindContours(out var hierarchy, RetrType.Tree); + var contourGroups = EmguContours.GetContoursInGroups(contours, hierarchy); + + var mask = src.NewBlank(); + uint drawContours = 0; + foreach (var contourGroup in contourGroups) { - if (threshold <= 0) return; - using var contours = src.FindContours(out var hierarchy, RetrType.Tree); - var contourGroups = EmguContours.GetContoursInGroups(contours, hierarchy); - - var mask = src.NewBlank(); - uint drawContours = 0; - foreach (var contourGroup in contourGroups) + using var selectedContours = new VectorOfVectorOfPoint(); + foreach (var group in contourGroup) { - using var selectedContours = new VectorOfVectorOfPoint(); - foreach (var group in contourGroup) - { - var area = EmguContours.GetContourArea(group); - if (area <= threshold) continue; - drawContours++; - selectedContours.Push(group); - } + var area = EmguContours.GetContourArea(group); + if (area <= threshold) continue; + drawContours++; + selectedContours.Push(group); + } - if (selectedContours.Size == 0) continue; - CvInvoke.DrawContours(mask, selectedContours, -1, WhiteColor, -1); + if (selectedContours.Size == 0) continue; + CvInvoke.DrawContours(mask, selectedContours, -1, WhiteColor, -1); - } - if(drawContours > 0) src.CopyTo(dst, mask); } - #endregion + if(drawContours > 0) src.CopyTo(dst, mask); + } + #endregion - #region Roi methods + #region Roi methods - /// - /// Gets a Roi, but return source when roi is empty or have same size as source - /// - /// - /// - /// - public static Mat Roi(this Mat mat, Rectangle roi) - { - return roi.IsEmpty || roi.Size == mat.Size ? mat : new Mat(mat, roi); - } + /// + /// Gets a Roi, but return source when roi is empty or have same size as source + /// + /// + /// + /// + public static Mat Roi(this Mat mat, Rectangle roi) + { + return roi.IsEmpty || roi.Size == mat.Size ? mat : new Mat(mat, roi); + } - /// - /// Gets a Roi at x=0 and y=0 given a size, but return source when roi is empty or have same size as source - /// - /// - /// - /// - public static Mat Roi(this Mat mat, Size size) - { - return size.IsEmpty || size == mat.Size ? mat : new Mat(mat, new(Point.Empty, size)); - } + /// + /// Gets a Roi at x=0 and y=0 given a size, but return source when roi is empty or have same size as source + /// + /// + /// + /// + public static Mat Roi(this Mat mat, Size size) + { + return size.IsEmpty || size == mat.Size ? mat : new Mat(mat, new(Point.Empty, size)); + } - /// - /// Gets a Roi from a mat size, but return source when roi is empty or have same size as source - /// - /// - /// - /// - public static Mat Roi(this Mat mat, Mat fromMat) => mat.Roi(fromMat.Size); + /// + /// Gets a Roi from a mat size, but return source when roi is empty or have same size as source + /// + /// + /// + /// + public static Mat Roi(this Mat mat, Mat fromMat) => mat.Roi(fromMat.Size); - /// - /// Gets a Roi from center, but return source when have same size as source - /// - /// - /// - /// - public static Mat RoiFromCenter(this Mat mat, Size size) - { - if(mat.Size == size) return mat; + /// + /// Gets a Roi from center, but return source when have same size as source + /// + /// + /// + /// + public static Mat RoiFromCenter(this Mat mat, Size size) + { + if(mat.Size == size) return mat; - var newRoi = mat.Roi(new Rectangle( - mat.Width / 2 - size.Width / 2, - mat.Height / 2 - size.Height / 2, - size.Width, - size.Height - )); + var newRoi = mat.Roi(new Rectangle( + mat.Width / 2 - size.Width / 2, + mat.Height / 2 - size.Height / 2, + size.Width, + size.Height + )); - return newRoi; - } + return newRoi; + } - /// - /// Gets a new mat obtained from it center at a target size and roi - /// - /// - /// - /// - /// - public static Mat NewMatFromCenterRoi(this Mat mat, Size targetSize, Rectangle roi) - { - if (targetSize == mat.Size) return mat.Clone(); - var newMat = InitMat(targetSize); - var roiMat = mat.Roi(roi); + /// + /// Gets a new mat obtained from it center at a target size and roi + /// + /// + /// + /// + /// + public static Mat NewMatFromCenterRoi(this Mat mat, Size targetSize, Rectangle roi) + { + if (targetSize == mat.Size) return mat.Clone(); + var newMat = InitMat(targetSize); + var roiMat = mat.Roi(roi); - //int xStart = mat.Width / 2 - targetSize.Width / 2; - //int yStart = mat.Height / 2 - targetSize.Height / 2; - var newMatRoi = newMat.RoiFromCenter(roi.Size); - /*var newMatRoi = new Mat(newMat, new Rectangle( - targetSize.Width / 2 - roi.Width / 2, - targetSize.Height / 2 - roi.Height / 2, - roi.Width, - roi.Height - ));*/ - roiMat.CopyTo(newMatRoi); - return newMat; - } - #endregion + //int xStart = mat.Width / 2 - targetSize.Width / 2; + //int yStart = mat.Height / 2 - targetSize.Height / 2; + var newMatRoi = newMat.RoiFromCenter(roi.Size); + /*var newMatRoi = new Mat(newMat, new Rectangle( + targetSize.Width / 2 - roi.Width / 2, + targetSize.Height / 2 - roi.Height / 2, + roi.Width, + roi.Height + ));*/ + roiMat.CopyTo(newMatRoi); + return newMat; + } + #endregion - #region Is methods + #region Is methods - /// - /// Gets if a is all zeroed by a threshold - /// - /// - /// Pixel brightness threshold - /// Start pixel position - /// Pixel span length - /// - public static bool IsZeroed(this Mat mat, byte threshold = 0, int startPos = 0, int length = 0) + /// + /// Gets if a is all zeroed by a threshold + /// + /// + /// Pixel brightness threshold + /// Start pixel position + /// Pixel span length + /// + public static bool IsZeroed(this Mat mat, byte threshold = 0, int startPos = 0, int length = 0) + { + return mat.FindFirstPixelGreaterThan(threshold, startPos, length) == -1; + } + #endregion + + #region Find methods + + /// + /// Finds the first negative (Black) pixel + /// + /// + /// Start pixel position + /// Pixel span length + /// Pixel position in the span, or -1 if not found + public static int FindFirstNegativePixel(this Mat mat, int startPos = 0, int length = 0) + { + return mat.FindFirstPixelEqualTo(0, startPos, length); + } + + /// + /// Finds the first positive pixel + /// + /// + /// Start pixel position + /// Pixel span length + /// Pixel position in the span, or -1 if not found + public static int FindFirstPositivePixel(this Mat mat, int startPos = 0, int length = 0) + { + return mat.FindFirstPixelGreaterThan(0, startPos, length); + } + + /// + /// Finds the first pixel that is + /// + /// + /// + /// Start pixel position + /// Pixel span length + /// Pixel position in the span, or -1 if not found + public static int FindFirstPixelEqualTo(this Mat mat, byte value, int startPos = 0, int length = 0) + { + var span = mat.GetDataByteSpan(); + if (length <= 0) length = span.Length; + for (var i = startPos; i < length; i++) { - return mat.FindFirstPixelGreaterThan(threshold, startPos, length) == -1; - } - #endregion - - #region Find methods - - /// - /// Finds the first negative (Black) pixel - /// - /// - /// Start pixel position - /// Pixel span length - /// Pixel position in the span, or -1 if not found - public static int FindFirstNegativePixel(this Mat mat, int startPos = 0, int length = 0) - { - return mat.FindFirstPixelEqualTo(0, startPos, length); + if (span[i] == value) return i; } - /// - /// Finds the first positive pixel - /// - /// - /// Start pixel position - /// Pixel span length - /// Pixel position in the span, or -1 if not found - public static int FindFirstPositivePixel(this Mat mat, int startPos = 0, int length = 0) + return -1; + } + + /// + /// Finds the first pixel that is at less than + /// + /// + /// + /// Start pixel position + /// Pixel span length + /// Pixel position in the span, or -1 if not found + public static int FindFirstPixelLessThan(this Mat mat, byte value, int startPos = 0, int length = 0) + { + var span = mat.GetDataByteSpan(); + if (length <= 0) length = span.Length; + for (var i = startPos; i < length; i++) { - return mat.FindFirstPixelGreaterThan(0, startPos, length); + if (span[i] < value) return i; } - /// - /// Finds the first pixel that is - /// - /// - /// - /// Start pixel position - /// Pixel span length - /// Pixel position in the span, or -1 if not found - public static int FindFirstPixelEqualTo(this Mat mat, byte value, int startPos = 0, int length = 0) - { - var span = mat.GetDataByteSpan(); - if (length <= 0) length = span.Length; - for (var i = startPos; i < length; i++) - { - if (span[i] == value) return i; - } + return -1; + } - return -1; + /// + /// Finds the first pixel that is at less or equal than + /// + /// + /// + /// Start pixel position + /// Pixel span length + /// Pixel position in the span, or -1 if not found + public static int FindFirstPixelEqualOrLessThan(this Mat mat, byte value, int startPos = 0, int length = 0) + { + var span = mat.GetDataByteSpan(); + if (length <= 0) length = span.Length; + for (var i = startPos; i < length; i++) + { + if (span[i] <= value) return i; } - /// - /// Finds the first pixel that is at less than - /// - /// - /// - /// Start pixel position - /// Pixel span length - /// Pixel position in the span, or -1 if not found - public static int FindFirstPixelLessThan(this Mat mat, byte value, int startPos = 0, int length = 0) - { - var span = mat.GetDataByteSpan(); - if (length <= 0) length = span.Length; - for (var i = startPos; i < length; i++) - { - if (span[i] < value) return i; - } + return -1; + } - return -1; + /// + /// Finds the first pixel that is at greater than + /// + /// + /// + /// Start pixel position + /// Pixel span length + /// Pixel position in the span, or -1 if not found + public static int FindFirstPixelGreaterThan(this Mat mat, byte value, int startPos = 0, int length = 0) + { + var span = mat.GetDataByteSpan(); + if (length <= 0) length = span.Length; + for (var i = startPos; i < length; i++) + { + if (span[i] > value) return i; } - /// - /// Finds the first pixel that is at less or equal than - /// - /// - /// - /// Start pixel position - /// Pixel span length - /// Pixel position in the span, or -1 if not found - public static int FindFirstPixelEqualOrLessThan(this Mat mat, byte value, int startPos = 0, int length = 0) - { - var span = mat.GetDataByteSpan(); - if (length <= 0) length = span.Length; - for (var i = startPos; i < length; i++) - { - if (span[i] <= value) return i; - } + return -1; + } - return -1; + /// + /// Finds the first pixel that is at equal or greater than + /// + /// + /// + /// Start pixel position + /// Pixel span length + /// Pixel position in the span, or -1 if not found + public static int FindFirstPixelEqualOrGreaterThan(this Mat mat, byte value, int startPos = 0, int length = 0) + { + var span = mat.GetDataByteSpan(); + if (length <= 0) length = span.Length; + for (var i = startPos; i < length; i++) + { + if (span[i] >= value) return i; } - /// - /// Finds the first pixel that is at greater than - /// - /// - /// - /// Start pixel position - /// Pixel span length - /// Pixel position in the span, or -1 if not found - public static int FindFirstPixelGreaterThan(this Mat mat, byte value, int startPos = 0, int length = 0) - { - var span = mat.GetDataByteSpan(); - if (length <= 0) length = span.Length; - for (var i = startPos; i < length; i++) - { - if (span[i] > value) return i; - } + return -1; + } + #endregion - return -1; + #region Transform methods + public static void Transform(this Mat src, double xScale, double yScale, double xTrans = 0, double yTrans = 0, Size dstSize = default, Inter interpolation = Inter.Linear) + { + //var dst = new Mat(src.Size, src.Depth, src.NumberOfChannels); + using var translateTransform = new Matrix(2, 3) + { + [0, 0] = xScale, // xScale + [1, 1] = yScale, // yScale + [0, 2] = xTrans, //x translation + compensation of x scaling + [1, 2] = yTrans // y translation + compensation of y scaling + }; + CvInvoke.WarpAffine(src, src, translateTransform, dstSize.IsEmpty ? src.Size : dstSize, interpolation); + } + + /// + /// Rotates a Mat by an angle while keeping the image size + /// + /// + /// Angle in degrees to rotate + /// + /// + public static void Rotate(this Mat src, double angle, Size newSize = default, double scale = 1.0) => Rotate(src, src, angle, newSize, scale); + + /// + /// Rotates a Mat by an angle while keeping the image size + /// + /// + /// + /// + /// + public static void Rotate(this Mat src, Mat dst, double angle, Size newSize = default, double scale = 1.0) + { + if (angle % 360 == 0 && scale == 1.0) return; + if (newSize.IsEmpty) + { + newSize = src.Size; } - - /// - /// Finds the first pixel that is at equal or greater than - /// - /// - /// - /// Start pixel position - /// Pixel span length - /// Pixel position in the span, or -1 if not found - public static int FindFirstPixelEqualOrGreaterThan(this Mat mat, byte value, int startPos = 0, int length = 0) - { - var span = mat.GetDataByteSpan(); - if (length <= 0) length = span.Length; - for (var i = startPos; i < length; i++) - { - if (span[i] >= value) return i; - } - - return -1; - } - #endregion - - #region Transform methods - public static void Transform(this Mat src, double xScale, double yScale, double xTrans = 0, double yTrans = 0, Size dstSize = default, Inter interpolation = Inter.Linear) - { - //var dst = new Mat(src.Size, src.Depth, src.NumberOfChannels); - using var translateTransform = new Matrix(2, 3) - { - [0, 0] = xScale, // xScale - [1, 1] = yScale, // yScale - [0, 2] = xTrans, //x translation + compensation of x scaling - [1, 2] = yTrans // y translation + compensation of y scaling - }; - CvInvoke.WarpAffine(src, src, translateTransform, dstSize.IsEmpty ? src.Size : dstSize, interpolation); - } - - /// - /// Rotates a Mat by an angle while keeping the image size - /// - /// - /// Angle in degrees to rotate - /// - /// - public static void Rotate(this Mat src, double angle, Size newSize = default, double scale = 1.0) => Rotate(src, src, angle, newSize, scale); - - /// - /// Rotates a Mat by an angle while keeping the image size - /// - /// - /// - /// - /// - public static void Rotate(this Mat src, Mat dst, double angle, Size newSize = default, double scale = 1.0) - { - if (angle % 360 == 0 && scale == 1.0) return; - if (newSize.IsEmpty) - { - newSize = src.Size; - } - var halfWidth = src.Width / 2.0f; - var halfHeight = src.Height / 2.0f; - using var translateTransform = new Matrix(2, 3); - CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, scale, translateTransform); + var halfWidth = src.Width / 2.0f; + var halfHeight = src.Height / 2.0f; + using var translateTransform = new Matrix(2, 3); + CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, scale, translateTransform); - if (src.Size != newSize) - { - // adjust the rotation matrix to take into account translation - translateTransform[0, 2] += newSize.Width / 2.0 - halfWidth; - translateTransform[1, 2] += newSize.Height / 2.0 - halfHeight; - } - - CvInvoke.WarpAffine(src, dst, translateTransform, newSize); - } - - /// - /// Rotates a Mat by an angle while adjusting bounds to fit the rotated content - /// - /// - /// - /// - public static void RotateAdjustBounds(this Mat src, double angle, double scale = 1.0) => RotateAdjustBounds(src, src, angle, scale); - - /// - /// Rotates a Mat by an angle while adjusting bounds to fit the rotated content - /// - /// - /// - /// - /// - public static void RotateAdjustBounds(this Mat src, Mat dst, double angle, double scale = 1.0) + if (src.Size != newSize) { - if (angle % 360 == 0 && scale == 1.0) return; - var halfWidth = src.Width / 2.0f; - var halfHeight = src.Height / 2.0f; - using var translateTransform = new Matrix(2, 3); - CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, scale, translateTransform); - var cos = Math.Abs(translateTransform[0, 0]); - var sin = Math.Abs(translateTransform[0, 1]); - - // compute the new bounding dimensions of the image - int newWidth = (int) (src.Height * sin + src.Width * cos); - int newHeight = (int) (src.Height * cos + src.Width * sin); - // adjust the rotation matrix to take into account translation - translateTransform[0, 2] += newWidth / 2.0 - halfWidth; - translateTransform[1, 2] += newHeight / 2.0 - halfHeight; - - - CvInvoke.WarpAffine(src, dst, translateTransform, new Size(newWidth, newHeight)); + translateTransform[0, 2] += newSize.Width / 2.0 - halfWidth; + translateTransform[1, 2] += newSize.Height / 2.0 - halfHeight; } - /// - /// Scale image from it center, preserving src bounds - /// https://stackoverflow.com/a/62543674/933976 - /// - /// to transform - /// X scale factor - /// Y scale factor - /// X translation - /// Y translation - /// Destination size - /// Interpolation mode - public static void TransformFromCenter(this Mat src, double xScale, double yScale, double xTrans = 0, double yTrans = 0, Size dstSize = default, Inter interpolation = Inter.Linear) + CvInvoke.WarpAffine(src, dst, translateTransform, newSize); + } + + /// + /// Rotates a Mat by an angle while adjusting bounds to fit the rotated content + /// + /// + /// + /// + public static void RotateAdjustBounds(this Mat src, double angle, double scale = 1.0) => RotateAdjustBounds(src, src, angle, scale); + + /// + /// Rotates a Mat by an angle while adjusting bounds to fit the rotated content + /// + /// + /// + /// + /// + public static void RotateAdjustBounds(this Mat src, Mat dst, double angle, double scale = 1.0) + { + if (angle % 360 == 0 && scale == 1.0) return; + var halfWidth = src.Width / 2.0f; + var halfHeight = src.Height / 2.0f; + using var translateTransform = new Matrix(2, 3); + CvInvoke.GetRotationMatrix2D(new PointF(halfWidth, halfHeight), -angle, scale, translateTransform); + var cos = Math.Abs(translateTransform[0, 0]); + var sin = Math.Abs(translateTransform[0, 1]); + + // compute the new bounding dimensions of the image + int newWidth = (int) (src.Height * sin + src.Width * cos); + int newHeight = (int) (src.Height * cos + src.Width * sin); + + // adjust the rotation matrix to take into account translation + translateTransform[0, 2] += newWidth / 2.0 - halfWidth; + translateTransform[1, 2] += newHeight / 2.0 - halfHeight; + + + CvInvoke.WarpAffine(src, dst, translateTransform, new Size(newWidth, newHeight)); + } + + /// + /// Scale image from it center, preserving src bounds + /// https://stackoverflow.com/a/62543674/933976 + /// + /// to transform + /// X scale factor + /// Y scale factor + /// X translation + /// Y translation + /// Destination size + /// Interpolation mode + public static void TransformFromCenter(this Mat src, double xScale, double yScale, double xTrans = 0, double yTrans = 0, Size dstSize = default, Inter interpolation = Inter.Linear) + { + src.Transform(xScale, yScale, + xTrans + (src.Width - src.Width * xScale) / 2.0, + yTrans + (src.Height - src.Height * yScale) / 2.0, dstSize, interpolation); + } + #endregion + + #region Draw Methods + + /// + /// Draw a rotated square around a center point + /// + /// + /// + /// + /// + /// + /// + /// + public static void DrawRotatedSquare(this Mat src, int size, Point center, MCvScalar color, int angle = 0, int thickness = -1, LineType lineType = LineType.EightConnected) + => src.DrawRotatedRectangle(new(size, size), center, color, angle, thickness, lineType); + + /// + /// Draw a rotated rectangle around a center point + /// + /// + /// + /// + /// + /// + /// + /// + public static void DrawRotatedRectangle(this Mat src, Size size, Point center, MCvScalar color, int angle = 0, int thickness = -1, LineType lineType = LineType.EightConnected) + { + var rect = new RotatedRect(center, size, angle); + var vertices = rect.GetVertices(); + var points = new Point[vertices.Length]; + + for (int i = 0; i < vertices.Length; i++) { - src.Transform(xScale, yScale, - xTrans + (src.Width - src.Width * xScale) / 2.0, - yTrans + (src.Height - src.Height * yScale) / 2.0, dstSize, interpolation); - } - #endregion - - #region Draw Methods - - /// - /// Draw a rotated square around a center point - /// - /// - /// - /// - /// - /// - /// - /// - public static void DrawRotatedSquare(this Mat src, int size, Point center, MCvScalar color, int angle = 0, int thickness = -1, LineType lineType = LineType.EightConnected) - => src.DrawRotatedRectangle(new(size, size), center, color, angle, thickness, lineType); - - /// - /// Draw a rotated rectangle around a center point - /// - /// - /// - /// - /// - /// - /// - /// - public static void DrawRotatedRectangle(this Mat src, Size size, Point center, MCvScalar color, int angle = 0, int thickness = -1, LineType lineType = LineType.EightConnected) - { - var rect = new RotatedRect(center, size, angle); - var vertices = rect.GetVertices(); - var points = new Point[vertices.Length]; - - for (int i = 0; i < vertices.Length; i++) - { - points[i] = new( - (int)Math.Round(vertices[i].X), - (int)Math.Round(vertices[i].Y) - ); - } - - if (thickness <= 0) - { - using var vec = new VectorOfPoint(points); - CvInvoke.FillConvexPoly(src, vec, color, lineType); - } - else - { - CvInvoke.Polylines(src, points, true, color, thickness, lineType); - } + points[i] = new( + (int)Math.Round(vertices[i].X), + (int)Math.Round(vertices[i].Y) + ); } - /// - /// Draw a polygon given number of sides and length - /// - /// - /// Number of polygon sides, Special: use 1 to draw a line and >= 100 to draw a native OpenCV circle - /// Radius - /// Center position - /// - /// - /// - /// - /// - public static void DrawPolygon(this Mat src, int sides, int radius, Point center, MCvScalar color, double startingAngle = 0, int thickness = -1, LineType lineType = LineType.EightConnected, FlipType? flip = null) + if (thickness <= 0) { - if (sides == 1) + using var vec = new VectorOfPoint(points); + CvInvoke.FillConvexPoly(src, vec, color, lineType); + } + else + { + CvInvoke.Polylines(src, points, true, color, thickness, lineType); + } + } + + /// + /// Draw a polygon given number of sides and length + /// + /// + /// Number of polygon sides, Special: use 1 to draw a line and >= 100 to draw a native OpenCV circle + /// Radius + /// Center position + /// + /// + /// + /// + /// + public static void DrawPolygon(this Mat src, int sides, int radius, Point center, MCvScalar color, double startingAngle = 0, int thickness = -1, LineType lineType = LineType.EightConnected, FlipType? flip = null) + { + if (sides == 1) + { + var point1 = new Point(center.X - radius, center.Y); + var point2 = new Point(center.X + radius, center.Y); + point1 = point1.Rotate(startingAngle, center); + point2 = point2.Rotate(startingAngle, center); + + if (flip is FlipType.Horizontal or FlipType.Both) { - var point1 = new Point(center.X - radius, center.Y); - var point2 = new Point(center.X + radius, center.Y); - point1 = point1.Rotate(startingAngle, center); - point2 = point2.Rotate(startingAngle, center); - - if (flip is FlipType.Horizontal or FlipType.Both) - { - var newPoint1 = new Point(point2.X, point1.Y); - var newPoint2 = new Point(point1.X, point2.Y); - point1 = newPoint1; - point2 = newPoint2; - } - - if (flip is FlipType.Vertical or FlipType.Both) - { - var newPoint1 = new Point(point1.X, point2.Y); - var newPoint2 = new Point(point2.X, point1.Y); - point1 = newPoint1; - point2 = newPoint2; - } - - CvInvoke.Line(src, point1, point2, color, thickness < 1 ? 1 : thickness, lineType); - return; - } - if (sides >= 100) - { - CvInvoke.Circle(src, center, radius, color, thickness, lineType); - return; + var newPoint1 = new Point(point2.X, point1.Y); + var newPoint2 = new Point(point1.X, point2.Y); + point1 = newPoint1; + point2 = newPoint2; } - var points = DrawingExtensions.GetPolygonVertices(sides, radius, center, startingAngle, - flip is FlipType.Horizontal or FlipType.Both, - flip is FlipType.Vertical or FlipType.Both); + if (flip is FlipType.Vertical or FlipType.Both) + { + var newPoint1 = new Point(point1.X, point2.Y); + var newPoint2 = new Point(point2.X, point1.Y); + point1 = newPoint1; + point2 = newPoint2; + } + + CvInvoke.Line(src, point1, point2, color, thickness < 1 ? 1 : thickness, lineType); + return; + } + if (sides >= 100) + { + CvInvoke.Circle(src, center, radius, color, thickness, lineType); + return; + } + + var points = DrawingExtensions.GetPolygonVertices(sides, radius, center, startingAngle, + flip is FlipType.Horizontal or FlipType.Both, + flip is FlipType.Vertical or FlipType.Both); - if (thickness <= 0) - { - using var vec = new VectorOfPoint(points); - CvInvoke.FillConvexPoly(src, vec, color, lineType); - } - else - { - CvInvoke.Polylines(src, points, true, color, thickness, lineType); - } + if (thickness <= 0) + { + using var vec = new VectorOfPoint(points); + CvInvoke.FillConvexPoly(src, vec, color, lineType); + } + else + { + CvInvoke.Polylines(src, points, true, color, thickness, lineType); + } - } - #endregion + } + #endregion - #region Text methods - public enum PutTextLineAlignment : byte + #region Text methods + public enum PutTextLineAlignment : byte + { + /// + /// Left aligned without trimming, openCV default call + /// + None, + + /// + /// Left aligned and trimmed + /// + Left, + + /// + /// Center aligned and trimmed + /// + Center, + + /// + /// Right aligned and trimmed + /// + Right + } + + public static string PutTextLineAlignmentTrim(string line, PutTextLineAlignment lineAlignment) + { + switch (lineAlignment) { - /// - /// Left aligned without trimming, openCV default call - /// - None, + case PutTextLineAlignment.None: + return line.TrimEnd(); + case PutTextLineAlignment.Left: + case PutTextLineAlignment.Center: + case PutTextLineAlignment.Right: + return line.Trim(); + default: + throw new ArgumentOutOfRangeException(nameof(lineAlignment), lineAlignment, null); + } + } - /// - /// Left aligned and trimmed - /// - Left, + public static Size GetTextSizeExtended(string text, FontFace fontFace, double fontScale, int thickness, ref int baseLine, PutTextLineAlignment lineAlignment = default) + => GetTextSizeExtended(text, fontFace, fontScale, thickness, 0, ref baseLine, lineAlignment); + public static Size GetTextSizeExtended(string text, FontFace fontFace, double fontScale, int thickness, int lineGapOffset, ref int baseLine, PutTextLineAlignment lineAlignment = default) + { + text = text.TrimEnd('\n', '\r', ' '); + var lines = text.Split(StaticObjects.LineBreakCharacters, StringSplitOptions.None); + var textSize = CvInvoke.GetTextSize(text, fontFace, fontScale, thickness, ref baseLine); - /// - /// Center aligned and trimmed - /// - Center, + if (lines.Length is 0 or 1) return textSize; - /// - /// Right aligned and trimmed - /// - Right + var lineGap = textSize.Height / 3 + lineGapOffset; + var width = 0; + var height = lines.Length * (lineGap + textSize.Height) - lineGap; + + for (var i = 0; i < lines.Length; i++) + { + lines[i] = PutTextLineAlignmentTrim(lines[i], lineAlignment); + + if (string.IsNullOrWhiteSpace(lines[i])) continue; + int baseLineRef = 0; + var lineSize = CvInvoke.GetTextSize(lines[i], fontFace, fontScale, thickness, ref baseLineRef); + width = Math.Max(width, lineSize.Width); } - public static string PutTextLineAlignmentTrim(string line, PutTextLineAlignment lineAlignment) + + return new(width, height); + } + + public static void PutTextExtended(this IInputOutputArray src, string text, Point org, FontFace fontFace, double fontScale, + MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, + bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default) + => src.PutTextExtended(text, org, fontFace, fontScale, color, thickness, 0, lineType, bottomLeftOrigin, lineAlignment); + + /// + /// Extended OpenCV PutText to accepting line breaks and line alignment + /// + public static void PutTextExtended(this IInputOutputArray src, string text, Point org, FontFace fontFace, double fontScale, + MCvScalar color, int thickness = 1, int lineGapOffset = 0, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default) + { + text = text.TrimEnd('\n', '\r', ' '); + var lines = text.Split(StaticObjects.LineBreakCharacters, StringSplitOptions.None); + + switch (lines.Length) { - switch (lineAlignment) - { - case PutTextLineAlignment.None: - return line.TrimEnd(); - case PutTextLineAlignment.Left: - case PutTextLineAlignment.Center: - case PutTextLineAlignment.Right: - return line.Trim(); - default: - throw new ArgumentOutOfRangeException(nameof(lineAlignment), lineAlignment, null); - } + case 0: + return; + case 1: + CvInvoke.PutText(src, text, org, fontFace, fontScale, color, thickness, lineType, bottomLeftOrigin); + return; } - public static Size GetTextSizeExtended(string text, FontFace fontFace, double fontScale, int thickness, ref int baseLine, PutTextLineAlignment lineAlignment = default) - => GetTextSizeExtended(text, fontFace, fontScale, thickness, 0, ref baseLine, lineAlignment); - public static Size GetTextSizeExtended(string text, FontFace fontFace, double fontScale, int thickness, int lineGapOffset, ref int baseLine, PutTextLineAlignment lineAlignment = default) + // Get height of text lines in pixels (height of all lines is the same) + int baseLine = 0; + var textSize = CvInvoke.GetTextSize(text, fontFace, fontScale, thickness, ref baseLine); + var lineGap = textSize.Height / 3 + lineGapOffset; + var linesSize = new Size[lines.Length]; + int width = 0; + + // Sanitize lines + for (var i = 0; i < lines.Length; i++) { - text = text.TrimEnd('\n', '\r', ' '); - var lines = text.Split(StaticObjects.LineBreakCharacters, StringSplitOptions.None); - var textSize = CvInvoke.GetTextSize(text, fontFace, fontScale, thickness, ref baseLine); - - if (lines.Length is 0 or 1) return textSize; - - var lineGap = textSize.Height / 3 + lineGapOffset; - var width = 0; - var height = lines.Length * (lineGap + textSize.Height) - lineGap; + lines[i] = PutTextLineAlignmentTrim(lines[i], lineAlignment); + } + // If line needs alignment, calculate the size for each line + if (lineAlignment is not PutTextLineAlignment.Left and not PutTextLineAlignment.None) + { for (var i = 0; i < lines.Length; i++) { - lines[i] = PutTextLineAlignmentTrim(lines[i], lineAlignment); - if (string.IsNullOrWhiteSpace(lines[i])) continue; int baseLineRef = 0; - var lineSize = CvInvoke.GetTextSize(lines[i], fontFace, fontScale, thickness, ref baseLineRef); - width = Math.Max(width, lineSize.Width); + linesSize[i] = CvInvoke.GetTextSize(lines[i], fontFace, fontScale, thickness, ref baseLineRef); + width = Math.Max(width, linesSize[i].Width); } - - - return new(width, height); } - public static void PutTextExtended(this Mat src, string text, Point org, FontFace fontFace, double fontScale, - MCvScalar color, int thickness = 1, LineType lineType = LineType.EightConnected, - bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default) - => src.PutTextExtended(text, org, fontFace, fontScale, color, thickness, 0, lineType, bottomLeftOrigin, lineAlignment); - - /// - /// Extended OpenCV PutText to accepting line breaks and line alignment - /// - public static void PutTextExtended(this Mat src, string text, Point org, FontFace fontFace, double fontScale, - MCvScalar color, int thickness = 1, int lineGapOffset = 0, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default) + for (var i = 0; i < lines.Length; i++) { - text = text.TrimEnd('\n', '\r', ' '); - var lines = text.Split(StaticObjects.LineBreakCharacters, StringSplitOptions.None); - - switch (lines.Length) - { - case 0: - return; - case 1: - CvInvoke.PutText(src, text, org, fontFace, fontScale, color, thickness, lineType, bottomLeftOrigin); - return; - } - - // Get height of text lines in pixels (height of all lines is the same) - int baseLine = 0; - var textSize = CvInvoke.GetTextSize(text, fontFace, fontScale, thickness, ref baseLine); - var lineGap = textSize.Height / 3 + lineGapOffset; - var linesSize = new Size[lines.Length]; - int width = 0; - - // Sanitize lines - for (var i = 0; i < lines.Length; i++) - { - lines[i] = PutTextLineAlignmentTrim(lines[i], lineAlignment); - } - - // If line needs alignment, calculate the size for each line - if (lineAlignment is not PutTextLineAlignment.Left and not PutTextLineAlignment.None) - { - for (var i = 0; i < lines.Length; i++) - { - if (string.IsNullOrWhiteSpace(lines[i])) continue; - int baseLineRef = 0; - linesSize[i] = CvInvoke.GetTextSize(lines[i], fontFace, fontScale, thickness, ref baseLineRef); - width = Math.Max(width, linesSize[i].Width); - } - } - - for (var i = 0; i < lines.Length; i++) - { - if(string.IsNullOrWhiteSpace(lines[i])) continue; + if(string.IsNullOrWhiteSpace(lines[i])) continue; - int x = lineAlignment switch - { - PutTextLineAlignment.None or PutTextLineAlignment.Left => org.X, - PutTextLineAlignment.Center => org.X + (width - linesSize[i].Width) / 2, - PutTextLineAlignment.Right => org.X + width - linesSize[i].Width, - _ => throw new ArgumentOutOfRangeException(nameof(lineAlignment), lineAlignment, null) - }; - - // Find total size of text block before this line - var lineYAdjustment = i * (lineGap + textSize.Height); - // Move text down from original line based on line number - int lineY = !bottomLeftOrigin ? org.Y + lineYAdjustment : org.Y - lineYAdjustment; - CvInvoke.PutText(src, lines[i], new Point(x, lineY), fontFace, fontScale, color, thickness, lineType, bottomLeftOrigin); - } - } - - /// - /// Extended OpenCV PutText to accepting line breaks, line alignment and rotation - /// - public static void PutTextRotated(this Mat src, string text, Point org, FontFace fontFace, double fontScale, - MCvScalar color, - int thickness = 1, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, - PutTextLineAlignment lineAlignment = default, double angle = 0) - => src.PutTextRotated(text, org, fontFace, fontScale, color, thickness, 0, lineType, bottomLeftOrigin, - lineAlignment, angle); - - /// - /// Extended OpenCV PutText to accepting line breaks, line alignment and rotation - /// - public static void PutTextRotated(this Mat src, string text, Point org, FontFace fontFace, double fontScale, MCvScalar color, - int thickness = 1, int lineGapOffset = 0, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default, double angle = 0) - { - if (angle % 360 == 0) // No rotation needed, cheaper cycle + int x = lineAlignment switch { - src.PutTextExtended(text, org, fontFace, fontScale, color, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); - return; - } + PutTextLineAlignment.None or PutTextLineAlignment.Left => org.X, + PutTextLineAlignment.Center => org.X + (width - linesSize[i].Width) / 2, + PutTextLineAlignment.Right => org.X + width - linesSize[i].Width, + _ => throw new ArgumentOutOfRangeException(nameof(lineAlignment), lineAlignment, null) + }; - using var rotatedSrc = src.Clone(); - rotatedSrc.RotateAdjustBounds(-angle); - var sizeDifference = (rotatedSrc.Size - src.Size).Half(); - org.Offset(sizeDifference.ToPoint()); - org = org.Rotate(-angle, new Point(rotatedSrc.Size.Width / 2, rotatedSrc.Size.Height / 2)); - rotatedSrc.PutTextExtended(text, org, fontFace, fontScale, color, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); - - using var mask = rotatedSrc.NewBlank(); - mask.PutTextExtended(text, org, fontFace, fontScale, WhiteColor, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); - - rotatedSrc.Rotate(angle, src.Size); - mask.Rotate(angle, src.Size); - - rotatedSrc.CopyTo(src, mask); + // Find total size of text block before this line + var lineYAdjustment = i * (lineGap + textSize.Height); + // Move text down from original line based on line number + int lineY = !bottomLeftOrigin ? org.Y + lineYAdjustment : org.Y - lineYAdjustment; + CvInvoke.PutText(src, lines[i], new Point(x, lineY), fontFace, fontScale, color, thickness, lineType, bottomLeftOrigin); } - #endregion - - #region Utilities methods - - /// - /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. - /// The function modifies the source image content - /// - /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content - /// Retrieval mode - /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). - /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context - /// The contour hierarchy - public static VectorOfVectorOfPoint FindContours(this Mat mat, RetrType mode = RetrType.List, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) - { - using var hierarchy = new Mat(); - var contours = new VectorOfVectorOfPoint(); - CvInvoke.FindContours(mat, contours, hierarchy, mode, method, offset); - return contours; - } - - /* - /// - /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. - /// The function modifies the source image content - /// - /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content - /// Detected contours. Each contour is stored as a vector of points. - /// Retrieval mode - /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). - /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context - /// The contour hierarchy - public static int[,] FindContours(this Mat mat, IOutputArray contours, RetrType mode, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) - { - using var hierarchy = new Mat(); - CvInvoke.FindContours(mat, contours, hierarchy, mode, method, offset); - var numArray = new int[hierarchy.Cols, 4]; - var gcHandle = GCHandle.Alloc(numArray, GCHandleType.Pinned); - using (var mat2 = new Mat(hierarchy.Rows, hierarchy.Cols, hierarchy.Depth, 4, gcHandle.AddrOfPinnedObject(), hierarchy.Step)) - hierarchy.CopyTo(mat2); - gcHandle.Free(); - return numArray; - }*/ - - /// - /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. - /// The function modifies the source image content - /// - /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content - /// The contour hierarchy - /// Retrieval mode - /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). - /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context - /// Detected contours. Each contour is stored as a vector of points. - public static VectorOfVectorOfPoint FindContours(this Mat mat, out int[,] hierarchy, RetrType mode, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) - { - var contours = new VectorOfVectorOfPoint(); - using var hierarchyMat = new Mat(); - CvInvoke.FindContours(mat, contours, hierarchyMat, mode, method, offset); - hierarchy = new int[hierarchyMat.Cols, 4]; - var gcHandle = GCHandle.Alloc(hierarchy, GCHandleType.Pinned); - using (var mat2 = new Mat(hierarchyMat.Rows, hierarchyMat.Cols, hierarchyMat.Depth, 4, gcHandle.AddrOfPinnedObject(), hierarchyMat.Step)) - hierarchyMat.CopyTo(mat2); - gcHandle.Free(); - return contours; - } - - - /// - /// Determine the area (i.e. total number of pixels in the image), - /// initialize the output skeletonized image, and construct the - /// morphological structuring element - /// - /// - /// Number of iterations required to perform the skeletoize - /// - /// - public static Mat Skeletonize(this Mat src, out int iterations, Size ksize = default, ElementShape elementShape = ElementShape.Rectangle) - { - if (ksize.IsEmpty) ksize = new Size(3, 3); - Point anchor = new(-1, -1); - var skeleton = src.NewBlank(); - var kernel = Kernel3x3Rectangle; - - var image = src; - using var temp = new Mat(); - iterations = 0; - while (true) - { - iterations++; - - // erode and dilate the image using the structuring element - using var eroded = new Mat(); - CvInvoke.Erode(image, eroded, kernel, anchor, 1, BorderType.Reflect101, default); - CvInvoke.Dilate(eroded, temp, kernel, anchor, 1, BorderType.Reflect101, default); - - // subtract the temporary image from the original, eroded - // image, then take the bitwise 'or' between the skeleton - // and the temporary image - CvInvoke.Subtract(image, temp, temp); - CvInvoke.BitwiseOr(skeleton, temp, skeleton); - image = eroded.Clone(); - - // if there are no more 'white' pixels in the image, then - // break from the loop - if (CvInvoke.CountNonZero(image) == 0) break; - } - - return skeleton; - } - - /// - /// Determine the area (i.e. total number of pixels in the image), - /// initialize the output skeletonized image, and construct the - /// morphological structuring element - /// - /// - /// - /// - public static Mat Skeletonize(this Mat src, Size ksize = default, ElementShape elementShape = ElementShape.Rectangle) - => src.Skeletonize(out _, ksize, elementShape); - #endregion - - #region Kernel methods - - /// - /// Reduces iterations to 1 and generate a kernel to match the iterations effect - /// - /// - /// - /// - public static Mat GetDynamicKernel(ref int iterations, ElementShape elementShape = ElementShape.Ellipse) - { - var size = Math.Max(iterations, 1) * 2 + 1; - iterations = 1; - return CvInvoke.GetStructuringElement(elementShape, new Size(size, size), new Point(-1, -1)); - } - #endregion - - #region Disposes - /// - /// Dispose this if it's a sub matrix / roi - /// - /// Mat to dispose - public static void DisposeIfSubMatrix(this Mat mat) - { - if(mat.IsSubmatrix) mat.Dispose(); - } - #endregion } -} + + /// + /// Extended OpenCV PutText to accepting line breaks, line alignment and rotation + /// + public static void PutTextRotated(this Mat src, string text, Point org, FontFace fontFace, double fontScale, + MCvScalar color, + int thickness = 1, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, + PutTextLineAlignment lineAlignment = default, double angle = 0) + => src.PutTextRotated(text, org, fontFace, fontScale, color, thickness, 0, lineType, bottomLeftOrigin, + lineAlignment, angle); + + /// + /// Extended OpenCV PutText to accepting line breaks, line alignment and rotation + /// + public static void PutTextRotated(this Mat src, string text, Point org, FontFace fontFace, double fontScale, MCvScalar color, + int thickness = 1, int lineGapOffset = 0, LineType lineType = LineType.EightConnected, bool bottomLeftOrigin = false, PutTextLineAlignment lineAlignment = default, double angle = 0) + { + if (angle % 360 == 0) // No rotation needed, cheaper cycle + { + src.PutTextExtended(text, org, fontFace, fontScale, color, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); + return; + } + + using var rotatedSrc = src.Clone(); + rotatedSrc.RotateAdjustBounds(-angle); + var sizeDifference = (rotatedSrc.Size - src.Size).Half(); + org.Offset(sizeDifference.ToPoint()); + org = org.Rotate(-angle, new Point(rotatedSrc.Size.Width / 2, rotatedSrc.Size.Height / 2)); + rotatedSrc.PutTextExtended(text, org, fontFace, fontScale, color, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); + + using var mask = rotatedSrc.NewBlank(); + mask.PutTextExtended(text, org, fontFace, fontScale, WhiteColor, thickness, lineGapOffset, lineType, bottomLeftOrigin, lineAlignment); + + rotatedSrc.Rotate(angle, src.Size); + mask.Rotate(angle, src.Size); + + rotatedSrc.CopyTo(src, mask); + } + #endregion + + #region Utilities methods + + /// + /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. + /// The function modifies the source image content + /// + /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content + /// Retrieval mode + /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). + /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context + /// The contour hierarchy + public static VectorOfVectorOfPoint FindContours(this IInputOutputArray mat, RetrType mode = RetrType.List, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) + { + using var hierarchy = new Mat(); + var contours = new VectorOfVectorOfPoint(); + CvInvoke.FindContours(mat, contours, hierarchy, mode, method, offset); + return contours; + } + + /* + /// + /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. + /// The function modifies the source image content + /// + /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content + /// Detected contours. Each contour is stored as a vector of points. + /// Retrieval mode + /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). + /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context + /// The contour hierarchy + public static int[,] FindContours(this Mat mat, IOutputArray contours, RetrType mode, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) + { + using var hierarchy = new Mat(); + CvInvoke.FindContours(mat, contours, hierarchy, mode, method, offset); + var numArray = new int[hierarchy.Cols, 4]; + var gcHandle = GCHandle.Alloc(numArray, GCHandleType.Pinned); + using (var mat2 = new Mat(hierarchy.Rows, hierarchy.Cols, hierarchy.Depth, 4, gcHandle.AddrOfPinnedObject(), hierarchy.Step)) + hierarchy.CopyTo(mat2); + gcHandle.Free(); + return numArray; + }*/ + + /// + /// Retrieves contours from the binary image as a contour tree. The pointer firstContour is filled by the function. It is provided as a convenient way to obtain the hierarchy value as int[,]. + /// The function modifies the source image content + /// + /// The source 8-bit single channel image. Non-zero pixels are treated as 1s, zero pixels remain 0s - that is image treated as binary. To get such a binary image from grayscale, one may use cvThreshold, cvAdaptiveThreshold or cvCanny. The function modifies the source image content + /// The contour hierarchy + /// Retrieval mode + /// Approximation method (for all the modes, except CV_RETR_RUNS, which uses built-in approximation). + /// Offset, by which every contour point is shifted. This is useful if the contours are extracted from the image ROI and then they should be analyzed in the whole image context + /// Detected contours. Each contour is stored as a vector of points. + public static VectorOfVectorOfPoint FindContours(this IInputOutputArray mat, out int[,] hierarchy, RetrType mode, ChainApproxMethod method = ChainApproxMethod.ChainApproxSimple, Point offset = default) + { + var contours = new VectorOfVectorOfPoint(); + using var hierarchyMat = new Mat(); + CvInvoke.FindContours(mat, contours, hierarchyMat, mode, method, offset); + hierarchy = new int[hierarchyMat.Cols, 4]; + var gcHandle = GCHandle.Alloc(hierarchy, GCHandleType.Pinned); + using (var mat2 = new Mat(hierarchyMat.Rows, hierarchyMat.Cols, hierarchyMat.Depth, 4, gcHandle.AddrOfPinnedObject(), hierarchyMat.Step)) + hierarchyMat.CopyTo(mat2); + gcHandle.Free(); + return contours; + } + + + /// + /// Determine the area (i.e. total number of pixels in the image), + /// initialize the output skeletonized image, and construct the + /// morphological structuring element + /// + /// + /// Number of iterations required to perform the skeletoize + /// + /// + public static Mat Skeletonize(this Mat src, out int iterations, Size ksize = default, ElementShape elementShape = ElementShape.Rectangle) + { + if (ksize.IsEmpty) ksize = new Size(3, 3); + Point anchor = new(-1, -1); + var skeleton = src.NewBlank(); + var kernel = Kernel3x3Rectangle; + + var image = src; + using var temp = new Mat(); + iterations = 0; + while (true) + { + iterations++; + + // erode and dilate the image using the structuring element + using var eroded = new Mat(); + CvInvoke.Erode(image, eroded, kernel, anchor, 1, BorderType.Reflect101, default); + CvInvoke.Dilate(eroded, temp, kernel, anchor, 1, BorderType.Reflect101, default); + + // subtract the temporary image from the original, eroded + // image, then take the bitwise 'or' between the skeleton + // and the temporary image + CvInvoke.Subtract(image, temp, temp); + CvInvoke.BitwiseOr(skeleton, temp, skeleton); + image = eroded.Clone(); + + // if there are no more 'white' pixels in the image, then + // break from the loop + if (CvInvoke.CountNonZero(image) == 0) break; + } + + return skeleton; + } + + /// + /// Determine the area (i.e. total number of pixels in the image), + /// initialize the output skeletonized image, and construct the + /// morphological structuring element + /// + /// + /// + /// + public static Mat Skeletonize(this Mat src, Size ksize = default, ElementShape elementShape = ElementShape.Rectangle) + => src.Skeletonize(out _, ksize, elementShape); + #endregion + + #region Kernel methods + + /// + /// Reduces iterations to 1 and generate a kernel to match the iterations effect + /// + /// + /// + /// + public static Mat GetDynamicKernel(ref int iterations, ElementShape elementShape = ElementShape.Ellipse) + { + var size = Math.Max(iterations, 1) * 2 + 1; + iterations = 1; + return CvInvoke.GetStructuringElement(elementShape, new Size(size, size), new Point(-1, -1)); + } + #endregion + + #region Disposes + /// + /// Dispose this if it's a sub matrix / roi + /// + /// Mat to dispose + public static void DisposeIfSubMatrix(this Mat mat) + { + if(mat.IsSubmatrix) mat.Dispose(); + } + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/EnumExtensions.cs b/UVtools.Core/Extensions/EnumExtensions.cs index 6847164..0d81996 100644 --- a/UVtools.Core/Extensions/EnumExtensions.cs +++ b/UVtools.Core/Extensions/EnumExtensions.cs @@ -12,28 +12,29 @@ using System.Globalization; using System.Linq; using UVtools.Core.Objects; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class EnumExtensions { - public static class EnumExtensions + public static string GetDescription(this Enum value) { - public static string GetDescription(this Enum value) + var attributes = value.GetType().GetField(value.ToString())?.GetCustomAttributes(typeof(DescriptionAttribute), false); + if (attributes is not null && attributes.Any()) { - var attributes = value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false); - if (attributes.Any()) - return (attributes.First() as DescriptionAttribute)?.Description; - - // If no description is found, the least we can do is replace underscores with spaces - // You can add your own custom default formatting logic here - var ti = CultureInfo.CurrentCulture.TextInfo; - return ti.ToTitleCase(ti.ToLower(value.ToString().Replace("_", " "))); + if (attributes.First() is DescriptionAttribute attr) return attr.Description; } - public static IEnumerable GetAllValuesAndDescriptions(Type t) - { - if (!t.IsEnum) - throw new ArgumentException($"{nameof(t)} must be an enum type"); - - return Enum.GetValues(t).Cast().OrderBy(e => e).Select(e => new ValueDescription(e, e.GetDescription())).ToList(); - } + // If no description is found, the least we can do is replace underscores with spaces + // You can add your own custom default formatting logic here + var ti = CultureInfo.CurrentCulture.TextInfo; + return ti.ToTitleCase(ti.ToLower(value.ToString().Replace("_", " "))); } -} + + public static IEnumerable GetAllValuesAndDescriptions(Type t) + { + if (!t.IsEnum) + throw new ArgumentException($"{nameof(t)} must be an enum type"); + + return Enum.GetValues(t).Cast().OrderBy(e => e).Select(e => new ValueDescription(e, e.GetDescription())).ToList(); + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/FileStreamExtensions.cs b/UVtools.Core/Extensions/FileStreamExtensions.cs index 9d2a130..12fb0d8 100644 --- a/UVtools.Core/Extensions/FileStreamExtensions.cs +++ b/UVtools.Core/Extensions/FileStreamExtensions.cs @@ -10,164 +10,163 @@ using System; using System.IO; using System.Text; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class FileStreamExtensions { - public static class FileStreamExtensions + public static uint ReadBytes(this FileStream fs, byte[] bytes, int offset = 0) { - public static uint ReadBytes(this FileStream fs, byte[] bytes, int offset = 0) - { - return (uint)fs.Read(bytes, offset, bytes.Length); - } - - public static byte[] ReadBytes(this FileStream fs, int length, int offset = 0) - { - var buffer = new byte[length]; - fs.Read(buffer, offset, length); - return buffer; - } - - public static byte[] ReadBytes(this FileStream fs, uint length, int offset = 0) - => fs.ReadBytes((int)length, offset); - - /// - /// Read from current position to the end of the file - /// - /// - /// - public static byte[] ReadToEnd(this FileStream fs) - { - return fs.ReadBytes((uint)(fs.Length - fs.Position)); - } - - public static uint ReadUShortLittleEndian(this FileStream fs, int offset = 0) - { - return BitExtensions.ToUShortLittleEndian(fs.ReadBytes(2, offset)); - } - - public static uint ReadUShortBigEndian(this FileStream fs, int offset = 0) - { - return BitExtensions.ToUShortBigEndian(fs.ReadBytes(2, offset)); - } - - public static uint ReadUIntLittleEndian(this FileStream fs, int offset = 0) - { - return BitExtensions.ToUIntLittleEndian(fs.ReadBytes(4, offset)); - } - - public static uint ReadUIntBigEndian(this FileStream fs, int offset = 0) - { - return BitExtensions.ToUIntBigEndian(fs.ReadBytes(4, offset)); - } - - public static void WriteUShortLittleEndian(this FileStream fs, ushort value, int offset = 0) - { - fs.WriteBytes(BitExtensions.ToBytesLittleEndian(value), offset); - } - - public static void WriteUShortBigEndian(this FileStream fs, ushort value, int offset = 0) - { - fs.WriteBytes(BitExtensions.ToBytesBigEndian(value), offset); - } - - public static void WriteUIntLittleEndian(this FileStream fs, uint value, int offset = 0) - { - fs.WriteBytes(BitExtensions.ToBytesLittleEndian(value), offset); - } - - public static void WriteUIntBigEndian(this FileStream fs, uint value, int offset = 0) - { - fs.WriteBytes(BitExtensions.ToBytesBigEndian(value), offset); - } - - public static void WriteFloatLittleEndian(this FileStream fs, float value, int offset = 0) - { - var bytes = BitConverter.GetBytes(value); - if (!BitConverter.IsLittleEndian) Array.Reverse(bytes); //reverse it so we get little endian. - fs.WriteBytes(BitConverter.GetBytes(value), offset); - } - public static void WriteFloatBigEndian(this FileStream fs, float value, int offset = 0) - { - var bytes = BitConverter.GetBytes(value); - if (BitConverter.IsLittleEndian) Array.Reverse(bytes); //reverse it so we get big endian. - fs.WriteBytes(BitConverter.GetBytes(value), offset); - } - - public static uint WriteStream(this FileStream fs, MemoryStream stream, int offset = 0) - { - return fs.WriteBytes(stream.ToArray(), offset); - } - - public static uint WriteBytes(this FileStream fs, byte[] bytes, int offset = 0) - { - fs.Write(bytes, offset, bytes.Length); - return (uint)bytes.Length; - } - - public static uint WriteBytes(this Stream stream, byte[] bytes, int offset = 0) - { - stream.Write(bytes, offset, bytes.Length); - return (uint)bytes.Length; - } - - public static uint WriteString(this FileStream fs, string text, int offset = 0) - => fs.WriteString(text, Encoding.UTF8, offset); - - public static uint WriteString(this FileStream fs, string text, Encoding encoding, int offset = 0) - { - var bytes = encoding.GetBytes(text); - fs.WriteBytes(bytes, offset); - return (uint)bytes.Length; - } - - public static uint WriteLine(this FileStream fs) => fs.WriteString(Environment.NewLine); - - public static uint WriteLine(this FileStream fs, string text, int offset = 0) - => fs.WriteLine(text, Encoding.UTF8, offset); - - public static uint WriteLine(this FileStream fs, string text, Encoding encoding, int offset = 0) - { - var bytes = encoding.GetBytes($"{text}{Environment.NewLine}"); - fs.WriteBytes(bytes, offset); - return (uint)bytes.Length; - } - - public static uint WriteLineLF(this FileStream fs) - { - fs.WriteByte(0x0A); - return 1; - } - - public static uint WriteLineLF(this FileStream fs, string text, int offset = 0) - => fs.WriteLineLF(text, Encoding.UTF8, offset); - - public static uint WriteLineLF(this FileStream fs, string text, Encoding encoding, int offset = 0) - { - var bytes = encoding.GetBytes($"{text}\n"); - fs.WriteBytes(bytes, offset); - return (uint)bytes.Length; - } - - public static uint WriteSerialize(this FileStream fs, object data, int offset = 0) - { - return Helpers.SerializeWriteFileStream(fs, data, offset); - } - - /// - /// Seek to a position, do work and rewind to initial position - /// - /// - /// - /// - /// - public static void SeekDoWorkAndRewind(this FileStream fs, long offset, Action action) - => fs.SeekDoWorkAndRewind(offset, SeekOrigin.Begin, action); - - public static void SeekDoWorkAndRewind(this FileStream fs, long offset, SeekOrigin seekOrigin, Action action) - { - var currentPos = fs.Position; - fs.Seek(offset, seekOrigin); // Go to - action.Invoke(); // Do work - fs.Seek(currentPos, SeekOrigin.Begin); // Rewind - } + return (uint)fs.Read(bytes, offset, bytes.Length); } -} + + public static byte[] ReadBytes(this FileStream fs, int length, int offset = 0) + { + var buffer = new byte[length]; + fs.Read(buffer, offset, length); + return buffer; + } + + public static byte[] ReadBytes(this FileStream fs, uint length, int offset = 0) + => fs.ReadBytes((int)length, offset); + + /// + /// Read from current position to the end of the file + /// + /// + /// + public static byte[] ReadToEnd(this FileStream fs) + { + return fs.ReadBytes((uint)(fs.Length - fs.Position)); + } + + public static uint ReadUShortLittleEndian(this FileStream fs, int offset = 0) + { + return BitExtensions.ToUShortLittleEndian(fs.ReadBytes(2, offset)); + } + + public static uint ReadUShortBigEndian(this FileStream fs, int offset = 0) + { + return BitExtensions.ToUShortBigEndian(fs.ReadBytes(2, offset)); + } + + public static uint ReadUIntLittleEndian(this FileStream fs, int offset = 0) + { + return BitExtensions.ToUIntLittleEndian(fs.ReadBytes(4, offset)); + } + + public static uint ReadUIntBigEndian(this FileStream fs, int offset = 0) + { + return BitExtensions.ToUIntBigEndian(fs.ReadBytes(4, offset)); + } + + public static void WriteUShortLittleEndian(this FileStream fs, ushort value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesLittleEndian(value), offset); + } + + public static void WriteUShortBigEndian(this FileStream fs, ushort value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesBigEndian(value), offset); + } + + public static void WriteUIntLittleEndian(this FileStream fs, uint value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesLittleEndian(value), offset); + } + + public static void WriteUIntBigEndian(this FileStream fs, uint value, int offset = 0) + { + fs.WriteBytes(BitExtensions.ToBytesBigEndian(value), offset); + } + + public static void WriteFloatLittleEndian(this FileStream fs, float value, int offset = 0) + { + var bytes = BitConverter.GetBytes(value); + if (!BitConverter.IsLittleEndian) Array.Reverse(bytes); //reverse it so we get little endian. + fs.WriteBytes(BitConverter.GetBytes(value), offset); + } + public static void WriteFloatBigEndian(this FileStream fs, float value, int offset = 0) + { + var bytes = BitConverter.GetBytes(value); + if (BitConverter.IsLittleEndian) Array.Reverse(bytes); //reverse it so we get big endian. + fs.WriteBytes(BitConverter.GetBytes(value), offset); + } + + public static uint WriteStream(this FileStream fs, MemoryStream stream, int offset = 0) + { + return fs.WriteBytes(stream.ToArray(), offset); + } + + public static uint WriteBytes(this FileStream fs, byte[] bytes, int offset = 0) + { + fs.Write(bytes, offset, bytes.Length); + return (uint)bytes.Length; + } + + public static uint WriteBytes(this Stream stream, byte[] bytes, int offset = 0) + { + stream.Write(bytes, offset, bytes.Length); + return (uint)bytes.Length; + } + + public static uint WriteString(this FileStream fs, string text, int offset = 0) + => fs.WriteString(text, Encoding.UTF8, offset); + + public static uint WriteString(this FileStream fs, string text, Encoding encoding, int offset = 0) + { + var bytes = encoding.GetBytes(text); + fs.WriteBytes(bytes, offset); + return (uint)bytes.Length; + } + + public static uint WriteLine(this FileStream fs) => fs.WriteString(Environment.NewLine); + + public static uint WriteLine(this FileStream fs, string text, int offset = 0) + => fs.WriteLine(text, Encoding.UTF8, offset); + + public static uint WriteLine(this FileStream fs, string text, Encoding encoding, int offset = 0) + { + var bytes = encoding.GetBytes($"{text}{Environment.NewLine}"); + fs.WriteBytes(bytes, offset); + return (uint)bytes.Length; + } + + public static uint WriteLineLF(this FileStream fs) + { + fs.WriteByte(0x0A); + return 1; + } + + public static uint WriteLineLF(this FileStream fs, string text, int offset = 0) + => fs.WriteLineLF(text, Encoding.UTF8, offset); + + public static uint WriteLineLF(this FileStream fs, string text, Encoding encoding, int offset = 0) + { + var bytes = encoding.GetBytes($"{text}\n"); + fs.WriteBytes(bytes, offset); + return (uint)bytes.Length; + } + + public static uint WriteSerialize(this FileStream fs, object data, int offset = 0) + { + return Helpers.SerializeWriteFileStream(fs, data, offset); + } + + /// + /// Seek to a position, do work and rewind to initial position + /// + /// + /// + /// + /// + public static void SeekDoWorkAndRewind(this FileStream fs, long offset, Action action) + => fs.SeekDoWorkAndRewind(offset, SeekOrigin.Begin, action); + + public static void SeekDoWorkAndRewind(this FileStream fs, long offset, SeekOrigin seekOrigin, Action action) + { + var currentPos = fs.Position; + fs.Seek(offset, seekOrigin); // Go to + action.Invoke(); // Do work + fs.Seek(currentPos, SeekOrigin.Begin); // Rewind + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/JsonExtensions.cs b/UVtools.Core/Extensions/JsonExtensions.cs new file mode 100644 index 0000000..e1d44cb --- /dev/null +++ b/UVtools.Core/Extensions/JsonExtensions.cs @@ -0,0 +1,18 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +using System.Text.Json; + +namespace UVtools.Core.Extensions; + +public static class JsonExtensions +{ + public static readonly JsonSerializerOptions SettingsIndent = new() + { + WriteIndented = true + }; +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/MathExtensions.cs b/UVtools.Core/Extensions/MathExtensions.cs index 5916926..23ea24a 100644 --- a/UVtools.Core/Extensions/MathExtensions.cs +++ b/UVtools.Core/Extensions/MathExtensions.cs @@ -9,150 +9,149 @@ using System; using System.Globalization; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class MathExtensions { - public static class MathExtensions + public static byte Clamp(this byte value, byte min, byte max) { - public static byte Clamp(this byte value, byte min, byte max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static sbyte Clamp(this sbyte value, sbyte min, sbyte max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static ushort Clamp(this ushort value, ushort min, ushort max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static short Clamp(this short value, short min, short max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static uint Clamp(this uint value, uint min, uint max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static int Clamp(this int value, int min, int max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static ulong Clamp(this ulong value, ulong min, ulong max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static long Clamp(this long value, long min, long max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static float Clamp(this float value, float min, float max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static double Clamp(this double value, double min, double max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static decimal Clamp(this decimal value, decimal min, decimal max) - { - return value <= min ? min : value >= max ? max : value; - } - - public static T Clamp(T value, T min, T max) where T : IComparable - { - if (value.CompareTo(min) < 0) - return min; - if (value.CompareTo(max) > 0) - return max; - - return value; - } - - public static uint DecimalDigits(this float val) - { - var valStr = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0'); - if (string.IsNullOrEmpty(valStr)) return 0; - - var index = valStr.IndexOf('.'); - if (index < 0) return 0; - - return (uint) (valStr[index..].Length - 1); - } - - public static uint DecimalDigits(this double val) - { - var valStr = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0'); - if (string.IsNullOrEmpty(valStr)) return 0; - - var index = valStr.IndexOf('.'); - if (index < 0) return 0; - - return (uint)(valStr[index..].Length - 1); - } - - public static uint DecimalDigits(this decimal val) - { - var valStr = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0'); - if (string.IsNullOrEmpty(valStr) || valStr[^1] == '.') return 0; - - var index = valStr.IndexOf('.'); - if (index < 0) return 0; - - return (uint)(valStr[index..].Length - 1); - } - - public static bool IsInteger(this float val, float tolerance = 0.0001f) => Math.Abs(val - Math.Floor(val)) < tolerance; - public static bool IsInteger(this double val, double tolerance = 0.0001) => Math.Abs(val - Math.Floor(val)) < tolerance; - public static bool IsInteger(this decimal val) => val == Math.Floor(val); - - public static int GCD(int a, int b) - { - while (a != 0 && b != 0) - { - if (a > b) - a %= b; - else - b %= a; - } - - return a | b; - } - - public static uint GCD(uint a, uint b) - { - while (a != 0 && b != 0) - { - if (a > b) - a %= b; - else - b %= a; - } - - return a | b; - } - - public static ulong GCD(ulong a, ulong b) - { - while (a != 0 && b != 0) - { - if (a > b) - a %= b; - else - b %= a; - } - - return a | b; - } - + return value <= min ? min : value >= max ? max : value; } -} + + public static sbyte Clamp(this sbyte value, sbyte min, sbyte max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static ushort Clamp(this ushort value, ushort min, ushort max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static short Clamp(this short value, short min, short max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static uint Clamp(this uint value, uint min, uint max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static int Clamp(this int value, int min, int max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static ulong Clamp(this ulong value, ulong min, ulong max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static long Clamp(this long value, long min, long max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static float Clamp(this float value, float min, float max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static double Clamp(this double value, double min, double max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static decimal Clamp(this decimal value, decimal min, decimal max) + { + return value <= min ? min : value >= max ? max : value; + } + + public static T Clamp(T value, T min, T max) where T : IComparable + { + if (value.CompareTo(min) < 0) + return min; + if (value.CompareTo(max) > 0) + return max; + + return value; + } + + public static uint DecimalDigits(this float val) + { + var valStr = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0'); + if (string.IsNullOrEmpty(valStr)) return 0; + + var index = valStr.IndexOf('.'); + if (index < 0) return 0; + + return (uint) (valStr[index..].Length - 1); + } + + public static uint DecimalDigits(this double val) + { + var valStr = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0'); + if (string.IsNullOrEmpty(valStr)) return 0; + + var index = valStr.IndexOf('.'); + if (index < 0) return 0; + + return (uint)(valStr[index..].Length - 1); + } + + public static uint DecimalDigits(this decimal val) + { + var valStr = val.ToString(CultureInfo.InvariantCulture).TrimEnd('0'); + if (string.IsNullOrEmpty(valStr) || valStr[^1] == '.') return 0; + + var index = valStr.IndexOf('.'); + if (index < 0) return 0; + + return (uint)(valStr[index..].Length - 1); + } + + public static bool IsInteger(this float val, float tolerance = 0.0001f) => Math.Abs(val - Math.Floor(val)) < tolerance; + public static bool IsInteger(this double val, double tolerance = 0.0001) => Math.Abs(val - Math.Floor(val)) < tolerance; + public static bool IsInteger(this decimal val) => val == Math.Floor(val); + + public static int GCD(int a, int b) + { + while (a != 0 && b != 0) + { + if (a > b) + a %= b; + else + b %= a; + } + + return a | b; + } + + public static uint GCD(uint a, uint b) + { + while (a != 0 && b != 0) + { + if (a > b) + a %= b; + else + b %= a; + } + + return a | b; + } + + public static ulong GCD(ulong a, ulong b) + { + while (a != 0 && b != 0) + { + if (a > b) + a %= b; + else + b %= a; + } + + return a | b; + } + +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/NetworkExtensions.cs b/UVtools.Core/Extensions/NetworkExtensions.cs index d508705..a09067e 100644 --- a/UVtools.Core/Extensions/NetworkExtensions.cs +++ b/UVtools.Core/Extensions/NetworkExtensions.cs @@ -13,50 +13,49 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class NetworkExtensions { - public static class NetworkExtensions + public static readonly HttpClient HttpClient = new() { - public static readonly HttpClient HttpClient = new() - { - DefaultRequestHeaders = { UserAgent = { new ProductInfoHeaderValue(About.Software, About.VersionStr) } } - }; + DefaultRequestHeaders = { UserAgent = { new ProductInfoHeaderValue(About.Software, About.VersionStr) } } + }; - public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<(long total, long bytes)> progress = null, CancellationToken cancellationToken = default) - { - // Get the http headers first to examine the content length - var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - var contentLength = response.Content.Headers.ContentLength; + public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<(long total, long bytes)>? progress = null, CancellationToken cancellationToken = default) + { + // Get the http headers first to examine the content length + var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + var contentLength = response.Content.Headers.ContentLength; - await using var download = await response.Content.ReadAsStreamAsync(cancellationToken); - // Ignore progress reporting when no progress reporter was - // passed or when the content length is unknown - if (progress is null || !contentLength.HasValue) - { - await download.CopyToAsync(destination, cancellationToken); - return response; - } - - // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%) - var relativeProgress = new Progress(downloadedBytes => progress.Report(new (contentLength.Value, downloadedBytes))); - // Use extension method to report progress while downloading - await download.CopyToAsync(destination, relativeProgress, cancellationToken); - progress.Report(new(contentLength.Value, contentLength.Value)); + await using var download = await response.Content.ReadAsStreamAsync(cancellationToken); + // Ignore progress reporting when no progress reporter was + // passed or when the content length is unknown + if (progress is null || !contentLength.HasValue) + { + await download.CopyToAsync(destination, cancellationToken); return response; } - - public static async Task DownloadAsync(this HttpClient client, string requestUri, string destinationFilePath, IProgress<(long total, long bytes)> progress = null, CancellationToken cancellationToken = default) - { - await using var stream = File.Open(destinationFilePath, FileMode.Create, FileAccess.Write); - return await DownloadAsync(client, requestUri, stream, progress, cancellationToken); - } - - - public static async Task DownloadAsync(string requestUri, Stream destination, IProgress<(long total, long bytes)> progress = null, CancellationToken cancellationToken = default) - => await HttpClient.DownloadAsync(requestUri, destination, progress, cancellationToken); - - public static async Task DownloadAsync(string requestUri, string destinationFilePath, IProgress<(long total, long bytes)> progress = null, CancellationToken cancellationToken = default) - => await HttpClient.DownloadAsync(requestUri, destinationFilePath, progress, cancellationToken); - + + // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%) + var relativeProgress = new Progress(downloadedBytes => progress.Report(new (contentLength.Value, downloadedBytes))); + // Use extension method to report progress while downloading + await download.CopyToAsync(destination, relativeProgress, cancellationToken); + progress.Report(new(contentLength.Value, contentLength.Value)); + return response; } -} + + public static async Task DownloadAsync(this HttpClient client, string requestUri, string destinationFilePath, IProgress<(long total, long bytes)>? progress = null, CancellationToken cancellationToken = default) + { + await using var stream = File.Open(destinationFilePath, FileMode.Create, FileAccess.Write); + return await DownloadAsync(client, requestUri, stream, progress, cancellationToken); + } + + + public static async Task DownloadAsync(string requestUri, Stream destination, IProgress<(long total, long bytes)>? progress = null, CancellationToken cancellationToken = default) + => await HttpClient.DownloadAsync(requestUri, destination, progress, cancellationToken); + + public static async Task DownloadAsync(string requestUri, string destinationFilePath, IProgress<(long total, long bytes)>? progress = null, CancellationToken cancellationToken = default) + => await HttpClient.DownloadAsync(requestUri, destinationFilePath, progress, cancellationToken); + +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/ParallelExtensions.cs b/UVtools.Core/Extensions/ParallelExtensions.cs index 2f747ad..0ab9346 100644 --- a/UVtools.Core/Extensions/ParallelExtensions.cs +++ b/UVtools.Core/Extensions/ParallelExtensions.cs @@ -11,39 +11,38 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class ParallelExtensions { - public static class ParallelExtensions + //public static readonly ParallelOptions ParallelSingleThread = new() { MaxDegreeOfParallelism = 1 }; + public static void ForAllInApproximateOrder(this ParallelQuery source, Action action) { - //public static readonly ParallelOptions ParallelSingleThread = new() { MaxDegreeOfParallelism = 1 }; - public static void ForAllInApproximateOrder(this ParallelQuery source, Action action) - { - Partitioner.Create(source) - .AsParallel() - .AsOrdered() - .ForAll(action); - } - - public static void ForEachInApproximateOrder(this ParallelQuery source, Action action) - { - - source = Partitioner.Create(source) - .AsParallel() - .AsOrdered(); - - Parallel.ForEach(source, action); - - } - - public static IEnumerable OrderedParallel(this IEnumerable list, Func action) - { - var unorderedResult = new ConcurrentBag<(long, T1)>(); - Parallel.ForEach(list, (o, state, i) => - { - unorderedResult.Add((i, action.Invoke(o))); - }); - var ordered = unorderedResult.OrderBy(o => o.Item1); - return ordered.Select(o => o.Item2); - } + Partitioner.Create(source) + .AsParallel() + .AsOrdered() + .ForAll(action); } -} + + public static void ForEachInApproximateOrder(this ParallelQuery source, Action action) + { + + source = Partitioner.Create(source) + .AsParallel() + .AsOrdered(); + + Parallel.ForEach(source, action); + + } + + public static IEnumerable OrderedParallel(this IEnumerable list, Func action) + { + var unorderedResult = new ConcurrentBag<(long, T1)>(); + Parallel.ForEach(list, (o, state, i) => + { + unorderedResult.Add((i, action.Invoke(o))); + }); + var ordered = unorderedResult.OrderBy(o => o.Item1); + return ordered.Select(o => o.Item2); + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/PathExtensions.cs b/UVtools.Core/Extensions/PathExtensions.cs index 3fff87b..3a7b970 100644 --- a/UVtools.Core/Extensions/PathExtensions.cs +++ b/UVtools.Core/Extensions/PathExtensions.cs @@ -9,51 +9,50 @@ using System; using System.Collections.Generic; using System.IO; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class PathExtensions { - public static class PathExtensions + public static string GetFileNameStripAllExtensions(string path) { - public static string GetFileNameStripAllExtensions(string path) - { - path = Path.GetFileName(path); - if(string.IsNullOrEmpty(path)) return string.Empty; - var splitPath = path.Split('.', 2, StringSplitOptions.TrimEntries); - return splitPath.Length == 0 ? string.Empty : splitPath[0]; - } - - public static string GetFileNameStripExtensions(string path, List extensions, out string strippedExtension) - { - strippedExtension = string.Empty; - path = Path.GetFileName(path); - if (string.IsNullOrEmpty(path)) return string.Empty; - foreach (var extension in extensions) - { - var dotExtension = $".{extension}"; - if (!path.EndsWith(dotExtension, StringComparison.OrdinalIgnoreCase)) continue; - strippedExtension = extension; - return path.Remove(path.Length - dotExtension.Length); - } - - return path; - } - - public static string GetTempFilePathWithFilename(string fileName) - { - var path = Path.GetTempPath(); - return Path.Combine(path, fileName); - } - - /// - /// Gets a temporary file with a extension and a prepend string - /// - /// Extension name without the dot (.) - /// Optional prepend file name - /// - public static string GetTempFilePathWithExtension(string extension, string prepend = null) - { - var path = Path.GetTempPath(); - var fileName = $"{prepend}{Guid.NewGuid()}.{extension}"; - return Path.Combine(path, fileName); - } + path = Path.GetFileName(path); + if(string.IsNullOrEmpty(path)) return string.Empty; + var splitPath = path.Split('.', 2, StringSplitOptions.TrimEntries); + return splitPath.Length == 0 ? string.Empty : splitPath[0]; } -} + + public static string GetFileNameStripExtensions(string path, List extensions, out string strippedExtension) + { + strippedExtension = string.Empty; + path = Path.GetFileName(path); + if (string.IsNullOrEmpty(path)) return string.Empty; + foreach (var extension in extensions) + { + var dotExtension = $".{extension}"; + if (!path.EndsWith(dotExtension, StringComparison.OrdinalIgnoreCase)) continue; + strippedExtension = extension; + return path.Remove(path.Length - dotExtension.Length); + } + + return path; + } + + public static string GetTempFilePathWithFilename(string fileName) + { + var path = Path.GetTempPath(); + return Path.Combine(path, fileName); + } + + /// + /// Gets a temporary file with a extension and a prepend string + /// + /// Extension name without the dot (.) + /// Optional prepend file name + /// + public static string GetTempFilePathWithExtension(string extension, string? prepend = null) + { + var path = Path.GetTempPath(); + var fileName = $"{prepend}{Guid.NewGuid()}.{extension}"; + return Path.Combine(path, fileName); + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/PointExtensions.cs b/UVtools.Core/Extensions/PointExtensions.cs index d95572e..01c6c54 100644 --- a/UVtools.Core/Extensions/PointExtensions.cs +++ b/UVtools.Core/Extensions/PointExtensions.cs @@ -8,76 +8,75 @@ using System; using System.Drawing; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class PointExtensions { - public static class PointExtensions + public static double FindLength(Point start, Point end) => Math.Sqrt(Math.Pow(end.Y - start.Y, 2) + Math.Pow(end.X - start.X, 2)); + + public static bool IsAnyNegative(this Point point) => point.X < 0 || point.Y < 0; + public static bool IsBothNegative(this Point point) => point.X < 0 && point.Y < 0; + public static bool IsBothZeroOrPositive(this Point point) => point.X >= 0 && point.Y >= 0; + + public static Point Rotate(this Point point, double angleDegree, Point pivot = default) { - public static double FindLength(Point start, Point end) => Math.Sqrt(Math.Pow(end.Y - start.Y, 2) + Math.Pow(end.X - start.X, 2)); + if (angleDegree % 360 == 0) return point; + double angle = angleDegree * Math.PI / 180; + double cos = Math.Cos(angle); + double sin = Math.Sin(angle); + int dx = point.X - pivot.X; + int dy = point.Y - pivot.Y; + double x = cos * dx - sin * dy + pivot.X; + double y = sin * dx + cos * dy + pivot.Y; - public static bool IsAnyNegative(this Point point) => point.X < 0 || point.Y < 0; - public static bool IsBothNegative(this Point point) => point.X < 0 && point.Y < 0; - public static bool IsBothZeroOrPositive(this Point point) => point.X >= 0 && point.Y >= 0; + return new((int)Math.Round(x), (int)Math.Round(y)); + } - public static Point Rotate(this Point point, double angleDegree, Point pivot = default) + public static PointF Rotate(this PointF point, double angleDegree, PointF pivot = default) + { + if (angleDegree % 360 == 0) return point; + double angle = angleDegree * Math.PI / 180; + double cos = Math.Cos(angle); + double sin = Math.Sin(angle); + double dx = point.X - pivot.X; + double dy = point.Y - pivot.Y; + double x = cos * dx - sin * dy + pivot.X; + double y = sin * dx + cos * dy + pivot.Y; + + return new((float) x, (float) y); + } + + public static void Rotate(Point[] points, double angleDegree, Point pivot = default) + { + for (int i = 0; i < points.Length; i++) { - if (angleDegree % 360 == 0) return point; - double angle = angleDegree * Math.PI / 180; - double cos = Math.Cos(angle); - double sin = Math.Sin(angle); - int dx = point.X - pivot.X; - int dy = point.Y - pivot.Y; - double x = cos * dx - sin * dy + pivot.X; - double y = sin * dx + cos * dy + pivot.Y; - - return new((int)Math.Round(x), (int)Math.Round(y)); + points[i] = points[i].Rotate(angleDegree, pivot); } + } - public static PointF Rotate(this PointF point, double angleDegree, PointF pivot = default) + public static void Rotate(PointF[] points, double angleDegree, PointF pivot = default) + { + for (int i = 0; i < points.Length; i++) { - if (angleDegree % 360 == 0) return point; - double angle = angleDegree * Math.PI / 180; - double cos = Math.Cos(angle); - double sin = Math.Sin(angle); - double dx = point.X - pivot.X; - double dy = point.Y - pivot.Y; - double x = cos * dx - sin * dy + pivot.X; - double y = sin * dx + cos * dy + pivot.Y; - - return new((float) x, (float) y); + points[i] = points[i].Rotate(angleDegree, pivot); } + } - public static void Rotate(Point[] points, double angleDegree, Point pivot = default) - { - for (int i = 0; i < points.Length; i++) - { - points[i] = points[i].Rotate(angleDegree, pivot); - } - } - - public static void Rotate(PointF[] points, double angleDegree, PointF pivot = default) - { - for (int i = 0; i < points.Length; i++) - { - points[i] = points[i].Rotate(angleDegree, pivot); - } - } - - public static Point OffsetBy(this Point point, int value)=> new(point.X + value, point.Y + value); - public static Point OffsetBy(this Point point, int x, int y) => new(point.X + x, point.Y + y); - public static Point OffsetBy(this Point point, Point other) => new(point.X + other.X, point.Y + other.Y); + public static Point OffsetBy(this Point point, int value)=> new(point.X + value, point.Y + value); + public static Point OffsetBy(this Point point, int x, int y) => new(point.X + x, point.Y + y); + public static Point OffsetBy(this Point point, Point other) => new(point.X + other.X, point.Y + other.Y); - public static Point Half(this Point point) => new(point.X / 2, point.Y / 2); - public static PointF Half(this PointF point) => new(point.X / 2, point.Y / 2); - public static Point ToPoint(this PointF point) => new((int) Math.Round(point.X), (int) Math.Round(point.Y)); + public static Point Half(this Point point) => new(point.X / 2, point.Y / 2); + public static PointF Half(this PointF point) => new(point.X / 2, point.Y / 2); + public static Point ToPoint(this PointF point) => new((int) Math.Round(point.X), (int) Math.Round(point.Y)); - public static Size ToSize(this Point point) => new(point.X, point.Y); + public static Size ToSize(this Point point) => new(point.X, point.Y); - public static SizeF ToSize(this PointF point) => new(point.X, point.Y); + public static SizeF ToSize(this PointF point) => new(point.X, point.Y); - } -} +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/RectangleExtensions.cs b/UVtools.Core/Extensions/RectangleExtensions.cs index dbdd653..7678488 100644 --- a/UVtools.Core/Extensions/RectangleExtensions.cs +++ b/UVtools.Core/Extensions/RectangleExtensions.cs @@ -7,14 +7,13 @@ */ using System.Drawing; -namespace UVtools.Core.Extensions -{ - public static class RectangleExtensions - { - public static Point Center(this Rectangle src) - { - return new Point(src.Left + src.Width / 2, src.Top + src.Height / 2); - } +namespace UVtools.Core.Extensions; +public static class RectangleExtensions +{ + public static Point Center(this Rectangle src) + { + return new Point(src.Left + src.Width / 2, src.Top + src.Height / 2); } -} + +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/SizeExtensions.cs b/UVtools.Core/Extensions/SizeExtensions.cs index bf600ff..2429186 100644 --- a/UVtools.Core/Extensions/SizeExtensions.cs +++ b/UVtools.Core/Extensions/SizeExtensions.cs @@ -8,118 +8,117 @@ using System; using System.Drawing; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class SizeExtensions { - public static class SizeExtensions + public static readonly string[] SizeSuffixes = + { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; + + public static string SizeSuffix(long value, byte decimalPlaces = 2) { - public static readonly string[] SizeSuffixes = - { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" }; + //if (decimalPlaces < 0) { throw new ArgumentOutOfRangeException("decimalPlaces"); } + if (value < 0) { return "-" + SizeSuffix(-value); } + if (value == 0) { return string.Format("{0:n" + decimalPlaces + "} bytes", 0); } - public static string SizeSuffix(long value, byte decimalPlaces = 2) + // mag is 0 for bytes, 1 for KB, 2, for MB, etc. + int mag = (int)Math.Log(value, 1024); + + // 1L << (mag * 10) == 2 ^ (10 * mag) + // [i.e. the number of bytes in the unit corresponding to mag] + decimal adjustedSize = (decimal)value / (1L << (mag * 10)); + + // make adjustment when the value is large enough that + // it would round up to 1000 or more + if (Math.Round(adjustedSize, decimalPlaces) >= 1000) { - //if (decimalPlaces < 0) { throw new ArgumentOutOfRangeException("decimalPlaces"); } - if (value < 0) { return "-" + SizeSuffix(-value); } - if (value == 0) { return string.Format("{0:n" + decimalPlaces + "} bytes", 0); } - - // mag is 0 for bytes, 1 for KB, 2, for MB, etc. - int mag = (int)Math.Log(value, 1024); - - // 1L << (mag * 10) == 2 ^ (10 * mag) - // [i.e. the number of bytes in the unit corresponding to mag] - decimal adjustedSize = (decimal)value / (1L << (mag * 10)); - - // make adjustment when the value is large enough that - // it would round up to 1000 or more - if (Math.Round(adjustedSize, decimalPlaces) >= 1000) - { - mag += 1; - adjustedSize /= 1024; - } - - return string.Format("{0:n" + decimalPlaces + "} {1}", - adjustedSize, - SizeSuffixes[mag]); - } - - public static Size Add(this Size size, Size otherSize) => new (size.Width + otherSize.Width, size.Height + otherSize.Height); - public static Size Add(this Size size) => size.Add(size); - public static Size Add(this Size size, int pixels) => new (size.Width + pixels, size.Height + pixels); - public static Size Add(this Size size, int width, int height) => new (size.Width + width, size.Height + height); - - public static Size Subtract(this Size size, Size otherSize) => new(size.Width - otherSize.Width, size.Height - otherSize.Height); - public static Size Subtract(this Size size) => size.Subtract(size); - public static Size Subtract(this Size size, int pixels) => new(size.Width - pixels, size.Height - pixels); - public static Size Subtract(this Size size, int width, int height) => new(size.Width - width, size.Height - height); - - public static Size Multiply(this Size size, SizeF otherSize) => new((int)(size.Width * otherSize.Width), (int)(size.Height * otherSize.Height)); - public static Size Multiply(this Size size) => size.Multiply(size); - public static Size Multiply(this Size size, double dxy) => new((int)(size.Width * dxy), (int)(size.Height * dxy)); - public static Size Multiply(this Size size, double dx, double dy) => new((int)(size.Width * dx), (int)(size.Height * dy)); - - public static Size Divide(this Size size, SizeF otherSize) => new((int)(size.Width / otherSize.Width), (int)(size.Height / otherSize.Height)); - public static Size Divide(this Size size) => size.Divide(size); - public static Size Divide(this Size size, double dxy) => new((int)(size.Width / dxy), (int)(size.Height / dxy)); - public static Size Divide(this Size size, double dx, double dy) => new((int)(size.Width / dx), (int)(size.Height / dy)); - - /// - /// Gets if this size have a zero value on width or height - /// - /// - /// - public static bool HaveZero(this Size size) => size.Width <= 0 && size.Height <= 0; - - /// - /// Exchange width with height - /// - /// - /// - public static Size Exchange(this Size size) => new(size.Height, size.Width); - - public static int Area(this Rectangle rect) => rect.Width * rect.Height; - - public static int Area(this Size size) => size.Width * size.Height; - - public static int Max(this Size size) => Math.Max(size.Width, size.Height); - - - public static float Area(this RectangleF rect, int round = -1) => round >= 0 ? (float) Math.Round(rect.Width * rect.Height, round) : rect.Width * rect.Height; - - /// - /// Gets if this size have a zero value on width or height - /// - /// - /// - public static bool HaveZero(this SizeF size) => size.Width <= 0 && size.Height <= 0; - - public static float Area(this SizeF size, int round = -1) => round >= 0 ? (float)Math.Round(size.Width * size.Height, round) : size.Width * size.Height; - - public static float Max(this SizeF size) => Math.Max(size.Width, size.Height); - - public static Size Half(this Size size) => new(size.Width / 2, size.Height / 2); - - public static SizeF Half(this SizeF size) => new(size.Width / 2f, size.Height / 2f); - - public static Point ToPoint(this Size size) => new(size.Width, size.Height); - public static PointF ToPoint(this SizeF size) => new(size.Width, size.Height); - - public static Size Rotate(this Size size, double angleDegree) - { - if (angleDegree % 360 == 0) return size; - var sizeHalf = size.Half(); - double angle = angleDegree * Math.PI / 180; - double cos = Math.Cos(angle); - double sin = Math.Sin(angle); - // var newImgWidth = + (float)(x0 + Math.Abs((x - x0) * Math.Cos(rad)) + Math.Abs((y - y0) * Math.Sin(rad))); - //var newImgHeight = -(float)(y0 + Math.Abs((x - x0) * Math.Sin(rad)) + Math.Abs((y - y0) * Math.Cos(rad))); - int dx = size.Width - sizeHalf.Width; - int dy = size.Height - sizeHalf.Height; - //double width = sizeHalf.Width + Math.Abs(dx * cos) + Math.Abs(dy * sin); - //double height = sizeHalf.Height + Math.Abs(dx * sin) + Math.Abs(dy * cos); - double width = Math.Abs(cos * dx) - Math.Abs(sin * dy) + sizeHalf.Width; - double height = Math.Abs(sin * dx) + Math.Abs(cos * dy) + sizeHalf.Height; - - return new((int)Math.Round(width), (int)Math.Round(height)); + mag += 1; + adjustedSize /= 1024; } + return string.Format("{0:n" + decimalPlaces + "} {1}", + adjustedSize, + SizeSuffixes[mag]); } -} + + public static Size Add(this Size size, Size otherSize) => new (size.Width + otherSize.Width, size.Height + otherSize.Height); + public static Size Add(this Size size) => size.Add(size); + public static Size Add(this Size size, int pixels) => new (size.Width + pixels, size.Height + pixels); + public static Size Add(this Size size, int width, int height) => new (size.Width + width, size.Height + height); + + public static Size Subtract(this Size size, Size otherSize) => new(size.Width - otherSize.Width, size.Height - otherSize.Height); + public static Size Subtract(this Size size) => size.Subtract(size); + public static Size Subtract(this Size size, int pixels) => new(size.Width - pixels, size.Height - pixels); + public static Size Subtract(this Size size, int width, int height) => new(size.Width - width, size.Height - height); + + public static Size Multiply(this Size size, SizeF otherSize) => new((int)(size.Width * otherSize.Width), (int)(size.Height * otherSize.Height)); + public static Size Multiply(this Size size) => size.Multiply(size); + public static Size Multiply(this Size size, double dxy) => new((int)(size.Width * dxy), (int)(size.Height * dxy)); + public static Size Multiply(this Size size, double dx, double dy) => new((int)(size.Width * dx), (int)(size.Height * dy)); + + public static Size Divide(this Size size, SizeF otherSize) => new((int)(size.Width / otherSize.Width), (int)(size.Height / otherSize.Height)); + public static Size Divide(this Size size) => size.Divide(size); + public static Size Divide(this Size size, double dxy) => new((int)(size.Width / dxy), (int)(size.Height / dxy)); + public static Size Divide(this Size size, double dx, double dy) => new((int)(size.Width / dx), (int)(size.Height / dy)); + + /// + /// Gets if this size have a zero value on width or height + /// + /// + /// + public static bool HaveZero(this Size size) => size.Width <= 0 && size.Height <= 0; + + /// + /// Exchange width with height + /// + /// + /// + public static Size Exchange(this Size size) => new(size.Height, size.Width); + + public static int Area(this Rectangle rect) => rect.Width * rect.Height; + + public static int Area(this Size size) => size.Width * size.Height; + + public static int Max(this Size size) => Math.Max(size.Width, size.Height); + + + public static float Area(this RectangleF rect, int round = -1) => round >= 0 ? (float) Math.Round(rect.Width * rect.Height, round) : rect.Width * rect.Height; + + /// + /// Gets if this size have a zero value on width or height + /// + /// + /// + public static bool HaveZero(this SizeF size) => size.Width <= 0 && size.Height <= 0; + + public static float Area(this SizeF size, int round = -1) => round >= 0 ? (float)Math.Round(size.Width * size.Height, round) : size.Width * size.Height; + + public static float Max(this SizeF size) => Math.Max(size.Width, size.Height); + + public static Size Half(this Size size) => new(size.Width / 2, size.Height / 2); + + public static SizeF Half(this SizeF size) => new(size.Width / 2f, size.Height / 2f); + + public static Point ToPoint(this Size size) => new(size.Width, size.Height); + public static PointF ToPoint(this SizeF size) => new(size.Width, size.Height); + + public static Size Rotate(this Size size, double angleDegree) + { + if (angleDegree % 360 == 0) return size; + var sizeHalf = size.Half(); + double angle = angleDegree * Math.PI / 180; + double cos = Math.Cos(angle); + double sin = Math.Sin(angle); + // var newImgWidth = + (float)(x0 + Math.Abs((x - x0) * Math.Cos(rad)) + Math.Abs((y - y0) * Math.Sin(rad))); + //var newImgHeight = -(float)(y0 + Math.Abs((x - x0) * Math.Sin(rad)) + Math.Abs((y - y0) * Math.Cos(rad))); + int dx = size.Width - sizeHalf.Width; + int dy = size.Height - sizeHalf.Height; + //double width = sizeHalf.Width + Math.Abs(dx * cos) + Math.Abs(dy * sin); + //double height = sizeHalf.Height + Math.Abs(dx * sin) + Math.Abs(dy * cos); + double width = Math.Abs(cos * dx) - Math.Abs(sin * dy) + sizeHalf.Width; + double height = Math.Abs(sin * dx) + Math.Abs(cos * dy) + sizeHalf.Height; + + return new((int)Math.Round(width), (int)Math.Round(height)); + } + +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/SpanExtensions.cs b/UVtools.Core/Extensions/SpanExtensions.cs index 204fde1..bf4f3a1 100644 --- a/UVtools.Core/Extensions/SpanExtensions.cs +++ b/UVtools.Core/Extensions/SpanExtensions.cs @@ -9,33 +9,32 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace UVtools.Core.Extensions -{ - public static class SpanExtensions - { - public static unsafe void Fill(this Span span, Func provider) where T : struct - { - int - cores = Environment.ProcessorCount, - batch = span.Length / cores, - mod = span.Length % cores, - size = Unsafe.SizeOf(); - ref T r0 = ref span.GetPinnableReference(); - fixed (byte* p0 = &Unsafe.As(ref r0)) - { - byte* p = p0; - Parallel.For(0, cores, i => - { - byte* pi = p + i * batch * size; - for (int j = 0; j < batch; j++, pi += size) - Unsafe.Write(pi, provider()); - }); +namespace UVtools.Core.Extensions; - // Remaining values - if (mod < 1) return; - for (int i = span.Length - mod; i < span.Length; i++) - Unsafe.Write(p + i * size, provider()); - } +public static class SpanExtensions +{ + public static unsafe void Fill(this Span span, Func provider) where T : struct + { + int + cores = Environment.ProcessorCount, + batch = span.Length / cores, + mod = span.Length % cores, + size = Unsafe.SizeOf(); + ref T r0 = ref span.GetPinnableReference(); + fixed (byte* p0 = &Unsafe.As(ref r0)) + { + byte* p = p0; + Parallel.For(0, cores, i => + { + byte* pi = p + i * batch * size; + for (int j = 0; j < batch; j++, pi += size) + Unsafe.Write(pi, provider()); + }); + + // Remaining values + if (mod < 1) return; + for (int i = span.Length - mod; i < span.Length; i++) + Unsafe.Write(p + i * size, provider()); } } -} +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/StreamExtensions.cs b/UVtools.Core/Extensions/StreamExtensions.cs index 985c8ae..d55a6a3 100644 --- a/UVtools.Core/Extensions/StreamExtensions.cs +++ b/UVtools.Core/Extensions/StreamExtensions.cs @@ -11,54 +11,53 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class StreamExtensions { - public static class StreamExtensions + //public const int DefaultCopyBufferSize = 81920; // 81.92 kilobytes, .NET default + //public const int DefaultCopyBufferSize = 512000; // 512 kilobytes + public const int DefaultCopyBufferSize = 1048576; // 1 MB + + /// + /// Converts stream into byte array + /// + /// Input + /// Byte array data + public static byte[] ToArray(this Stream stream) { - //public const int DefaultCopyBufferSize = 81920; // 81.92 kilobytes, .NET default - //public const int DefaultCopyBufferSize = 512000; // 512 kilobytes - public const int DefaultCopyBufferSize = 1048576; // 1 MB + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + return memoryStream.ToArray(); + } - /// - /// Converts stream into byte array - /// - /// Input - /// Byte array data - public static byte[] ToArray(this Stream stream) + public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress? progress = null, CancellationToken cancellationToken = default) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new ArgumentException("Has to be readable", nameof(source)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new ArgumentException("Has to be writable", nameof(destination)); + if (bufferSize < 0) + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + if (bufferSize == 0) bufferSize = DefaultCopyBufferSize; + + var buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) != 0) { - using var memoryStream = new MemoryStream(); - stream.CopyTo(memoryStream); - return memoryStream.ToArray(); - } - - public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress = null, CancellationToken cancellationToken = default) - { - if (source == null) - throw new ArgumentNullException(nameof(source)); - if (!source.CanRead) - throw new ArgumentException("Has to be readable", nameof(source)); - if (destination == null) - throw new ArgumentNullException(nameof(destination)); - if (!destination.CanWrite) - throw new ArgumentException("Has to be writable", nameof(destination)); - if (bufferSize < 0) - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - if (bufferSize == 0) bufferSize = DefaultCopyBufferSize; - - var buffer = new byte[bufferSize]; - long totalBytesRead = 0; - int bytesRead; - while ((bytesRead = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) != 0) - { - await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); - totalBytesRead += bytesRead; - progress?.Report(totalBytesRead); - } - } - - public static async Task CopyToAsync(this Stream source, Stream destination, IProgress progress = null, CancellationToken cancellationToken = default) - { - await CopyToAsync(source, destination, DefaultCopyBufferSize, progress, cancellationToken); + await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); } } -} + + public static async Task CopyToAsync(this Stream source, Stream destination, IProgress? progress = null, CancellationToken cancellationToken = default) + { + await CopyToAsync(source, destination, DefaultCopyBufferSize, progress, cancellationToken); + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/StringExtensions.cs b/UVtools.Core/Extensions/StringExtensions.cs index 825f7aa..23943d4 100644 --- a/UVtools.Core/Extensions/StringExtensions.cs +++ b/UVtools.Core/Extensions/StringExtensions.cs @@ -8,51 +8,47 @@ using System; using System.ComponentModel; -using System.Linq; using System.Text; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class StringExtensions { - public static class StringExtensions + public static string RemoveFromEnd(this string input, int count) { - public static string RemoveFromEnd(this string input, int count) - { - return input.Remove(input.Length - count); - } - - /// - /// Upper the first character in a string - /// - /// Input string - /// Modified string with fist character upper - public static string FirstCharToUpper(this string input) - { - switch (input) - { - case null: throw new ArgumentNullException(nameof(input)); - case "": throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)); - default: return $"{char.ToUpper(input[0])}{input[1..]}"; - } - } - - public static string Repeat(this string s, int n) - => n <= 0 ? string.Empty : new StringBuilder(s.Length * n).Insert(0, s, n).ToString(); - - /// - /// Converts a string into a target type - /// - /// Target type to convert into - /// Value - /// Converted value into target type - public static T Convert(this string input) - { - var converter = TypeDescriptor.GetConverter(typeof(T)); - if (converter is not null) - { - //Cast ConvertFromString(string text) : object to (T) - return (T)converter.ConvertFromString(input); - } - return default; - } + return input.Remove(input.Length - count); } -} + + /// + /// Upper the first character in a string + /// + /// Input string + /// Modified string with fist character upper + public static string FirstCharToUpper(this string input) + { + return input switch + { + null => throw new ArgumentNullException(nameof(input)), + "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)), + _ => $"{char.ToUpper(input[0])}{input[1..]}" + }; + } + + public static string Repeat(this string s, int n) + => n <= 0 ? string.Empty : new StringBuilder(s.Length * n).Insert(0, s, n).ToString(); + + /// + /// Converts a string into a target type + /// + /// Target type to convert into + /// Value + /// Converted value into target type + public static T? Convert(this string input) + { + var converter = TypeDescriptor.GetConverter(typeof(T)); + //Cast ConvertFromString(string text) : object to (T) + var result = converter.ConvertFromString(input); + if (result is null) return default; + return (T) result; + } +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/TimeExtensions.cs b/UVtools.Core/Extensions/TimeExtensions.cs index cc90453..2988613 100644 --- a/UVtools.Core/Extensions/TimeExtensions.cs +++ b/UVtools.Core/Extensions/TimeExtensions.cs @@ -9,31 +9,30 @@ using System; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class TimeExtensions { - public static class TimeExtensions - { - /// - /// Converts seconds to milliseconds - /// - /// - /// - /// - public static float SecondsToMilliseconds(float value, byte rounding = 2) => (float)Math.Round(value * 1000f, rounding); + /// + /// Converts seconds to milliseconds + /// + /// + /// + /// + public static float SecondsToMilliseconds(float value, byte rounding = 2) => (float)Math.Round(value * 1000f, rounding); - /// - /// Converts seconds to milliseconds - /// - /// - /// - public static uint SecondsToMillisecondsUint(float value) => (uint)(value * 1000f); + /// + /// Converts seconds to milliseconds + /// + /// + /// + public static uint SecondsToMillisecondsUint(float value) => (uint)(value * 1000f); - /// - /// Converts milliseconds to seconds - /// - /// - /// - /// - public static float MillisecondsToSeconds(float value, byte rounding = 2) => (float)Math.Round(value / 1000f, rounding); - } -} + /// + /// Converts milliseconds to seconds + /// + /// + /// + /// + public static float MillisecondsToSeconds(float value, byte rounding = 2) => (float)Math.Round(value / 1000f, rounding); +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/TypeExtensions.cs b/UVtools.Core/Extensions/TypeExtensions.cs index bc00201..99118f3 100644 --- a/UVtools.Core/Extensions/TypeExtensions.cs +++ b/UVtools.Core/Extensions/TypeExtensions.cs @@ -8,30 +8,32 @@ using System; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class TypeExtensions { - public static class TypeExtensions + /// + /// Creates a new instance of this type + /// + /// + public static object? CreateInstance(this Type type, params object[]? paramArray) { - /// - /// Creates a new instance of this type - /// - /// - public static object CreateInstance(this Type type, params object[]? paramArray) - { - return Activator.CreateInstance(type, paramArray); - } - - /// - /// Creates a new instance of this type - /// - /// Target type - /// - /// New instance of - public static T CreateInstance(this Type type, params object[]? paramArray) - { - return (T)Activator.CreateInstance(type, paramArray); - } - - public static byte ToByte(this bool value) => (byte)(value ? 1 : 0); + return Activator.CreateInstance(type, paramArray); } -} + + /// + /// Creates a new instance of this type + /// + /// Target type + /// + /// + /// New instance of + public static T? CreateInstance(this Type type, params object[]? paramArray) + { + var instance = Activator.CreateInstance(type, paramArray); + if (instance is null) return default; + return (T)instance; + } + + public static byte ToByte(this bool value) => (byte)(value ? 1 : 0); +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/UnitExtensions.cs b/UVtools.Core/Extensions/UnitExtensions.cs index 765c198..9e4f355 100644 --- a/UVtools.Core/Extensions/UnitExtensions.cs +++ b/UVtools.Core/Extensions/UnitExtensions.cs @@ -6,15 +6,12 @@ * of this license document, but changing it is not allowed. */ -using System; +namespace UVtools.Core.Extensions; -namespace UVtools.Core.Extensions +public static class UnitExtensions { - public static class UnitExtensions - { - /// - /// 1 mm to 1 inch - /// - public const double MillimeterInInch = 0.0393700787; - } -} + /// + /// 1 mm to 1 inch + /// + public const double MillimeterInInch = 0.0393700787; +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/XmlExtensions.cs b/UVtools.Core/Extensions/XmlExtensions.cs index ef24788..5caf55d 100644 --- a/UVtools.Core/Extensions/XmlExtensions.cs +++ b/UVtools.Core/Extensions/XmlExtensions.cs @@ -10,55 +10,122 @@ using System.Text; using System.Xml; using System.Xml.Serialization; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class XmlExtensions { - public static class XmlExtensions + public static readonly XmlWriterSettings SettingsIndent = new() { - public static string SerializeObject(object toSerialize) + CloseOutput = false, + Indent = true, + }; + + public static void Serialize(object toSerialize, Stream stream, bool noNameSpace = false) + { + var xmlSerializer = new XmlSerializer(toSerialize.GetType()); + XmlSerializerNamespaces? ns = null; + if (noNameSpace) { - var xmlSerializer = new XmlSerializer(toSerialize.GetType()); - - using var textWriter = new StringWriter(); - xmlSerializer.Serialize(textWriter, toSerialize); - return textWriter.ToString(); + ns = new(); + ns.Add("", ""); } - - public static string SerializeObject(object toSerialize, XmlWriterSettings settings, bool standalone = false) - { - settings.CloseOutput = false; - - using var ms = new MemoryStream(); - using (var xw = XmlWriter.Create(ms, settings)) - { - xw.WriteStartDocument(standalone); // that bool parameter is called "standalone" - - var s = new XmlSerializer(toSerialize.GetType()); - s.Serialize(xw, toSerialize); - } - - return settings.Encoding.GetString(ms.ToArray()); - } - - public static string SerializeObject(object toSerialize, Encoding encoding, bool indent = true, bool omitXmlDeclaration = false, bool standalone = false) - { - var settings = new XmlWriterSettings - { - // If set to true XmlWriter would close MemoryStream automatically and using would then do double dispose - // Code analysis does not understand that. That's why there is a suppress message. - CloseOutput = false, - Encoding = encoding, - OmitXmlDeclaration = omitXmlDeclaration, - Indent = indent, - }; - return SerializeObject(toSerialize, settings, standalone); - } - - public static T DeserializeObject(string text) - { - var serializer = new XmlSerializer(typeof(T)); - using TextReader reader = new StringReader(text); - return (T)serializer.Deserialize(reader); - } - + xmlSerializer.Serialize(stream, toSerialize, ns); } -} + + public static void Serialize(object toSerialize, Stream stream, XmlWriterSettings settings, bool noNameSpace = false, bool standalone = false) + { + settings.CloseOutput = false; + + using var xw = XmlWriter.Create(stream, settings); + xw.WriteStartDocument(standalone); // that bool parameter is called "standalone" + + var s = new XmlSerializer(toSerialize.GetType()); + XmlSerializerNamespaces? ns = null; + if (noNameSpace) + { + ns = new(); + ns.Add("", ""); + } + s.Serialize(xw, toSerialize, ns); + } + + public static void Serialize(object toSerialize, Stream stream, Encoding encoding, bool indent = true, bool omitXmlDeclaration = false, bool noNameSpace = false, bool standalone = false) + { + var settings = new XmlWriterSettings + { + // If set to true XmlWriter would close MemoryStream automatically and using would then do double dispose + // Code analysis does not understand that. That's why there is a suppress message. + CloseOutput = false, + Encoding = encoding, + OmitXmlDeclaration = omitXmlDeclaration, + Indent = indent, + }; + Serialize(toSerialize, stream, settings, noNameSpace, standalone); + } + + public static void SerializeToFile(object toSerialize, string path, bool noNameSpace = false) + { + using var stream = new FileStream(path, FileMode.Create); + Serialize(toSerialize, stream, noNameSpace); + } + + public static void SerializeToFile(object toSerialize, string path, XmlWriterSettings settings, bool noNameSpace = false, bool standalone = false) + { + using var stream = new FileStream(path, FileMode.Create); + Serialize(toSerialize, stream, settings, noNameSpace, standalone); + } + + public static void SerializeToFile(object toSerialize, string path, Encoding encoding, bool indent = true, bool omitXmlDeclaration = false, bool noNameSpace = false, bool standalone = false) + { + using var stream = new FileStream(path, FileMode.Create); + Serialize(toSerialize, stream, encoding, indent, omitXmlDeclaration, noNameSpace, standalone); + } + + + public static string SerializeObject(object toSerialize, bool noNameSpace = false) + { + using var stream = new MemoryStream(); + Serialize(toSerialize, stream, noNameSpace); + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static string SerializeObject(object toSerialize, XmlWriterSettings settings, bool noNameSpace = false, bool standalone = false) + { + using var stream = new MemoryStream(); + Serialize(toSerialize, stream, settings, noNameSpace, standalone); + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static string SerializeObject(object toSerialize, Encoding encoding, bool indent = true, bool omitXmlDeclaration = false, bool noNameSpace = false, bool standalone = false) + { + using var stream = new MemoryStream(); + Serialize(toSerialize, stream, encoding, indent, omitXmlDeclaration, noNameSpace, standalone); + stream.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public static T DeserializeFromStream(Stream stream) + { + var serializer = new XmlSerializer(typeof(T)); + return (T)serializer.Deserialize(stream)!; + } + + public static T DeserializeFromFile(string filePath) + { + using var stream = File.OpenRead(filePath); + return DeserializeFromStream(stream); + } + + public static T DeserializeFromText(string text) + { + var serializer = new XmlSerializer(typeof(T)); + using TextReader reader = new StringReader(text); + return (T)serializer.Deserialize(reader)!; + } + +} \ No newline at end of file diff --git a/UVtools.Core/Extensions/ZipArchiveExtensions.cs b/UVtools.Core/Extensions/ZipArchiveExtensions.cs index 7550073..8446283 100644 --- a/UVtools.Core/Extensions/ZipArchiveExtensions.cs +++ b/UVtools.Core/Extensions/ZipArchiveExtensions.cs @@ -12,360 +12,361 @@ using System.IO; using System.IO.Compression; using System.Linq; -namespace UVtools.Core.Extensions +namespace UVtools.Core.Extensions; + +public static class ZipArchiveExtensions { - public static class ZipArchiveExtensions + /// + /// Used to specify what our overwrite policy + /// is for files we are extracting. + /// + public enum Overwrite { - /// - /// Used to specify what our overwrite policy - /// is for files we are extracting. - /// - public enum Overwrite + Always, + IfNewer, + Never + } + + /// + /// Used to identify what we will do if we are + /// trying to create a zip file and it already + /// exists. + /// + public enum ArchiveAction + { + Merge, + Replace, + Error, + Ignore + } + + /// + /// Unzips the specified file to the given folder in a safe + /// manner. This plans for missing paths and existing files + /// and handles them gracefully. + /// + /// + /// The name of the zip file to be extracted + /// + /// + /// The directory to extract the zip file to + /// + /// + /// Specifies how we are going to handle an existing file. + /// The default is IfNewer. + /// + public static void ImprovedExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer) + { + //Opens the zip file up to be read + using var archive = ZipFile.OpenRead(sourceArchiveFileName); + archive.ImprovedExtractToDirectory(sourceArchiveFileName, destinationDirectoryName, overwriteMethod); + } + + /// + /// Unzips the specified file to the given folder in a safe + /// manner. This plans for missing paths and existing files + /// and handles them gracefully. + /// + /// + /// The name of the zip file to be extracted + /// + /// + /// The directory to extract the zip file to + /// + /// + /// Specifies how we are going to handle an existing file. + /// The default is IfNewer. + /// + public static void ImprovedExtractToDirectory(this ZipArchive archive, string sourceArchiveFileName, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer) + { + //Loops through each file in the zip file + foreach (var file in archive.Entries) { - Always, - IfNewer, - Never - } - - /// - /// Used to identify what we will do if we are - /// trying to create a zip file and it already - /// exists. - /// - public enum ArchiveAction - { - Merge, - Replace, - Error, - Ignore - } - - /// - /// Unzips the specified file to the given folder in a safe - /// manner. This plans for missing paths and existing files - /// and handles them gracefully. - /// - /// - /// The name of the zip file to be extracted - /// - /// - /// The directory to extract the zip file to - /// - /// - /// Specifies how we are going to handle an existing file. - /// The default is IfNewer. - /// - public static void ImprovedExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer) - { - //Opens the zip file up to be read - using var archive = ZipFile.OpenRead(sourceArchiveFileName); - archive.ImprovedExtractToDirectory(sourceArchiveFileName, destinationDirectoryName, overwriteMethod); - } - - /// - /// Unzips the specified file to the given folder in a safe - /// manner. This plans for missing paths and existing files - /// and handles them gracefully. - /// - /// - /// The name of the zip file to be extracted - /// - /// - /// The directory to extract the zip file to - /// - /// - /// Specifies how we are going to handle an existing file. - /// The default is IfNewer. - /// - public static void ImprovedExtractToDirectory(this ZipArchive archive, string sourceArchiveFileName, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer) - { - //Loops through each file in the zip file - foreach (var file in archive.Entries) - { - file.ImprovedExtractToFile(destinationDirectoryName, overwriteMethod); - } - } - - /// - /// Safely extracts a single file from a zip file - /// - /// - /// The zip entry we are pulling the file from - /// - /// - /// The root of where the file is going - /// - /// - /// Specifies how we are going to handle an existing file. - /// The default is Overwrite.IfNewer. - /// - public static void ImprovedExtractToFile(this ZipArchiveEntry file, string destinationPath, Overwrite overwriteMethod = Overwrite.IfNewer) - { - //Gets the complete path for the destination file, including any - //relative paths that were in the zip file - string destinationFileName = Path.Combine(destinationPath, file.FullName); - - //Gets just the new path, minus the file name so we can create the - //directory if it does not exist - string destinationFilePath = Path.GetDirectoryName(destinationFileName); - - //Creates the directory (if it doesn't exist) for the new path - Directory.CreateDirectory(destinationFilePath); - - //Determines what to do with the file based upon the - //method of overwriting chosen - switch (overwriteMethod) - { - case Overwrite.Always: - //Just put the file in and overwrite anything that is found - file.ExtractToFile(destinationFileName, true); - break; - case Overwrite.IfNewer: - //Checks to see if the file exists, and if so, if it should - //be overwritten - if (!File.Exists(destinationFileName) || File.GetLastWriteTime(destinationFileName) < file.LastWriteTime) - { - //Either the file didn't exist or this file is newer, so - //we will extract it and overwrite any existing file - file.ExtractToFile(destinationFileName, true); - } - break; - case Overwrite.Never: - //Put the file in if it is new but ignores the - //file if it already exists - if (!File.Exists(destinationFileName)) - { - file.ExtractToFile(destinationFileName); - } - break; - } - } - - /// - /// Allows you to add files to an archive, whether the archive - /// already exists or not - /// - /// - /// The name of the archive to you want to add your files to - /// - /// - /// A set of file names that are to be added - /// - /// - /// Specifies how we are going to handle an existing archive - /// - /// - /// Specifies what type of compression to use - defaults to Optimal - /// - public static void AddToArchive(string archiveFullName, - List files, - ArchiveAction action = ArchiveAction.Replace, - Overwrite fileOverwrite = Overwrite.IfNewer, - CompressionLevel compression = CompressionLevel.Optimal) - { - //Identifies the mode we will be using - the default is Create - ZipArchiveMode mode = ZipArchiveMode.Create; - - //Determines if the zip file even exists - bool archiveExists = File.Exists(archiveFullName); - - //Figures out what to do based upon our specified overwrite method - switch (action) - { - case ArchiveAction.Merge: - //Sets the mode to update if the file exists, otherwise - //the default of Create is fine - if (archiveExists) - { - mode = ZipArchiveMode.Update; - } - break; - case ArchiveAction.Replace: - //Deletes the file if it exists. Either way, the default - //mode of Create is fine - if (archiveExists) - { - File.Delete(archiveFullName); - } - break; - case ArchiveAction.Error: - //Throws an error if the file exists - if (archiveExists) - { - throw new IOException(String.Format("The zip file {0} already exists.", archiveFullName)); - } - break; - case ArchiveAction.Ignore: - //Closes the method silently and does nothing - if (archiveExists) - { - return; - } - break; - } - - //Opens the zip file in the mode we specified - using ZipArchive zipFile = ZipFile.Open(archiveFullName, mode); - //This is a bit of a hack and should be refactored - I am - //doing a similar foreach loop for both modes, but for Create - //I am doing very little work while Update gets a lot of - //code. This also does not handle any other mode (of - //which there currently wouldn't be one since we don't - //use Read here). - if (mode == ZipArchiveMode.Create) - { - foreach (string file in files) - { - //Adds the file to the archive - zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); - } - } - else - { - foreach (string file in files) - { - var fileInZip = (from f in zipFile.Entries - where f.Name == Path.GetFileName(file) - select f).FirstOrDefault(); - - switch (fileOverwrite) - { - case Overwrite.Always: - //Deletes the file if it is found - if (fileInZip != null) - { - fileInZip.Delete(); - } - - //Adds the file to the archive - zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); - - break; - case Overwrite.IfNewer: - //This is a bit trickier - we only delete the file if it is - //newer, but if it is newer or if the file isn't already in - //the zip file, we will write it to the zip file - if (fileInZip != null) - { - //Deletes the file only if it is older than our file. - //Note that the file will be ignored if the existing file - //in the archive is newer. - if (fileInZip.LastWriteTime < File.GetLastWriteTime(file)) - { - fileInZip.Delete(); - - //Adds the file to the archive - zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); - } - } - else - { - //The file wasn't already in the zip file so add it to the archive - zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); - } - break; - case Overwrite.Never: - //Don't do anything - this is a decision that you need to - //consider, however, since this will mean that no file will - //be written. You could write a second copy to the zip with - //the same name (not sure that is wise, however). - break; - } - } - } - } - - /// - /// Get or put a file into archive - /// - /// - /// Filename to create - /// Created - public static ZipArchiveEntry GetPutFile(this ZipArchive input, string filename) - { - return input.GetEntry(filename) ?? input.CreateEntry(filename); - } - - /// - /// Create or update a file into archive and write content to it - /// - /// - /// Filename to create - /// Content to write - /// - /// Created - public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, string content, ZipArchiveMode mode) - { - ZipArchiveEntry entry; - if (mode == ZipArchiveMode.Update) - { - entry = input.GetEntry(filename) ?? input.CreateEntry(filename); - } - else - { - entry = input.CreateEntry(filename); - } - - if (string.IsNullOrEmpty(content)) return entry; - using var stream = entry.Open(); - if (mode == ZipArchiveMode.Update) stream.SetLength(0); - using TextWriter tw = new StreamWriter(stream); - tw.Write(content); - return entry; - } - - /// - /// Create or update a file into archive and write content to it - /// - /// - /// Filename to create - /// Content to write - /// - /// Created - public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, byte[] content, ZipArchiveMode mode) - { - ZipArchiveEntry entry; - if (mode == ZipArchiveMode.Update) - { - entry = input.GetEntry(filename) ?? input.CreateEntry(filename); - } - else - { - entry = input.CreateEntry(filename); - } - - if (content is null) return entry; - using var stream = entry.Open(); - if (mode == ZipArchiveMode.Update) stream.SetLength(0); - stream.Write(content, 0, content.Length); - return entry; - } - - /// - /// Create or update a file into archive and write content to it - /// - /// - /// Filename to create - /// Content to write - /// - /// Created - public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, Stream content, ZipArchiveMode mode) - { - ZipArchiveEntry entry; - if (mode == ZipArchiveMode.Update) - { - entry = input.GetEntry(filename) ?? input.CreateEntry(filename); - } - else - { - entry = input.CreateEntry(filename); - } - - if (content is null) return entry; - using var stream = entry.Open(); - if (mode == ZipArchiveMode.Update) stream.SetLength(0); - content.CopyTo(stream); - return entry; + file.ImprovedExtractToFile(destinationDirectoryName, overwriteMethod); } } -} + + /// + /// Safely extracts a single file from a zip file + /// + /// + /// The zip entry we are pulling the file from + /// + /// + /// The root of where the file is going + /// + /// + /// Specifies how we are going to handle an existing file. + /// The default is Overwrite.IfNewer. + /// + public static void ImprovedExtractToFile(this ZipArchiveEntry file, string destinationPath, Overwrite overwriteMethod = Overwrite.IfNewer) + { + //Gets the complete path for the destination file, including any + //relative paths that were in the zip file + string destinationFileName = Path.Combine(destinationPath, file.FullName); + + //Gets just the new path, minus the file name so we can create the + //directory if it does not exist + string? destinationFilePath = Path.GetDirectoryName(destinationFileName); + + destinationFilePath ??= string.Empty; + + //Creates the directory (if it doesn't exist) for the new path + Directory.CreateDirectory(destinationFilePath); + + //Determines what to do with the file based upon the + //method of overwriting chosen + switch (overwriteMethod) + { + case Overwrite.Always: + //Just put the file in and overwrite anything that is found + file.ExtractToFile(destinationFileName, true); + break; + case Overwrite.IfNewer: + //Checks to see if the file exists, and if so, if it should + //be overwritten + if (!File.Exists(destinationFileName) || File.GetLastWriteTime(destinationFileName) < file.LastWriteTime) + { + //Either the file didn't exist or this file is newer, so + //we will extract it and overwrite any existing file + file.ExtractToFile(destinationFileName, true); + } + break; + case Overwrite.Never: + //Put the file in if it is new but ignores the + //file if it already exists + if (!File.Exists(destinationFileName)) + { + file.ExtractToFile(destinationFileName); + } + break; + } + } + + /// + /// Allows you to add files to an archive, whether the archive + /// already exists or not + /// + /// + /// The name of the archive to you want to add your files to + /// + /// + /// A set of file names that are to be added + /// + /// + /// Specifies how we are going to handle an existing archive + /// + /// + /// Specifies what type of compression to use - defaults to Optimal + /// + public static void AddToArchive(string archiveFullName, + List files, + ArchiveAction action = ArchiveAction.Replace, + Overwrite fileOverwrite = Overwrite.IfNewer, + CompressionLevel compression = CompressionLevel.Optimal) + { + //Identifies the mode we will be using - the default is Create + ZipArchiveMode mode = ZipArchiveMode.Create; + + //Determines if the zip file even exists + bool archiveExists = File.Exists(archiveFullName); + + //Figures out what to do based upon our specified overwrite method + switch (action) + { + case ArchiveAction.Merge: + //Sets the mode to update if the file exists, otherwise + //the default of Create is fine + if (archiveExists) + { + mode = ZipArchiveMode.Update; + } + break; + case ArchiveAction.Replace: + //Deletes the file if it exists. Either way, the default + //mode of Create is fine + if (archiveExists) + { + File.Delete(archiveFullName); + } + break; + case ArchiveAction.Error: + //Throws an error if the file exists + if (archiveExists) + { + throw new IOException(String.Format("The zip file {0} already exists.", archiveFullName)); + } + break; + case ArchiveAction.Ignore: + //Closes the method silently and does nothing + if (archiveExists) + { + return; + } + break; + } + + //Opens the zip file in the mode we specified + using ZipArchive zipFile = ZipFile.Open(archiveFullName, mode); + //This is a bit of a hack and should be refactored - I am + //doing a similar foreach loop for both modes, but for Create + //I am doing very little work while Update gets a lot of + //code. This also does not handle any other mode (of + //which there currently wouldn't be one since we don't + //use Read here). + if (mode == ZipArchiveMode.Create) + { + foreach (string file in files) + { + //Adds the file to the archive + zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); + } + } + else + { + foreach (string file in files) + { + var fileInZip = (from f in zipFile.Entries + where f.Name == Path.GetFileName(file) + select f).FirstOrDefault(); + + switch (fileOverwrite) + { + case Overwrite.Always: + //Deletes the file if it is found + if (fileInZip != null) + { + fileInZip.Delete(); + } + + //Adds the file to the archive + zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); + + break; + case Overwrite.IfNewer: + //This is a bit trickier - we only delete the file if it is + //newer, but if it is newer or if the file isn't already in + //the zip file, we will write it to the zip file + if (fileInZip != null) + { + //Deletes the file only if it is older than our file. + //Note that the file will be ignored if the existing file + //in the archive is newer. + if (fileInZip.LastWriteTime < File.GetLastWriteTime(file)) + { + fileInZip.Delete(); + + //Adds the file to the archive + zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); + } + } + else + { + //The file wasn't already in the zip file so add it to the archive + zipFile.CreateEntryFromFile(file, Path.GetFileName(file), compression); + } + break; + case Overwrite.Never: + //Don't do anything - this is a decision that you need to + //consider, however, since this will mean that no file will + //be written. You could write a second copy to the zip with + //the same name (not sure that is wise, however). + break; + } + } + } + } + + /// + /// Get or put a file into archive + /// + /// + /// Filename to create + /// Created + public static ZipArchiveEntry GetPutFile(this ZipArchive input, string filename) + { + return input.GetEntry(filename) ?? input.CreateEntry(filename); + } + + /// + /// Create or update a file into archive and write content to it + /// + /// + /// Filename to create + /// Content to write + /// + /// Created + public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, string? content, ZipArchiveMode mode) + { + ZipArchiveEntry entry; + if (mode == ZipArchiveMode.Update) + { + entry = input.GetEntry(filename) ?? input.CreateEntry(filename); + } + else + { + entry = input.CreateEntry(filename); + } + + if (string.IsNullOrEmpty(content)) return entry; + using var stream = entry.Open(); + if (mode == ZipArchiveMode.Update) stream.SetLength(0); + using TextWriter tw = new StreamWriter(stream); + tw.Write(content); + return entry; + } + + /// + /// Create or update a file into archive and write content to it + /// + /// + /// Filename to create + /// Content to write + /// + /// Created + public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, byte[]? content, ZipArchiveMode mode) + { + ZipArchiveEntry entry; + if (mode == ZipArchiveMode.Update) + { + entry = input.GetEntry(filename) ?? input.CreateEntry(filename); + } + else + { + entry = input.CreateEntry(filename); + } + + if (content is null) return entry; + using var stream = entry.Open(); + if (mode == ZipArchiveMode.Update) stream.SetLength(0); + stream.Write(content, 0, content.Length); + return entry; + } + + /// + /// Create or update a file into archive and write content to it + /// + /// + /// Filename to create + /// Content to write + /// + /// Created + public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, Stream? content, ZipArchiveMode mode) + { + ZipArchiveEntry entry; + if (mode == ZipArchiveMode.Update) + { + entry = input.GetEntry(filename) ?? input.CreateEntry(filename); + } + else + { + entry = input.CreateEntry(filename); + } + + if (content is null) return entry; + using var stream = entry.Open(); + if (mode == ZipArchiveMode.Update) stream.SetLength(0); + content.CopyTo(stream); + return entry; + } +} \ No newline at end of file diff --git a/UVtools.Core/FileFormats/CTBEncryptedFile.cs b/UVtools.Core/FileFormats/CTBEncryptedFile.cs index 4142cab..c1b9b2a 100644 --- a/UVtools.Core/FileFormats/CTBEncryptedFile.cs +++ b/UVtools.Core/FileFormats/CTBEncryptedFile.cs @@ -15,1122 +15,1124 @@ using UVtools.Core.Extensions; using UVtools.Core.Layers; using UVtools.Core.Operations; -namespace UVtools.Core.FileFormats +namespace UVtools.Core.FileFormats; + +public class CTBEncryptedFile : FileFormat { - public class CTBEncryptedFile : FileFormat - { - #region Constants - public const uint MAGIC_CBT_ENCRYPTED = 0x12FD0107; - public const ushort REPEATRGB15MASK = 0x20; - public const ushort RLE16EncodingLimit = 0xFFF; - public const ushort RLEEncryptedMinimumLength = 512; + #region Constants + public const uint MAGIC_CBT_ENCRYPTED = 0x12FD0107; + public const ushort REPEATRGB15MASK = 0x20; + public const ushort RLE16EncodingLimit = 0xFFF; + public const ushort RLEEncryptedMinimumLength = 512; - public const uint PERLAYER_SETTINGS_DISALLOW_NO_AA = 7; // 7 (This disallow per layer settings and follow global table only) No AA - public const uint PERLAYER_SETTINGS_DISALLOW = 15; // 15 (This disallow per layer settings and follow global table only) with AA - public const uint PERLAYER_SETTINGS_ALLOW = 0x5000000F; // 1342177295 (This allow per layer settings) + public const uint PERLAYER_SETTINGS_DISALLOW_NO_AA = 7; // 7 (This disallow per layer settings and follow global table only) No AA + public const uint PERLAYER_SETTINGS_DISALLOW = 15; // 15 (This disallow per layer settings and follow global table only) with AA + public const uint PERLAYER_SETTINGS_ALLOW = 0x5000000F; // 1342177295 (This allow per layer settings) - private const string CTB_DISCLAIMER = "Layout and record format for the ctb and cbddlp file types are the copyrighted programs or codes of CBD Technology (China) Inc..The Customer or User shall not in any manner reproduce, distribute, modify, decompile, disassemble, decrypt, extract, reverse engineer, lease, assign, or sublicense the said programs or codes."; - private const ushort CTB_DISCLAIMER_SIZE = 320; + private const string CTB_DISCLAIMER = "Layout and record format for the ctb and cbddlp file types are the copyrighted programs or codes of CBD Technology (China) Inc..The Customer or User shall not in any manner reproduce, distribute, modify, decompile, disassemble, decrypt, extract, reverse engineer, lease, assign, or sublicense the said programs or codes."; + private const ushort CTB_DISCLAIMER_SIZE = 320; - public const byte HASH_LENGTH = 32; - public const uint LAYER_XOR_KEY = 0xEFBEADDE; + public const byte HASH_LENGTH = 32; + public const uint LAYER_XOR_KEY = 0xEFBEADDE; - public const string Secret0 = "XxUBHR0JHSE6DU8YCVMxORpIG0wSOTobGE8KGjkzVBwOGhZ6MxoMAAgWeiQRDB0JEiE/GwFPAx11JgEdHwMAMHYbAU8YGzwlVAoBDwEsJgAKC0wVPDoRTwkDATg3AEFlOxZ1NwYKTw0UND8aHBtMHToiVB8KHh48IgAKC0wGJjMGTwsNBzR2EQEMHgolIh0AAUBTOTkXBBxAUzY5GhwbHhI8OAdDTx4WJiIGBgwYGjo4B0NPARw7OQQAAwUJNCIdAAFMEjsyVAEAAl4mMxocCkwDOjodDAYJAHUiHA4bTAMnMwIKARgAdTkABwoeAHUwBgACTBAnMxUbCkwSOzJUAwoNF3gwGx0YDQExdgcAAxkHPDkaHE8NATojGgtPGBY2PhoAAwMULHh+KRoAH3UlAR8fAwEhPxoITxgbPCVUCQYAFnUwGx0CDQd1IRsaAwhTNzNUHBsJA3g0FQwETBU6JFRcK0wHMDUcAQAAHDIvVA4BCFMhPhFPDAMeOCMaBhsVUzogER0OAB97dicbBgAfeXYDCk8NHzk5A08bA1MnMxULTxgbMHYSBgMJUzM5Bk8dCQU8MwNDTx4WNjkCCh1MFzQiFU8OAhd1MhEbCg8HdSYGAA0AFjglVBsATB40PRFPFgMGdTdUDQYYUzg5BgpPDxwjMwYKC0wVJzkZTwIFACE3HwocQnkFOhEOHAlTODcfCk8VHCAkVBwHBRUhdhIdAAFTIT4dHE8cAToyAQwbH1M0OBBPBwkfJXYABwpMQBF2AAoMBB06OhsIFkwUOnYSAB0bEicyVA4BCFM6JhEBTmY="; - public const string Secret1 = "hQ36XB6yTk+zO02ysyiowt8yC1buK+nbLWyfY40EXoU="; - public const string Secret2 = "Wld+ampndVJecmVjYH5cWQ=="; + public const string Secret0 = "HDgSAB0BEiE/AgpPAhwhM1QAAUwHPT8HTywEGiEjVAoBDwEsJgAKC0wVPDoRTwkDATg3AE9HQhAhNF1VZWYkMHYVHQpMEjI3HQEcGFM7OQBPHwkBOD8AGwoIUyAlER1PCBIhN1QKAQ8BLCYABgACX3U6GwwEH191NRsBHBgBND8aHENMATAlAB0GDwc8ORocQ0weOjgbHwAAGi83AAYAAlM0OBBPAQMdeCURARwJUyU5GAYMBRYmdgAHDhhTJSQRGQoCByZ2GxsHCQEmdhIdAAFTNiQRDhsJUzQ4EE8DCRIxexIAHRsSJzJUHAAABiE/GwEcTBInOQEBC0wHMDUcAQAAHDIvWmU8GQMlOQYbBgIUdSIcBhxMFTw6EU8JAwE4NwBPBh9TNHYHGwocXjc3FwRPChwndkcrTxgWNj4aAAMDFCx2FQELTBU6JFQbBwlTNjkZAhoCGiEvVAAZCQE0OhhBTz8HPDoYQ08NHTF2HQFPDhY9NxgJTwMVdSMHCh0fUyIzVA4DABwidgAATx4WNDJYTxwNBTB2FQELTB40OB0fGgASITNUGwcJUzM/GApPChwndgYKGQUWInpUHQoPHCMzBk8LDQc0dhUBC0wXMCIRDBtMAyc5FgMKAQB1IhtPAg0YMHYNABpMEDogER0KCFMzJBsCTwEaJiIVBAofUzQ4EE8KHgE6JAdBZTwfMDcHCkNMHjQ9EU8WAwYndgcHBgoHdTAGAAJMBz0/B08fHhwxIxcbHEwSOzJUBwoAA3UiHApPXzd1IhEMBwIcOTkTFk8LHHUwGx0YDQExdhUBC0wcJTMaTk8/BiUmGx0bTBwlMxpCHAMGJzURTxwDHyAiHQABH191IhwOG0wENC9UGApMEDQ4VAwdCRIhM1QNChgHMCRUHx0DFyA1ABxPChwndgAHCkwQOjgHGgIJASZ4"; + public const string Secret1 = "hQ36XB6yTk+zO02ysyiowt8yC1buK+nbLWyfY40EXoU="; + public const string Secret2 = "Wld+ampndVJecmVjYH5cWQ=="; - public static readonly string Preamble = CryptExtensions.XORCipherString(System.Convert.FromBase64String(Secret0), About.Software); - public static byte[] Bigfoot = CryptExtensions.XORCipher(System.Convert.FromBase64String(Secret1), About.Software); - public static byte[] CookieMonster = CryptExtensions.XORCipher(System.Convert.FromBase64String(Secret2), About.Software); + public static readonly string Preamble = CryptExtensions.XORCipherString(System.Convert.FromBase64String(Secret0), About.Software); + public static byte[] Bigfoot = CryptExtensions.XORCipher(System.Convert.FromBase64String(Secret1), About.Software); + public static byte[] CookieMonster = CryptExtensions.XORCipher(System.Convert.FromBase64String(Secret2), About.Software); - #endregion + #endregion - #region Sub Classes + #region Sub Classes - public class FileHeader + public class FileHeader + { + public const byte TABLE_SIZE = 48; + + [FieldOrder(0)] public uint Magic { get; set; } = MAGIC_CBT_ENCRYPTED; + [FieldOrder(1)] public uint SettingsSize { get; set; } = SlicerSettings.TABLE_SIZE; + [FieldOrder(2)] public uint SettingsOffset { get; set; } = TABLE_SIZE; + [FieldOrder(3)] public uint Unknown1 { get; set; } // set to 0 + [FieldOrder(4)] public uint Unknown2 { get; set; } = 4; // set to 4 + [FieldOrder(5)] public uint SignatureSize { get; set; } + [FieldOrder(6)] public uint SignatureOffset { get; set; } + [FieldOrder(7)] public uint Unknown { get; set; } //set to 0 + [FieldOrder(8)] public ushort Unknown4 { get; set; } = 1; // set to 1 + [FieldOrder(9)] public ushort Unknown5 { get; set; } = 1; // set to 1 + [FieldOrder(10)] public uint Unknown6 { get; set; } // set to 0 + [FieldOrder(11)] public uint Unknown7 { get; set; } = 42; // probably 0x2A + [FieldOrder(12)] public uint Unknown8 { get; set; } // probably 0 or 1 + + public override string ToString() { - public const byte TABLE_SIZE = 48; + return $"{nameof(Magic)}: {Magic}, {nameof(SettingsSize)}: {SettingsSize}, {nameof(SettingsOffset)}: {SettingsOffset}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(SignatureSize)}: {SignatureSize}, {nameof(SignatureOffset)}: {SignatureOffset}, {nameof(Unknown)}: {Unknown}, {nameof(Unknown4)}: {Unknown4}, {nameof(Unknown5)}: {Unknown5}, {nameof(Unknown6)}: {Unknown6}, {nameof(Unknown7)}: {Unknown7}, {nameof(Unknown8)}: {Unknown8}"; + } + } - [FieldOrder(0)] public uint Magic { get; set; } = MAGIC_CBT_ENCRYPTED; - [FieldOrder(1)] public uint SettingsSize { get; set; } = SlicerSettings.TABLE_SIZE; - [FieldOrder(2)] public uint SettingsOffset { get; set; } = TABLE_SIZE; - [FieldOrder(3)] public uint Unknown1 { get; set; } // set to 0 - [FieldOrder(4)] public uint Unknown2 { get; set; } = 4; // set to 4 - [FieldOrder(5)] public uint SignatureSize { get; set; } - [FieldOrder(6)] public uint SignatureOffset { get; set; } - [FieldOrder(7)] public uint Unknown { get; set; } //set to 0 - [FieldOrder(8)] public ushort Unknown4 { get; set; } = 1; // set to 1 - [FieldOrder(9)] public ushort Unknown5 { get; set; } = 1; // set to 1 - [FieldOrder(10)] public uint Unknown6 { get; set; } // set to 0 - [FieldOrder(11)] public uint Unknown7 { get; set; } = 42; // probably 0x2A - [FieldOrder(12)] public uint Unknown8 { get; set; } // probably 0 or 1 + public class SlicerSettings + { + public const ushort TABLE_SIZE = 288; - public override string ToString() + private string _machineName = DefaultMachineName; + + /// + /// Checksum of unix timestamp + /// + [FieldOrder(0)] public ulong ChecksumValue { get; set; } = 0xCAFEBABE; + [FieldOrder(1)] public uint LayerPointersOffset { get; set; } + [FieldOrder(2)] public float DisplayWidth { get; set; } + [FieldOrder(3)] public float DisplayHeight { get; set; } + [FieldOrder(4)] public float MachineZ { get; set; } + [FieldOrder(5)] public uint Unknown1 { get; set; } + [FieldOrder(6)] public uint Unknown2 { get; set; } + [FieldOrder(7)] public float TotalHeightMillimeter { get; set; } + [FieldOrder(8)] public float LayerHeight { get; set; } + [FieldOrder(9)] public float ExposureTime { get; set; } + [FieldOrder(10)] public float BottomExposureTime { get; set; } + [FieldOrder(11)] public float LightOffDelay { get; set; } + [FieldOrder(12)] public uint BottomLayerCount { get; set; } + [FieldOrder(13)] public uint ResolutionX { get; set; } + [FieldOrder(14)] public uint ResolutionY { get; set; } + [FieldOrder(15)] public uint LayerCount { get; set; } + [FieldOrder(16)] public uint LargePreviewOffset { get; set; } + [FieldOrder(17)] public uint SmallPreviewOffset { get; set; } + [FieldOrder(18)] public uint PrintTime { get; set; } + [FieldOrder(19)] public uint ProjectorType { get; set; } + [FieldOrder(20)] public float BottomLiftHeight { get; set; } + [FieldOrder(21)] public float BottomLiftSpeed { get; set; } + [FieldOrder(22)] public float LiftHeight { get; set; } + [FieldOrder(23)] public float LiftSpeed { get; set; } + [FieldOrder(24)] public float RetractSpeed { get; set; } + [FieldOrder(25)] public float MaterialMilliliters { get; set; } + [FieldOrder(26)] public float MaterialGrams { get; set; } + [FieldOrder(27)] public float MaterialCost { get; set; } + [FieldOrder(28)] public float BottomLightOffDelay { get; set; } + [FieldOrder(29)] public uint Unknown3 { get; set; } = 1; + [FieldOrder(30)] public ushort LightPWM { get; set; } + [FieldOrder(31)] public ushort BottomLightPWM { get; set; } + [FieldOrder(32)] public uint LayerXorKey { get; set; } + [FieldOrder(33)] public float BottomLiftHeight2 { get; set; } + [FieldOrder(34)] public float BottomLiftSpeed2 { get; set; } + [FieldOrder(35)] public float LiftHeight2 { get; set; } + [FieldOrder(36)] public float LiftSpeed2 { get; set; } + [FieldOrder(37)] public float RetractHeight2 { get; set; } + [FieldOrder(38)] public float RetractSpeed2 { get; set; } + [FieldOrder(39)] public float RestTimeAfterLift { get; set; } + [FieldOrder(40)] public uint MachineNameOffset { get; set; } + [FieldOrder(41)] public uint MachineNameSize { get; set; } = (uint)(string.IsNullOrEmpty(DefaultMachineName) ? 0 : DefaultMachineName.Length); + [FieldOrder(42)] public uint PerLayerSettings { get; set; } = PERLAYER_SETTINGS_DISALLOW; + [FieldOrder(43)] public uint Unknown4 { get; set; } + [FieldOrder(44)] public uint Unknown5 { get; set; } = 8; // Also 1 + [FieldOrder(45)] public float RestTimeAfterRetract { get; set; } + [FieldOrder(46)] public float RestTimeAfterLift2 { get; set; } + [FieldOrder(47)] public uint TransitionLayerCount { get; set; } + [FieldOrder(48)] public float BottomRetractSpeed { get; set; } + [FieldOrder(49)] public float BottomRetractSpeed2 { get; set; } + [FieldOrder(50)] public uint Padding1 { get; set; } + [FieldOrder(51)] public float Four1 { get; set; } = 4; // Same as CTBv4.PrintParametersV4.Four1) + [FieldOrder(52)] public uint Padding2 { get; set; } + [FieldOrder(53)] public float Four2 { get; set; } = 4; // Same as CTBv4.PrintParametersV4.Four2) + [FieldOrder(54)] public float RestTimeAfterRetract2 { get; set; } + [FieldOrder(55)] public float RestTimeAfterLift3 { get; set; } + [FieldOrder(56)] public float RestTimeBeforeLift { get; set; } + [FieldOrder(57)] public float BottomRetractHeight2 { get; set; } + [FieldOrder(58)] public uint Unknown6 { get; set; } // Same as CTBv4.PrintParametersV4.Unknown1) + [FieldOrder(59)] public uint Unknown7 { get; set; } // Same as CTBv4.PrintParametersV4.Unknown2) + [FieldOrder(60)] public uint Unknown8 { get; set; } = 4; // Same as CTBv4.PrintParametersV4.Unknown3) + [FieldOrder(61)] public uint LastLayerIndex { get; set; } + [FieldOrder(62)] public uint Padding3 { get; set; } + [FieldOrder(63)] public uint Padding4 { get; set; } + [FieldOrder(64)] public uint Padding5 { get; set; } + [FieldOrder(65)] public uint Padding6 { get; set; } + [FieldOrder(66)] public uint DisclaimerOffset { get; set; } + [FieldOrder(67)] public uint DisclaimerSize { get; set; } + [FieldOrder(68)] public uint Padding7 { get; set; } + [FieldOrder(69)] public uint Padding8 { get; set; } + [FieldOrder(70)] public uint Padding9 { get; set; } + [FieldOrder(71)] public uint Padding10 { get; set; } + + [Ignore] + public string MachineName + { + get => _machineName; + set { - return $"{nameof(Magic)}: {Magic}, {nameof(SettingsSize)}: {SettingsSize}, {nameof(SettingsOffset)}: {SettingsOffset}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(SignatureSize)}: {SignatureSize}, {nameof(SignatureOffset)}: {SignatureOffset}, {nameof(Unknown)}: {Unknown}, {nameof(Unknown4)}: {Unknown4}, {nameof(Unknown5)}: {Unknown5}, {nameof(Unknown6)}: {Unknown6}, {nameof(Unknown7)}: {Unknown7}, {nameof(Unknown8)}: {Unknown8}"; + if (string.IsNullOrEmpty(value)) value = DefaultMachineName; + _machineName = value; + MachineNameSize = string.IsNullOrEmpty(_machineName) ? 0 : (uint)_machineName.Length; } } - public class SlicerSettings + public override string ToString() { - public const ushort TABLE_SIZE = 288; + return $"{nameof(ChecksumValue)}: {ChecksumValue}, {nameof(LayerPointersOffset)}: {LayerPointersOffset}, {nameof(DisplayWidth)}: {DisplayWidth}, {nameof(DisplayHeight)}: {DisplayHeight}, {nameof(MachineZ)}: {MachineZ}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(TotalHeightMillimeter)}: {TotalHeightMillimeter}, {nameof(LayerHeight)}: {LayerHeight}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(BottomExposureTime)}: {BottomExposureTime}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(BottomLayerCount)}: {BottomLayerCount}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(LayerCount)}: {LayerCount}, {nameof(LargePreviewOffset)}: {LargePreviewOffset}, {nameof(SmallPreviewOffset)}: {SmallPreviewOffset}, {nameof(PrintTime)}: {PrintTime}, {nameof(ProjectorType)}: {ProjectorType}, {nameof(BottomLiftHeight)}: {BottomLiftHeight}, {nameof(BottomLiftSpeed)}: {BottomLiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(MaterialMilliliters)}: {MaterialMilliliters}, {nameof(MaterialGrams)}: {MaterialGrams}, {nameof(MaterialCost)}: {MaterialCost}, {nameof(BottomLightOffDelay)}: {BottomLightOffDelay}, {nameof(Unknown3)}: {Unknown3}, {nameof(LightPWM)}: {LightPWM}, {nameof(BottomLightPWM)}: {BottomLightPWM}, {nameof(LayerXorKey)}: {LayerXorKey}, {nameof(BottomLiftHeight2)}: {BottomLiftHeight2}, {nameof(BottomLiftSpeed2)}: {BottomLiftSpeed2}, {nameof(LiftHeight2)}: {LiftHeight2}, {nameof(LiftSpeed2)}: {LiftSpeed2}, {nameof(RetractHeight2)}: {RetractHeight2}, {nameof(RetractSpeed2)}: {RetractSpeed2}, {nameof(RestTimeAfterLift)}: {RestTimeAfterLift}, {nameof(MachineNameOffset)}: {MachineNameOffset}, {nameof(MachineNameSize)}: {MachineNameSize}, {nameof(PerLayerSettings)}: {PerLayerSettings}, {nameof(Unknown4)}: {Unknown4}, {nameof(Unknown5)}: {Unknown5}, {nameof(RestTimeAfterRetract)}: {RestTimeAfterRetract}, {nameof(RestTimeAfterLift2)}: {RestTimeAfterLift2}, {nameof(TransitionLayerCount)}: {TransitionLayerCount}, {nameof(BottomRetractSpeed)}: {BottomRetractSpeed}, {nameof(BottomRetractSpeed2)}: {BottomRetractSpeed2}, {nameof(Padding1)}: {Padding1}, {nameof(Four1)}: {Four1}, {nameof(Padding2)}: {Padding2}, {nameof(Four2)}: {Four2}, {nameof(RestTimeAfterRetract2)}: {RestTimeAfterRetract2}, {nameof(RestTimeAfterLift3)}: {RestTimeAfterLift3}, {nameof(RestTimeBeforeLift)}: {RestTimeBeforeLift}, {nameof(BottomRetractHeight2)}: {BottomRetractHeight2}, {nameof(Unknown6)}: {Unknown6}, {nameof(Unknown7)}: {Unknown7}, {nameof(Unknown8)}: {Unknown8}, {nameof(LastLayerIndex)}: {LastLayerIndex}, {nameof(Padding3)}: {Padding3}, {nameof(Padding4)}: {Padding4}, {nameof(Padding5)}: {Padding5}, {nameof(Padding6)}: {Padding6}, {nameof(DisclaimerOffset)}: {DisclaimerOffset}, {nameof(DisclaimerSize)}: {DisclaimerSize}, {nameof(Padding7)}: {Padding7}, {nameof(Padding8)}: {Padding8}, {nameof(Padding9)}: {Padding9}, {nameof(Padding10)}: {Padding10}, {nameof(MachineName)}: {MachineName}"; + } + } - private string _machineName = DefaultMachineName; + public class LayerPointer + { + [FieldOrder(0)] public uint LayerOffset { get; set; } + [FieldOrder(1)] public uint Padding1 { get; set; } // 0 + [FieldOrder(2)] public uint LayerTableSize { get; set; } = LayerDef.TABLE_SIZE; // always 0x58 + [FieldOrder(3)] public uint Padding2 { get; set; } // 0 - /// - /// Checksum of unix timestamp - /// - [FieldOrder(0)] public ulong ChecksumValue { get; set; } = 0xCAFEBABE; - [FieldOrder(1)] public uint LayerPointersOffset { get; set; } - [FieldOrder(2)] public float DisplayWidth { get; set; } - [FieldOrder(3)] public float DisplayHeight { get; set; } - [FieldOrder(4)] public float MachineZ { get; set; } - [FieldOrder(5)] public uint Unknown1 { get; set; } - [FieldOrder(6)] public uint Unknown2 { get; set; } - [FieldOrder(7)] public float TotalHeightMillimeter { get; set; } - [FieldOrder(8)] public float LayerHeight { get; set; } - [FieldOrder(9)] public float ExposureTime { get; set; } - [FieldOrder(10)] public float BottomExposureTime { get; set; } - [FieldOrder(11)] public float LightOffDelay { get; set; } - [FieldOrder(12)] public uint BottomLayerCount { get; set; } - [FieldOrder(13)] public uint ResolutionX { get; set; } - [FieldOrder(14)] public uint ResolutionY { get; set; } - [FieldOrder(15)] public uint LayerCount { get; set; } - [FieldOrder(16)] public uint LargePreviewOffset { get; set; } - [FieldOrder(17)] public uint SmallPreviewOffset { get; set; } - [FieldOrder(18)] public uint PrintTime { get; set; } - [FieldOrder(19)] public uint ProjectorType { get; set; } - [FieldOrder(20)] public float BottomLiftHeight { get; set; } - [FieldOrder(21)] public float BottomLiftSpeed { get; set; } - [FieldOrder(22)] public float LiftHeight { get; set; } - [FieldOrder(23)] public float LiftSpeed { get; set; } - [FieldOrder(24)] public float RetractSpeed { get; set; } - [FieldOrder(25)] public float MaterialMilliliters { get; set; } - [FieldOrder(26)] public float MaterialGrams { get; set; } - [FieldOrder(27)] public float MaterialCost { get; set; } - [FieldOrder(28)] public float BottomLightOffDelay { get; set; } - [FieldOrder(29)] public uint Unknown3 { get; set; } = 1; - [FieldOrder(30)] public ushort LightPWM { get; set; } - [FieldOrder(31)] public ushort BottomLightPWM { get; set; } - [FieldOrder(32)] public uint LayerXorKey { get; set; } - [FieldOrder(33)] public float BottomLiftHeight2 { get; set; } - [FieldOrder(34)] public float BottomLiftSpeed2 { get; set; } - [FieldOrder(35)] public float LiftHeight2 { get; set; } - [FieldOrder(36)] public float LiftSpeed2 { get; set; } - [FieldOrder(37)] public float RetractHeight2 { get; set; } - [FieldOrder(38)] public float RetractSpeed2 { get; set; } - [FieldOrder(39)] public float RestTimeAfterLift { get; set; } - [FieldOrder(40)] public uint MachineNameOffset { get; set; } - [FieldOrder(41)] public uint MachineNameSize { get; set; } = (uint)(string.IsNullOrEmpty(DefaultMachineName) ? 0 : DefaultMachineName.Length); - [FieldOrder(42)] public uint PerLayerSettings { get; set; } = PERLAYER_SETTINGS_DISALLOW; - [FieldOrder(43)] public uint Unknown4 { get; set; } - [FieldOrder(44)] public uint Unknown5 { get; set; } = 8; // Also 1 - [FieldOrder(45)] public float RestTimeAfterRetract { get; set; } - [FieldOrder(46)] public float RestTimeAfterLift2 { get; set; } - [FieldOrder(47)] public uint TransitionLayerCount { get; set; } - [FieldOrder(48)] public float BottomRetractSpeed { get; set; } - [FieldOrder(49)] public float BottomRetractSpeed2 { get; set; } - [FieldOrder(50)] public uint Padding1 { get; set; } - [FieldOrder(51)] public float Four1 { get; set; } = 4; // Same as CTBv4.PrintParametersV4.Four1) - [FieldOrder(52)] public uint Padding2 { get; set; } - [FieldOrder(53)] public float Four2 { get; set; } = 4; // Same as CTBv4.PrintParametersV4.Four2) - [FieldOrder(54)] public float RestTimeAfterRetract2 { get; set; } - [FieldOrder(55)] public float RestTimeAfterLift3 { get; set; } - [FieldOrder(56)] public float RestTimeBeforeLift { get; set; } - [FieldOrder(57)] public float BottomRetractHeight2 { get; set; } - [FieldOrder(58)] public uint Unknown6 { get; set; } // Same as CTBv4.PrintParametersV4.Unknown1) - [FieldOrder(59)] public uint Unknown7 { get; set; } // Same as CTBv4.PrintParametersV4.Unknown2) - [FieldOrder(60)] public uint Unknown8 { get; set; } = 4; // Same as CTBv4.PrintParametersV4.Unknown3) - [FieldOrder(61)] public uint LastLayerIndex { get; set; } - [FieldOrder(62)] public uint Padding3 { get; set; } - [FieldOrder(63)] public uint Padding4 { get; set; } - [FieldOrder(64)] public uint Padding5 { get; set; } - [FieldOrder(65)] public uint Padding6 { get; set; } - [FieldOrder(66)] public uint DisclaimerOffset { get; set; } - [FieldOrder(67)] public uint DisclaimerSize { get; set; } - [FieldOrder(68)] public uint Padding7 { get; set; } - [FieldOrder(69)] public uint Padding8 { get; set; } - [FieldOrder(70)] public uint Padding9 { get; set; } - [FieldOrder(71)] public uint Padding10 { get; set; } - - [Ignore] - public string MachineName - { - get => _machineName; - set - { - if (string.IsNullOrEmpty(value)) value = DefaultMachineName; - _machineName = value; - MachineNameSize = string.IsNullOrEmpty(_machineName) ? 0 : (uint)_machineName.Length; - } - } - - public override string ToString() - { - return $"{nameof(ChecksumValue)}: {ChecksumValue}, {nameof(LayerPointersOffset)}: {LayerPointersOffset}, {nameof(DisplayWidth)}: {DisplayWidth}, {nameof(DisplayHeight)}: {DisplayHeight}, {nameof(MachineZ)}: {MachineZ}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(TotalHeightMillimeter)}: {TotalHeightMillimeter}, {nameof(LayerHeight)}: {LayerHeight}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(BottomExposureTime)}: {BottomExposureTime}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(BottomLayerCount)}: {BottomLayerCount}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(LayerCount)}: {LayerCount}, {nameof(LargePreviewOffset)}: {LargePreviewOffset}, {nameof(SmallPreviewOffset)}: {SmallPreviewOffset}, {nameof(PrintTime)}: {PrintTime}, {nameof(ProjectorType)}: {ProjectorType}, {nameof(BottomLiftHeight)}: {BottomLiftHeight}, {nameof(BottomLiftSpeed)}: {BottomLiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(MaterialMilliliters)}: {MaterialMilliliters}, {nameof(MaterialGrams)}: {MaterialGrams}, {nameof(MaterialCost)}: {MaterialCost}, {nameof(BottomLightOffDelay)}: {BottomLightOffDelay}, {nameof(Unknown3)}: {Unknown3}, {nameof(LightPWM)}: {LightPWM}, {nameof(BottomLightPWM)}: {BottomLightPWM}, {nameof(LayerXorKey)}: {LayerXorKey}, {nameof(BottomLiftHeight2)}: {BottomLiftHeight2}, {nameof(BottomLiftSpeed2)}: {BottomLiftSpeed2}, {nameof(LiftHeight2)}: {LiftHeight2}, {nameof(LiftSpeed2)}: {LiftSpeed2}, {nameof(RetractHeight2)}: {RetractHeight2}, {nameof(RetractSpeed2)}: {RetractSpeed2}, {nameof(RestTimeAfterLift)}: {RestTimeAfterLift}, {nameof(MachineNameOffset)}: {MachineNameOffset}, {nameof(MachineNameSize)}: {MachineNameSize}, {nameof(PerLayerSettings)}: {PerLayerSettings}, {nameof(Unknown4)}: {Unknown4}, {nameof(Unknown5)}: {Unknown5}, {nameof(RestTimeAfterRetract)}: {RestTimeAfterRetract}, {nameof(RestTimeAfterLift2)}: {RestTimeAfterLift2}, {nameof(TransitionLayerCount)}: {TransitionLayerCount}, {nameof(BottomRetractSpeed)}: {BottomRetractSpeed}, {nameof(BottomRetractSpeed2)}: {BottomRetractSpeed2}, {nameof(Padding1)}: {Padding1}, {nameof(Four1)}: {Four1}, {nameof(Padding2)}: {Padding2}, {nameof(Four2)}: {Four2}, {nameof(RestTimeAfterRetract2)}: {RestTimeAfterRetract2}, {nameof(RestTimeAfterLift3)}: {RestTimeAfterLift3}, {nameof(RestTimeBeforeLift)}: {RestTimeBeforeLift}, {nameof(BottomRetractHeight2)}: {BottomRetractHeight2}, {nameof(Unknown6)}: {Unknown6}, {nameof(Unknown7)}: {Unknown7}, {nameof(Unknown8)}: {Unknown8}, {nameof(LastLayerIndex)}: {LastLayerIndex}, {nameof(Padding3)}: {Padding3}, {nameof(Padding4)}: {Padding4}, {nameof(Padding5)}: {Padding5}, {nameof(Padding6)}: {Padding6}, {nameof(DisclaimerOffset)}: {DisclaimerOffset}, {nameof(DisclaimerSize)}: {DisclaimerSize}, {nameof(Padding7)}: {Padding7}, {nameof(Padding8)}: {Padding8}, {nameof(Padding9)}: {Padding9}, {nameof(Padding10)}: {Padding10}, {nameof(MachineName)}: {MachineName}"; - } + public override string ToString() + { + return $"{nameof(LayerOffset)}: {LayerOffset}, {nameof(Padding1)}: {Padding1}, {nameof(LayerTableSize)}: {LayerTableSize}, {nameof(Padding2)}: {Padding2}"; } - public class LayerPointer + public LayerPointer() { - [FieldOrder(0)] public uint LayerOffset { get; set; } - [FieldOrder(1)] public uint Padding1 { get; set; } // 0 - [FieldOrder(2)] public uint LayerTableSize { get; set; } = LayerDef.TABLE_SIZE; // always 0x58 - [FieldOrder(3)] public uint Padding2 { get; set; } // 0 - - public override string ToString() - { - return $"{nameof(LayerOffset)}: {LayerOffset}, {nameof(Padding1)}: {Padding1}, {nameof(LayerTableSize)}: {LayerTableSize}, {nameof(Padding2)}: {Padding2}"; - } - - public LayerPointer() - { - } - - public LayerPointer(uint layerOffset) - { - LayerOffset = layerOffset; - } } - public class LayerDef + public LayerPointer(uint layerOffset) { - public const byte TABLE_SIZE = 88; + LayerOffset = layerOffset; + } + } - [FieldOrder(0)] public uint TableSize { get; set; } = TABLE_SIZE; - [FieldOrder(1)] public float PositionZ { get; set; } - [FieldOrder(2)] public float ExposureTime { get; set; } - [FieldOrder(3)] public float LightOffDelay { get; set; } - [FieldOrder(4)] public uint LayerDefOffset { get; set; } - [FieldOrder(5)] public uint Unknown2 { get; set; } - [FieldOrder(6)] public uint DataLength { get; set; } - [FieldOrder(7)] public uint Unknown3 { get; set; } - [FieldOrder(8)] public uint EncryptedDataOffset { get; set; } - [FieldOrder(9)] public uint EncryptedDataLength { get; set; } - [FieldOrder(10)] public float LiftHeight { get; set; } - [FieldOrder(11)] public float LiftSpeed { get; set; } - [FieldOrder(12)] public float LiftHeight2 { get; set; } - [FieldOrder(13)] public float LiftSpeed2 { get; set; } - [FieldOrder(14)] public float RetractSpeed { get; set; } - [FieldOrder(15)] public float RetractHeight2 { get; set; } - [FieldOrder(16)] public float RetractSpeed2 { get; set; } - [FieldOrder(17)] public float RestTimeBeforeLift { get; set; } - [FieldOrder(18)] public float RestTimeAfterLift { get; set; } - [FieldOrder(19)] public float RestTimeAfterRetract { get; set; } - [FieldOrder(20)] public float LightPWM { get; set; } - [FieldOrder(21)] public uint Unknown6 { get; set; } + public class LayerDef + { + public const byte TABLE_SIZE = 88; - [Ignore] public CTBEncryptedFile Parent { get; set; } + [FieldOrder(0)] public uint TableSize { get; set; } = TABLE_SIZE; + [FieldOrder(1)] public float PositionZ { get; set; } + [FieldOrder(2)] public float ExposureTime { get; set; } + [FieldOrder(3)] public float LightOffDelay { get; set; } + [FieldOrder(4)] public uint LayerDefOffset { get; set; } + [FieldOrder(5)] public uint Unknown2 { get; set; } + [FieldOrder(6)] public uint DataLength { get; set; } + [FieldOrder(7)] public uint Unknown3 { get; set; } + [FieldOrder(8)] public uint EncryptedDataOffset { get; set; } + [FieldOrder(9)] public uint EncryptedDataLength { get; set; } + [FieldOrder(10)] public float LiftHeight { get; set; } + [FieldOrder(11)] public float LiftSpeed { get; set; } + [FieldOrder(12)] public float LiftHeight2 { get; set; } + [FieldOrder(13)] public float LiftSpeed2 { get; set; } + [FieldOrder(14)] public float RetractSpeed { get; set; } + [FieldOrder(15)] public float RetractHeight2 { get; set; } + [FieldOrder(16)] public float RetractSpeed2 { get; set; } + [FieldOrder(17)] public float RestTimeBeforeLift { get; set; } + [FieldOrder(18)] public float RestTimeAfterLift { get; set; } + [FieldOrder(19)] public float RestTimeAfterRetract { get; set; } + [FieldOrder(20)] public float LightPWM { get; set; } + [FieldOrder(21)] public uint Unknown6 { get; set; } - //[FieldOrder(22)] [FieldLength(nameof(DataLength))] public byte[] RLEData { get; set; } - [Ignore] public byte[] RLEData { get; set; } + [Ignore] public CTBEncryptedFile? Parent { get; set; } - public LayerDef() { } + //[FieldOrder(22)] [FieldLength(nameof(DataLength))] public byte[] RLEData { get; set; } + [Ignore] public byte[]? RLEData { get; set; } - public LayerDef(CTBEncryptedFile parent, Layer layer) + public LayerDef() { } + + public LayerDef(CTBEncryptedFile parent, Layer layer) + { + Parent = parent; + SetFrom(layer); + } + + public void SetFrom(Layer layer) + { + PositionZ = layer.PositionZ; + ExposureTime = layer.ExposureTime; + LightOffDelay = layer.LightOffDelay; + LiftHeight = layer.LiftHeightTotal; + LiftSpeed = layer.LiftSpeed; + LiftHeight2 = layer.LiftHeight2; + LiftSpeed2 = layer.LiftSpeed2; + RetractSpeed = layer.RetractSpeed; + RetractHeight2 = layer.RetractHeight2; + RetractSpeed2 = layer.RetractSpeed2; + RestTimeBeforeLift = layer.WaitTimeAfterCure; + RestTimeAfterLift = layer.WaitTimeAfterLift; + RestTimeAfterRetract = layer.WaitTimeBeforeCure; + LightPWM = layer.LightPWM; + } + + public void CopyTo(Layer layer) + { + layer.PositionZ = PositionZ; + layer.ExposureTime = ExposureTime; + layer.LightOffDelay = LightOffDelay; + layer.LiftHeight = LiftHeight - LiftHeight2; + layer.LiftSpeed = LiftSpeed; + layer.LiftHeight2 = LiftHeight2; + layer.LiftSpeed2 = LiftSpeed2; + layer.RetractSpeed = RetractSpeed; + layer.RetractHeight2 = RetractHeight2; + layer.RetractSpeed2 = RetractSpeed2; + layer.WaitTimeAfterCure = RestTimeBeforeLift; + layer.WaitTimeAfterLift = RestTimeAfterLift; + layer.WaitTimeBeforeCure = RestTimeAfterRetract; + layer.LightPWM = (byte)LightPWM; + } + + public Mat DecodeImage(uint layerIndex, bool consumeRle = true) + { + var mat = EmguExtensions.InitMat(Parent!.Resolution); + //var span = mat.GetBytePointer(); + + if (Parent.Settings.LayerXorKey > 0) { - Parent = parent; - SetFrom(layer); + ChituboxFile.LayerRleCryptBuffer(Parent.Settings.LayerXorKey, layerIndex, RLEData!); } - public void SetFrom(Layer layer) + int pixel = 0; + for (var n = 0; n < RLEData!.Length; n++) { - PositionZ = layer.PositionZ; - ExposureTime = layer.ExposureTime; - LightOffDelay = layer.LightOffDelay; - LiftHeight = layer.LiftHeightTotal; - LiftSpeed = layer.LiftSpeed; - LiftHeight2 = layer.LiftHeight2; - LiftSpeed2 = layer.LiftSpeed2; - RetractSpeed = layer.RetractSpeed; - RetractHeight2 = layer.RetractHeight2; - RetractSpeed2 = layer.RetractSpeed2; - RestTimeBeforeLift = layer.WaitTimeAfterCure; - RestTimeAfterLift = layer.WaitTimeAfterLift; - RestTimeAfterRetract = layer.WaitTimeBeforeCure; - LightPWM = layer.LightPWM; - } + byte code = RLEData[n]; + int stride = 1; - public void CopyTo(Layer layer) - { - layer.PositionZ = PositionZ; - layer.ExposureTime = ExposureTime; - layer.LightOffDelay = LightOffDelay; - layer.LiftHeight = LiftHeight - LiftHeight2; - layer.LiftSpeed = LiftSpeed; - layer.LiftHeight2 = LiftHeight2; - layer.LiftSpeed2 = LiftSpeed2; - layer.RetractSpeed = RetractSpeed; - layer.RetractHeight2 = RetractHeight2; - layer.RetractSpeed2 = RetractSpeed2; - layer.WaitTimeAfterCure = RestTimeBeforeLift; - layer.WaitTimeAfterLift = RestTimeAfterLift; - layer.WaitTimeBeforeCure = RestTimeAfterRetract; - layer.LightPWM = (byte)LightPWM; - } - - public Mat DecodeImage(uint layerIndex, bool consumeRle = true) - { - var mat = EmguExtensions.InitMat(Parent.Resolution); - //var span = mat.GetBytePointer(); - - if (Parent.Settings.LayerXorKey > 0) + if ((code & 0x80) == 0x80) // It's a run { - ChituboxFile.LayerRleCryptBuffer(Parent.Settings.LayerXorKey, layerIndex, RLEData); - } + code &= 0x7f; // Get the run length + n++; - int pixel = 0; - for (var n = 0; n < RLEData.Length; n++) - { - byte code = RLEData[n]; - int stride = 1; + var slen = RLEData[n]; - if ((code & 0x80) == 0x80) // It's a run + if ((slen & 0x80) == 0) { - code &= 0x7f; // Get the run length + stride = slen; + } + else if ((slen & 0xc0) == 0x80) + { + stride = ((slen & 0x3f) << 8) + RLEData[n + 1]; n++; - - var slen = RLEData[n]; - - if ((slen & 0x80) == 0) - { - stride = slen; - } - else if ((slen & 0xc0) == 0x80) - { - stride = ((slen & 0x3f) << 8) + RLEData[n + 1]; - n++; - } - else if ((slen & 0xe0) == 0xc0) - { - stride = ((slen & 0x1f) << 16) + (RLEData[n + 1] << 8) + RLEData[n + 2]; - n += 2; - } - else if ((slen & 0xf0) == 0xe0) - { - stride = ((slen & 0xf) << 24) + (RLEData[n + 1] << 16) + (RLEData[n + 2] << 8) + RLEData[n + 3]; - n += 3; - } - else - { - mat.Dispose(); - throw new FileLoadException("Corrupted RLE data"); - } } - - // Bit extend from 7-bit to 8-bit greymap - if (code != 0) + else if ((slen & 0xe0) == 0xc0) { - code = (byte)((code << 1) | 1); + stride = ((slen & 0x1f) << 16) + (RLEData[n + 1] << 8) + RLEData[n + 2]; + n += 2; } - - mat.FillSpan(ref pixel, stride, code); - - //if (stride <= 0) continue; // Nothing to do - - /*if (code == 0) // Ignore blacks, spare cycles + else if ((slen & 0xf0) == 0xe0) { - pixel += stride; - continue; - }*/ - - /*for (; stride > 0; stride--) - { - span[pixel] = code; - pixel++; - }*/ - } - - if (consumeRle) RLEData = null; - - return mat; - } - - public unsafe byte[] EncodeImage(Mat image, uint layerIndex) - { - List rawData = new(); - byte color = byte.MaxValue >> 1; - uint stride = 0; - var span = image.GetBytePointer(); - var imageLength = image.GetLength(); - - void AddRep() - { - if (stride == 0) - { - return; - } - - if (stride > 1) - { - color |= 0x80; - } - rawData.Add(color); - - if (stride <= 1) - { - // no run needed - return; - } - - if (stride <= 0x7f) - { - rawData.Add((byte)stride); - return; - } - - if (stride <= 0x3fff) - { - rawData.Add((byte)((stride >> 8) | 0x80)); - rawData.Add((byte)stride); - return; - } - - if (stride <= 0x1fffff) - { - rawData.Add((byte)((stride >> 16) | 0xc0)); - rawData.Add((byte)(stride >> 8)); - rawData.Add((byte)stride); - return; - } - - if (stride <= 0xfffffff) - { - rawData.Add((byte)((stride >> 24) | 0xe0)); - rawData.Add((byte)(stride >> 16)); - rawData.Add((byte)(stride >> 8)); - rawData.Add((byte)stride); - } - } - - - for (int pixel = 0; pixel < imageLength; pixel++) - { - var grey7 = (byte)(span[pixel] >> 1); - - if (grey7 == color) - { - stride++; + stride = ((slen & 0xf) << 24) + (RLEData[n + 1] << 16) + (RLEData[n + 2] << 8) + RLEData[n + 3]; + n += 3; } else { - AddRep(); - color = grey7; - stride = 1; + mat.Dispose(); + throw new FileLoadException("Corrupted RLE data"); } } - AddRep(); - - if (Parent.Settings.LayerXorKey > 0) + // Bit extend from 7-bit to 8-bit greymap + if (code != 0) { - RLEData = ChituboxFile.LayerRleCrypt(Parent.Settings.LayerXorKey, layerIndex, rawData); + code = (byte)((code << 1) | 1); + } + + mat.FillSpan(ref pixel, stride, code); + + //if (stride <= 0) continue; // Nothing to do + + /*if (code == 0) // Ignore blacks, spare cycles + { + pixel += stride; + continue; + }*/ + + /*for (; stride > 0; stride--) + { + span[pixel] = code; + pixel++; + }*/ + } + + if (consumeRle) RLEData = null; + + return mat; + } + + public unsafe byte[] EncodeImage(Mat image, uint layerIndex) + { + List rawData = new(); + byte color = byte.MaxValue >> 1; + uint stride = 0; + var span = image.GetBytePointer(); + var imageLength = image.GetLength(); + + void AddRep() + { + if (stride == 0) + { + return; + } + + if (stride > 1) + { + color |= 0x80; + } + rawData.Add(color); + + if (stride <= 1) + { + // no run needed + return; + } + + if (stride <= 0x7f) + { + rawData.Add((byte)stride); + return; + } + + if (stride <= 0x3fff) + { + rawData.Add((byte)((stride >> 8) | 0x80)); + rawData.Add((byte)stride); + return; + } + + if (stride <= 0x1fffff) + { + rawData.Add((byte)((stride >> 16) | 0xc0)); + rawData.Add((byte)(stride >> 8)); + rawData.Add((byte)stride); + return; + } + + if (stride <= 0xfffffff) + { + rawData.Add((byte)((stride >> 24) | 0xe0)); + rawData.Add((byte)(stride >> 16)); + rawData.Add((byte)(stride >> 8)); + rawData.Add((byte)stride); + } + } + + + for (int pixel = 0; pixel < imageLength; pixel++) + { + var grey7 = (byte)(span[pixel] >> 1); + + if (grey7 == color) + { + stride++; } else { - RLEData = rawData.ToArray(); + AddRep(); + color = grey7; + stride = 1; } - - DataLength = (uint)RLEData.Length; - - return RLEData; } - public override string ToString() + AddRep(); + + if (Parent!.Settings.LayerXorKey > 0) { - return $"{nameof(TableSize)}: {TableSize}, {nameof(PositionZ)}: {PositionZ}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(LayerDefOffset)}: {LayerDefOffset}, {nameof(Unknown2)}: {Unknown2}, {nameof(DataLength)}: {DataLength}, {nameof(Unknown3)}: {Unknown3}, {nameof(EncryptedDataOffset)}: {EncryptedDataOffset}, {nameof(EncryptedDataLength)}: {EncryptedDataLength}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(LiftHeight2)}: {LiftHeight2}, {nameof(LiftSpeed2)}: {LiftSpeed2}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(RetractHeight2)}: {RetractHeight2}, {nameof(RetractSpeed2)}: {RetractSpeed2}, {nameof(RestTimeBeforeLift)}: {RestTimeBeforeLift}, {nameof(RestTimeAfterLift)}: {RestTimeAfterLift}, {nameof(RestTimeAfterRetract)}: {RestTimeAfterRetract}, {nameof(LightPWM)}: {LightPWM}, {nameof(Unknown6)}: {Unknown6}, {nameof(RLEData)}: {RLEData?.Length}"; + RLEData = ChituboxFile.LayerRleCrypt(Parent.Settings.LayerXorKey, layerIndex, rawData); } + else + { + RLEData = rawData.ToArray(); + } + + DataLength = (uint)RLEData.Length; + + return RLEData; } - #region Preview - /// - /// The files contain two preview images. - /// These are shown on the printer display when choosing which file to print, sparing the poor printer from needing to render a 3D image from scratch. - /// - public class Preview + public override string ToString() { - /// - /// Gets the X dimension of the preview image, in pixels. - /// - [FieldOrder(0)] public uint ResolutionX { get; set; } + return $"{nameof(TableSize)}: {TableSize}, {nameof(PositionZ)}: {PositionZ}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(LayerDefOffset)}: {LayerDefOffset}, {nameof(Unknown2)}: {Unknown2}, {nameof(DataLength)}: {DataLength}, {nameof(Unknown3)}: {Unknown3}, {nameof(EncryptedDataOffset)}: {EncryptedDataOffset}, {nameof(EncryptedDataLength)}: {EncryptedDataLength}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(LiftHeight2)}: {LiftHeight2}, {nameof(LiftSpeed2)}: {LiftSpeed2}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(RetractHeight2)}: {RetractHeight2}, {nameof(RetractSpeed2)}: {RetractSpeed2}, {nameof(RestTimeBeforeLift)}: {RestTimeBeforeLift}, {nameof(RestTimeAfterLift)}: {RestTimeAfterLift}, {nameof(RestTimeAfterRetract)}: {RestTimeAfterRetract}, {nameof(LightPWM)}: {LightPWM}, {nameof(Unknown6)}: {Unknown6}, {nameof(RLEData)}: {RLEData?.Length}"; + } + } - /// - /// Gets the Y dimension of the preview image, in pixels. - /// - [FieldOrder(1)] public uint ResolutionY { get; set; } + #region Preview + /// + /// The files contain two preview images. + /// These are shown on the printer display when choosing which file to print, sparing the poor printer from needing to render a 3D image from scratch. + /// + public class Preview + { + /// + /// Gets the X dimension of the preview image, in pixels. + /// + [FieldOrder(0)] public uint ResolutionX { get; set; } - /// - /// Gets the image offset of the encoded data blob. - /// - [FieldOrder(2)] public uint ImageOffset { get; set; } + /// + /// Gets the Y dimension of the preview image, in pixels. + /// + [FieldOrder(1)] public uint ResolutionY { get; set; } - /// - /// Gets the image length in bytes. - /// - [FieldOrder(3)] public uint ImageLength { get; set; } + /// + /// Gets the image offset of the encoded data blob. + /// + [FieldOrder(2)] public uint ImageOffset { get; set; } + + /// + /// Gets the image length in bytes. + /// + [FieldOrder(3)] public uint ImageLength { get; set; } - public unsafe Mat Decode(byte[] rawImageData) + public unsafe Mat Decode(byte[] rawImageData) + { + var image = new Mat(new Size((int)ResolutionX, (int)ResolutionY), DepthType.Cv8U, 3); + var span = image.GetBytePointer(); + + int pixel = 0; + for (int n = 0; n < rawImageData.Length; n++) { - var image = new Mat(new Size((int)ResolutionX, (int)ResolutionY), DepthType.Cv8U, 3); - var span = image.GetBytePointer(); - - int pixel = 0; - for (int n = 0; n < rawImageData.Length; n++) + uint dot = (uint)(rawImageData[n] & 0xFF | ((rawImageData[++n] & 0xFF) << 8)); + byte red = (byte)(((dot >> 11) & 0x1F) << 3); + byte green = (byte)(((dot >> 6) & 0x1F) << 3); + byte blue = (byte)((dot & 0x1F) << 3); + int repeat = 1; + if ((dot & 0x0020) == 0x0020) { - uint dot = (uint)(rawImageData[n] & 0xFF | ((rawImageData[++n] & 0xFF) << 8)); - byte red = (byte)(((dot >> 11) & 0x1F) << 3); - byte green = (byte)(((dot >> 6) & 0x1F) << 3); - byte blue = (byte)((dot & 0x1F) << 3); - int repeat = 1; - if ((dot & 0x0020) == 0x0020) - { - repeat += rawImageData[++n] & 0xFF | ((rawImageData[++n] & 0x0F) << 8); - } - - for (int j = 0; j < repeat; j++) - { - span[pixel++] = blue; - span[pixel++] = green; - span[pixel++] = red; - } + repeat += rawImageData[++n] & 0xFF | ((rawImageData[++n] & 0x0F) << 8); } - return image; - } - - public override string ToString() - { - return $"{nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(ImageOffset)}: {ImageOffset}, {nameof(ImageLength)}: {ImageLength}"; - } - - public unsafe byte[] Encode(Mat image) - { - List rawData = new(); - ushort color15 = 0; - uint rep = 0; - - var span = image.GetBytePointer(); - var imageLength = image.GetLength(); - - void RleRGB15() + for (int j = 0; j < repeat; j++) { - switch (rep) - { - case 0: - return; - case 1: + span[pixel++] = blue; + span[pixel++] = green; + span[pixel++] = red; + } + } + + return image; + } + + public override string ToString() + { + return $"{nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(ImageOffset)}: {ImageOffset}, {nameof(ImageLength)}: {ImageLength}"; + } + + public unsafe byte[] Encode(Mat image) + { + List rawData = new(); + ushort color15 = 0; + uint rep = 0; + + var span = image.GetBytePointer(); + var imageLength = image.GetLength(); + + void RleRGB15() + { + switch (rep) + { + case 0: + return; + case 1: + rawData.Add((byte)(color15 & ~REPEATRGB15MASK)); + rawData.Add((byte)((color15 & ~REPEATRGB15MASK) >> 8)); + break; + case 2: + for (int i = 0; i < 2; i++) + { rawData.Add((byte)(color15 & ~REPEATRGB15MASK)); rawData.Add((byte)((color15 & ~REPEATRGB15MASK) >> 8)); - break; - case 2: - for (int i = 0; i < 2; i++) - { - rawData.Add((byte)(color15 & ~REPEATRGB15MASK)); - rawData.Add((byte)((color15 & ~REPEATRGB15MASK) >> 8)); - } - - break; - default: - rawData.Add((byte)(color15 | REPEATRGB15MASK)); - rawData.Add((byte)((color15 | REPEATRGB15MASK) >> 8)); - rawData.Add((byte)((rep - 1) | 0x3000)); - rawData.Add((byte)(((rep - 1) | 0x3000) >> 8)); - break; - } - } - - int pixel = 0; - while (pixel < imageLength) - { - var ncolor15 = - // bgr - (span[pixel++] >> 3) | ((span[pixel++] >> 2) << 5) | ((span[pixel++] >> 3) << 11); - - if (ncolor15 == color15) - { - rep++; - if (rep == RLE16EncodingLimit) - { - RleRGB15(); - rep = 0; } - } - else + + break; + default: + rawData.Add((byte)(color15 | REPEATRGB15MASK)); + rawData.Add((byte)((color15 | REPEATRGB15MASK) >> 8)); + rawData.Add((byte)((rep - 1) | 0x3000)); + rawData.Add((byte)(((rep - 1) | 0x3000) >> 8)); + break; + } + } + + int pixel = 0; + while (pixel < imageLength) + { + var ncolor15 = + // bgr + (span[pixel++] >> 3) | ((span[pixel++] >> 2) << 5) | ((span[pixel++] >> 3) << 11); + + if (ncolor15 == color15) + { + rep++; + if (rep == RLE16EncodingLimit) { RleRGB15(); - color15 = (ushort)ncolor15; - rep = 1; + rep = 0; } } - - RleRGB15(); - - ImageLength = (uint)rawData.Count; - - return rawData.ToArray(); + else + { + RleRGB15(); + color15 = (ushort)ncolor15; + rep = 1; + } } + + RleRGB15(); + + ImageLength = (uint)rawData.Count; + + return rawData.ToArray(); } - #endregion + } + #endregion - #endregion + #endregion - #region Properties - public override FileFormatType FileType => FileFormatType.Binary; + #region Properties + public override FileFormatType FileType => FileFormatType.Binary; - public override FileExtension[] FileExtensions { get; } = { - new(typeof(CTBEncryptedFile), "ctb", "Chitubox CTB (Encrypted)"), - new(typeof(CTBEncryptedFile), "encrypted.ctb", "Chitubox CTB (Encrypted)", false, false), - }; + public override FileExtension[] FileExtensions { get; } = { + new(typeof(CTBEncryptedFile), "ctb", "Chitubox CTB (Encrypted)"), + new(typeof(CTBEncryptedFile), "encrypted.ctb", "Chitubox CTB (Encrypted)", false, false), + }; - public override Size[] ThumbnailsOriginalSize { get; } = - { - new(400, 300), - new(200, 125) - }; + public override Size[]? ThumbnailsOriginalSize { get; } = + { + new(400, 300), + new(200, 125) + }; - public Preview[] Previews { get; protected internal set; } + public Preview[] Previews { get; protected internal set; } - public FileHeader Header { get; protected internal set; } = new(); + public FileHeader Header { get; protected internal set; } = new(); - public SlicerSettings Settings { get; protected internal set; } = new(); - public LayerPointer[] LayersPointer { get; protected internal set; } - public LayerDef[] LayersDefinition { get; protected internal set; } + public SlicerSettings Settings { get; protected internal set; } = new(); + public LayerPointer[]? LayersPointer { get; protected internal set; } + public LayerDef[]? LayersDefinition { get; protected internal set; } - public override PrintParameterModifier[] PrintParameterModifiers { get; } = { - PrintParameterModifier.BottomLayerCount, - PrintParameterModifier.TransitionLayerCount, + public override PrintParameterModifier[]? PrintParameterModifiers { get; } = { + PrintParameterModifier.BottomLayerCount, + PrintParameterModifier.TransitionLayerCount, - PrintParameterModifier.BottomLightOffDelay, - PrintParameterModifier.LightOffDelay, + PrintParameterModifier.BottomLightOffDelay, + PrintParameterModifier.LightOffDelay, - PrintParameterModifier.BottomWaitTimeBeforeCure, - PrintParameterModifier.WaitTimeBeforeCure, + PrintParameterModifier.BottomWaitTimeBeforeCure, + PrintParameterModifier.WaitTimeBeforeCure, - PrintParameterModifier.BottomExposureTime, - PrintParameterModifier.ExposureTime, + PrintParameterModifier.BottomExposureTime, + PrintParameterModifier.ExposureTime, - PrintParameterModifier.BottomWaitTimeAfterCure, - PrintParameterModifier.WaitTimeAfterCure, + PrintParameterModifier.BottomWaitTimeAfterCure, + PrintParameterModifier.WaitTimeAfterCure, - PrintParameterModifier.BottomLiftHeight, - PrintParameterModifier.BottomLiftSpeed, - PrintParameterModifier.LiftHeight, - PrintParameterModifier.LiftSpeed, - PrintParameterModifier.BottomLiftHeight2, - PrintParameterModifier.BottomLiftSpeed2, - PrintParameterModifier.LiftHeight2, - PrintParameterModifier.LiftSpeed2, + PrintParameterModifier.BottomLiftHeight, + PrintParameterModifier.BottomLiftSpeed, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.BottomLiftHeight2, + PrintParameterModifier.BottomLiftSpeed2, + PrintParameterModifier.LiftHeight2, + PrintParameterModifier.LiftSpeed2, - PrintParameterModifier.BottomWaitTimeAfterLift, - PrintParameterModifier.WaitTimeAfterLift, + PrintParameterModifier.BottomWaitTimeAfterLift, + PrintParameterModifier.WaitTimeAfterLift, - PrintParameterModifier.BottomRetractSpeed, - PrintParameterModifier.RetractSpeed, - PrintParameterModifier.BottomRetractHeight2, - PrintParameterModifier.BottomRetractSpeed2, - PrintParameterModifier.RetractHeight2, - PrintParameterModifier.RetractSpeed2, + PrintParameterModifier.BottomRetractSpeed, + PrintParameterModifier.RetractSpeed, + PrintParameterModifier.BottomRetractHeight2, + PrintParameterModifier.BottomRetractSpeed2, + PrintParameterModifier.RetractHeight2, + PrintParameterModifier.RetractSpeed2, - PrintParameterModifier.BottomLightPWM, - PrintParameterModifier.LightPWM - }; + PrintParameterModifier.BottomLightPWM, + PrintParameterModifier.LightPWM + }; - public override PrintParameterModifier[] PrintParameterPerLayerModifiers { get; } = { - PrintParameterModifier.PositionZ, - PrintParameterModifier.LightOffDelay, - PrintParameterModifier.WaitTimeBeforeCure, - PrintParameterModifier.ExposureTime, - PrintParameterModifier.WaitTimeAfterCure, - PrintParameterModifier.LiftHeight, - PrintParameterModifier.LiftSpeed, - PrintParameterModifier.LiftHeight2, - PrintParameterModifier.LiftSpeed2, - PrintParameterModifier.WaitTimeAfterLift, - PrintParameterModifier.RetractSpeed, - PrintParameterModifier.RetractHeight2, - PrintParameterModifier.RetractSpeed2, - PrintParameterModifier.LightPWM - }; + public override PrintParameterModifier[]? PrintParameterPerLayerModifiers { get; } = { + PrintParameterModifier.PositionZ, + PrintParameterModifier.LightOffDelay, + PrintParameterModifier.WaitTimeBeforeCure, + PrintParameterModifier.ExposureTime, + PrintParameterModifier.WaitTimeAfterCure, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.LiftHeight2, + PrintParameterModifier.LiftSpeed2, + PrintParameterModifier.WaitTimeAfterLift, + PrintParameterModifier.RetractSpeed, + PrintParameterModifier.RetractHeight2, + PrintParameterModifier.RetractSpeed2, + PrintParameterModifier.LightPWM + }; - public override uint ResolutionX + public override uint ResolutionX + { + get => Settings.ResolutionX; + set { - get => Settings.ResolutionX; - set + Settings.ResolutionX = value; + RaisePropertyChanged(); + } + } + + public override uint ResolutionY + { + get => Settings.ResolutionY; + set + { + Settings.ResolutionY = value; + RaisePropertyChanged(); + } + } + + public override float LayerHeight + { + get => Settings.LayerHeight; + set + { + Settings.LayerHeight = value; + RaisePropertyChanged(); + } + } + + public override byte AntiAliasing { get => 8; set { } } + + public override float DisplayWidth + { + get => Settings.DisplayWidth; + set + { + Settings.DisplayWidth = (float)Math.Round(value, 2); + RaisePropertyChanged(); + } + } + + public override float DisplayHeight + { + get => Settings.DisplayHeight; + set + { + Settings.DisplayHeight = (float)Math.Round(value, 2); + RaisePropertyChanged(); + } + } + + public override float MachineZ + { + get => Settings.MachineZ > 0 ? Settings.MachineZ : base.MachineZ; + set => base.MachineZ = Settings.MachineZ = (float)Math.Round(value, 2); + } + + public override Enumerations.FlipDirection DisplayMirror + { + get => Settings.ProjectorType == 0 ? Enumerations.FlipDirection.None : Enumerations.FlipDirection.Horizontally; + set + { + Settings.ProjectorType = value == Enumerations.FlipDirection.None ? 0u : 1; + RaisePropertyChanged(); + } + } + + /* TODO: Find AntiAliasLevel in file */ + /* + public override byte AntiAliasing + { + get => (byte)(Settings.AntiAliasLevel); + set + { + Settings.AntiAliasLevel = value; + RaisePropertyChanged(); + } + }*/ + + public override float PrintHeight + { + get => base.PrintHeight; + set => base.PrintHeight = Settings.TotalHeightMillimeter = base.PrintHeight; + } + + public override uint LayerCount + { + get => base.LayerCount; + set + { + base.LayerCount = Settings.LayerCount = base.LayerCount; + Settings.LastLayerIndex = LastLayerIndex; + } + } + + public override ushort BottomLayerCount + { + get => (ushort)Settings.BottomLayerCount; + set => base.BottomLayerCount = (ushort)(Settings.BottomLayerCount = value); + } + + public override TransitionLayerTypes TransitionLayerType => TransitionLayerTypes.Software; + + public override ushort TransitionLayerCount + { + get => (ushort)Settings.TransitionLayerCount; + set => base.TransitionLayerCount = (ushort)(Settings.TransitionLayerCount = Math.Min(value, MaximumPossibleTransitionLayerCount)); + } + + public override float BottomLightOffDelay + { + get => Settings.BottomLightOffDelay; + set + { + base.BottomLightOffDelay = Settings.BottomLightOffDelay = (float)Math.Round(value, 2); + if (value > 0) { - Settings.ResolutionX = value; - RaisePropertyChanged(); + WaitTimeBeforeCure = 0; + WaitTimeAfterCure = 0; + WaitTimeAfterLift = 0; } } + } - public override uint ResolutionY + public override float LightOffDelay + { + get => Settings.LightOffDelay; + set { - get => Settings.ResolutionY; - set + base.LightOffDelay = Settings.LightOffDelay = (float)Math.Round(value, 2); + if (value > 0) { - Settings.ResolutionY = value; - RaisePropertyChanged(); + WaitTimeBeforeCure = 0; + WaitTimeAfterCure = 0; + WaitTimeAfterLift = 0; } } + } - public override float LayerHeight + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure > 0 ? base.BottomWaitTimeBeforeCure : FirstLayer?.WaitTimeBeforeCure ?? 0; + set => base.BottomWaitTimeBeforeCure = value; + } + + + public override float WaitTimeBeforeCure + { + get => Settings.RestTimeAfterRetract; + set { - get => Settings.LayerHeight; - set + base.WaitTimeBeforeCure = Settings.RestTimeAfterRetract = Settings.RestTimeAfterRetract2 = (float)Math.Round(value, 2); + if (value > 0) { - Settings.LayerHeight = value; - RaisePropertyChanged(); + BottomLightOffDelay = 0; + LightOffDelay = 0; } } + } - public override byte AntiAliasing { get => 8; set { } } + public override float BottomExposureTime + { + get => Settings.BottomExposureTime; + set => base.BottomExposureTime = Settings.BottomExposureTime = (float)Math.Round(value, 2); + } - public override float DisplayWidth + public override float BottomWaitTimeAfterCure + { + get => base.BottomWaitTimeAfterCure > 0 ? base.BottomWaitTimeAfterCure : FirstLayer?.WaitTimeAfterCure ?? 0; + set => base.BottomWaitTimeAfterCure = value; + } + + public override float WaitTimeAfterCure + { + get => Settings.RestTimeBeforeLift; + set { - get => Settings.DisplayWidth; - set + base.WaitTimeAfterCure = Settings.RestTimeBeforeLift = (float)Math.Round(value, 2); + if (value > 0) { - Settings.DisplayWidth = (float)Math.Round(value, 2); - RaisePropertyChanged(); + BottomLightOffDelay = 0; + LightOffDelay = 0; } } + } - public override float DisplayHeight + public override float ExposureTime + { + get => Settings.ExposureTime; + set => base.ExposureTime = Settings.ExposureTime = (float)Math.Round(value, 2); + } + + public override float BottomLiftHeight + { + get => Math.Max(0,Settings.BottomLiftHeight - Settings.BottomLiftHeight2); + set { - get => Settings.DisplayHeight; - set + value = (float)Math.Round(value, 2); + Settings.BottomLiftHeight = (float)Math.Round(value + Settings.BottomLiftHeight2, 2); + base.BottomLiftHeight = value; + } + } + + public override float BottomLiftSpeed + { + get => Settings.BottomLiftSpeed; + set => base.BottomLiftSpeed = Settings.BottomLiftSpeed = (float)Math.Round(value, 2); + } + + public override float LiftHeight + { + get => Math.Max(0,Settings.LiftHeight - Settings.LiftHeight2); + set + { + value = (float)Math.Round(value, 2); + Settings.LiftHeight = (float)Math.Round(value + Settings.LiftHeight2, 2); + base.LiftHeight = value; + } + } + + public override float LiftSpeed + { + get => Settings.LiftSpeed; + set => base.LiftSpeed = Settings.LiftSpeed = (float)Math.Round(value, 2); + } + + public override float BottomLiftHeight2 + { + get => Settings.BottomLiftHeight2; + set + { + var bottomLiftHeight = BottomLiftHeight; + Settings.BottomLiftHeight2 = (float)Math.Round(value, 2); + BottomLiftHeight = bottomLiftHeight; + base.BottomLiftHeight2 = Settings.BottomLiftHeight2; + } + } + + public override float BottomLiftSpeed2 + { + get => Settings.BottomLiftSpeed2; + set => base.BottomLiftSpeed2 = Settings.BottomLiftSpeed2 = (float)Math.Round(value, 2); + } + + public override float LiftHeight2 + { + get => Settings.LiftHeight2; + set + { + var liftHeight = LiftHeight; + Settings.LiftHeight2 = (float)Math.Round(value, 2); + LiftHeight = liftHeight; + base.LiftHeight2 = Settings.LiftHeight2; + } + } + + public override float LiftSpeed2 + { + get => Settings.LiftSpeed2; + set => base.LiftSpeed2 = Settings.LiftSpeed2 = (float)Math.Round(value, 2); + } + + public override float BottomWaitTimeAfterLift + { + get => base.BottomWaitTimeAfterLift > 0 ? base.BottomWaitTimeAfterLift : FirstLayer?.WaitTimeAfterLift ?? 0; + set => base.BottomWaitTimeAfterLift = value; + } + + public override float WaitTimeAfterLift + { + get => Settings.RestTimeAfterLift; + set + { + base.WaitTimeAfterLift = Settings.RestTimeAfterLift = Settings.RestTimeAfterLift2 = Settings.RestTimeAfterLift3 = (float)Math.Round(value, 2); + if (value > 0) { - Settings.DisplayHeight = (float)Math.Round(value, 2); - RaisePropertyChanged(); + BottomLightOffDelay = 0; + LightOffDelay = 0; } } + } - public override float MachineZ + public override float BottomRetractSpeed + { + get => Settings.BottomRetractSpeed; + set => base.BottomRetractSpeed = Settings.BottomRetractSpeed = (float)Math.Round(value, 2); + } + + public override float RetractSpeed + { + get => Settings.RetractSpeed; + set => base.RetractSpeed = Settings.RetractSpeed = (float)Math.Round(value, 2); + } + + public override float BottomRetractHeight2 + { + get => Settings.BottomRetractHeight2; + set { - get => Settings.MachineZ > 0 ? Settings.MachineZ : base.MachineZ; - set => base.MachineZ = Settings.MachineZ = (float)Math.Round(value, 2); + value = Math.Clamp((float)Math.Round(value, 2), 0, RetractHeightTotal); + base.BottomRetractHeight2 = Settings.BottomRetractHeight2 = value; } + } - public override Enumerations.FlipDirection DisplayMirror + public override float BottomRetractSpeed2 + { + get => Settings.BottomRetractSpeed2; + set => base.BottomRetractSpeed2 = Settings.BottomRetractSpeed2 = (float)Math.Round(value, 2); + } + + public override float RetractHeight2 + { + get => Settings.RetractHeight2; + set { - get => Settings.ProjectorType == 0 ? Enumerations.FlipDirection.None : Enumerations.FlipDirection.Horizontally; - set - { - Settings.ProjectorType = value == Enumerations.FlipDirection.None ? 0u : 1; - RaisePropertyChanged(); - } + value = Math.Clamp((float)Math.Round(value, 2), 0, RetractHeightTotal); + base.RetractHeight2 = Settings.RetractHeight2 = value; } + } - /* TODO: Find AntiAliasLevel in file */ - /* - public override byte AntiAliasing + public override float RetractSpeed2 + { + get => Settings.RetractSpeed2; + set => base.RetractSpeed2 = Settings.RetractSpeed2 = (float)Math.Round(value, 2); + } + + public override byte BottomLightPWM + { + get => (byte)Settings.BottomLightPWM; + set => base.BottomLightPWM = (byte)(Settings.BottomLightPWM = value); + } + + public override byte LightPWM + { + get => (byte)Settings.LightPWM; + set => base.LightPWM = (byte)(Settings.LightPWM = value); + } + + public override float PrintTime + { + get => base.PrintTime; + set { - get => (byte)(Settings.AntiAliasLevel); - set - { - Settings.AntiAliasLevel = value; - RaisePropertyChanged(); - } + base.PrintTime = value; + Settings.PrintTime = (uint)base.PrintTime; + } + } + + public override string MachineName + { + get => Settings.MachineName; + set => base.MachineName = Settings.MachineName = value; + } + + public override float MaterialMilliliters + { + get => base.MaterialMilliliters; + set + { + base.MaterialMilliliters = value; + Settings.MaterialMilliliters = base.MaterialMilliliters; + } + } + + public override float MaterialGrams + { + get => Settings.MaterialGrams; + set => base.MaterialGrams = Settings.MaterialGrams = (float)Math.Round(value, 3); + } + + public override float MaterialCost + { + get => (float)Math.Round(Settings.MaterialCost, 3); + set => base.MaterialCost = Settings.MaterialCost = (float)Math.Round(value, 3); + } + + public override object[] Configs + { + get + { + return new object[] { Settings }; + } + } + + + + #endregion + + #region Constructors + public CTBEncryptedFile() + { + Previews = new Preview[ThumbnailsCount]; + + /*if (Bigfoot is not null && Bigfoot[0] == 0 && File.Exists("MAGIC.ectb")) + { + using var fs = new FileStream("MAGIC.ectb", FileMode.Open); + fs.ReadBytes(Bigfoot); + fs.ReadBytes(CookieMonster); }*/ + } + #endregion - public override float PrintHeight + #region Methods + + public override void Clear() + { + base.Clear(); + + for (byte i = 0; i < ThumbnailsCount; i++) { - get => base.PrintHeight; - set => base.PrintHeight = Settings.TotalHeightMillimeter = base.PrintHeight; + Previews[i] = new Preview(); + } + } + + public override bool CanProcess(string fileFullPath) + { + if (!base.CanProcess(fileFullPath)) return false; + + try + { + using var fs = new BinaryReader(new FileStream(fileFullPath, FileMode.Open, FileAccess.Read)); + var magic = fs.ReadUInt32(); + return magic is MAGIC_CBT_ENCRYPTED; + } + catch (Exception e) + { + Debug.WriteLine(e); } - public override uint LayerCount + return false; + } + + private void SanitizeProperties() + { + Settings.PerLayerSettings = AllLayersAreUsingGlobalParameters + ? PERLAYER_SETTINGS_DISALLOW + : PERLAYER_SETTINGS_ALLOW; + } + + + protected override void DecodeInternally(OperationProgress progress) + { + using var inputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Read); + Header = Helpers.Deserialize(inputFile); + Debug.WriteLine($"Header: {Header}"); + + if (Header.Magic is not MAGIC_CBT_ENCRYPTED) { - get => base.LayerCount; - set - { - base.LayerCount = Settings.LayerCount = base.LayerCount; - Settings.LastLayerIndex = LastLayerIndex; - } + throw new FileLoadException($"Not a valid CTB encrypted file! Magic Value: {Header.Magic}", FileFullPath); } - public override ushort BottomLayerCount - { - get => (ushort)Settings.BottomLayerCount; - set => base.BottomLayerCount = (ushort)(Settings.BottomLayerCount = value); - } - - public override TransitionLayerTypes TransitionLayerType => TransitionLayerTypes.Software; - - public override ushort TransitionLayerCount - { - get => (ushort)Settings.TransitionLayerCount; - set => base.TransitionLayerCount = (ushort)(Settings.TransitionLayerCount = Math.Min(value, MaximumPossibleTransitionLayerCount)); - } - - public override float BottomLightOffDelay - { - get => Settings.BottomLightOffDelay; - set - { - base.BottomLightOffDelay = Settings.BottomLightOffDelay = (float)Math.Round(value, 2); - if (value > 0) - { - WaitTimeBeforeCure = 0; - WaitTimeAfterCure = 0; - WaitTimeAfterLift = 0; - } - } - } - - public override float LightOffDelay - { - get => Settings.LightOffDelay; - set - { - base.LightOffDelay = Settings.LightOffDelay = (float)Math.Round(value, 2); - if (value > 0) - { - WaitTimeBeforeCure = 0; - WaitTimeAfterCure = 0; - WaitTimeAfterLift = 0; - } - } - } - - public override float BottomWaitTimeBeforeCure - { - get => base.BottomWaitTimeBeforeCure > 0 ? base.BottomWaitTimeBeforeCure : FirstLayer?.WaitTimeBeforeCure ?? 0; - set => base.BottomWaitTimeBeforeCure = value; - } - - - public override float WaitTimeBeforeCure - { - get => Settings.RestTimeAfterRetract; - set - { - base.WaitTimeBeforeCure = Settings.RestTimeAfterRetract = Settings.RestTimeAfterRetract2 = (float)Math.Round(value, 2); - if (value > 0) - { - BottomLightOffDelay = 0; - LightOffDelay = 0; - } - } - } - - public override float BottomExposureTime - { - get => Settings.BottomExposureTime; - set => base.BottomExposureTime = Settings.BottomExposureTime = (float)Math.Round(value, 2); - } - - public override float BottomWaitTimeAfterCure - { - get => base.BottomWaitTimeAfterCure > 0 ? base.BottomWaitTimeAfterCure : FirstLayer?.WaitTimeAfterCure ?? 0; - set => base.BottomWaitTimeAfterCure = value; - } - - public override float WaitTimeAfterCure - { - get => Settings.RestTimeBeforeLift; - set - { - base.WaitTimeAfterCure = Settings.RestTimeBeforeLift = (float)Math.Round(value, 2); - if (value > 0) - { - BottomLightOffDelay = 0; - LightOffDelay = 0; - } - } - } - - public override float ExposureTime - { - get => Settings.ExposureTime; - set => base.ExposureTime = Settings.ExposureTime = (float)Math.Round(value, 2); - } - - public override float BottomLiftHeight - { - get => Math.Max(0,Settings.BottomLiftHeight - Settings.BottomLiftHeight2); - set - { - value = (float)Math.Round(value, 2); - Settings.BottomLiftHeight = (float)Math.Round(value + Settings.BottomLiftHeight2, 2); - base.BottomLiftHeight = value; - } - } - - public override float BottomLiftSpeed - { - get => Settings.BottomLiftSpeed; - set => base.BottomLiftSpeed = Settings.BottomLiftSpeed = (float)Math.Round(value, 2); - } - - public override float LiftHeight - { - get => Math.Max(0,Settings.LiftHeight - Settings.LiftHeight2); - set - { - value = (float)Math.Round(value, 2); - Settings.LiftHeight = (float)Math.Round(value + Settings.LiftHeight2, 2); - base.LiftHeight = value; - } - } - - public override float LiftSpeed - { - get => Settings.LiftSpeed; - set => base.LiftSpeed = Settings.LiftSpeed = (float)Math.Round(value, 2); - } - - public override float BottomLiftHeight2 - { - get => Settings.BottomLiftHeight2; - set - { - var bottomLiftHeight = BottomLiftHeight; - Settings.BottomLiftHeight2 = (float)Math.Round(value, 2); - BottomLiftHeight = bottomLiftHeight; - base.BottomLiftHeight2 = Settings.BottomLiftHeight2; - } - } - - public override float BottomLiftSpeed2 - { - get => Settings.BottomLiftSpeed2; - set => base.BottomLiftSpeed2 = Settings.BottomLiftSpeed2 = (float)Math.Round(value, 2); - } - - public override float LiftHeight2 - { - get => Settings.LiftHeight2; - set - { - var liftHeight = LiftHeight; - Settings.LiftHeight2 = (float)Math.Round(value, 2); - LiftHeight = liftHeight; - base.LiftHeight2 = Settings.LiftHeight2; - } - } - - public override float LiftSpeed2 - { - get => Settings.LiftSpeed2; - set => base.LiftSpeed2 = Settings.LiftSpeed2 = (float)Math.Round(value, 2); - } - - public override float BottomWaitTimeAfterLift - { - get => base.BottomWaitTimeAfterLift > 0 ? base.BottomWaitTimeAfterLift : FirstLayer?.WaitTimeAfterLift ?? 0; - set => base.BottomWaitTimeAfterLift = value; - } - - public override float WaitTimeAfterLift - { - get => Settings.RestTimeAfterLift; - set - { - base.WaitTimeAfterLift = Settings.RestTimeAfterLift = Settings.RestTimeAfterLift2 = Settings.RestTimeAfterLift3 = (float)Math.Round(value, 2); - if (value > 0) - { - BottomLightOffDelay = 0; - LightOffDelay = 0; - } - } - } - - public override float BottomRetractSpeed - { - get => Settings.BottomRetractSpeed; - set => base.BottomRetractSpeed = Settings.BottomRetractSpeed = (float)Math.Round(value, 2); - } - - public override float RetractSpeed - { - get => Settings.RetractSpeed; - set => base.RetractSpeed = Settings.RetractSpeed = (float)Math.Round(value, 2); - } - - public override float BottomRetractHeight2 - { - get => Settings.BottomRetractHeight2; - set - { - value = Math.Clamp((float)Math.Round(value, 2), 0, RetractHeightTotal); - base.BottomRetractHeight2 = Settings.BottomRetractHeight2 = value; - } - } - - public override float BottomRetractSpeed2 - { - get => Settings.BottomRetractSpeed2; - set => base.BottomRetractSpeed2 = Settings.BottomRetractSpeed2 = (float)Math.Round(value, 2); - } - - public override float RetractHeight2 - { - get => Settings.RetractHeight2; - set - { - value = Math.Clamp((float)Math.Round(value, 2), 0, RetractHeightTotal); - base.RetractHeight2 = Settings.RetractHeight2 = value; - } - } - - public override float RetractSpeed2 - { - get => Settings.RetractSpeed2; - set => base.RetractSpeed2 = Settings.RetractSpeed2 = (float)Math.Round(value, 2); - } - - public override byte BottomLightPWM - { - get => (byte)Settings.BottomLightPWM; - set => base.BottomLightPWM = (byte)(Settings.BottomLightPWM = value); - } - - public override byte LightPWM - { - get => (byte)Settings.LightPWM; - set => base.LightPWM = (byte)(Settings.LightPWM = value); - } - - public override float PrintTime - { - get => base.PrintTime; - set - { - base.PrintTime = value; - Settings.PrintTime = (uint)base.PrintTime; - } - } - - public override string MachineName - { - get => Settings.MachineName; - set => base.MachineName = Settings.MachineName = value; - } - - public override float MaterialMilliliters - { - get => base.MaterialMilliliters; - set - { - base.MaterialMilliliters = value; - Settings.MaterialMilliliters = base.MaterialMilliliters; - } - } - - public override float MaterialGrams - { - get => Settings.MaterialGrams; - set => base.MaterialGrams = Settings.MaterialGrams = (float)Math.Round(value, 3); - } - - public override float MaterialCost - { - get => (float)Math.Round(Settings.MaterialCost, 3); - set => base.MaterialCost = Settings.MaterialCost = (float)Math.Round(value, 3); - } - - public override object[] Configs - { - get - { - return new object[] { Settings }; - } - } - - - - #endregion - - #region Constructors - public CTBEncryptedFile() - { - Previews = new Preview[ThumbnailsCount]; - - /*if (Bigfoot is not null && Bigfoot[0] == 0 && File.Exists("MAGIC.ectb")) - { - using var fs = new FileStream("MAGIC.ectb", FileMode.Open); - fs.ReadBytes(Bigfoot); - fs.ReadBytes(CookieMonster); - }*/ - } - #endregion - - #region Methods - - public override void Clear() - { - base.Clear(); - - for (byte i = 0; i < ThumbnailsCount; i++) - { - Previews[i] = new Preview(); - } - } - - public override bool CanProcess(string fileFullPath) - { - if (!base.CanProcess(fileFullPath)) return false; - - try - { - using var fs = new BinaryReader(new FileStream(fileFullPath, FileMode.Open, FileAccess.Read)); - var magic = fs.ReadUInt32(); - return magic is MAGIC_CBT_ENCRYPTED; - } - catch (Exception e) - { - Debug.WriteLine(e); - } - - return false; - } - - private void SanitizeProperties() - { - Settings.PerLayerSettings = LayerManager.AllLayersAreUsingGlobalParameters - ? PERLAYER_SETTINGS_DISALLOW - : PERLAYER_SETTINGS_ALLOW; - } - - - protected override void DecodeInternally(OperationProgress progress) - { - using var inputFile = new FileStream(FileFullPath, FileMode.Open, FileAccess.Read); - Header = Helpers.Deserialize(inputFile); - Debug.WriteLine($"Header: {Header}"); - - if (Header.Magic is not MAGIC_CBT_ENCRYPTED) - { - throw new FileLoadException($"Not a valid CTB encrypted file! Magic Value: {Header.Magic}", FileFullPath); - } - - inputFile.Seek(Header.SettingsOffset, SeekOrigin.Begin); + inputFile.Seek(Header.SettingsOffset, SeekOrigin.Begin); - var encryptedBlock = inputFile.ReadBytes(Header.SettingsSize); - using (var ms = CryptExtensions.AesCryptMemoryStream(encryptedBlock, Bigfoot, CipherMode.CBC, PaddingMode.None, false, CookieMonster)) - { - Settings = Helpers.Deserialize(ms); - } + var encryptedBlock = inputFile.ReadBytes(Header.SettingsSize); + using (var ms = CryptExtensions.AesCryptMemoryStream(encryptedBlock, Bigfoot, CipherMode.CBC, PaddingMode.None, false, CookieMonster)) + { + Settings = Helpers.Deserialize(ms); + } - Debug.WriteLine($"Settings: {Settings}"); + Debug.WriteLine($"Settings: {Settings}"); - /* validate hash */ - var checksumBytes = BitExtensions.ToBytesLittleEndian(Settings.ChecksumValue); - var checksumHash = CryptExtensions.ComputeSHA256Hash(checksumBytes); - var encryptedHash = CryptExtensions.AesCryptBytes(checksumHash, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); + /* validate hash */ + var checksumBytes = BitExtensions.ToBytesLittleEndian(Settings.ChecksumValue); + var checksumHash = CryptExtensions.ComputeSHA256Hash(checksumBytes); + var encryptedHash = CryptExtensions.AesCryptBytes(checksumHash, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); - inputFile.Seek(-HASH_LENGTH, SeekOrigin.End); - var hash = inputFile.ReadBytes(HASH_LENGTH); - if (!hash.SequenceEqual(encryptedHash)) - { - throw new FileLoadException("The file checksum does not match, malformed file.", FileFullPath); - } + inputFile.Seek(-HASH_LENGTH, SeekOrigin.End); + var hash = inputFile.ReadBytes(HASH_LENGTH); + if (!hash.SequenceEqual(encryptedHash)) + { + throw new FileLoadException("The file checksum does not match, malformed file.", FileFullPath); + } - progress.Reset(OperationProgress.StatusDecodePreviews, ThumbnailsCount); + progress.Reset(OperationProgress.StatusDecodePreviews, ThumbnailsCount); + if (Thumbnails is not null) + { for (byte i = 0; i < ThumbnailsCount; i++) { uint offsetAddress = i == 0 @@ -1146,451 +1148,444 @@ namespace UVtools.Core.FileFormats inputFile.Seek(Previews[i].ImageOffset, SeekOrigin.Begin); byte[] rawImageData = new byte[Previews[i].ImageLength]; - inputFile.Read(rawImageData, 0, (int)Previews[i].ImageLength); + inputFile.Read(rawImageData, 0, (int) Previews[i].ImageLength); Thumbnails[i] = Previews[i].Decode(rawImageData); progress++; } + } - /* Read the settings and disclaimer */ - inputFile.Seek(Settings.MachineNameOffset, SeekOrigin.Begin); - var machineNameBytes = inputFile.ReadBytes(Settings.MachineNameSize); - Settings.MachineName = Encoding.UTF8.GetString(machineNameBytes); + /* Read the settings and disclaimer */ + inputFile.Seek(Settings.MachineNameOffset, SeekOrigin.Begin); + var machineNameBytes = inputFile.ReadBytes(Settings.MachineNameSize); + Settings.MachineName = Encoding.UTF8.GetString(machineNameBytes); - /* TODO: read the disclaimer here? we can really just ignore it though...*/ + /* TODO: read the disclaimer here? we can really just ignore it though...*/ - /* start gathering up the layers */ - progress.Reset(OperationProgress.StatusGatherLayers, Settings.LayerCount); - inputFile.Seek(Settings.LayerPointersOffset, SeekOrigin.Begin); + /* start gathering up the layers */ + progress.Reset(OperationProgress.StatusGatherLayers, Settings.LayerCount); + inputFile.Seek(Settings.LayerPointersOffset, SeekOrigin.Begin); - LayersPointer = new LayerPointer[Settings.LayerCount]; - for (uint layerIndex = 0; layerIndex < Settings.LayerCount; layerIndex++) + LayersPointer = new LayerPointer[Settings.LayerCount]; + for (uint layerIndex = 0; layerIndex < Settings.LayerCount; layerIndex++) + { + progress.Token.ThrowIfCancellationRequested(); + LayersPointer[layerIndex] = Helpers.Deserialize(inputFile); + Debug.WriteLine($"pointer[{layerIndex}]: {LayersPointer[layerIndex]}"); + progress++; + } + + progress.Reset(OperationProgress.StatusDecodeLayers, Settings.LayerCount); + Init(Settings.LayerCount, DecodeType == FileDecodeType.Partial); + LayersDefinition = new LayerDef[LayerCount]; + var buggyLayers = new ConcurrentBag(); + + foreach (var batch in BatchLayersIndexes()) + { + foreach (var layerIndex in batch) { progress.Token.ThrowIfCancellationRequested(); - LayersPointer[layerIndex] = Helpers.Deserialize(inputFile); - Debug.WriteLine($"pointer[{layerIndex}]: {LayersPointer[layerIndex]}"); - progress++; + + inputFile.Seek(LayersPointer[layerIndex].LayerOffset, SeekOrigin.Begin); + LayersDefinition[layerIndex] = Helpers.Deserialize(inputFile); + LayersDefinition[layerIndex].Parent = this; + if (DecodeType == FileDecodeType.Full) LayersDefinition[layerIndex].RLEData = inputFile.ReadBytes(LayersDefinition[layerIndex].DataLength); + Debug.WriteLine($"layer[{layerIndex}]: {LayersDefinition[layerIndex]}"); } - progress.Reset(OperationProgress.StatusDecodeLayers, Settings.LayerCount); - LayerManager.Init(Settings.LayerCount, DecodeType == FileDecodeType.Partial); - LayersDefinition = new LayerDef[LayerCount]; - var buggyLayers = new ConcurrentBag(); - - foreach (var batch in BatchLayersIndexes()) + if (DecodeType == FileDecodeType.Full) { - foreach (var layerIndex in batch) + Parallel.ForEach(batch, CoreSettings.ParallelOptions, layerIndex => { - progress.Token.ThrowIfCancellationRequested(); + if (progress.Token.IsCancellationRequested) return; - inputFile.Seek(LayersPointer[layerIndex].LayerOffset, SeekOrigin.Begin); - LayersDefinition[layerIndex] = Helpers.Deserialize(inputFile); - LayersDefinition[layerIndex].Parent = this; - if (DecodeType == FileDecodeType.Full) LayersDefinition[layerIndex].RLEData = inputFile.ReadBytes(LayersDefinition[layerIndex].DataLength); - Debug.WriteLine($"layer[{layerIndex}]: {LayersDefinition[layerIndex]}"); - } + var layerDef = LayersDefinition[layerIndex]; - if (DecodeType == FileDecodeType.Full) - { - Parallel.ForEach(batch, CoreSettings.ParallelOptions, layerIndex => + + if (layerDef.EncryptedDataLength > 0) { - if (progress.Token.IsCancellationRequested) return; + /* Decrypt RLE data here */ - var layerDef = LayersDefinition[layerIndex]; + var byteBuffer = new byte[layerDef.EncryptedDataLength]; + Array.Copy(layerDef.RLEData!, (int)layerDef.EncryptedDataOffset, byteBuffer, 0, + (int)layerDef.EncryptedDataLength); - - if (layerDef.EncryptedDataLength > 0) - { - /* Decrypt RLE data here */ - - var byteBuffer = new byte[layerDef.EncryptedDataLength]; - Array.Copy(layerDef.RLEData, (int)layerDef.EncryptedDataOffset, byteBuffer, 0, - (int)layerDef.EncryptedDataLength); - - byteBuffer = CryptExtensions.AesCryptBytes(byteBuffer, Bigfoot, CipherMode.CBC, - PaddingMode.None, false, CookieMonster); - Array.Copy(byteBuffer, 0, layerDef.RLEData, layerDef.EncryptedDataOffset, - layerDef.EncryptedDataLength); - } - - bool isBugged = false; - /* bug fix for Chitubox when a small layer RLE data is encrypted */ - if (layerDef.EncryptedDataLength > 0 && layerDef.RLEData.Length < RLEEncryptedMinimumLength && - layerDef.RLEData.Length % 16 != 0) - { - buggyLayers.Add((uint)layerIndex); - isBugged = true; - layerDef.RLEData = null; /* clean up RLE data */ - } - - this[layerIndex] = new Layer((uint)layerIndex, isBugged ? null : layerDef.DecodeImage((uint)layerIndex), this); - - progress.LockAndIncrement(); - }); - } - } - - if (buggyLayers.Count == LayerCount) - { - throw new FileLoadException("Unable to load this file due to Chitubox bug affecting every layer." + - "Please increase the portion of the plate in use and reslice the file."); - } - - for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - LayersDefinition[layerIndex].CopyTo(this[layerIndex]); - } - - if (!buggyLayers.IsEmpty) - { - var sortedLayerIndexes = buggyLayers.ToArray(); - Array.Sort(sortedLayerIndexes); - uint correctedLayerCount = 0; - - foreach (var layerIndex in sortedLayerIndexes) - { - int direction = layerIndex == 0 ? 1 : -1; - - /* clone from the next one that has a mat */ - for (int layerIndexForClone = (int)(layerIndex + direction); - layerIndexForClone >= 0 && layerIndexForClone < LayerCount; - layerIndexForClone += direction) - { - if (!this[layerIndexForClone].HaveImage) continue; - this[layerIndexForClone].CopyImageTo(this[layerIndex]); - correctedLayerCount++; - - /* TODO: Report to the user that a layer was cloned to work around chitubox crypto bug */ - - break; + byteBuffer = CryptExtensions.AesCryptBytes(byteBuffer, Bigfoot, CipherMode.CBC, + PaddingMode.None, false, CookieMonster); + Array.Copy(byteBuffer, 0, layerDef.RLEData!, layerDef.EncryptedDataOffset, + layerDef.EncryptedDataLength); } - } - if (correctedLayerCount < buggyLayers.Count) - { - throw new FileLoadException( - "Unable to load this file due to an Chitubox bug and the impossibility to auto correct some of these layers.\n" + - "Please increase the portion of the plate in use and re-slice the file."); - } + bool isBugged = false; + /* bug fix for Chitubox when a small layer RLE data is encrypted */ + if (layerDef.EncryptedDataLength > 0 && layerDef.RLEData!.Length < RLEEncryptedMinimumLength && + layerDef.RLEData.Length % 16 != 0) + { + buggyLayers.Add((uint)layerIndex); + isBugged = true; + layerDef.RLEData = null; /* clean up RLE data */ + } + + if (isBugged) + { + this[layerIndex] = new Layer((uint)layerIndex, this); + } + else + { + this[layerIndex] = new Layer((uint) layerIndex, layerDef.DecodeImage((uint) layerIndex), this); + } + + progress.LockAndIncrement(); + }); } - //inputFile.ReadBytes(HashLength); } - protected override void EncodeInternally(OperationProgress progress) + if (buggyLayers.Count == LayerCount) { -#if !DEBUG - if (Settings.LayerPointersOffset > 0) throw new NotSupportedException(Preamble); -#endif - - using var outputFile = new FileStream(FileFullPath, FileMode.Create, FileAccess.Write); - //uint currentOffset = 0; - /* Create the file header and fill out what we can. SignatureOffset will have to be populated later - * this will be the last thing written to file */ - Header.SettingsSize = (uint)Helpers.Serializer.SizeOf(Settings); - Header.SettingsOffset = (uint)Helpers.Serializer.SizeOf(Header); - - SanitizeProperties(); - - if (Settings.LayerXorKey == 0) - { - Settings.LayerXorKey = LAYER_XOR_KEY; - } - - outputFile.Seek(Header.SettingsOffset + Header.SettingsSize, SeekOrigin.Begin); - - progress.Reset(OperationProgress.StatusEncodePreviews, 2); - - Mat[] thumbnails = { GetThumbnail(true), GetThumbnail(false) }; - for (byte i = 0; i < thumbnails.Length; i++) - { - var image = thumbnails[i]; - if (image is null) continue; - - Preview preview = new() - { - ResolutionX = (uint)image.Width, - ResolutionY = (uint)image.Height, - }; - - var previewBytes = preview.Encode(image); - -#if !DEBUG - if (Settings.LayerPointersOffset > 0) { Bigfoot = new byte[32]; CookieMonster = new byte[16]; } -#endif - - if (previewBytes.Length == 0) continue; - - if (i == 0) - { - Settings.LargePreviewOffset = (uint)outputFile.Position; - } - else - { - Settings.SmallPreviewOffset = (uint)outputFile.Position; - } - - preview.ImageOffset = (uint)(outputFile.Position + Helpers.Serializer.SizeOf(preview)); - - Helpers.SerializeWriteFileStream(outputFile, preview); - outputFile.WriteBytes(previewBytes); - progress++; - } - - Settings.MachineNameOffset = (uint)outputFile.Position; - Settings.MachineNameSize = (uint)Settings.MachineName.Length; - var machineNameBytes = Encoding.UTF8.GetBytes(Settings.MachineName); - - outputFile.WriteBytes(machineNameBytes); - - Settings.DisclaimerOffset = (uint)outputFile.Position; - Settings.DisclaimerSize = CTB_DISCLAIMER_SIZE; - outputFile.WriteBytes(Encoding.UTF8.GetBytes(CTB_DISCLAIMER)); - - Settings.LayerPointersOffset = (uint)outputFile.Position; - - /* we'll write this after we write out all of the layers ... */ - LayersPointer = new LayerPointer[LayerCount]; - LayersDefinition = new LayerDef[LayerCount]; - - uint layerTableSize = (uint)Helpers.Serializer.SizeOf(new LayerPointer()) * LayerCount; - - outputFile.Seek(outputFile.Position + layerTableSize, SeekOrigin.Begin); - - progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); - Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, layerIndex => - { - if (progress.Token.IsCancellationRequested) return; - var layerDef = new LayerDef(this, this[layerIndex]); - using (var mat = this[layerIndex].LayerMat) - { - layerDef.EncodeImage(mat, (uint)layerIndex); - LayersDefinition[layerIndex] = layerDef; - } - - progress.LockAndIncrement(); - }); - - /* -#if !DEBUG - Bigfoot = new byte[32]; CookieMonster = new byte[16]; -#endif - */ - - progress.Reset(OperationProgress.StatusWritingFile, LayerCount); - for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - var layerDef = LayersDefinition[layerIndex]; - LayersPointer[layerIndex] = new LayerPointer((uint)outputFile.Position); - - layerDef.LayerDefOffset = LayersPointer[layerIndex].LayerOffset + LayerDef.TABLE_SIZE; - Helpers.SerializeWriteFileStream(outputFile, layerDef); - outputFile.WriteBytes(layerDef.RLEData); - progress++; - } - - /* write the final hash */ - var hash = CryptExtensions.ComputeSHA256Hash(BitExtensions.ToBytesLittleEndian(Settings.ChecksumValue)); - var encryptedHash = CryptExtensions.AesCryptBytes(hash, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); - Header.SignatureOffset = (uint)outputFile.Position; - Header.SignatureSize = (uint)encryptedHash.Length; - outputFile.WriteBytes(encryptedHash); - - // Rewind - - // Layer pointers - outputFile.Seek(Settings.LayerPointersOffset, SeekOrigin.Begin); - for (uint layerIndex = 0; layerIndex < LayersPointer.Length; layerIndex++) - { - Helpers.SerializeWriteFileStream(outputFile, LayersPointer[layerIndex]); - } - - // Settings - outputFile.Seek(Header.SettingsOffset, SeekOrigin.Begin); - var settingsBytes = Helpers.Serialize(Settings).ToArray(); - var encryptedSettings = CryptExtensions.AesCryptBytes(settingsBytes, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); - outputFile.WriteBytes(encryptedSettings); - - // Header - outputFile.Seek(0, SeekOrigin.Begin); - Helpers.SerializeWriteFileStream(outputFile, Header); + throw new FileLoadException("Unable to load this file due to Chitubox bug affecting every layer." + + "Please increase the portion of the plate in use and reslice the file."); } - protected override void PartialSaveInternally(OperationProgress progress) + for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { - SanitizeProperties(); - - using var outputFile = new FileStream(FileFullPath, FileMode.Open, FileAccess.Write); - - outputFile.Seek(Header.SettingsOffset, SeekOrigin.Begin); - var settingsBytes = Helpers.Serialize(Settings).ToArray(); - var encryptedSettings = CryptExtensions.AesCryptBytes(settingsBytes, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); - outputFile.WriteBytes(encryptedSettings); - - for (uint layerIndex = 0; layerIndex < LayersPointer.Length; layerIndex++) - { - LayersDefinition[layerIndex].SetFrom(this[layerIndex]); - outputFile.Seek(LayersPointer[layerIndex].LayerOffset, SeekOrigin.Begin); - Helpers.SerializeWriteFileStream(outputFile, LayersDefinition[layerIndex]); - } + LayersDefinition[layerIndex].CopyTo(this[layerIndex]); } - #endregion - #region Static Methods - public static void CryptFile(string filePath) + if (!buggyLayers.IsEmpty) { - using var msReader = new MemoryStream(File.ReadAllBytes(filePath)); - using var msWriter = new MemoryStream(); - msReader.CopyTo(msWriter); - msWriter.Position = 0; - msReader.Position = 0; - var writer = new BinaryWriter(msWriter); - var reader = new BinaryReader(msReader); + var sortedLayerIndexes = buggyLayers.ToArray(); + Array.Sort(sortedLayerIndexes); + uint correctedLayerCount = 0; - /* magic */ - var magic = reader.ReadUInt32(); - if (magic != MAGIC_CBT_ENCRYPTED) + foreach (var layerIndex in sortedLayerIndexes) { - Console.Write("File does not appear to be an encrypted CTB. Aborting."); - return; - } + int direction = layerIndex == 0 ? 1 : -1; - writer.Write(magic); - var headerLength = reader.ReadUInt32(); - writer.Write(headerLength); - var headerOffset = reader.ReadUInt32(); - writer.Write(headerOffset); - - var currentPos = msReader.Position; - msReader.Seek(headerOffset + 24, SeekOrigin.Begin); // Paddings of 0 - - - var encrypt = reader.ReadUInt32() == 0 && reader.ReadUInt32() == 0; // Both paddings must be zero so we know we need to encrypt! - msReader.Seek(currentPos, SeekOrigin.Begin); - - Console.Write($"Crypt mode: {(encrypt ? "Encrypting" : "Decrypting")}"); - - /* pass through rest of data until encrypted header */ - var bytesToPassthru = headerOffset - msReader.Position; - var temp = reader.ReadBytes((int)bytesToPassthru); - writer.Write(temp); - uint printerNameLength = 0; - - var originalHeader = reader.ReadBytes((int)headerLength); - - if (encrypt) - { - printerNameLength = BitExtensions.ToUIntLittleEndian(originalHeader, 164); - } - - /* decrypt header with recovered keys */ - var cryptedData = CryptExtensions.AesCryptBytes(originalHeader, Bigfoot, CipherMode.CBC, PaddingMode.None, encrypt, CookieMonster); - writer.Write(cryptedData); - - /* get machine name length */ - if (!encrypt) - { - printerNameLength = BitExtensions.ToUIntLittleEndian(cryptedData, 164); - } - - /* pass through the next 2 dwords */ - writer.Write(reader.ReadUInt32()); - writer.Write(reader.ReadUInt32()); - - /* get offset and length of next section (not encrypted) */ - var nextOffset = reader.ReadUInt32(); - var nextLength = reader.ReadUInt32(); - - writer.Write(nextOffset); - writer.Write(nextLength); - - /* how many bytes from current position till the next offset */ - bytesToPassthru = nextOffset - msReader.Position; - writer.Write(reader.ReadBytes((int)bytesToPassthru)); - - /* passthrough this whole block */ - writer.Write(reader.ReadBytes((int)nextLength)); - - /* pass throught the next 2 dwords */ - writer.Write(reader.ReadUInt32()); - writer.Write(reader.ReadUInt32()); - - /* get offset and length of next section (not encrypted) */ - nextOffset = reader.ReadUInt32(); - nextLength = reader.ReadUInt32(); - - writer.Write(nextOffset); - writer.Write(nextLength); - - /* how many bytes from current position till the next offset */ - bytesToPassthru = nextOffset - msReader.Position; - writer.Write(reader.ReadBytes((int)bytesToPassthru)); - - /* passthrough this whole block */ - writer.Write(reader.ReadBytes((int)nextLength)); - - /* passes printer name and disclaimer */ - var x = reader.ReadBytes((int)(CTB_DISCLAIMER_SIZE + printerNameLength)); - writer.Write(x); - - /* we're at the layer offset table now */ - var layerOffsets = new List(); - var startOfTable = msReader.Position; - ulong layerOffset = reader.ReadUInt64(); - ulong firstLayer = layerOffset; - while (msReader.Position < (int)firstLayer) - { - layerOffsets.Add(layerOffset); - reader.ReadUInt64(); - layerOffset = reader.ReadUInt64(); - } - - msReader.Position = (int)startOfTable; - - /* copy the rest of the file to the output memory stream, we'll decrypt layers next */ - writer.Write(reader.ReadBytes((int)(msReader.Length - msReader.Position))); - - byte[] cryptedFile = msWriter.ToArray(); - var layerCounter = 0; - foreach (var offset in layerOffsets) - { - layerCounter++; - msReader.Position = (int)(offset + 0x10); - - msReader.Position += 0x10; - var encryptedOffset = reader.ReadUInt32(); - var encryptedLength = reader.ReadUInt32(); - msReader.Position -= 0x18; - - if (encryptedLength > 0) + /* clone from the next one that has a mat */ + for (int layerIndexForClone = (int)(layerIndex + direction); + layerIndexForClone >= 0 && layerIndexForClone < LayerCount; + layerIndexForClone += direction) { - Debug.WriteLine($"Layer {layerCounter} has {encryptedLength} bytes of encrypted data. (Layer data offset: {encryptedOffset})"); - var layerDataOffset = reader.ReadUInt32(); - - msReader.Position = layerDataOffset + encryptedOffset; - - var encryptedLayerData = reader.ReadBytes((int)encryptedLength); - var decryptedLayerData = CryptExtensions.AesCryptBytes(encryptedLayerData, Bigfoot, CipherMode.CBC, PaddingMode.None, encrypt, CookieMonster); - - Array.Copy(decryptedLayerData, 0, cryptedFile, layerDataOffset + encryptedOffset, encryptedLength); - - - /* update encrypted markers in the layer header */ - cryptedFile.AsSpan((int)(layerDataOffset - 0x38), 8).Fill(0); - } - else - { - Debug.WriteLine($"Layer {layerCounter} is not encrypted"); + if (!this[layerIndexForClone].HaveImage) continue; + this[layerIndexForClone].CopyImageTo(this[layerIndex]); + correctedLayerCount++; + + /* TODO: Report to the user that a layer was cloned to work around chitubox crypto bug */ + + break; } } - /* last 20 bytes are an encrypted sha256 */ - var cipheredHash = cryptedFile[^0x20..]; - - var plainHash = CryptExtensions.AesCryptBytes(cipheredHash, Bigfoot, CipherMode.CBC, PaddingMode.None, encrypt, CookieMonster); - Array.Copy(plainHash, 0, cryptedFile, cryptedFile.Length - 0x20, 0x20); - - File.WriteAllBytes(filePath, cryptedFile); + if (correctedLayerCount < buggyLayers.Count) + { + throw new FileLoadException( + "Unable to load this file due to an Chitubox bug and the impossibility to auto correct some of these layers.\n" + + "Please increase the portion of the plate in use and re-slice the file."); + } } - #endregion + //inputFile.ReadBytes(HashLength); } -} + + protected override void EncodeInternally(OperationProgress progress) + { + using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.Write); + //uint currentOffset = 0; + /* Create the file header and fill out what we can. SignatureOffset will have to be populated later + * this will be the last thing written to file */ + Header.SettingsSize = (uint)Helpers.Serializer.SizeOf(Settings); + Header.SettingsOffset = (uint)Helpers.Serializer.SizeOf(Header); + + SanitizeProperties(); + + if (Settings.LayerXorKey == 0) + { + Settings.LayerXorKey = LAYER_XOR_KEY; + } + + outputFile.Seek(Header.SettingsOffset + Header.SettingsSize, SeekOrigin.Begin); + + progress.Reset(OperationProgress.StatusEncodePreviews, 2); + + Mat?[] thumbnails = { GetThumbnail(true), GetThumbnail(false) }; + for (byte i = 0; i < thumbnails.Length; i++) + { + var image = thumbnails[i]; + if (image is null) continue; + + Preview preview = new() + { + ResolutionX = (uint)image.Width, + ResolutionY = (uint)image.Height, + }; + + var previewBytes = preview.Encode(image); + + if (previewBytes.Length == 0) continue; + + if (i == 0) + { + Settings.LargePreviewOffset = (uint)outputFile.Position; + } + else + { + Settings.SmallPreviewOffset = (uint)outputFile.Position; + } + + preview.ImageOffset = (uint)(outputFile.Position + Helpers.Serializer.SizeOf(preview)); + + Helpers.SerializeWriteFileStream(outputFile, preview); + outputFile.WriteBytes(previewBytes); + progress++; + } + + Settings.MachineNameOffset = (uint)outputFile.Position; + Settings.MachineNameSize = (uint)Settings.MachineName.Length; + var machineNameBytes = Encoding.UTF8.GetBytes(Settings.MachineName); + + outputFile.WriteBytes(machineNameBytes); + + Settings.DisclaimerOffset = (uint)outputFile.Position; + Settings.DisclaimerSize = CTB_DISCLAIMER_SIZE; + outputFile.WriteBytes(Encoding.UTF8.GetBytes(CTB_DISCLAIMER)); + + Settings.LayerPointersOffset = (uint)outputFile.Position; + + /* we'll write this after we write out all of the layers ... */ + LayersPointer = new LayerPointer[LayerCount]; + LayersDefinition = new LayerDef[LayerCount]; + + uint layerTableSize = (uint)Helpers.Serializer.SizeOf(new LayerPointer()) * LayerCount; + + outputFile.Seek(outputFile.Position + layerTableSize, SeekOrigin.Begin); + + progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); + Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + var layerDef = new LayerDef(this, this[layerIndex]); + using (var mat = this[layerIndex].LayerMat) + { + layerDef.EncodeImage(mat!, (uint)layerIndex); + LayersDefinition[layerIndex] = layerDef; + } + + progress.LockAndIncrement(); + }); + + progress.Reset(OperationProgress.StatusWritingFile, LayerCount); + for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + var layerDef = LayersDefinition[layerIndex]; + LayersPointer[layerIndex] = new LayerPointer((uint)outputFile.Position); + + layerDef.LayerDefOffset = LayersPointer[layerIndex].LayerOffset + LayerDef.TABLE_SIZE; + Helpers.SerializeWriteFileStream(outputFile, layerDef); + outputFile.WriteBytes(layerDef.RLEData!); + progress++; + } + + /* write the final hash */ + var hash = CryptExtensions.ComputeSHA256Hash(BitExtensions.ToBytesLittleEndian(Settings.ChecksumValue)); + var encryptedHash = CryptExtensions.AesCryptBytes(hash, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); + Header.SignatureOffset = (uint)outputFile.Position; + Header.SignatureSize = (uint)encryptedHash.Length; + outputFile.WriteBytes(encryptedHash); + + // Rewind + + // Layer pointers + outputFile.Seek(Settings.LayerPointersOffset, SeekOrigin.Begin); + for (uint layerIndex = 0; layerIndex < LayersPointer.Length; layerIndex++) + { + Helpers.SerializeWriteFileStream(outputFile, LayersPointer[layerIndex]); + } + + // Settings + outputFile.Seek(Header.SettingsOffset, SeekOrigin.Begin); + var settingsBytes = Helpers.Serialize(Settings).ToArray(); + var encryptedSettings = CryptExtensions.AesCryptBytes(settingsBytes, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); + outputFile.WriteBytes(encryptedSettings); + + // Header + outputFile.Seek(0, SeekOrigin.Begin); + Helpers.SerializeWriteFileStream(outputFile, Header); + } + + protected override void PartialSaveInternally(OperationProgress progress) + { + SanitizeProperties(); + + using var outputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Write); + + outputFile.Seek(Header.SettingsOffset, SeekOrigin.Begin); + var settingsBytes = Helpers.Serialize(Settings).ToArray(); + var encryptedSettings = CryptExtensions.AesCryptBytes(settingsBytes, Bigfoot, CipherMode.CBC, PaddingMode.None, true, CookieMonster); + outputFile.WriteBytes(encryptedSettings); + + for (uint layerIndex = 0; layerIndex < LayersPointer!.Length; layerIndex++) + { + LayersDefinition![layerIndex].SetFrom(this[layerIndex]); + outputFile.Seek(LayersPointer[layerIndex].LayerOffset, SeekOrigin.Begin); + Helpers.SerializeWriteFileStream(outputFile, LayersDefinition[layerIndex]); + } + } + #endregion + + #region Static Methods + public static void CryptFile(string filePath) + { + using var msReader = new MemoryStream(File.ReadAllBytes(filePath)); + using var msWriter = new MemoryStream(); + msReader.CopyTo(msWriter); + msWriter.Position = 0; + msReader.Position = 0; + var writer = new BinaryWriter(msWriter); + var reader = new BinaryReader(msReader); + + /* magic */ + var magic = reader.ReadUInt32(); + if (magic != MAGIC_CBT_ENCRYPTED) + { + Console.Write("File does not appear to be an encrypted CTB. Aborting."); + return; + } + + writer.Write(magic); + var headerLength = reader.ReadUInt32(); + writer.Write(headerLength); + var headerOffset = reader.ReadUInt32(); + writer.Write(headerOffset); + + var currentPos = msReader.Position; + msReader.Seek(headerOffset + 24, SeekOrigin.Begin); // Paddings of 0 + + + var encrypt = reader.ReadUInt32() == 0 && reader.ReadUInt32() == 0; // Both paddings must be zero so we know we need to encrypt! + msReader.Seek(currentPos, SeekOrigin.Begin); + + Console.Write($"Crypt mode: {(encrypt ? "Encrypting" : "Decrypting")}"); + + /* pass through rest of data until encrypted header */ + var bytesToPassthru = headerOffset - msReader.Position; + var temp = reader.ReadBytes((int)bytesToPassthru); + writer.Write(temp); + uint printerNameLength = 0; + + var originalHeader = reader.ReadBytes((int)headerLength); + + if (encrypt) + { + printerNameLength = BitExtensions.ToUIntLittleEndian(originalHeader, 164); + } + + /* decrypt header with recovered keys */ + var cryptedData = CryptExtensions.AesCryptBytes(originalHeader, Bigfoot, CipherMode.CBC, PaddingMode.None, encrypt, CookieMonster); + writer.Write(cryptedData); + + /* get machine name length */ + if (!encrypt) + { + printerNameLength = BitExtensions.ToUIntLittleEndian(cryptedData, 164); + } + + /* pass through the next 2 dwords */ + writer.Write(reader.ReadUInt32()); + writer.Write(reader.ReadUInt32()); + + /* get offset and length of next section (not encrypted) */ + var nextOffset = reader.ReadUInt32(); + var nextLength = reader.ReadUInt32(); + + writer.Write(nextOffset); + writer.Write(nextLength); + + /* how many bytes from current position till the next offset */ + bytesToPassthru = nextOffset - msReader.Position; + writer.Write(reader.ReadBytes((int)bytesToPassthru)); + + /* passthrough this whole block */ + writer.Write(reader.ReadBytes((int)nextLength)); + + /* pass throught the next 2 dwords */ + writer.Write(reader.ReadUInt32()); + writer.Write(reader.ReadUInt32()); + + /* get offset and length of next section (not encrypted) */ + nextOffset = reader.ReadUInt32(); + nextLength = reader.ReadUInt32(); + + writer.Write(nextOffset); + writer.Write(nextLength); + + /* how many bytes from current position till the next offset */ + bytesToPassthru = nextOffset - msReader.Position; + writer.Write(reader.ReadBytes((int)bytesToPassthru)); + + /* passthrough this whole block */ + writer.Write(reader.ReadBytes((int)nextLength)); + + /* passes printer name and disclaimer */ + var x = reader.ReadBytes((int)(CTB_DISCLAIMER_SIZE + printerNameLength)); + writer.Write(x); + + /* we're at the layer offset table now */ + var layerOffsets = new List(); + var startOfTable = msReader.Position; + ulong layerOffset = reader.ReadUInt64(); + ulong firstLayer = layerOffset; + while (msReader.Position < (int)firstLayer) + { + layerOffsets.Add(layerOffset); + reader.ReadUInt64(); + layerOffset = reader.ReadUInt64(); + } + + msReader.Position = (int)startOfTable; + + /* copy the rest of the file to the output memory stream, we'll decrypt layers next */ + writer.Write(reader.ReadBytes((int)(msReader.Length - msReader.Position))); + + byte[] cryptedFile = msWriter.ToArray(); + var layerCounter = 0; + foreach (var offset in layerOffsets) + { + layerCounter++; + msReader.Position = (int)(offset + 0x10); + + msReader.Position += 0x10; + var encryptedOffset = reader.ReadUInt32(); + var encryptedLength = reader.ReadUInt32(); + msReader.Position -= 0x18; + + if (encryptedLength > 0) + { + Debug.WriteLine($"Layer {layerCounter} has {encryptedLength} bytes of encrypted data. (Layer data offset: {encryptedOffset})"); + var layerDataOffset = reader.ReadUInt32(); + + msReader.Position = layerDataOffset + encryptedOffset; + + var encryptedLayerData = reader.ReadBytes((int)encryptedLength); + var decryptedLayerData = CryptExtensions.AesCryptBytes(encryptedLayerData, Bigfoot, CipherMode.CBC, PaddingMode.None, encrypt, CookieMonster); + + Array.Copy(decryptedLayerData, 0, cryptedFile, layerDataOffset + encryptedOffset, encryptedLength); + + + /* update encrypted markers in the layer header */ + cryptedFile.AsSpan((int)(layerDataOffset - 0x38), 8).Fill(0); + } + else + { + Debug.WriteLine($"Layer {layerCounter} is not encrypted"); + } + } + + /* last 20 bytes are an encrypted sha256 */ + var cipheredHash = cryptedFile[^0x20..]; + + var plainHash = CryptExtensions.AesCryptBytes(cipheredHash, Bigfoot, CipherMode.CBC, PaddingMode.None, encrypt, CookieMonster); + Array.Copy(plainHash, 0, cryptedFile, cryptedFile.Length - 0x20, 0x20); + + File.WriteAllBytes(filePath, cryptedFile); + } + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/FileFormats/CWSFile.cs b/UVtools.Core/FileFormats/CWSFile.cs index efefca6..3bb3d99 100644 --- a/UVtools.Core/FileFormats/CWSFile.cs +++ b/UVtools.Core/FileFormats/CWSFile.cs @@ -6,9 +6,12 @@ * of this license document, but changing it is not allowed. */ +using Emgu.CV; +using Emgu.CV.CvEnum; using System; using System.Collections.Generic; using System.ComponentModel; +using System.Drawing; using System.IO; using System.IO.Compression; using System.Linq; @@ -16,653 +19,647 @@ using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Xml.Serialization; -using Emgu.CV; -using Emgu.CV.CvEnum; using UVtools.Core.Extensions; using UVtools.Core.GCode; using UVtools.Core.Layers; using UVtools.Core.Operations; -namespace UVtools.Core.FileFormats +namespace UVtools.Core.FileFormats; + +#region Wanhao + +[Serializable] +[XmlRoot(ElementName = "manifest")] +public sealed class CWSManifest { - #region Wanhao + public const string FileName = "manifest.xml"; - [Serializable] - [XmlRoot(ElementName = "manifest")] - public sealed class CWSManifest + [XmlAttribute] + public byte FileVersion { get; set; } = 1; + + public sealed class Model { - public const string FileName = "manifest.xml"; + [XmlElement("name")] + public string? Name { get; set; } - [XmlAttribute] - public byte FileVersion { get; set; } = 1; - - public sealed class Model - { - [XmlElement("name")] - public string Name { get; set; } - - [XmlElement("tag")] - public string Tag { get; set; } - } - - public sealed class Slice - { - [XmlElement("name")] - public string Name { get; set; } - - public Slice() - { - } - - public Slice(string name) - { - Name = name; - } - } - - [XmlArrayItem("model")] - public Model[] Models { get; set; } - public Slice[] Slices { get; set; } - - public Slice SliceProfile { get; set; } = new Slice(); - public Slice GCode { get; set; } = new Slice(); + [XmlElement("tag")] + public string? Tag { get; set; } } - [Serializable] - [XmlRoot(ElementName = "SliceBuildConfig")] - public sealed class CWSSliceBuildConfig + public sealed class Slice { - public const string GCODESTART = - ";********** Header Start ********\r\n" + - "G21 ; Set units to be mm\r\n" + - "G91 ; Relative Positioning\r\n" + - "M17 ; Enable motors\r\n" + - "; G28 Z ; Auto-home on print - commented by default"; + [XmlElement("name")] + public string? Name { get; set; } - public const string GCODEEND = - ";********** Footer ********\r\n" + - "@cmdCloseShutter ; UV off\r\n" + - "G4 P0 ; wait for last lift to complete\r\n" + - "G1 Z40.0 F150.0 ; lift model clear of resin\r\n" + - "G4 P0 ; sync\r\n" + - "M18 ; Disable Motors\r\n" + - ";<Completed>"; - - public const string GCODELIFT = - ";********** Lift Sequence %d$CURSLICE ********\r\n" + - "G1 Z($ZLiftDist * $ZDir) F{$CURSLICE < $NumFirstLayers?$ZBottomLiftRate:$ZLiftRate}\r\n;" + - "<Takes> {$CURSLICE<$NumFirstLayers?%d($ZLiftDist*1000*60/$ZBottomLiftRate):%d($ZLiftDist*1000*60/$ZLiftRate)}\r\n" + - "G4 P0 ; Wait for lift rise to complete\r\n;" + - "<Delay> %d$TopTime\r\n" + - "G1 Z(($LayerThickness-$ZLiftDist) * $ZDir) F$ZRetractRate\r\n" + - ";<Takes> %d(($ZLiftDist*1000-$LayerThickness*1000)*60/$ZRetractRate)"; - - public const string GCODELAYER = - ";********** Layer %d$CURSLICE ********\r\n" + - ";<Slice> %d$CURSLICE\r\n" + - "@cmdOpenShutter ; UV on\r\n" + - ";{$CURSLICE<$NumFirstLayers?<Delay> %d$FirstLayerTime:<Delay> %d$LayerTime}\r\n" + - "@cmdCloseShutter ; UV off\r\n" + - ";<Slice> Blank\r\n" + - ";<Delay> %d$BlankTime"; - - public sealed class InkConfig + public Slice() { - public string Name { get; set; } - public float SliceHeight { get; set; } - public uint LayerTime { get; set; } - public uint FirstLayerTime { get; set; } - public ushort NumberofBottomLayers { get; set; } - public float ResinPriceL { get; set; } } - [XmlAttribute] - public byte FileVersion { get; set; } = 2; - - public float DotsPermmX { get; set; } = 21.164f; - public float DotsPermmY { get; set; } = 21.164f; - - public ushort XResolution { get; set; } - public ushort YResolution { get; set; } - public uint BlankTime { get; set; } - public uint TopTime { get; set; } - public uint PlatformTemp { get; set; } = 75; - public byte ExportSVG { get; set; } - public string Export { get; set; } = "False"; - public string ExportPNG { get; set; } = "False"; - public string CalcSliceSize { get; set; } = "False"; - public uint XOffset { get; set; } - public uint YOffset { get; set; } - public string Direction { get; set; } = "Bottom_Up"; - public float LiftDistance { get; set; } = 6; - public float SlideTiltValue { get; set; } - public uint SettleTime { get; set; } = 1600; - public string AntiAliasing { get; set; } = "True"; - public string UseMainLiftGCode { get; set; } = "False"; - public uint AntiAliasingValue { get; set; } = 4; - public float LiftFeedRate { get; set; } = 40; - public float BottomLiftFeedRate { get; set; } = 25; - public float LiftRetractRate { get; set; } = 80; - public string ExportOption { get; set; } = "ZIP"; - public string RenderOutlines { get; set; } = "False"; - public uint OutlineWidth_Inset { get; set; } = 2; - public uint OutlineWidth_Outset { get; set; } - public string FlipX { get; set; } = "False"; - public string FlipY { get; set; } = "False"; - - public string Notes { get; set; } = - "Use these as a place to start; tune them for your specific printer and resins.\r\nAlways run a calibration on your resin to dial in for your machine.\r\nProfiles created by Earl Miller for CW v1.0.0.75."; - - public string GCodeHeader { get; set; } = GCODESTART; - public string GCodeFooter { get; set; } = GCODEEND; - public string GCodeLift { get; set; } = GCODELIFT; - public string GCodeLayer { get; set; } = GCODELAYER; - - [XmlElement("InkConfig")] - public List InkConfigs { get; set; } - - public string SelectedInk { get; set; } - public uint MinTestExposure { get; set; } = 5000; - public uint TestExposureStep { get; set; } = 100; - public string ExportPreview { get; set; } = "None"; - public string UseMask { get; set; } = "False"; - public string MaskFilename { get; set; } = "False"; + public Slice(string name) + { + Name = name; + } } + [XmlArrayItem("model")] + public Model[]? Models { get; set; } + public Slice[]? Slices { get; set; } + + public Slice SliceProfile { get; set; } = new(); + public Slice GCode { get; set; } = new(); +} + +[Serializable] +[XmlRoot(ElementName = "SliceBuildConfig")] +public sealed class CWSSliceBuildConfig +{ + public const string GCODESTART = + ";********** Header Start ********\r\n" + + "G21 ; Set units to be mm\r\n" + + "G91 ; Relative Positioning\r\n" + + "M17 ; Enable motors\r\n" + + "; G28 Z ; Auto-home on print - commented by default"; + + public const string GCODEEND = + ";********** Footer ********\r\n" + + "@cmdCloseShutter ; UV off\r\n" + + "G4 P0 ; wait for last lift to complete\r\n" + + "G1 Z40.0 F150.0 ; lift model clear of resin\r\n" + + "G4 P0 ; sync\r\n" + + "M18 ; Disable Motors\r\n" + + ";<Completed>"; + + public const string GCODELIFT = + ";********** Lift Sequence %d$CURSLICE ********\r\n" + + "G1 Z($ZLiftDist * $ZDir) F{$CURSLICE < $NumFirstLayers?$ZBottomLiftRate:$ZLiftRate}\r\n;" + + "<Takes> {$CURSLICE<$NumFirstLayers?%d($ZLiftDist*1000*60/$ZBottomLiftRate):%d($ZLiftDist*1000*60/$ZLiftRate)}\r\n" + + "G4 P0 ; Wait for lift rise to complete\r\n;" + + "<Delay> %d$TopTime\r\n" + + "G1 Z(($LayerThickness-$ZLiftDist) * $ZDir) F$ZRetractRate\r\n" + + ";<Takes> %d(($ZLiftDist*1000-$LayerThickness*1000)*60/$ZRetractRate)"; + + public const string GCODELAYER = + ";********** Layer %d$CURSLICE ********\r\n" + + ";<Slice> %d$CURSLICE\r\n" + + "@cmdOpenShutter ; UV on\r\n" + + ";{$CURSLICE<$NumFirstLayers?<Delay> %d$FirstLayerTime:<Delay> %d$LayerTime}\r\n" + + "@cmdCloseShutter ; UV off\r\n" + + ";<Slice> Blank\r\n" + + ";<Delay> %d$BlankTime"; + + public sealed class InkConfig + { + public string? Name { get; set; } + public float SliceHeight { get; set; } + public uint LayerTime { get; set; } + public uint FirstLayerTime { get; set; } + public ushort NumberofBottomLayers { get; set; } + public float ResinPriceL { get; set; } + } + + [XmlAttribute] + public byte FileVersion { get; set; } = 2; + + public float DotsPermmX { get; set; } = 21.164f; + public float DotsPermmY { get; set; } = 21.164f; + + public ushort XResolution { get; set; } + public ushort YResolution { get; set; } + public uint BlankTime { get; set; } + public uint TopTime { get; set; } + public uint PlatformTemp { get; set; } = 75; + public byte ExportSVG { get; set; } + public string Export { get; set; } = "False"; + public string ExportPNG { get; set; } = "False"; + public string CalcSliceSize { get; set; } = "False"; + public uint XOffset { get; set; } + public uint YOffset { get; set; } + public string Direction { get; set; } = "Bottom_Up"; + public float LiftDistance { get; set; } = 6; + public float SlideTiltValue { get; set; } + public uint SettleTime { get; set; } = 1600; + public string AntiAliasing { get; set; } = "True"; + public string UseMainLiftGCode { get; set; } = "False"; + public uint AntiAliasingValue { get; set; } = 4; + public float LiftFeedRate { get; set; } = 40; + public float BottomLiftFeedRate { get; set; } = 25; + public float LiftRetractRate { get; set; } = 80; + public string ExportOption { get; set; } = "ZIP"; + public string RenderOutlines { get; set; } = "False"; + public uint OutlineWidth_Inset { get; set; } = 2; + public uint OutlineWidth_Outset { get; set; } + public string FlipX { get; set; } = "False"; + public string FlipY { get; set; } = "False"; + + public string Notes { get; set; } = + "Use these as a place to start; tune them for your specific printer and resins.\r\nAlways run a calibration on your resin to dial in for your machine.\r\nProfiles created by Earl Miller for CW v1.0.0.75."; + + public string GCodeHeader { get; set; } = GCODESTART; + public string GCodeFooter { get; set; } = GCODEEND; + public string GCodeLift { get; set; } = GCODELIFT; + public string GCodeLayer { get; set; } = GCODELAYER; + + [XmlElement("InkConfig")] + public List? InkConfigs { get; set; } + + public string? SelectedInk { get; set; } + public uint MinTestExposure { get; set; } = 5000; + public uint TestExposureStep { get; set; } = 100; + public string ExportPreview { get; set; } = "None"; + public string UseMask { get; set; } = "False"; + public string MaskFilename { get; set; } = "False"; +} + +#endregion + +public class CWSFile : FileFormat +{ + #region Constants + + public const string GCodeStart = "G28 ;Auto Home{0}" + + "G21 ;Set units to be mm{0}" + + "G91 ;Relative Positioning{0}" + + "M17 ;Enable motors{0}" + + " Blank{0}" + + "M106 S0{0}{0}"; + + public const string GCodeEnd = "M106 S0 ;UV off{0}" + + "G1 Z{1}{0}" + + "{0}M18 ;Disable Motors{0}" + + ";{0}"; + + public const string GCodeKeywordSlice = ";"; + public const string GCodeKeywordSliceBlank = "; Blank"; + public const string GCodeKeywordDelay = ";"; + public const string GCodeKeywordTakes = ";"; + + public const string WanhaoStartGCode = + ";********** Header Start ********{0}" + + "G21 ; Set units to be mm{0}" + + "G91 ; Relative Positioning{0}" + + "M17 ; Enable motors{0}" + + "; G28 Z ; Auto-home on print - commented by default{0}"; + + public const string WanhaoPreSliceGCode = + ";********** Pre-Slice {1} ********{0}" + + "G4 P0; Make sure any previous relative moves are complete{0}" + + "; {2}{0}" + + ";********** Layer {1} ********{0}"; + + public const string WanhaoGCodeEnd = + ";********** Footer ********{0}" + + "M106 S0 ; UV off{0}" + + "G4 P0 ; wait for last lift to complete{0}" + + "G1 Z40.0 F150.0 ; lift model clear of resin{0}" + + "G4 P0 ; sync{0}" + + "M18 ; Disable Motors{0}" + + ";{0}"; + #endregion - public class CWSFile : FileFormat + #region Sub Classes + + public class Output { - #region Constants + // ;(****Build and Slicing Parameters****) + [DisplayName("Pix per mm X")] public float PixPermmX { get; set; } = 19.324f; + [DisplayName("Pix per mm Y")] public float PixPermmY { get; set; } = 19.324f; + [DisplayName("X Resolution")] public ushort XResolution { get; set; } + [DisplayName("Y Resolution")] public ushort YResolution { get; set; } + [DisplayName("Layer Thickness")] public float LayerThickness { get; set; } + [DisplayName("Layer Time")] public uint LayerTime { get; set; } = 5500; + [DisplayName("Render Outlines")] public bool RenderOutlines { get; set; } = false; + [DisplayName("Outline Width Inset")] public ushort OutlineWidthInset { get; set; } = 2; + [DisplayName("Outline Width Outset")] public ushort OutlineWidthOutset { get; set; } = 0; + [DisplayName("Bottom Layers Time")] public uint BottomLayersTime { get; set; } = 35000; + [DisplayName("Number of Bottom Layers")] public ushort NumberBottomLayers { get; set; } = 3; + [DisplayName("Blanking Layer Time")] public uint BlankingLayerTime { get; set; } = 1000; + [DisplayName("Build Direction")] public string BuildDirection { get; set; } = "Bottom_Up"; + [DisplayName("Lift Distance")] public float LiftDistance { get; set; } = 4; + [DisplayName("Slide/Tilt Value")] public byte TiltValue { get; set; } + [DisplayName("Use Mainlift GCode Tab")] public bool UseMainliftGCodeTab { get; set; } + [DisplayName("Settle Time")] public uint SettleTime { get; set; } = 1600; + [DisplayName("Anti Aliasing")] public bool AntiAliasing { get; set; } = true; + [DisplayName("Anti Aliasing Value")] public float AntiAliasingValue { get; set; } = 2; + [DisplayName("Z Lift Feed Rate")] public float ZLiftFeedRate { get; set; } = 120; + [DisplayName("Z Bottom Lift Feed Rate")] public float ZBottomLiftFeedRate { get; set; } = 120; + [DisplayName("Z Lift Retract Rate")] public float ZLiftRetractRate { get; set; } = 120; + [DisplayName("Flip X")] public bool FlipX { get; set; } + [DisplayName("Flip Y")] public bool FlipY { get; set; } + [DisplayName("Number of Slices")] public uint LayersNum { get; set; } - public const string GCodeStart = "G28 ;Auto Home{0}" + - "G21 ;Set units to be mm{0}" + - "G91 ;Relative Positioning{0}" + - "M17 ;Enable motors{0}" + - " Blank{0}" + - "M106 S0{0}{0}"; + // ;(****Machine Configuration ******) + [DisplayName("Platform X Size")] public float PlatformXSize { get; set; } + [DisplayName("Platform Y Size")] public float PlatformYSize { get; set; } + [DisplayName("Platform Z Size")] public float PlatformZSize { get; set; } + [DisplayName("Max X Feedrate")] public ushort MaxXFeedrate { get; set; } = 200; + [DisplayName("Max Y Feedrate")] public ushort MaxYFeedrate { get; set; } = 200; + [DisplayName("Max Z Feedrate")] public ushort MaxZFeedrate { get; set; } = 200; + [DisplayName("Machine Type")] public string MachineType { get; set; } = "UV_LCD"; - public const string GCodeEnd = "M106 S0 ;UV off{0}" + - "G1 Z{1}{0}" + - "{0}M18 ;Disable Motors{0}" + - ";{0}"; + // ;(****UVtools Configuration ******) + [DisplayName("Bottom Layer Light PWM")] public byte BottomLightPWM { get; set; } = 255; + [DisplayName("Layer Light PWM")] public byte LightPWM { get; set; } = 255; + } - public const string GCodeKeywordSlice = ";"; - public const string GCodeKeywordSliceBlank = "; Blank"; - public const string GCodeKeywordDelay = ";"; - public const string GCodeKeywordTakes = ";"; - - public const string WanhaoStartGCode = - ";********** Header Start ********{0}" + - "G21 ; Set units to be mm{0}" + - "G91 ; Relative Positioning{0}" + - "M17 ; Enable motors{0}" + - "; G28 Z ; Auto-home on print - commented by default{0}"; - - public const string WanhaoPreSliceGCode = - ";********** Pre-Slice {1} ********{0}" + - "G4 P0; Make sure any previous relative moves are complete{0}" + - "; {2}{0}" + - ";********** Layer {1} ********{0}"; - - public const string WanhaoGCodeEnd = - ";********** Footer ********{0}" + - "M106 S0 ; UV off{0}" + - "G4 P0 ; wait for last lift to complete{0}" + - "G1 Z40.0 F150.0 ; lift model clear of resin{0}" + - "G4 P0 ; sync{0}" + - "M18 ; Disable Motors{0}" + - ";{0}"; - - #endregion - - #region Sub Classes - - public class Output - { - // ;(****Build and Slicing Parameters****) - [DisplayName("Pix per mm X")] public float PixPermmX { get; set; } = 19.324f; - [DisplayName("Pix per mm Y")] public float PixPermmY { get; set; } = 19.324f; - [DisplayName("X Resolution")] public ushort XResolution { get; set; } - [DisplayName("Y Resolution")] public ushort YResolution { get; set; } - [DisplayName("Layer Thickness")] public float LayerThickness { get; set; } - [DisplayName("Layer Time")] public uint LayerTime { get; set; } = 5500; - [DisplayName("Render Outlines")] public bool RenderOutlines { get; set; } = false; - [DisplayName("Outline Width Inset")] public ushort OutlineWidthInset { get; set; } = 2; - [DisplayName("Outline Width Outset")] public ushort OutlineWidthOutset { get; set; } = 0; - [DisplayName("Bottom Layers Time")] public uint BottomLayersTime { get; set; } = 35000; - [DisplayName("Number of Bottom Layers")] public ushort NumberBottomLayers { get; set; } = 3; - [DisplayName("Blanking Layer Time")] public uint BlankingLayerTime { get; set; } = 1000; - [DisplayName("Build Direction")] public string BuildDirection { get; set; } = "Bottom_Up"; - [DisplayName("Lift Distance")] public float LiftDistance { get; set; } = 4; - [DisplayName("Slide/Tilt Value")] public byte TiltValue { get; set; } - [DisplayName("Use Mainlift GCode Tab")] public bool UseMainliftGCodeTab { get; set; } - [DisplayName("Settle Time")] public uint SettleTime { get; set; } = 1600; - [DisplayName("Anti Aliasing")] public bool AntiAliasing { get; set; } = true; - [DisplayName("Anti Aliasing Value")] public float AntiAliasingValue { get; set; } = 2; - [DisplayName("Z Lift Feed Rate")] public float ZLiftFeedRate { get; set; } = 120; - [DisplayName("Z Bottom Lift Feed Rate")] public float ZBottomLiftFeedRate { get; set; } = 120; - [DisplayName("Z Lift Retract Rate")] public float ZLiftRetractRate { get; set; } = 120; - [DisplayName("Flip X")] public bool FlipX { get; set; } - [DisplayName("Flip Y")] public bool FlipY { get; set; } - [DisplayName("Number of Slices")] public uint LayersNum { get; set; } - - // ;(****Machine Configuration ******) - [DisplayName("Platform X Size")] public float PlatformXSize { get; set; } - [DisplayName("Platform Y Size")] public float PlatformYSize { get; set; } - [DisplayName("Platform Z Size")] public float PlatformZSize { get; set; } - [DisplayName("Max X Feedrate")] public ushort MaxXFeedrate { get; set; } = 200; - [DisplayName("Max Y Feedrate")] public ushort MaxYFeedrate { get; set; } = 200; - [DisplayName("Max Z Feedrate")] public ushort MaxZFeedrate { get; set; } = 200; - [DisplayName("Machine Type")] public string MachineType { get; set; } = "UV_LCD"; - - // ;(****UVtools Configuration ******) - [DisplayName("Bottom Layer Light PWM")] public byte BottomLightPWM { get; set; } = 255; - [DisplayName("Layer Light PWM")] public byte LightPWM { get; set; } = 255; - } - - public class Slice - { - [DisplayName("xppm")] public float Xppm { get; set; } = 19.324f; - [DisplayName("yppm")] public float Yppm { get; set; } = 19.324f; - [DisplayName("xres")] public ushort Xres { get; set; } - [DisplayName("yres")] public ushort Yres { get; set; } - [DisplayName("thickness")] public float Thickness { get; set; } - [DisplayName("layers_num")] public uint LayersNum { get; set; } - [DisplayName("head_layers_num")] public ushort HeadLayersNum { get; set; } = 3; - [DisplayName("layers_expo_ms")] public uint LayersExpoMs { get; set; } = 5500; - [DisplayName("head_layers_expo_ms")] public uint HeadLayersExpoMs { get; set; } = 35000; - [DisplayName("wait_before_expo_ms")] public uint WaitBeforeExpoMs { get; set; } = 2000; - [DisplayName("lift_distance")] public float LiftDistance { get; set; } = 4; - [DisplayName("lift_up_speed")] public float LiftUpSpeed { get; set; } = 120; - [DisplayName("lift_down_speed")] public float LiftDownSpeed { get; set; } = 120; - [DisplayName("lift_when_finished")] public byte LiftWhenFinished { get; set; } = 80; - } + public class Slice + { + [DisplayName("xppm")] public float Xppm { get; set; } = 19.324f; + [DisplayName("yppm")] public float Yppm { get; set; } = 19.324f; + [DisplayName("xres")] public ushort Xres { get; set; } + [DisplayName("yres")] public ushort Yres { get; set; } + [DisplayName("thickness")] public float Thickness { get; set; } + [DisplayName("layers_num")] public uint LayersNum { get; set; } + [DisplayName("head_layers_num")] public ushort HeadLayersNum { get; set; } = 3; + [DisplayName("layers_expo_ms")] public uint LayersExpoMs { get; set; } = 5500; + [DisplayName("head_layers_expo_ms")] public uint HeadLayersExpoMs { get; set; } = 35000; + [DisplayName("wait_before_expo_ms")] public uint WaitBeforeExpoMs { get; set; } = 2000; + [DisplayName("lift_distance")] public float LiftDistance { get; set; } = 4; + [DisplayName("lift_up_speed")] public float LiftUpSpeed { get; set; } = 120; + [DisplayName("lift_down_speed")] public float LiftDownSpeed { get; set; } = 120; + [DisplayName("lift_when_finished")] public byte LiftWhenFinished { get; set; } = 80; + } - #endregion + #endregion - #region Properties - public Slice SliceSettings { get; } = new(); - public Output OutputSettings { get; } = new(); - public CWSSliceBuildConfig SliceBuildConfig { get; set; } = new(); + #region Properties + public Slice SliceSettings { get; } = new(); + public Output OutputSettings { get; } = new(); + public CWSSliceBuildConfig SliceBuildConfig { get; set; } = new(); - public override FileFormatType FileType => FileFormatType.Archive; + public override FileFormatType FileType => FileFormatType.Archive; - public enum PrinterType : byte + public enum PrinterType : byte + { + Unknown, + Elfin, + BeneMono, + Wanhao, + } + + public PrinterType Printer { get; set; } = PrinterType.Unknown; + + public override FileExtension[] FileExtensions { get; } = { + new (typeof(CWSFile), "cws", "NovaMaker CWS"), + new (typeof(CWSFile), "rgb.cws", "NovaMaker Bene4 Mono / Elfin2 Mono SE (CWS)"), + new (typeof(CWSFile), "xml.cws", "Creation Workshop X (CWS)"), + }; + + public override PrintParameterModifier[]? PrintParameterModifiers { get; } = { + PrintParameterModifier.BottomLayerCount, + + PrintParameterModifier.BottomWaitTimeBeforeCure, + PrintParameterModifier.WaitTimeBeforeCure, + + PrintParameterModifier.BottomExposureTime, + PrintParameterModifier.ExposureTime, + + PrintParameterModifier.BottomWaitTimeAfterCure, + PrintParameterModifier.WaitTimeAfterCure, + + PrintParameterModifier.BottomLiftHeight, + PrintParameterModifier.BottomLiftSpeed, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + + PrintParameterModifier.BottomLiftHeight2, + PrintParameterModifier.BottomLiftSpeed2, + PrintParameterModifier.LiftHeight2, + PrintParameterModifier.LiftSpeed2, + + PrintParameterModifier.BottomWaitTimeAfterLift, + PrintParameterModifier.WaitTimeAfterLift, + + PrintParameterModifier.BottomRetractSpeed, + PrintParameterModifier.RetractSpeed, + + PrintParameterModifier.BottomRetractHeight2, + PrintParameterModifier.BottomRetractSpeed2, + PrintParameterModifier.RetractHeight2, + PrintParameterModifier.RetractSpeed2, + + PrintParameterModifier.BottomLightPWM, + PrintParameterModifier.LightPWM, + }; + + public override PrintParameterModifier[]? PrintParameterPerLayerModifiers { get; } = { + PrintParameterModifier.PositionZ, + PrintParameterModifier.WaitTimeBeforeCure, + PrintParameterModifier.ExposureTime, + PrintParameterModifier.WaitTimeAfterCure, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.LiftHeight2, + PrintParameterModifier.LiftSpeed2, + PrintParameterModifier.WaitTimeAfterLift, + PrintParameterModifier.RetractSpeed, + PrintParameterModifier.RetractHeight2, + PrintParameterModifier.RetractSpeed2, + PrintParameterModifier.LightPWM, + }; + + public override Size[]? ThumbnailsOriginalSize => null; + + public override uint ResolutionX + { + get => SliceSettings.Xres > 0 ? SliceSettings.Xres : SliceBuildConfig.XResolution; + set { - Unknown, - Elfin, - BeneMono, - Wanhao, - } - - public PrinterType Printer { get; set; } = PrinterType.Unknown; - - public override FileExtension[] FileExtensions { get; } = { - new (typeof(CWSFile), "cws", "NovaMaker CWS"), - new (typeof(CWSFile), "rgb.cws", "NovaMaker Bene4 Mono / Elfin2 Mono SE (CWS)"), - new (typeof(CWSFile), "xml.cws", "Creation Workshop X (CWS)"), - }; - - public override PrintParameterModifier[] PrintParameterModifiers { get; } = { - PrintParameterModifier.BottomLayerCount, - - PrintParameterModifier.BottomWaitTimeBeforeCure, - PrintParameterModifier.WaitTimeBeforeCure, - - PrintParameterModifier.BottomExposureTime, - PrintParameterModifier.ExposureTime, - - PrintParameterModifier.BottomWaitTimeAfterCure, - PrintParameterModifier.WaitTimeAfterCure, - - PrintParameterModifier.BottomLiftHeight, - PrintParameterModifier.BottomLiftSpeed, - PrintParameterModifier.LiftHeight, - PrintParameterModifier.LiftSpeed, - - PrintParameterModifier.BottomLiftHeight2, - PrintParameterModifier.BottomLiftSpeed2, - PrintParameterModifier.LiftHeight2, - PrintParameterModifier.LiftSpeed2, - - PrintParameterModifier.BottomWaitTimeAfterLift, - PrintParameterModifier.WaitTimeAfterLift, - - PrintParameterModifier.BottomRetractSpeed, - PrintParameterModifier.RetractSpeed, - - PrintParameterModifier.BottomRetractHeight2, - PrintParameterModifier.BottomRetractSpeed2, - PrintParameterModifier.RetractHeight2, - PrintParameterModifier.RetractSpeed2, - - PrintParameterModifier.BottomLightPWM, - PrintParameterModifier.LightPWM, - }; - - public override PrintParameterModifier[] PrintParameterPerLayerModifiers { get; } = { - PrintParameterModifier.PositionZ, - PrintParameterModifier.WaitTimeBeforeCure, - PrintParameterModifier.ExposureTime, - PrintParameterModifier.WaitTimeAfterCure, - PrintParameterModifier.LiftHeight, - PrintParameterModifier.LiftSpeed, - PrintParameterModifier.LiftHeight2, - PrintParameterModifier.LiftSpeed2, - PrintParameterModifier.WaitTimeAfterLift, - PrintParameterModifier.RetractSpeed, - PrintParameterModifier.RetractHeight2, - PrintParameterModifier.RetractSpeed2, - PrintParameterModifier.LightPWM, - }; - - public override System.Drawing.Size[] ThumbnailsOriginalSize => null; - - public override uint ResolutionX - { - get => SliceSettings.Xres > 0 ? SliceSettings.Xres : SliceBuildConfig.XResolution; - set - { - SliceBuildConfig.XResolution = + SliceBuildConfig.XResolution = OutputSettings.XResolution = - SliceSettings.Xres = (ushort) value; - RaisePropertyChanged(); - } + SliceSettings.Xres = (ushort) value; + RaisePropertyChanged(); } + } - public override uint ResolutionY + public override uint ResolutionY + { + get => SliceSettings.Yres > 0 ? SliceSettings.Yres : SliceBuildConfig.YResolution; + set { - get => SliceSettings.Yres > 0 ? SliceSettings.Yres : SliceBuildConfig.YResolution; - set - { - SliceBuildConfig.YResolution = + SliceBuildConfig.YResolution = OutputSettings.YResolution = - SliceSettings.Yres = (ushort) value; - RaisePropertyChanged(); - } + SliceSettings.Yres = (ushort) value; + RaisePropertyChanged(); } + } - public override float DisplayWidth + public override float DisplayWidth + { + get => OutputSettings.PlatformXSize; + set { - get => OutputSettings.PlatformXSize; - set - { - OutputSettings.PlatformXSize = (float)Math.Round(value, 2); - RaisePropertyChanged(); - } + OutputSettings.PlatformXSize = (float)Math.Round(value, 2); + RaisePropertyChanged(); } + } - public override float DisplayHeight + public override float DisplayHeight + { + get => OutputSettings.PlatformYSize; + set { - get => OutputSettings.PlatformYSize; - set - { - OutputSettings.PlatformYSize = (float)Math.Round(value, 2); - RaisePropertyChanged(); - } + OutputSettings.PlatformYSize = (float)Math.Round(value, 2); + RaisePropertyChanged(); } + } - public override float MachineZ + public override float MachineZ + { + get => OutputSettings.PlatformZSize > 0 ? OutputSettings.PlatformZSize : base.MachineZ; + set => base.MachineZ = OutputSettings.PlatformZSize = (float)Math.Round(value, 2); + } + + public override Enumerations.FlipDirection DisplayMirror + { + get { - get => OutputSettings.PlatformZSize > 0 ? OutputSettings.PlatformZSize : base.MachineZ; - set => base.MachineZ = OutputSettings.PlatformZSize = (float)Math.Round(value, 2); + if (OutputSettings.FlipX && OutputSettings.FlipY) return Enumerations.FlipDirection.Both; + if (OutputSettings.FlipX) return Enumerations.FlipDirection.Horizontally; + if (OutputSettings.FlipY) return Enumerations.FlipDirection.Vertically; + return Enumerations.FlipDirection.None; } - - public override Enumerations.FlipDirection DisplayMirror + set { - get - { - if (OutputSettings.FlipX && OutputSettings.FlipY) return Enumerations.FlipDirection.Both; - if (OutputSettings.FlipX) return Enumerations.FlipDirection.Horizontally; - if (OutputSettings.FlipY) return Enumerations.FlipDirection.Vertically; - return Enumerations.FlipDirection.None; - } - set - { - OutputSettings.FlipX = value is Enumerations.FlipDirection.Horizontally or Enumerations.FlipDirection.Both; - OutputSettings.FlipY = value is Enumerations.FlipDirection.Vertically or Enumerations.FlipDirection.Both; - RaisePropertyChanged(); - } + OutputSettings.FlipX = value is Enumerations.FlipDirection.Horizontally or Enumerations.FlipDirection.Both; + OutputSettings.FlipY = value is Enumerations.FlipDirection.Vertically or Enumerations.FlipDirection.Both; + RaisePropertyChanged(); } + } - public override byte AntiAliasing + public override byte AntiAliasing + { + get => (byte) OutputSettings.AntiAliasingValue; + set { - get => (byte) OutputSettings.AntiAliasingValue; - set - { - base.AntiAliasing = (byte)(OutputSettings.AntiAliasingValue = value.Clamp(1, 16)); - OutputSettings.AntiAliasing = OutputSettings.AntiAliasingValue > 1; - } + base.AntiAliasing = (byte)(OutputSettings.AntiAliasingValue = value.Clamp(1, 16)); + OutputSettings.AntiAliasing = OutputSettings.AntiAliasingValue > 1; } + } - public override float LayerHeight + public override float LayerHeight + { + get => SliceSettings.Thickness > 0 ? SliceSettings.Thickness : OutputSettings.LayerThickness; + set { - get => SliceSettings.Thickness > 0 ? SliceSettings.Thickness : OutputSettings.LayerThickness; - set - { - OutputSettings.LayerThickness = SliceSettings.Thickness = Layer.RoundHeight(value); - RaisePropertyChanged(); - } + OutputSettings.LayerThickness = SliceSettings.Thickness = Layer.RoundHeight(value); + RaisePropertyChanged(); } + } - public override uint LayerCount + public override uint LayerCount + { + get => base.LayerCount; + set => base.LayerCount = OutputSettings.LayersNum = SliceSettings.LayersNum = base.LayerCount; + } + + public override ushort BottomLayerCount + { + get => SliceSettings.HeadLayersNum; + set => base.BottomLayerCount = SliceSettings.HeadLayersNum = value; + } + + public override float BottomLightOffDelay + { + get => BottomWaitTimeBeforeCure; + set => BottomWaitTimeBeforeCure = value; + } + + public override float LightOffDelay + { + get => WaitTimeBeforeCure; + set => WaitTimeBeforeCure = value; + } + + public override float BottomWaitTimeBeforeCure + { + get => base.BottomWaitTimeBeforeCure; + set => base.BottomWaitTimeBeforeCure = base.BottomLightOffDelay = value; + } + + public override float WaitTimeBeforeCure + { + get => TimeExtensions.MillisecondsToSeconds(SliceSettings.WaitBeforeExpoMs); + set { - get => base.LayerCount; - set => base.LayerCount = OutputSettings.LayersNum = SliceSettings.LayersNum = base.LayerCount; + SliceSettings.WaitBeforeExpoMs = TimeExtensions.SecondsToMillisecondsUint(value); + base.WaitTimeBeforeCure = base.LightOffDelay = value; } + } - public override ushort BottomLayerCount + public override float BottomExposureTime + { + get => TimeExtensions.MillisecondsToSeconds(OutputSettings.BottomLayersTime); + set { - get => SliceSettings.HeadLayersNum; - set => base.BottomLayerCount = SliceSettings.HeadLayersNum = value; - } - - public override float BottomLightOffDelay - { - get => BottomWaitTimeBeforeCure; - set => BottomWaitTimeBeforeCure = value; - } - - public override float LightOffDelay - { - get => WaitTimeBeforeCure; - set => WaitTimeBeforeCure = value; - } - - public override float BottomWaitTimeBeforeCure - { - get => base.BottomWaitTimeBeforeCure; - set => base.BottomWaitTimeBeforeCure = base.BottomLightOffDelay = value; - } - - public override float WaitTimeBeforeCure - { - get => TimeExtensions.MillisecondsToSeconds(SliceSettings.WaitBeforeExpoMs); - set - { - SliceSettings.WaitBeforeExpoMs = TimeExtensions.SecondsToMillisecondsUint(value); - base.WaitTimeBeforeCure = base.LightOffDelay = value; - } - } - - public override float BottomExposureTime - { - get => TimeExtensions.MillisecondsToSeconds(OutputSettings.BottomLayersTime); - set - { - OutputSettings.BottomLayersTime = + OutputSettings.BottomLayersTime = SliceSettings.HeadLayersExpoMs = TimeExtensions.SecondsToMillisecondsUint(value); - base.BottomExposureTime = value; - } + base.BottomExposureTime = value; } + } - public override float ExposureTime + public override float ExposureTime + { + get => TimeExtensions.MillisecondsToSeconds(OutputSettings.LayerTime); + set { - get => TimeExtensions.MillisecondsToSeconds(OutputSettings.LayerTime); - set - { - OutputSettings.LayerTime = + OutputSettings.LayerTime = SliceSettings.LayersExpoMs = TimeExtensions.SecondsToMillisecondsUint(value); - base.ExposureTime = value; - } + base.ExposureTime = value; } + } - public override float LiftHeight - { - get => OutputSettings.LiftDistance; - set => base.LiftHeight = OutputSettings.LiftDistance = SliceSettings.LiftDistance = (float)Math.Round(value, 2); - } + public override float LiftHeight + { + get => OutputSettings.LiftDistance; + set => base.LiftHeight = OutputSettings.LiftDistance = SliceSettings.LiftDistance = (float)Math.Round(value, 2); + } - public override float BottomLiftSpeed - { - get => OutputSettings.ZBottomLiftFeedRate; - set => base.BottomLiftSpeed = OutputSettings.ZBottomLiftFeedRate = (float)Math.Round(value, 2); - } + public override float BottomLiftSpeed + { + get => OutputSettings.ZBottomLiftFeedRate; + set => base.BottomLiftSpeed = OutputSettings.ZBottomLiftFeedRate = (float)Math.Round(value, 2); + } - public override float LiftSpeed - { - get => OutputSettings.ZLiftFeedRate; - set => - base.LiftSpeed = + public override float LiftSpeed + { + get => OutputSettings.ZLiftFeedRate; + set => + base.LiftSpeed = OutputSettings.ZLiftFeedRate = - SliceSettings.LiftUpSpeed = (float)Math.Round(value, 2); - } + SliceSettings.LiftUpSpeed = (float)Math.Round(value, 2); + } - public override float RetractSpeed + public override float RetractSpeed + { + get => OutputSettings.ZLiftRetractRate; + set => base.RetractSpeed = OutputSettings.ZLiftRetractRate = SliceSettings.LiftDownSpeed = (float)Math.Round(value, 2); + } + + public override byte BottomLightPWM + { + get => OutputSettings.BottomLightPWM; + set => base.BottomLightPWM = OutputSettings.BottomLightPWM = value; + } + + public override byte LightPWM + { + get => OutputSettings.LightPWM; + set => base.LightPWM = OutputSettings.LightPWM = value; + } + + + public override object[] Configs => Printer == PrinterType.Wanhao ? new object[] { SliceBuildConfig, OutputSettings } : new object[] { SliceSettings, OutputSettings}; + #endregion + + #region Constructor + public CWSFile() + { + GCode = new GCodeBuilder { - get => OutputSettings.ZLiftRetractRate; - set => base.RetractSpeed = OutputSettings.ZLiftRetractRate = SliceSettings.LiftDownSpeed = (float)Math.Round(value, 2); - } + SyncMovementsWithDelay = true, + UseComments = true, + GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Partial, + GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, + GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, + GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndex0Started, + LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G1, + EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G1 + }; + GCode.CommandShowImageM6054.Set(";", "{0}"); + GCode.CommandWaitG4.Set(";", "{0}"); + } + #endregion - public override byte BottomLightPWM + #region Methods + + protected override void EncodeInternally(OperationProgress progress) + { + //var filename = fileFullPath.EndsWith(TemporaryFileAppend) ? Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(fileFullPath)) : Path.GetFileNameWithoutExtension(fileFullPath); + + if (Printer == PrinterType.Unknown) { - get => OutputSettings.BottomLightPWM; - set => base.BottomLightPWM = OutputSettings.BottomLightPWM = value; - } - - public override byte LightPWM - { - get => OutputSettings.LightPWM; - set => base.LightPWM = OutputSettings.LightPWM = value; - } - - - public override object[] Configs => Printer == PrinterType.Wanhao ? new object[] { SliceBuildConfig, OutputSettings } : new object[] { SliceSettings, OutputSettings}; - #endregion - - #region Constructor - public CWSFile() - { - GCode = new GCodeBuilder + Printer = PrinterType.Elfin; + for (int i = 0; i < FileExtensions.Length; i++) { - SyncMovementsWithDelay = true, - UseComments = true, - GCodePositioningType = GCodeBuilder.GCodePositioningTypes.Partial, - GCodeSpeedUnit = GCodeBuilder.GCodeSpeedUnits.MillimetersPerMinute, - GCodeTimeUnit = GCodeBuilder.GCodeTimeUnits.Milliseconds, - GCodeShowImageType = GCodeBuilder.GCodeShowImageTypes.LayerIndex0Started, - LayerMoveCommand = GCodeBuilder.GCodeMoveCommands.G1, - EndGCodeMoveCommand = GCodeBuilder.GCodeMoveCommands.G1 + if (!FileEndsWith(FileExtensions[i].Extension)) continue; + Printer = (PrinterType) i+1; + } + } + + if (Printer == PrinterType.BeneMono) + { + if (ResolutionX % 3 != 0) + { + throw new InvalidOperationException($"Resolution width of {ResolutionX}px is invalid. Width must be in multiples of 3.\n" + + "Fix your printer slicing settings with the correct width that is multiple of 3."); + } + } + + /*var filename = Path.GetFileNameWithoutExtension(fileFullPath); + if (fileFullPath.EndsWith(TemporaryFileAppend)) + { + filename = Path.GetFileNameWithoutExtension(filename); + }*/ + var filename = About.Software; + + if (char.IsDigit(filename[^1])) + { + throw new InvalidOperationException($"Filename for this format should not end with a digit: {filename}"); + } + + using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Create); + if (Printer == PrinterType.Wanhao) + { + var manifest = new CWSManifest + { + GCode = {Name = $"{filename}.gcode"}, + Slices = new CWSManifest.Slice[LayerCount], + SliceProfile = {Name = $"{filename}.slicing"} }; - GCode.CommandShowImageM6054.Set(";", "{0}"); - GCode.CommandWaitG4.Set(";", "{0}"); + + for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + manifest.Slices[layerIndex] = new CWSManifest.Slice(this[layerIndex].FormatFileName(filename)); + } + + var entry = outputFile.CreateEntry(CWSManifest.FileName); + using (var stream = entry.Open()) + { + XmlExtensions.Serialize(manifest, stream, XmlExtensions.SettingsIndent, true); + } + + entry = outputFile.CreateEntry($"{filename}.slicing"); + using (var stream = entry.Open()) + { + XmlExtensions.Serialize(SliceBuildConfig, stream, XmlExtensions.SettingsIndent, true); + } } - #endregion - - #region Methods - - protected override void EncodeInternally(OperationProgress progress) + else { - //var filename = fileFullPath.EndsWith(TemporaryFileAppend) ? Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(fileFullPath)) : Path.GetFileNameWithoutExtension(fileFullPath); + string arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; + var entry = outputFile.CreateEntry("slice.conf"); - if (Printer == PrinterType.Unknown) + using TextWriter tw = new StreamWriter(entry.Open()); + tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); + tw.WriteLine("# conf version 1.0"); + tw.WriteLine(""); + + foreach (var propertyInfo in SliceSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { - Printer = PrinterType.Elfin; - for (int i = 0; i < FileExtensions.Length; i++) - { - if (!FileEndsWith(FileExtensions[i].Extension)) continue; - Printer = (PrinterType) i+1; - } + var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); + if (displayNameAttribute is null) continue; + tw.WriteLine($"{displayNameAttribute.DisplayName.PadRight(24)}= {propertyInfo.GetValue(SliceSettings)}"); } + } - if (Printer == PrinterType.BeneMono) - { - if (ResolutionX % 3 != 0) - { - throw new InvalidOperationException($"Resolution width of {ResolutionX}px is invalid. Width must be in multiples of 3.\n" + - "Fix your printer slicing settings with the correct width that is multiple of 3."); - } - } - - /*var filename = Path.GetFileNameWithoutExtension(fileFullPath); - if (fileFullPath.EndsWith(TemporaryFileAppend)) - { - filename = Path.GetFileNameWithoutExtension(filename); - }*/ - var filename = About.Software; - - if (char.IsDigit(filename[^1])) - { - throw new InvalidOperationException($"Filename for this format should not end with a digit: {filename}"); - } - - using var outputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Create); - if (Printer == PrinterType.Wanhao) - { - var manifest = new CWSManifest - { - GCode = {Name = $"{filename}.gcode"}, - Slices = new CWSManifest.Slice[LayerCount], - SliceProfile = {Name = $"{filename}.slicing"} - }; - - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - manifest.Slices[layerIndex] = new CWSManifest.Slice(this[layerIndex].FormatFileName(filename)); - } - - XmlSerializer serializer = new(manifest.GetType()); - XmlSerializerNamespaces ns = new(); - ns.Add("", ""); - var entry = outputFile.CreateEntry(CWSManifest.FileName); - using (var stream = entry.Open()) - { - serializer.Serialize(stream, manifest, ns); - } - - serializer = new XmlSerializer(SliceBuildConfig.GetType()); - entry = outputFile.CreateEntry($"{filename}.slicing"); - using (var stream = entry.Open()) - { - serializer.Serialize(stream, SliceBuildConfig, ns); - } - } - else - { - string arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; - var entry = outputFile.CreateEntry("slice.conf"); - - using TextWriter tw = new StreamWriter(entry.Open()); - tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); - tw.WriteLine("# conf version 1.0"); - tw.WriteLine(""); - - foreach (var propertyInfo in SliceSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); - if (displayNameAttribute is null) continue; - tw.WriteLine($"{displayNameAttribute.DisplayName.PadRight(24)}= {propertyInfo.GetValue(SliceSettings)}"); - } - } - - if (Printer == PrinterType.BeneMono) - { - Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, + if (Printer == PrinterType.BeneMono) + { + Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, //new ParallelOptions { MaxDegreeOfParallelism = Printer == PrinterType.BeneMono ? 1 : 1 }, layerIndex => { @@ -671,7 +668,7 @@ namespace UVtools.Core.FileFormats var layer = this[layerIndex]; var layerImagePath = layer.FormatFileName(filename); - using var mat = layer.LayerMat; + using var mat = layer.LayerMat!; using var matEncode = new Mat(mat.Height, mat.GetRealStep() / 3, DepthType.Cv8U, 3); var span = mat.GetDataByteSpan(); var spanEncode = matEncode.GetDataByteSpan(); @@ -687,357 +684,357 @@ namespace UVtools.Core.FileFormats progress++; } }); - } - else + } + else + { + for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { - for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - progress.Token.ThrowIfCancellationRequested(); - var layer = this[layerIndex]; - var layerImagePath = layer.FormatFileName(filename); - outputFile.PutFileContent(layerImagePath, layer.CompressedBytes, ZipArchiveMode.Create); - progress++; - } + progress.Token.ThrowIfCancellationRequested(); + var layer = this[layerIndex]; + var layerImagePath = layer.FormatFileName(filename); + outputFile.PutFileContent(layerImagePath, layer.CompressedBytes, ZipArchiveMode.Create); + progress++; } - - RebuildGCode(); - outputFile.PutFileContent($"{filename}.gcode", GCodeStr, ZipArchiveMode.Create); } - protected override void DecodeInternally(OperationProgress progress) - { - using var inputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Read); - var entry = inputFile.GetEntry("manifest.xml"); - if (entry is not null) // Wanhao - { - //DecodeXML(fileFullPath, inputFile, progress); - Printer = PrinterType.Wanhao; + RebuildGCode(); + outputFile.PutFileContent($"{filename}.gcode", GCodeStr, ZipArchiveMode.Create); + } + protected override void DecodeInternally(OperationProgress progress) + { + using var inputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Read); + var entry = inputFile.GetEntry("manifest.xml"); + if (entry is not null) // Wanhao + { + //DecodeXML(fileFullPath, inputFile, progress); + Printer = PrinterType.Wanhao; + + try + { + using var stream = entry.Open(); + var manifest = XmlExtensions.DeserializeFromStream(stream); + if (manifest.Slices is null) + { + throw new NullReferenceException("Slices information are not present on the manifest file"); + } + OutputSettings.LayersNum = (uint) manifest.Slices.Length; + } + catch (Exception e) + { + Clear(); + throw new FileLoadException($"Unable to deserialize '{entry.Name}'\n{e}", FileFullPath); + } + + + entry = inputFile.Entries.FirstOrDefault(e => e.Name.EndsWith(".slicing")); + + if (entry is not null) + { + //Clear(); + //throw new FileLoadException(".slicing file not found, unable to proceed.", fileFullPath); try { - var serializer = new XmlSerializer(typeof(CWSManifest)); using var stream = entry.Open(); - var manifest = (CWSManifest)serializer.Deserialize(stream); - OutputSettings.LayersNum = (uint) manifest.Slices.Length; + SliceBuildConfig = XmlExtensions.DeserializeFromStream(stream); } catch (Exception e) { Clear(); throw new FileLoadException($"Unable to deserialize '{entry.Name}'\n{e}", FileFullPath); } - - - entry = inputFile.Entries.FirstOrDefault(e => e.Name.EndsWith(".slicing")); - - if (entry is not null) - { - //Clear(); - //throw new FileLoadException(".slicing file not found, unable to proceed.", fileFullPath); - try - { - var serializer = new XmlSerializer(typeof(CWSSliceBuildConfig)); - using var stream = entry.Open(); - SliceBuildConfig = (CWSSliceBuildConfig)serializer.Deserialize(stream); - } - catch (Exception e) - { - Clear(); - throw new FileLoadException($"Unable to deserialize '{entry.Name}'\n{e}", FileFullPath); - } - } } - else // Novamaker - { - entry = inputFile.GetEntry("slice.conf"); - if (entry is null) - { - Clear(); - throw new FileLoadException("slice.conf not found", FileFullPath); - } - - using TextReader tr = new StreamReader(entry.Open()); - string line; - while ((line = tr.ReadLine()) != null) - { - line = line.Replace("# ", string.Empty); - if (string.IsNullOrEmpty(line)) continue; - //if(line[0] == '#') continue; - - var splitLine = line.Split('='); - if (splitLine.Length < 2) continue; - - foreach (var propertyInfo in SliceSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); - if (displayNameAttribute is null) continue; - if (!splitLine[0].Trim().Equals(displayNameAttribute.DisplayName)) continue; - Helpers.SetPropertyValue(propertyInfo, SliceSettings, splitLine[1].Trim()); - } - } - tr.Close(); - } - - entry = inputFile.Entries.FirstOrDefault(e => e.Name.EndsWith(".gcode")); + } + else // Novamaker + { + entry = inputFile.GetEntry("slice.conf"); if (entry is null) { Clear(); - throw new FileLoadException("Unable to find .gcode file", FileFullPath); + throw new FileLoadException("slice.conf not found", FileFullPath); } - using (TextReader tr = new StreamReader(entry.Open())) + using TextReader tr = new StreamReader(entry.Open()); + string? line; + while ((line = tr.ReadLine()) != null) { - string line; - GCode.Clear(); - while ((line = tr.ReadLine()) != null) - { - GCode.AppendLine(line); - if (string.IsNullOrEmpty(line)) continue; + line = line.Replace("# ", string.Empty); + if (string.IsNullOrEmpty(line)) continue; + //if(line[0] == '#') continue; - if (line[0] != ';') - { - continue; - } - - var splitLine = line.Split('='); - if (splitLine.Length < 2) continue; - - foreach (var propertyInfo in OutputSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); - if (displayNameAttribute is null) continue; - if (!splitLine[0].Trim(' ', ';', '(').Equals(displayNameAttribute.DisplayName)) continue; - try - { - Helpers.SetPropertyValue(propertyInfo, OutputSettings, splitLine[1].Trim(' ', ')', 'p', 'x', 'm', 'n', 's', '/')); - } - catch - { - // ignored - } - - //Debug.WriteLine(splitLine[1].Trim(' ', ')', 'm', 'n', '/')); - } - } - tr.Close(); - } - - LayerManager.Init(OutputSettings.LayersNum, DecodeType == FileDecodeType.Partial); - - progress.ItemCount = OutputSettings.LayersNum; - - if(LayerCount > 0) - { - if (DecodeType == FileDecodeType.Full) - { - var inputFilename = Path.GetFileNameWithoutExtension(FileFullPath); - foreach (var pngEntry in inputFile.Entries) - { - if (!pngEntry.Name.EndsWith(".png")) continue; - var filename = Path.GetFileNameWithoutExtension(pngEntry.Name) - .Replace(inputFilename, string.Empty, StringComparison.Ordinal); - - var layerIndexStr = string.Empty; - var layerStr = filename; - for (int i = layerStr.Length - 1; i >= 0; i--) - { - if (layerStr[i] < '0' || layerStr[i] > '9') break; - layerIndexStr = $"{layerStr[i]}{layerIndexStr}"; - } - - if (string.IsNullOrEmpty(layerIndexStr)) continue; - if (!uint.TryParse(layerIndexStr, out var layerIndex)) continue; - using var stream = pngEntry.Open(); - this[layerIndex] = new Layer(layerIndex, stream, LayerManager); - } - } - - GCode.ParseLayersFromGCode(this); - - var firstLayer = FirstLayer; - if (firstLayer is not null && DecodeType == FileDecodeType.Full) - { - if (Printer == PrinterType.Unknown) - { - using Mat mat = new (); - CvInvoke.Imdecode(firstLayer.CompressedBytes, ImreadModes.AnyColor, mat); - Printer = mat.NumberOfChannels == 1 ? PrinterType.Elfin : PrinterType.BeneMono; - } - - if (Printer == PrinterType.BeneMono) - { - Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, layerIndex => - { - if (progress.Token.IsCancellationRequested) return; - var layer = this[layerIndex]; - using Mat mat = new(); - CvInvoke.Imdecode(layer.CompressedBytes, ImreadModes.Color, mat); - using Mat matDecode = new(mat.Height, mat.GetRealStep(), DepthType.Cv8U, 1); - var span = mat.GetDataByteSpan(); - var spanDecode = matDecode.GetDataByteSpan(); - for (int i = 0; i < span.Length; i++) - { - spanDecode[i] = span[i]; - } - - layer.LayerMat = matDecode; - layer.IsModified = false; - progress.LockAndIncrement(); - }); - } - } - } - - LayerManager.GetBoundingRectangle(progress); - } - - public override void RebuildGCode() - { - if (!SupportsGCode || SuppressRebuildGCode) return; - //string arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; - //GCode.Clear(); - //GCode.AppendLine($"; {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); - StringBuilder sb = new(); - sb.AppendLine(";(**** Build and Slicing Parameters ****)"); - - foreach (var propertyInfo in OutputSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); - if (displayNameAttribute is null) continue; - if (propertyInfo.Name.Equals(nameof(OutputSettings.LayersNum))) - { - sb.AppendLine($";{displayNameAttribute.DisplayName} = {propertyInfo.GetValue(OutputSettings)}"); - } - else - { - sb.AppendLine($";({displayNameAttribute.DisplayName} = {propertyInfo.GetValue(OutputSettings)})"); - } - } - - GCode.RebuildGCode(this, sb); - /*GCode.AppendLine(); - - GCode.AppendFormat(Printer == PrinterType.Wanhao ? WanhaoStartGCode : GCodeStart, Environment.NewLine); - - float lastZPosition = 0; - - for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - Layer layer = this[layerIndex]; - - if (Printer == PrinterType.Wanhao) - { - GCode.AppendFormat(WanhaoPreSliceGCode, Environment.NewLine, layerIndex, OutputSettings.SettleTime); - } - - GCode.AppendLine($"{GCodeKeywordSlice} {layerIndex}"); - if (layer.ExposureTime > 0 && layer.LightPWM > 0) - { - GCode.AppendLine($"M106 S{layer.LightPWM} ; UV on"); - GCode.AppendLine($"{GCodeKeywordDelay} {layer.ExposureTime * 1000}"); - GCode.AppendLine("M106 S0 ; UV off"); - } - GCode.AppendLine(GCodeKeywordSliceBlank); - - if (Printer == PrinterType.Wanhao) - { - GCode.AppendLine($"{GCodeKeywordDelay} 0"); - } - - if (lastZPosition != layer.PositionZ) - { - if (layer.LiftHeight > 0) - { - float downPos = Layer.RoundHeight(layer.LiftHeight - layer.PositionZ + lastZPosition); - if (Printer == PrinterType.Wanhao) - { - GCode.AppendLine($";********** Lift Sequence {layerIndex} ********"); - } - GCode.AppendLine($"G1 Z{layer.LiftHeight} F{layer.LiftSpeed}"); - if (Printer == PrinterType.Wanhao) - { - GCode.AppendLine($"{GCodeKeywordTakes} {OperationCalculator.LightOffDelayC.CalculateMillisecondsLiftOnly(layer.LiftHeight, layer.LiftSpeed)}"); - GCode.AppendLine($"G4 P0 ; Wait for lift rise to complete"); - GCode.AppendLine($"{GCodeKeywordDelay} 0"); - } - GCode.AppendLine($"G1 Z-{downPos} F{layer.RetractSpeed}"); - if (Printer == PrinterType.Wanhao) - { - GCode.AppendLine($"{GCodeKeywordTakes} {OperationCalculator.LightOffDelayC.CalculateMillisecondsLiftOnly(downPos, layer.RetractSpeed)}"); - } - } - else - { - GCode.AppendLine($"G1 Z{Layer.RoundHeight(layer.PositionZ - lastZPosition)} F{layer.LiftSpeed}"); - } - } - - if (Printer != PrinterType.Wanhao) - { - // delay = max(extra['wait'], 500) + int(((int(lift)/(extra['lift_feed']/60)) + (int(lift)/(extra['lift_retract']/60)))*1000) - /*uint extraDelay = - OperationCalculator.LightOffDelayC.CalculateMilliseconds(layer.LiftHeight, layer.LiftSpeed, - layer.RetractSpeed); - if (layerIndex < BottomLayerCount) - { - extraDelay = (uint) Math.Max(extraDelay + 10000, layer.ExposureTime * 1000); - } - else - { - extraDelay += Math.Max(OutputSettings.BlankingLayerTime, 500); - } - - GCode.AppendLine($"{GCodeKeywordDelay} {layer.LightOffDelay * 1000}"); - GCode.AppendLine(); - } - - lastZPosition = layer.PositionZ; - } - - GCode.AppendFormat(Printer == PrinterType.Wanhao ? WanhaoGCodeEnd : GCodeEnd, Environment.NewLine, SliceSettings.LiftWhenFinished);*/ - RaisePropertyChanged(nameof(GCodeStr)); - } - - protected override void PartialSaveInternally(OperationProgress progress) - { - using var outputFile = ZipFile.Open(FileFullPath, ZipArchiveMode.Update); - var arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; - var entry = outputFile.GetPutFile("slice.conf"); - var stream = entry.Open(); - stream.SetLength(0); - - using (TextWriter tw = new StreamWriter(stream)) - { - - tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); - tw.WriteLine("# conf version 1.0"); - tw.WriteLine(""); + var splitLine = line.Split('='); + if (splitLine.Length < 2) continue; foreach (var propertyInfo in SliceSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); if (displayNameAttribute is null) continue; - tw.WriteLine($"{displayNameAttribute.DisplayName.PadRight(24)}= {propertyInfo.GetValue(SliceSettings)}"); + if (!splitLine[0].Trim().Equals(displayNameAttribute.DisplayName)) continue; + Helpers.SetPropertyValue(propertyInfo, SliceSettings, splitLine[1].Trim()); + } + } + tr.Close(); + } + + entry = inputFile.Entries.FirstOrDefault(e => e.Name.EndsWith(".gcode")); + if (entry is null) + { + Clear(); + throw new FileLoadException("Unable to find .gcode file", FileFullPath); + } + + using (TextReader tr = new StreamReader(entry.Open())) + { + string? line; + GCode!.Clear(); + while ((line = tr.ReadLine()) != null) + { + GCode.AppendLine(line); + if (string.IsNullOrEmpty(line)) continue; + + if (line[0] != ';') + { + continue; + } + + var splitLine = line.Split('='); + if (splitLine.Length < 2) continue; + + foreach (var propertyInfo in OutputSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); + if (displayNameAttribute is null) continue; + if (!splitLine[0].Trim(' ', ';', '(').Equals(displayNameAttribute.DisplayName)) continue; + try + { + Helpers.SetPropertyValue(propertyInfo, OutputSettings, splitLine[1].Trim(' ', ')', 'p', 'x', 'm', 'n', 's', '/')); + } + catch + { + // ignored + } + + //Debug.WriteLine(splitLine[1].Trim(' ', ')', 'm', 'n', '/')); + } + } + tr.Close(); + } + + Init(OutputSettings.LayersNum, DecodeType == FileDecodeType.Partial); + + progress.ItemCount = OutputSettings.LayersNum; + + if(LayerCount > 0) + { + if (DecodeType == FileDecodeType.Full) + { + var inputFilename = Path.GetFileNameWithoutExtension(FileFullPath)!; + foreach (var pngEntry in inputFile.Entries) + { + if (!pngEntry.Name.EndsWith(".png")) continue; + var filename = Path.GetFileNameWithoutExtension(pngEntry.Name).Replace(inputFilename, string.Empty, StringComparison.Ordinal); + + var layerIndexStr = string.Empty; + var layerStr = filename; + for (int i = layerStr.Length - 1; i >= 0; i--) + { + if (layerStr[i] < '0' || layerStr[i] > '9') break; + layerIndexStr = $"{layerStr[i]}{layerIndexStr}"; + } + + if (string.IsNullOrEmpty(layerIndexStr)) continue; + if (!uint.TryParse(layerIndexStr, out var layerIndex)) continue; + using var stream = pngEntry.Open(); + this[layerIndex] = new Layer(layerIndex, stream, this); + } + } + + GCode.ParseLayersFromGCode(this); + + var firstLayer = FirstLayer; + if (firstLayer is not null && DecodeType == FileDecodeType.Full) + { + if (Printer == PrinterType.Unknown) + { + using Mat mat = new (); + CvInvoke.Imdecode(firstLayer.CompressedBytes, ImreadModes.AnyColor, mat); + Printer = mat.NumberOfChannels == 1 ? PrinterType.Elfin : PrinterType.BeneMono; + } + + if (Printer == PrinterType.BeneMono) + { + Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + var layer = this[layerIndex]; + using Mat mat = new(); + CvInvoke.Imdecode(layer.CompressedBytes, ImreadModes.Color, mat); + using Mat matDecode = new(mat.Height, mat.GetRealStep(), DepthType.Cv8U, 1); + var span = mat.GetDataByteSpan(); + var spanDecode = matDecode.GetDataByteSpan(); + for (int i = 0; i < span.Length; i++) + { + spanDecode[i] = span[i]; + } + + layer.LayerMat = matDecode; + layer.IsModified = false; + progress.LockAndIncrement(); + }); + } + } + } + + GetBoundingRectangle(progress); + } + + public override void RebuildGCode() + { + if (!SupportsGCode || SuppressRebuildGCode) return; + //string arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; + //GCode.Clear(); + //GCode.AppendLine($"; {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); + StringBuilder sb = new(); + sb.AppendLine(";(**** Build and Slicing Parameters ****)"); + + foreach (var propertyInfo in OutputSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); + if (displayNameAttribute is null) continue; + if (propertyInfo.Name.Equals(nameof(OutputSettings.LayersNum))) + { + sb.AppendLine($";{displayNameAttribute.DisplayName} = {propertyInfo.GetValue(OutputSettings)}"); + } + else + { + sb.AppendLine($";({displayNameAttribute.DisplayName} = {propertyInfo.GetValue(OutputSettings)})"); + } + } + + GCode?.RebuildGCode(this, sb); + /*GCode.AppendLine(); + + GCode.AppendFormat(Printer == PrinterType.Wanhao ? WanhaoStartGCode : GCodeStart, Environment.NewLine); + + float lastZPosition = 0; + + for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + Layer layer = this[layerIndex]; + + if (Printer == PrinterType.Wanhao) + { + GCode.AppendFormat(WanhaoPreSliceGCode, Environment.NewLine, layerIndex, OutputSettings.SettleTime); + } + + GCode.AppendLine($"{GCodeKeywordSlice} {layerIndex}"); + if (layer.ExposureTime > 0 && layer.LightPWM > 0) + { + GCode.AppendLine($"M106 S{layer.LightPWM} ; UV on"); + GCode.AppendLine($"{GCodeKeywordDelay} {layer.ExposureTime * 1000}"); + GCode.AppendLine("M106 S0 ; UV off"); + } + GCode.AppendLine(GCodeKeywordSliceBlank); + + if (Printer == PrinterType.Wanhao) + { + GCode.AppendLine($"{GCodeKeywordDelay} 0"); + } + + if (lastZPosition != layer.PositionZ) + { + if (layer.LiftHeight > 0) + { + float downPos = Layer.RoundHeight(layer.LiftHeight - layer.PositionZ + lastZPosition); + if (Printer == PrinterType.Wanhao) + { + GCode.AppendLine($";********** Lift Sequence {layerIndex} ********"); + } + GCode.AppendLine($"G1 Z{layer.LiftHeight} F{layer.LiftSpeed}"); + if (Printer == PrinterType.Wanhao) + { + GCode.AppendLine($"{GCodeKeywordTakes} {OperationCalculator.LightOffDelayC.CalculateMillisecondsLiftOnly(layer.LiftHeight, layer.LiftSpeed)}"); + GCode.AppendLine($"G4 P0 ; Wait for lift rise to complete"); + GCode.AppendLine($"{GCodeKeywordDelay} 0"); + } + GCode.AppendLine($"G1 Z-{downPos} F{layer.RetractSpeed}"); + if (Printer == PrinterType.Wanhao) + { + GCode.AppendLine($"{GCodeKeywordTakes} {OperationCalculator.LightOffDelayC.CalculateMillisecondsLiftOnly(downPos, layer.RetractSpeed)}"); + } + } + else + { + GCode.AppendLine($"G1 Z{Layer.RoundHeight(layer.PositionZ - lastZPosition)} F{layer.LiftSpeed}"); } } - var entriesToRemove = outputFile.Entries.Where(zipEntry => zipEntry.Name.EndsWith(".gcode")).ToArray(); - foreach (var zipEntry in entriesToRemove) + if (Printer != PrinterType.Wanhao) { - zipEntry.Delete(); + // delay = max(extra['wait'], 500) + int(((int(lift)/(extra['lift_feed']/60)) + (int(lift)/(extra['lift_retract']/60)))*1000) + /*uint extraDelay = + OperationCalculator.LightOffDelayC.CalculateMilliseconds(layer.LiftHeight, layer.LiftSpeed, + layer.RetractSpeed); + if (layerIndex < BottomLayerCount) + { + extraDelay = (uint) Math.Max(extraDelay + 10000, layer.ExposureTime * 1000); + } + else + { + extraDelay += Math.Max(OutputSettings.BlankingLayerTime, 500); + } + + GCode.AppendLine($"{GCodeKeywordDelay} {layer.LightOffDelay * 1000}"); + GCode.AppendLine(); } - outputFile.PutFileContent($"{About.Software}.gcode", GCodeStr, ZipArchiveMode.Update); - - /*foreach (var layer in this) - { - if (!layer.IsModified) continue; - outputFile.PutFileContent(layer.Filename, layer.CompressedBytes, ZipArchiveMode.Update); - layer.IsModified = false; - }*/ - - //Decode(FileFullPath, progress); + lastZPosition = layer.PositionZ; } - #endregion + GCode.AppendFormat(Printer == PrinterType.Wanhao ? WanhaoGCodeEnd : GCodeEnd, Environment.NewLine, SliceSettings.LiftWhenFinished);*/ + RaisePropertyChanged(nameof(GCodeStr)); } -} + + protected override void PartialSaveInternally(OperationProgress progress) + { + using var outputFile = ZipFile.Open(FileFullPath!, ZipArchiveMode.Update); + var arch = Environment.Is64BitOperatingSystem ? "64-bits" : "32-bits"; + var entry = outputFile.GetPutFile("slice.conf"); + var stream = entry.Open(); + stream.SetLength(0); + + using (TextWriter tw = new StreamWriter(stream)) + { + + tw.WriteLine($"# {About.Website} {About.Software} {Assembly.GetExecutingAssembly().GetName().Version} {arch} {DateTime.UtcNow}"); + tw.WriteLine("# conf version 1.0"); + tw.WriteLine(""); + + foreach (var propertyInfo in SliceSettings.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + var displayNameAttribute = propertyInfo.GetCustomAttributes(false).OfType().FirstOrDefault(); + if (displayNameAttribute is null) continue; + tw.WriteLine($"{displayNameAttribute.DisplayName.PadRight(24)}= {propertyInfo.GetValue(SliceSettings)}"); + } + } + + var entriesToRemove = outputFile.Entries.Where(zipEntry => zipEntry.Name.EndsWith(".gcode")).ToArray(); + foreach (var zipEntry in entriesToRemove) + { + zipEntry.Delete(); + } + + outputFile.PutFileContent($"{About.Software}.gcode", GCodeStr, ZipArchiveMode.Update); + + /*foreach (var layer in this) + { + if (!layer.IsModified) continue; + outputFile.PutFileContent(layer.Filename, layer.CompressedBytes, ZipArchiveMode.Update); + layer.IsModified = false; + }*/ + + //Decode(FileFullPath, progress); + } + + #endregion +} \ No newline at end of file diff --git a/UVtools.Core/FileFormats/CXDLPFile.cs b/UVtools.Core/FileFormats/CXDLPFile.cs index 469a556..ecd7fd6 100644 --- a/UVtools.Core/FileFormats/CXDLPFile.cs +++ b/UVtools.Core/FileFormats/CXDLPFile.cs @@ -6,6 +6,9 @@ * of this license document, but changing it is not allowed. */ +using BinarySerialization; +using Emgu.CV; +using Emgu.CV.Structure; using System; using System.Collections.Generic; using System.Diagnostics; @@ -15,735 +18,720 @@ using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using BinarySerialization; -using Emgu.CV; -using Emgu.CV.Structure; using UVtools.Core.Extensions; using UVtools.Core.Layers; using UVtools.Core.Objects; using UVtools.Core.Operations; -namespace UVtools.Core.FileFormats +namespace UVtools.Core.FileFormats; + +public class CXDLPFile : FileFormat { - public class CXDLPFile : FileFormat + #region Constants + private const byte HEADER_SIZE = 9; // CXSW3DV2 + private const string HEADER_VALUE = "CXSW3DV2"; + #endregion + + #region Sub Classes + #region Header + public sealed class Header { - #region Constants - private const byte HEADER_SIZE = 9; // CXSW3DV2 - private const string HEADER_VALUE = "CXSW3DV2"; - #endregion + //private string _printerModel = "CL-89"; - #region Sub Classes - #region Header - public sealed class Header + /// + /// Gets the size of the header + /// + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint HeaderSize { get; set; } = HEADER_SIZE; + + /// + /// Gets the header name + /// + [FieldOrder(1)] + [FieldLength(HEADER_SIZE)] + [SerializeAs(SerializedType.TerminatedString)] + public string HeaderValue { get; set; } = HEADER_VALUE; + + [FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public ushort Version { get; set; } = 2; + + /// + /// Gets the size of the printer model + /// + [FieldOrder(3)] + [FieldEndianness(Endianness.Big)] + public uint PrinterModelSize { get; set; } = 6; + + /// + /// Gets the printer model + /// + /*[FieldOrder(4)] + [FieldLength(nameof(PrinterModelSize), BindingMode = BindingMode.OneWay)] + [SerializeAs(SerializedType.TerminatedString)] + public string PrinterModel { - //private string _printerModel = "CL-89"; - - /// - /// Gets the size of the header - /// - [FieldOrder(0)] - [FieldEndianness(Endianness.Big)] - public uint HeaderSize { get; set; } = HEADER_SIZE; - - /// - /// Gets the header name - /// - [FieldOrder(1)] - [FieldLength(HEADER_SIZE)] - [SerializeAs(SerializedType.TerminatedString)] - public string HeaderValue { get; set; } = HEADER_VALUE; - - [FieldOrder(2)] - [FieldEndianness(Endianness.Big)] - public ushort Version { get; set; } = 2; - - /// - /// Gets the size of the printer model - /// - [FieldOrder(3)] - [FieldEndianness(Endianness.Big)] - public uint PrinterModelSize { get; set; } = 6; - - /// - /// Gets the printer model - /// - /*[FieldOrder(4)] - [FieldLength(nameof(PrinterModelSize), BindingMode = BindingMode.OneWay)] - [SerializeAs(SerializedType.TerminatedString)] - public string PrinterModel - { - get => _printerModel; - set - { - _printerModel = value; - PrinterModelSize = string.IsNullOrEmpty(value) ? 0 : (uint)value.Length+1; - } - }*/ - - [FieldOrder(4)] - [FieldLength(nameof(PrinterModelSize))] - public byte[] PrinterModelArray { get; set; } = Array.Empty(); // CL-89 { 0x43, 0x4C, 0x2D, 0x38, 0x39, 0x0 } - - [Ignore] - public string PrinterModel - { - get => Encoding.ASCII.GetString(PrinterModelArray).TrimEnd(char.MinValue); - set - { - PrinterModelArray = Encoding.ASCII.GetBytes(value + char.MinValue); - PrinterModelSize = (uint) PrinterModelArray.Length; - } - } - - /// - /// Gets the number of records in the layer table - /// - [FieldOrder(5)] - [FieldEndianness(Endianness.Big)] - public ushort LayerCount { get; set; } - - /// - /// Gets the printer resolution along X axis, in pixels. This information is critical to correctly decoding layer images. - /// - [FieldOrder(6)] - [FieldEndianness(Endianness.Big)] - public ushort ResolutionX { get; set; } - - /// - /// Gets the printer resolution along Y axis, in pixels. This information is critical to correctly decoding layer images. - /// - [FieldOrder(7)] - [FieldEndianness(Endianness.Big)] - public ushort ResolutionY { get; set; } - - [FieldOrder(8)] - [FieldLength(64)] - public byte[] Offset { get; set; } = new byte[64]; - - public void Validate() - { - if (HeaderSize != HEADER_SIZE || HeaderValue != HEADER_VALUE) - { - throw new FileLoadException("Not a valid CXDLP file!"); - } - } - - public override string ToString() - { - return $"{nameof(HeaderSize)}: {HeaderSize}, {nameof(HeaderValue)}: {HeaderValue}, {nameof(Version)}: {Version}, {nameof(PrinterModelSize)}: {PrinterModelSize}, {nameof(PrinterModelArray)}: {PrinterModelArray}, {nameof(PrinterModel)}: {PrinterModel}, {nameof(LayerCount)}: {LayerCount}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(Offset)}: {Offset}"; - } - } - - #endregion - - #region SlicerInfo - // Address: 363407 - public sealed class SlicerInfo - { - [FieldOrder(0)] - [FieldEndianness(Endianness.Big)] - public uint DisplayWidthDataSize { get; set; } = 20; - - [FieldOrder(1)] - [FieldLength(nameof(DisplayWidthDataSize))] - public byte[] DisplayWidthBytes { get; set; } - - [FieldOrder(2)] - [FieldEndianness(Endianness.Big)] - public uint DisplayHeightDataSize { get; set; } = 20; - - [FieldOrder(3)] - [FieldLength(nameof(DisplayHeightDataSize))] - public byte[] DisplayHeightBytes { get; set; } - - [FieldOrder(4)] - [FieldEndianness(Endianness.Big)] - public uint LayerHeightDataSize { get; set; } = 16; - - [FieldOrder(5)] - [FieldLength(nameof(LayerHeightDataSize))] - public byte[] LayerHeightBytes { get; set; } - - [FieldOrder(6)] - [FieldEndianness(Endianness.Big)] - public ushort ExposureTime { get; set; } - - [FieldOrder(7)] - [FieldEndianness(Endianness.Big)] - public ushort WaitTimeBeforeCure { get; set; } = 1; // 1 as minimum or it wont print! - - [FieldOrder(8)] - [FieldEndianness(Endianness.Big)] - public ushort BottomExposureTime { get; set; } - - [FieldOrder(9)] - [FieldEndianness(Endianness.Big)] - public ushort BottomLayersCount { get; set; } - - [FieldOrder(10)] - [FieldEndianness(Endianness.Big)] - public ushort BottomLiftHeight { get; set; } - - [FieldOrder(11)] - [FieldEndianness(Endianness.Big)] - public ushort BottomLiftSpeed { get; set; } - - [FieldOrder(12)] - [FieldEndianness(Endianness.Big)] - public ushort LiftHeight { get; set; } - - [FieldOrder(13)] - [FieldEndianness(Endianness.Big)] - public ushort LiftSpeed { get; set; } - - [FieldOrder(14)] - [FieldEndianness(Endianness.Big)] - public ushort RetractSpeed { get; set; } - - [FieldOrder(15)] - [FieldEndianness(Endianness.Big)] - public ushort BottomLightPWM { get; set; } = 255; - - [FieldOrder(16)] - [FieldEndianness(Endianness.Big)] - public ushort LightPWM { get; set; } = 255; - - public override string ToString() - { - return $"{nameof(DisplayWidthDataSize)}: {DisplayWidthDataSize}, {nameof(DisplayWidthBytes)}: {DisplayWidthBytes}, {nameof(DisplayHeightDataSize)}: {DisplayHeightDataSize}, {nameof(DisplayHeightBytes)}: {DisplayHeightBytes}, {nameof(LayerHeightDataSize)}: {LayerHeightDataSize}, {nameof(LayerHeightBytes)}: {LayerHeightBytes}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(WaitTimeBeforeCure)}: {WaitTimeBeforeCure}, {nameof(BottomExposureTime)}: {BottomExposureTime}, {nameof(BottomLayersCount)}: {BottomLayersCount}, {nameof(BottomLiftHeight)}: {BottomLiftHeight}, {nameof(BottomLiftSpeed)}: {BottomLiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(BottomLightPWM)}: {BottomLightPWM}, {nameof(LightPWM)}: {LightPWM}"; - } - } - #endregion - - #region PreLayer - - public sealed class PreLayer - { - [FieldOrder(0)] - [FieldEndianness(Endianness.Big)] - public uint LayerArea { get; set; } - - public PreLayer() - { - } - - public PreLayer(uint layerArea) - { - LayerArea = layerArea; - } - } - - #endregion - - #region SlicerInfoV3 - - public sealed class SlicerInfoV3 - { - //[FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint SoftwareNameSize { get; set; } = (uint)About.SoftwareWithVersion.Length + 1; - - [FieldOrder(0)] public NullTerminatedUintStringBigEndian SoftwareName { get; set; } = new(About.SoftwareWithVersion); - - //[FieldOrder(2)] [FieldEndianness(Endianness.Big)] public uint MaterialNameSize { get; set; } - - [FieldOrder(1)] public NullTerminatedUintStringBigEndian MaterialName { get; set; } = new(); - - [FieldOrder(2)] public uint Unknown1 { get; set; } - [FieldOrder(3)] public uint Unknown2 { get; set; } - [FieldOrder(4)] public uint Unknown3 { get; set; } - [FieldOrder(5)] public uint Unknown4 { get; set; } - [FieldOrder(6)] public byte Unknown5 { get; set; } = 1; - [FieldOrder(7)] public byte LightPWM { get; set; } = byte.MaxValue; - [FieldOrder(8)] public ushort Unknown6 { get; set; } = 2; - [FieldOrder(9)] public PageBreak PageBreak { get; set; } = new(); - - public override string ToString() - { - return $"{nameof(SoftwareName)}: {SoftwareName}, {nameof(MaterialName)}: {MaterialName}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(Unknown3)}: {Unknown3}, {nameof(Unknown4)}: {Unknown4}, {nameof(Unknown5)}: {Unknown5}, {nameof(LightPWM)}: {LightPWM}, {nameof(Unknown6)}: {Unknown6}, {nameof(PageBreak)}: {PageBreak}"; - } - } - - #endregion - - #region Layer Def - - public sealed class LayerDef - { - [FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint LayerArea { get; set; } - [FieldOrder(1)] [FieldEndianness(Endianness.Big)] public uint LineCount { get; set; } - [FieldOrder(2)] [FieldCount(nameof(LineCount))] public LayerLine[] Lines { get; set; } - [FieldOrder(3)] public PageBreak PageBreak { get; set; } = new(); - - public static byte[] GetHeaderBytes(uint layerArea, uint lineCount) - { - var bytes = new byte[8]; - BitExtensions.ToBytesBigEndian(layerArea, bytes); - BitExtensions.ToBytesBigEndian(lineCount, bytes, 4); - return bytes; - } - - public LayerDef() { } - - public LayerDef(uint layerArea, uint lineCount, LayerLine[] lines) - { - LayerArea = layerArea; - LineCount = lineCount; - Lines = lines; - } - } - - public sealed class LayerLine - { - public const byte CoordinateCount = 5; - [FieldOrder(0)] [FieldCount(CoordinateCount)] public byte[] Coordinates { get; set; } = new byte[CoordinateCount]; - //[FieldOrder(0)] [FieldEndianness(Endianness.Big)] [FieldBitLength(13)] public ushort StartY { get; set; } - //[FieldOrder(1)] [FieldEndianness(Endianness.Big)] [FieldBitLength(13)] public ushort EndY { get; set; } - //[FieldOrder(2)] [FieldEndianness(Endianness.Big)] [FieldBitLength(14)] public ushort StartX { get; set; } - [FieldOrder(1)] public byte Gray { get; set; } - - [Ignore] public ushort StartY => (ushort) ((((Coordinates[0] << 8) + Coordinates[1]) >> 3) & 0x1FFF); // 13 bits - - [Ignore] public ushort EndY => (ushort)((((Coordinates[1] << 16) + (Coordinates[2] << 8) + Coordinates[3]) >> 6) & 0x1FFF); // 13 bits - - [Ignore] public ushort StartX => (ushort)(((Coordinates[3] << 8) + Coordinates[4]) & 0x3FFF); // 14 bits - [Ignore] public ushort Length => (ushort) (EndY - StartY); - - public static byte[] GetBytes(ushort startY, ushort endY, ushort startX, byte gray) - { - var bytes = new byte[CoordinateCount + 1]; - bytes[0] = (byte)((startY >> 5) & 0xFF); - bytes[1] = (byte)(((startY << 3) + (endY >> 10)) & 0xFF); - bytes[2] = (byte)((endY >> 2) & 0xFF); - bytes[3] = (byte)(((endY << 6) + (startX >> 8)) & 0xFF); - bytes[4] = (byte)startX; - bytes[5] = gray; - return bytes; - } - - public LayerLine() { } - - public LayerLine(ushort startY, ushort endY, ushort startX, byte gray) - { - Coordinates[0] = (byte) ((startY >> 5) & 0xFF); - Coordinates[1] = (byte) (((startY << 3) + (endY >> 10)) & 0xFF); - Coordinates[2] = (byte) ((endY >> 2) & 0xFF); - Coordinates[3] = (byte)(((endY << 6) + (startX >> 8)) & 0xFF); - Coordinates[4] = (byte) startX; - /*StartY = startY; - EndY = endY; - StartX = startX;*/ - Gray = gray; - } - - public override string ToString() - { - return $"{nameof(Gray)}: {Gray}, {nameof(StartY)}: {StartY}, {nameof(EndY)}: {EndY}, {nameof(StartX)}: {StartX}, {nameof(Length)}: {Length}"; - } - } - - public sealed class PageBreak - { - public static byte[] Bytes => new byte[] {0x0D, 0x0A}; - - [FieldOrder(0)] public byte Line { get; set; } = 0x0D; - [FieldOrder(1)] public byte Break { get; set; } = 0x0A; - } - - #endregion - - #region Footer - public sealed class Footer - { - /// - /// Gets the size of the header - /// - [FieldOrder(0)] - [FieldEndianness(Endianness.Big)] - public uint FooterSize { get; set; } = HEADER_SIZE; - - /// - /// Gets the header name - /// - [FieldOrder(1)] - [FieldLength(HEADER_SIZE)] - [SerializeAs(SerializedType.TerminatedString)] - public string FooterValue { get; set; } = HEADER_VALUE; - - /*[FieldOrder(2)] - [FieldEndianness(Endianness.Big)] - public uint CheckSum { get; set; }*/ - - public void Validate() - { - if (FooterSize != HEADER_SIZE || FooterValue != HEADER_VALUE) - { - throw new FileLoadException("Not a valid CXDLP file!"); - } - } - } - #endregion - - #endregion - - #region Properties - - public Header HeaderSettings { get; protected internal set; } = new(); - public SlicerInfo SlicerInfoSettings { get; protected internal set; } = new(); - public SlicerInfoV3 SlicerInfoV3Settings { get; protected internal set; } = new(); - public Footer FooterSettings { get; protected internal set; } = new(); - - public override FileFormatType FileType => FileFormatType.Binary; - - public override FileExtension[] FileExtensions { get; } = { - new(typeof(CXDLPFile), "cxdlp", "Creality CXDLP"), - }; - - public override PrintParameterModifier[] PrintParameterModifiers { get; } = - { - PrintParameterModifier.BottomLayerCount, - - PrintParameterModifier.WaitTimeBeforeCure, - - PrintParameterModifier.BottomExposureTime, - PrintParameterModifier.ExposureTime, - - PrintParameterModifier.BottomLiftHeight, - PrintParameterModifier.BottomLiftSpeed, - PrintParameterModifier.LiftHeight, - PrintParameterModifier.LiftSpeed, - PrintParameterModifier.RetractSpeed, - - PrintParameterModifier.BottomLightPWM, - PrintParameterModifier.LightPWM, - }; - - public override Size[] ThumbnailsOriginalSize { get; } = - { - new(116, 116), - new(290, 290), - new(290, 290) - }; - - public override uint[] AvailableVersions { get; } = { 2, 3 }; - - public override uint DefaultVersion => 2; - - public override uint Version - { - get => HeaderSettings.Version; + get => _printerModel; set { - base.Version = value; - HeaderSettings.Version = (ushort)base.Version; + _printerModel = value; + PrinterModelSize = string.IsNullOrEmpty(value) ? 0 : (uint)value.Length+1; } - } - - public override uint ResolutionX - { - get => HeaderSettings.ResolutionX; - set - { - HeaderSettings.ResolutionX = (ushort) value; - RaisePropertyChanged(); - } - } - - public override uint ResolutionY - { - get => HeaderSettings.ResolutionY; - set - { - HeaderSettings.ResolutionY = (ushort) value; - RaisePropertyChanged(); - } - } - - public override float DisplayWidth - { - get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.DisplayWidthBytes.Where(b => b != 0).ToArray())); - set - { - string str = Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); - //string str = Math.Round(value, 2).ToString("0.000000"); - SlicerInfoSettings.DisplayWidthDataSize = (uint)(str.Length * 2); - var data = new byte[SlicerInfoSettings.DisplayWidthDataSize]; - for (var i = 0; i < str.Length; i++) - { - data[i * 2 + 1] = System.Convert.ToByte(str[i]); - } - - SlicerInfoSettings.DisplayWidthBytes = data; - RaisePropertyChanged(); - } - } - - public override float DisplayHeight - { - get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.DisplayHeightBytes.Where(b => b != 0).ToArray())); - set - { - string str = Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); - //string str = Math.Round(value, 2).ToString("0.000000"); - SlicerInfoSettings.DisplayHeightDataSize = (uint)(str.Length * 2); - var data = new byte[SlicerInfoSettings.DisplayHeightDataSize]; - for (var i = 0; i < str.Length; i++) - { - data[i * 2 + 1] = System.Convert.ToByte(str[i]); - } - - SlicerInfoSettings.DisplayHeightBytes = data; - RaisePropertyChanged(); - } - } - - public override float LayerHeight - { - get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.LayerHeightBytes.Where(b => b != 0).ToArray())); - set - { - string str = Layer.RoundHeight(value).ToString(CultureInfo.InvariantCulture); - //string str = Layer.RoundHeight(value).ToString("0.000000"); - SlicerInfoSettings.LayerHeightDataSize = (uint)(str.Length * 2); - var data = new byte[SlicerInfoSettings.LayerHeightDataSize]; - for (var i = 0; i < str.Length; i++) - { - data[i * 2 + 1] = System.Convert.ToByte(str[i]); - } - - SlicerInfoSettings.LayerHeightBytes = data; - RaisePropertyChanged(); - } - } - - public override uint LayerCount - { - get => base.LayerCount; - set => base.LayerCount = HeaderSettings.LayerCount = (ushort) base.LayerCount; - } - - public override ushort BottomLayerCount - { - get => SlicerInfoSettings.BottomLayersCount; - set => base.BottomLayerCount = SlicerInfoSettings.BottomLayersCount = value; - } - - /*public override float BottomLightOffDelay => SlicerInfoSettings.WaitTimeBeforeCure; - - public override float LightOffDelay - { - get => SlicerInfoSettings.WaitTimeBeforeCure; - set => base.LightOffDelay = SlicerInfoSettings.WaitTimeBeforeCure = (ushort)value; }*/ - public override float BottomWaitTimeBeforeCure => WaitTimeBeforeCure; + [FieldOrder(4)] + [FieldLength(nameof(PrinterModelSize))] + public byte[] PrinterModelArray { get; set; } = Array.Empty(); // CL-89 { 0x43, 0x4C, 0x2D, 0x38, 0x39, 0x0 } - public override float WaitTimeBeforeCure + [Ignore] + public string PrinterModel { - get => SlicerInfoSettings.WaitTimeBeforeCure; - set => base.WaitTimeBeforeCure = SlicerInfoSettings.WaitTimeBeforeCure = (ushort)Math.Max(1, value); - } - - public override float BottomExposureTime - { - get => SlicerInfoSettings.BottomExposureTime; - set => base.BottomExposureTime = SlicerInfoSettings.BottomExposureTime = (ushort) value; - } - - public override float ExposureTime - { - get => SlicerInfoSettings.ExposureTime; - set => base.ExposureTime = SlicerInfoSettings.ExposureTime = (ushort) value; - } - - public override float BottomLiftHeight - { - get => SlicerInfoSettings.BottomLiftHeight; - set => base.BottomLiftHeight = SlicerInfoSettings.BottomLiftHeight = (ushort) value; - } - - public override float LiftHeight - { - get => SlicerInfoSettings.LiftHeight; - set => base.LiftHeight = SlicerInfoSettings.LiftHeight = (ushort)value; - } - - public override float BottomLiftSpeed - { - get => SlicerInfoSettings.BottomLiftSpeed; - set => base.BottomLiftSpeed = SlicerInfoSettings.BottomLiftSpeed = (ushort)value; - } - - public override float LiftSpeed - { - get => SlicerInfoSettings.LiftSpeed; - set => base.LiftSpeed = SlicerInfoSettings.LiftSpeed = (ushort)value; - } - - public override float BottomRetractSpeed => RetractSpeed; - - public override float RetractSpeed - { - get => SlicerInfoSettings.RetractSpeed; - set => base.RetractSpeed = SlicerInfoSettings.RetractSpeed = (ushort)value; - } - - public override byte BottomLightPWM - { - get => (byte) SlicerInfoSettings.BottomLightPWM; - set => base.BottomLightPWM = (byte) (SlicerInfoSettings.BottomLightPWM = value); - } - - public override byte LightPWM - { - get => (byte)SlicerInfoSettings.LightPWM; - set => base.LightPWM = (byte) (SlicerInfoSettings.LightPWM = value); - } - - public override string MachineName - { - get => HeaderSettings.PrinterModel; - set => base.MachineName = HeaderSettings.PrinterModel = value; - } - - public override string MaterialName - { - get => SlicerInfoV3Settings.MaterialName.Value; - set => base.MaterialName = SlicerInfoV3Settings.MaterialName.Value = value; - } - - public override object[] Configs => new object[] { HeaderSettings, SlicerInfoSettings, SlicerInfoV3Settings, FooterSettings }; - - #endregion - - #region Constructors - #endregion - - #region Methods - - private void SanitizeProperties() - { - SlicerInfoSettings.WaitTimeBeforeCure = (ushort)Math.Max(1, WaitTimeBeforeCure); - } - - protected override void EncodeInternally(OperationProgress progress) - { - using var outputFile = new FileStream(FileFullPath, FileMode.Create, FileAccess.ReadWrite); - - if (!MachineName.StartsWith("CL-")) + get => Encoding.ASCII.GetString(PrinterModelArray).TrimEnd(char.MinValue); + set { - if (ResolutionX == 1620 && ResolutionY == 2560) - { - MachineName = "CL-60"; // One - } - else if (ResolutionX == 3840 && ResolutionY == 2400) // Sky or lite - { - MachineName = "CL-89"; // Sky - } - else if (ResolutionX == 3840 && ResolutionY == 2160) - { - MachineName = "CL-133"; // Max - } - else - { - throw new InvalidDataException("Unable to detect the printer model from resolution, check if resolution is well defined on slicer for your printer model."); - } + PrinterModelArray = Encoding.ASCII.GetBytes(value + char.MinValue); + PrinterModelSize = (uint) PrinterModelArray.Length; + } + } + + /// + /// Gets the number of records in the layer table + /// + [FieldOrder(5)] + [FieldEndianness(Endianness.Big)] + public ushort LayerCount { get; set; } + + /// + /// Gets the printer resolution along X axis, in pixels. This information is critical to correctly decoding layer images. + /// + [FieldOrder(6)] + [FieldEndianness(Endianness.Big)] + public ushort ResolutionX { get; set; } + + /// + /// Gets the printer resolution along Y axis, in pixels. This information is critical to correctly decoding layer images. + /// + [FieldOrder(7)] + [FieldEndianness(Endianness.Big)] + public ushort ResolutionY { get; set; } + + [FieldOrder(8)] + [FieldLength(64)] + public byte[] Offset { get; set; } = new byte[64]; + + public void Validate() + { + if (HeaderSize != HEADER_SIZE || HeaderValue != HEADER_VALUE) + { + throw new FileLoadException("Not a valid CXDLP file!"); + } + } + + public override string ToString() + { + return $"{nameof(HeaderSize)}: {HeaderSize}, {nameof(HeaderValue)}: {HeaderValue}, {nameof(Version)}: {Version}, {nameof(PrinterModelSize)}: {PrinterModelSize}, {nameof(PrinterModelArray)}: {PrinterModelArray}, {nameof(PrinterModel)}: {PrinterModel}, {nameof(LayerCount)}: {LayerCount}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(Offset)}: {Offset}"; + } + } + + #endregion + + #region SlicerInfo + // Address: 363407 + public sealed class SlicerInfo + { + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint DisplayWidthDataSize { get; set; } = 20; + + [FieldOrder(1)] + [FieldLength(nameof(DisplayWidthDataSize))] + public byte[] DisplayWidthBytes { get; set; } = null!; + + [FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public uint DisplayHeightDataSize { get; set; } = 20; + + [FieldOrder(3)] + [FieldLength(nameof(DisplayHeightDataSize))] + public byte[] DisplayHeightBytes { get; set; } = null!; + + [FieldOrder(4)] + [FieldEndianness(Endianness.Big)] + public uint LayerHeightDataSize { get; set; } = 16; + + [FieldOrder(5)] + [FieldLength(nameof(LayerHeightDataSize))] + public byte[] LayerHeightBytes { get; set; } = null!; + + [FieldOrder(6)] + [FieldEndianness(Endianness.Big)] + public ushort ExposureTime { get; set; } + + [FieldOrder(7)] + [FieldEndianness(Endianness.Big)] + public ushort WaitTimeBeforeCure { get; set; } = 1; // 1 as minimum or it wont print! + + [FieldOrder(8)] + [FieldEndianness(Endianness.Big)] + public ushort BottomExposureTime { get; set; } + + [FieldOrder(9)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLayersCount { get; set; } + + [FieldOrder(10)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLiftHeight { get; set; } + + [FieldOrder(11)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLiftSpeed { get; set; } + + [FieldOrder(12)] + [FieldEndianness(Endianness.Big)] + public ushort LiftHeight { get; set; } + + [FieldOrder(13)] + [FieldEndianness(Endianness.Big)] + public ushort LiftSpeed { get; set; } + + [FieldOrder(14)] + [FieldEndianness(Endianness.Big)] + public ushort RetractSpeed { get; set; } + + [FieldOrder(15)] + [FieldEndianness(Endianness.Big)] + public ushort BottomLightPWM { get; set; } = 255; + + [FieldOrder(16)] + [FieldEndianness(Endianness.Big)] + public ushort LightPWM { get; set; } = 255; + + public override string ToString() + { + return $"{nameof(DisplayWidthDataSize)}: {DisplayWidthDataSize}, {nameof(DisplayWidthBytes)}: {DisplayWidthBytes}, {nameof(DisplayHeightDataSize)}: {DisplayHeightDataSize}, {nameof(DisplayHeightBytes)}: {DisplayHeightBytes}, {nameof(LayerHeightDataSize)}: {LayerHeightDataSize}, {nameof(LayerHeightBytes)}: {LayerHeightBytes}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(WaitTimeBeforeCure)}: {WaitTimeBeforeCure}, {nameof(BottomExposureTime)}: {BottomExposureTime}, {nameof(BottomLayersCount)}: {BottomLayersCount}, {nameof(BottomLiftHeight)}: {BottomLiftHeight}, {nameof(BottomLiftSpeed)}: {BottomLiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(BottomLightPWM)}: {BottomLightPWM}, {nameof(LightPWM)}: {LightPWM}"; + } + } + #endregion + + #region PreLayer + + public sealed class PreLayer + { + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint LayerArea { get; set; } + + public PreLayer() + { + } + + public PreLayer(uint layerArea) + { + LayerArea = layerArea; + } + } + + #endregion + + #region SlicerInfoV3 + + public sealed class SlicerInfoV3 + { + //[FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint SoftwareNameSize { get; set; } = (uint)About.SoftwareWithVersion.Length + 1; + + [FieldOrder(0)] public NullTerminatedUintStringBigEndian SoftwareName { get; set; } = new(About.SoftwareWithVersion); + + //[FieldOrder(2)] [FieldEndianness(Endianness.Big)] public uint MaterialNameSize { get; set; } + + [FieldOrder(1)] public NullTerminatedUintStringBigEndian MaterialName { get; set; } = new(); + + [FieldOrder(2)] public uint Unknown1 { get; set; } + [FieldOrder(3)] public uint Unknown2 { get; set; } + [FieldOrder(4)] public uint Unknown3 { get; set; } + [FieldOrder(5)] public uint Unknown4 { get; set; } + [FieldOrder(6)] public byte Unknown5 { get; set; } = 1; + [FieldOrder(7)] public byte LightPWM { get; set; } = byte.MaxValue; + [FieldOrder(8)] public ushort Unknown6 { get; set; } = 2; + [FieldOrder(9)] public PageBreak PageBreak { get; set; } = new(); + + public override string ToString() + { + return $"{nameof(SoftwareName)}: {SoftwareName}, {nameof(MaterialName)}: {MaterialName}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(Unknown3)}: {Unknown3}, {nameof(Unknown4)}: {Unknown4}, {nameof(Unknown5)}: {Unknown5}, {nameof(LightPWM)}: {LightPWM}, {nameof(Unknown6)}: {Unknown6}, {nameof(PageBreak)}: {PageBreak}"; + } + } + + #endregion + + #region Layer Def + + public sealed class LayerDef + { + public static byte[] GetHeaderBytes(uint layerArea, uint lineCount) + { + var bytes = new byte[8]; + BitExtensions.ToBytesBigEndian(layerArea, bytes); + BitExtensions.ToBytesBigEndian(lineCount, bytes, 4); + return bytes; + } + + [FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint LayerArea { get; set; } + [FieldOrder(1)] [FieldEndianness(Endianness.Big)] public uint LineCount { get; set; } + [FieldOrder(2)] [FieldCount(nameof(LineCount))] public LayerLine[] Lines { get; set; } = Array.Empty(); + [FieldOrder(3)] public PageBreak PageBreak { get; set; } = new(); + + public LayerDef() { } + + public LayerDef(uint layerArea, uint lineCount, LayerLine[] lines) + { + LayerArea = layerArea; + LineCount = lineCount; + Lines = lines; + } + } + + public sealed class LayerLine + { + public const byte CoordinateCount = 5; + [FieldOrder(0)] [FieldCount(CoordinateCount)] public byte[] Coordinates { get; set; } = new byte[CoordinateCount]; + //[FieldOrder(0)] [FieldEndianness(Endianness.Big)] [FieldBitLength(13)] public ushort StartY { get; set; } + //[FieldOrder(1)] [FieldEndianness(Endianness.Big)] [FieldBitLength(13)] public ushort EndY { get; set; } + //[FieldOrder(2)] [FieldEndianness(Endianness.Big)] [FieldBitLength(14)] public ushort StartX { get; set; } + [FieldOrder(1)] public byte Gray { get; set; } + + [Ignore] public ushort StartY => (ushort) ((((Coordinates[0] << 8) + Coordinates[1]) >> 3) & 0x1FFF); // 13 bits + + [Ignore] public ushort EndY => (ushort)((((Coordinates[1] << 16) + (Coordinates[2] << 8) + Coordinates[3]) >> 6) & 0x1FFF); // 13 bits + + [Ignore] public ushort StartX => (ushort)(((Coordinates[3] << 8) + Coordinates[4]) & 0x3FFF); // 14 bits + [Ignore] public ushort Length => (ushort) (EndY - StartY); + + public static byte[] GetBytes(ushort startY, ushort endY, ushort startX, byte gray) + { + var bytes = new byte[CoordinateCount + 1]; + bytes[0] = (byte)((startY >> 5) & 0xFF); + bytes[1] = (byte)(((startY << 3) + (endY >> 10)) & 0xFF); + bytes[2] = (byte)((endY >> 2) & 0xFF); + bytes[3] = (byte)(((endY << 6) + (startX >> 8)) & 0xFF); + bytes[4] = (byte)startX; + bytes[5] = gray; + return bytes; + } + + public LayerLine() { } + + public LayerLine(ushort startY, ushort endY, ushort startX, byte gray) + { + Coordinates[0] = (byte) ((startY >> 5) & 0xFF); + Coordinates[1] = (byte) (((startY << 3) + (endY >> 10)) & 0xFF); + Coordinates[2] = (byte) ((endY >> 2) & 0xFF); + Coordinates[3] = (byte)(((endY << 6) + (startX >> 8)) & 0xFF); + Coordinates[4] = (byte) startX; + /*StartY = startY; + EndY = endY; + StartX = startX;*/ + Gray = gray; + } + + public override string ToString() + { + return $"{nameof(Gray)}: {Gray}, {nameof(StartY)}: {StartY}, {nameof(EndY)}: {EndY}, {nameof(StartX)}: {StartX}, {nameof(Length)}: {Length}"; + } + } + + public sealed class PageBreak + { + public static byte[] Bytes => new byte[] {0x0D, 0x0A}; + + [FieldOrder(0)] public byte Line { get; set; } = 0x0D; + [FieldOrder(1)] public byte Break { get; set; } = 0x0A; + } + + #endregion + + #region Footer + public sealed class Footer + { + /// + /// Gets the size of the header + /// + [FieldOrder(0)] + [FieldEndianness(Endianness.Big)] + public uint FooterSize { get; set; } = HEADER_SIZE; + + /// + /// Gets the header name + /// + [FieldOrder(1)] + [FieldLength(HEADER_SIZE)] + [SerializeAs(SerializedType.TerminatedString)] + public string FooterValue { get; set; } = HEADER_VALUE; + + /*[FieldOrder(2)] + [FieldEndianness(Endianness.Big)] + public uint CheckSum { get; set; }*/ + + public void Validate() + { + if (FooterSize != HEADER_SIZE || FooterValue != HEADER_VALUE) + { + throw new FileLoadException("Not a valid CXDLP file!"); + } + } + } + #endregion + + #endregion + + #region Properties + + public Header HeaderSettings { get; protected internal set; } = new(); + public SlicerInfo SlicerInfoSettings { get; protected internal set; } = new(); + public SlicerInfoV3 SlicerInfoV3Settings { get; protected internal set; } = new(); + public Footer FooterSettings { get; protected internal set; } = new(); + + public override FileFormatType FileType => FileFormatType.Binary; + + public override FileExtension[] FileExtensions { get; } = { + new(typeof(CXDLPFile), "cxdlp", "Creality CXDLP"), + }; + + public override PrintParameterModifier[]? PrintParameterModifiers { get; } = + { + PrintParameterModifier.BottomLayerCount, + + PrintParameterModifier.WaitTimeBeforeCure, + + PrintParameterModifier.BottomExposureTime, + PrintParameterModifier.ExposureTime, + + PrintParameterModifier.BottomLiftHeight, + PrintParameterModifier.BottomLiftSpeed, + PrintParameterModifier.LiftHeight, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.RetractSpeed, + + PrintParameterModifier.BottomLightPWM, + PrintParameterModifier.LightPWM, + }; + + public override Size[]? ThumbnailsOriginalSize { get; } = + { + new(116, 116), + new(290, 290), + new(290, 290) + }; + + public override uint[] AvailableVersions { get; } = { 2, 3 }; + + public override uint DefaultVersion => 2; + + public override uint Version + { + get => HeaderSettings.Version; + set + { + base.Version = value; + HeaderSettings.Version = (ushort)base.Version; + } + } + + public override uint ResolutionX + { + get => HeaderSettings.ResolutionX; + set + { + HeaderSettings.ResolutionX = (ushort) value; + RaisePropertyChanged(); + } + } + + public override uint ResolutionY + { + get => HeaderSettings.ResolutionY; + set + { + HeaderSettings.ResolutionY = (ushort) value; + RaisePropertyChanged(); + } + } + + public override float DisplayWidth + { + get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.DisplayWidthBytes.Where(b => b != 0).ToArray())); + set + { + string str = Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); + //string str = Math.Round(value, 2).ToString("0.000000"); + SlicerInfoSettings.DisplayWidthDataSize = (uint)(str.Length * 2); + var data = new byte[SlicerInfoSettings.DisplayWidthDataSize]; + for (var i = 0; i < str.Length; i++) + { + data[i * 2 + 1] = System.Convert.ToByte(str[i]); } - SanitizeProperties(); + SlicerInfoSettings.DisplayWidthBytes = data; + RaisePropertyChanged(); + } + } - var pageBreak = PageBreak.Bytes; - - Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); - - var previews = new byte[ThumbnailsOriginalSize.Length][]; - - // Previews - Parallel.For(0, previews.Length, CoreSettings.ParallelOptions, previewIndex => + public override float DisplayHeight + { + get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.DisplayHeightBytes.Where(b => b != 0).ToArray())); + set + { + string str = Math.Round(value, 2).ToString(CultureInfo.InvariantCulture); + //string str = Math.Round(value, 2).ToString("0.000000"); + SlicerInfoSettings.DisplayHeightDataSize = (uint)(str.Length * 2); + var data = new byte[SlicerInfoSettings.DisplayHeightDataSize]; + for (var i = 0; i < str.Length; i++) { - if (progress.Token.IsCancellationRequested) return; - var encodeLength = ThumbnailsOriginalSize[previewIndex].Area() * 2; - if (Thumbnails[previewIndex] is null) - { - previews[previewIndex] = new byte[encodeLength]; - return; - } - - previews[previewIndex] = EncodeImage(DATATYPE_RGB565_BE, Thumbnails[previewIndex]); - - if (encodeLength != previews[previewIndex].Length) - { - throw new FileLoadException($"Preview encode incomplete encode, expected: {previews[previewIndex].Length}, encoded: {encodeLength}"); - } - }); - - for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) - { - Helpers.SerializeWriteFileStream(outputFile, previews[i]); - outputFile.WriteBytes(pageBreak); - previews[i] = null; + data[i * 2 + 1] = System.Convert.ToByte(str[i]); } - Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); - progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); - //var preLayers = new PreLayer[LayerCount]; - //var layerDefs = new LayerDef[LayerCount]; - //var layersStreams = new MemoryStream[LayerCount]; + SlicerInfoSettings.DisplayHeightBytes = data; + RaisePropertyChanged(); + } + } + + public override float LayerHeight + { + get => float.Parse(Encoding.ASCII.GetString(SlicerInfoSettings.LayerHeightBytes.Where(b => b != 0).ToArray())); + set + { + string str = Layer.RoundHeight(value).ToString(CultureInfo.InvariantCulture); + //string str = Layer.RoundHeight(value).ToString("0.000000"); + SlicerInfoSettings.LayerHeightDataSize = (uint)(str.Length * 2); + var data = new byte[SlicerInfoSettings.LayerHeightDataSize]; + for (var i = 0; i < str.Length; i++) + { + data[i * 2 + 1] = System.Convert.ToByte(str[i]); + } + + SlicerInfoSettings.LayerHeightBytes = data; + RaisePropertyChanged(); + } + } + + public override uint LayerCount + { + get => base.LayerCount; + set => base.LayerCount = HeaderSettings.LayerCount = (ushort) base.LayerCount; + } + + public override ushort BottomLayerCount + { + get => SlicerInfoSettings.BottomLayersCount; + set => base.BottomLayerCount = SlicerInfoSettings.BottomLayersCount = value; + } + + /*public override float BottomLightOffDelay => SlicerInfoSettings.WaitTimeBeforeCure; + + public override float LightOffDelay + { + get => SlicerInfoSettings.WaitTimeBeforeCure; + set => base.LightOffDelay = SlicerInfoSettings.WaitTimeBeforeCure = (ushort)value; + }*/ + + public override float BottomWaitTimeBeforeCure => WaitTimeBeforeCure; + + public override float WaitTimeBeforeCure + { + get => SlicerInfoSettings.WaitTimeBeforeCure; + set => base.WaitTimeBeforeCure = SlicerInfoSettings.WaitTimeBeforeCure = (ushort)Math.Max(1, value); + } + + public override float BottomExposureTime + { + get => SlicerInfoSettings.BottomExposureTime; + set => base.BottomExposureTime = SlicerInfoSettings.BottomExposureTime = (ushort) value; + } + + public override float ExposureTime + { + get => SlicerInfoSettings.ExposureTime; + set => base.ExposureTime = SlicerInfoSettings.ExposureTime = (ushort) value; + } + + public override float BottomLiftHeight + { + get => SlicerInfoSettings.BottomLiftHeight; + set => base.BottomLiftHeight = SlicerInfoSettings.BottomLiftHeight = (ushort) value; + } + + public override float LiftHeight + { + get => SlicerInfoSettings.LiftHeight; + set => base.LiftHeight = SlicerInfoSettings.LiftHeight = (ushort)value; + } + + public override float BottomLiftSpeed + { + get => SlicerInfoSettings.BottomLiftSpeed; + set => base.BottomLiftSpeed = SlicerInfoSettings.BottomLiftSpeed = (ushort)value; + } + + public override float LiftSpeed + { + get => SlicerInfoSettings.LiftSpeed; + set => base.LiftSpeed = SlicerInfoSettings.LiftSpeed = (ushort)value; + } + + public override float BottomRetractSpeed => RetractSpeed; + + public override float RetractSpeed + { + get => SlicerInfoSettings.RetractSpeed; + set => base.RetractSpeed = SlicerInfoSettings.RetractSpeed = (ushort)value; + } + + public override byte BottomLightPWM + { + get => (byte) SlicerInfoSettings.BottomLightPWM; + set => base.BottomLightPWM = (byte) (SlicerInfoSettings.BottomLightPWM = value); + } + + public override byte LightPWM + { + get => (byte)SlicerInfoSettings.LightPWM; + set => base.LightPWM = (byte) (SlicerInfoSettings.LightPWM = value); + } + + public override string MachineName + { + get => HeaderSettings.PrinterModel; + set => base.MachineName = HeaderSettings.PrinterModel = value; + } + + public override string? MaterialName + { + get => SlicerInfoV3Settings.MaterialName.Value; + set => base.MaterialName = SlicerInfoV3Settings.MaterialName.Value = value; + } + + public override object[] Configs => new object[] { HeaderSettings, SlicerInfoSettings, SlicerInfoV3Settings, FooterSettings }; + + #endregion + + #region Constructors + #endregion + + #region Methods + + private void SanitizeProperties() + { + SlicerInfoSettings.WaitTimeBeforeCure = (ushort)Math.Max(1, WaitTimeBeforeCure); + } + + protected override void EncodeInternally(OperationProgress progress) + { + using var outputFile = new FileStream(FileFullPath!, FileMode.Create, FileAccess.ReadWrite); + + if (!MachineName.StartsWith("CL-")) + { + if (ResolutionX == 1620 && ResolutionY == 2560) + { + MachineName = "CL-60"; // One + } + else if (ResolutionX == 3840 && ResolutionY == 2400) // Sky or lite + { + MachineName = "CL-89"; // Sky + } + else if (ResolutionX == 3840 && ResolutionY == 2160) + { + MachineName = "CL-133"; // Max + } + else + { + throw new InvalidDataException("Unable to detect the printer model from resolution, check if resolution is well defined on slicer for your printer model."); + } + } + + SanitizeProperties(); + + var pageBreak = PageBreak.Bytes; + + Helpers.SerializeWriteFileStream(outputFile, HeaderSettings); + + var previews = new byte[ThumbnailsOriginalSize!.Length][]; + + // Previews + Parallel.For(0, previews.Length, CoreSettings.ParallelOptions, previewIndex => + { + if (progress.Token.IsCancellationRequested) return; + var encodeLength = ThumbnailsOriginalSize[previewIndex].Area() * 2; + if (Thumbnails[previewIndex] is null) + { + previews[previewIndex] = new byte[encodeLength]; + return; + } + + previews[previewIndex] = EncodeImage(DATATYPE_RGB565_BE, Thumbnails[previewIndex]!); + + if (encodeLength != previews[previewIndex].Length) + { + throw new FileLoadException($"Preview encode incomplete encode, expected: {previews[previewIndex].Length}, encoded: {encodeLength}"); + } + }); + + for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) + { + Helpers.SerializeWriteFileStream(outputFile, previews[i]); + outputFile.WriteBytes(pageBreak); + previews[i] = null!; + } + Helpers.SerializeWriteFileStream(outputFile, SlicerInfoSettings); + + progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); + //var preLayers = new PreLayer[LayerCount]; + //var layerDefs = new LayerDef[LayerCount]; + //var layersStreams = new MemoryStream[LayerCount]; - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) - { - //var layer = this[layerIndex]; - outputFile.WriteBytes(BitExtensions.ToBytesBigEndian((uint)Math.Round(this[layerIndex].BoundingRectangleMillimeters.Area()*1000))); - //preLayers[layerIndex] = new(layer.NonZeroPixelCount); - } - //Helpers.SerializeWriteFileStream(outputFile, preLayers); - //Helpers.SerializeWriteFileStream(outputFile, pageBreak); - outputFile.WriteBytes(pageBreak); + for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + //var layer = this[layerIndex]; + outputFile.WriteBytes(BitExtensions.ToBytesBigEndian((uint)Math.Round(this[layerIndex].BoundingRectangleMillimeters.Area()*1000))); + //preLayers[layerIndex] = new(layer.NonZeroPixelCount); + } + //Helpers.SerializeWriteFileStream(outputFile, preLayers); + //Helpers.SerializeWriteFileStream(outputFile, pageBreak); + outputFile.WriteBytes(pageBreak); - if (HeaderSettings.Version >= 3) - { - Helpers.SerializeWriteFileStream(outputFile, SlicerInfoV3Settings); - } + if (HeaderSettings.Version >= 3) + { + Helpers.SerializeWriteFileStream(outputFile, SlicerInfoV3Settings); + } - var layerBytes = new List[LayerCount]; - foreach (var batch in BatchLayersIndexes()) - { - progress.Token.ThrowIfCancellationRequested(); + var layerBytes = new List[LayerCount]; + foreach (var batch in BatchLayersIndexes()) + { + progress.Token.ThrowIfCancellationRequested(); - Parallel.ForEach(batch, CoreSettings.ParallelOptions, layerIndex => + Parallel.ForEach(batch, CoreSettings.ParallelOptions, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + var layer = this[layerIndex]; + using (var mat = layer.LayerMat) { - if (progress.Token.IsCancellationRequested) return; - var layer = this[layerIndex]; - using (var mat = layer.LayerMat) + var span = mat.GetDataByteSpan(); + + layerBytes[layerIndex] = new(); + + uint lineCount = 0; + + for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) { - var span = mat.GetDataByteSpan(); - - layerBytes[layerIndex] = new(); - - uint lineCount = 0; - - for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) + int y = layer.BoundingRectangle.Y; + int startY = -1; + byte lastColor = 0; + for (; y < layer.BoundingRectangle.Bottom; y++) { - int y = layer.BoundingRectangle.Y; - int startY = -1; - byte lastColor = 0; - for (; y < layer.BoundingRectangle.Bottom; y++) - { - int pos = mat.GetPixelPos(x, y); - byte color = span[pos]; + int pos = mat.GetPixelPos(x, y); + byte color = span[pos]; - if (lastColor == color && color != 0) continue; - - if (startY >= 0) - { - layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), - (ushort)x, lastColor)); - lineCount++; - } - - startY = color == 0 ? -1 : y; - - lastColor = color; - } + if (lastColor == color && color != 0) continue; if (startY >= 0) { @@ -751,331 +739,342 @@ namespace UVtools.Core.FileFormats (ushort)x, lastColor)); lineCount++; } + + startY = color == 0 ? -1 : y; + + lastColor = color; } - layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes( - (uint)Math.Round(layer.BoundingRectangleMillimeters.Area() * 1000), - lineCount)); - layerBytes[layerIndex].AddRange(pageBreak); - } - - progress.LockAndIncrement(); - }); - - progress.Token.ThrowIfCancellationRequested(); - - foreach (var layerIndex in batch) - { - outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); - layerBytes[layerIndex] = null; - } - } - - - /*Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, - //new ParallelOptions{MaxDegreeOfParallelism = 1}, - layerIndex => - { - if (progress.Token.IsCancellationRequested) return; - //List layerLines = new(); - var layer = this[layerIndex]; - using var mat = layer.LayerMat; - var span = mat.GetDataByteSpan(); - - layerBytes[layerIndex] = new(); - - for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) - { - int y = layer.BoundingRectangle.Y; - int startY = -1; - byte lastColor = 0; - for (; y < layer.BoundingRectangle.Bottom; y++) - { - int pos = mat.GetPixelPos(x, y); - byte color = span[pos]; - - if (lastColor == color && color != 0) continue; - if (startY >= 0) { - layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); - //layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); - //Debug.WriteLine(layerLines[^1]); + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), + (ushort)x, lastColor)); + lineCount++; } - - startY = color == 0 ? -1 : y; - - lastColor = color; } + layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes( + (uint)Math.Round(layer.BoundingRectangleMillimeters.Area() * 1000), + lineCount)); + layerBytes[layerIndex].AddRange(pageBreak); + } + + progress.LockAndIncrement(); + }); + + progress.Token.ThrowIfCancellationRequested(); + + foreach (var layerIndex in batch) + { + outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); + layerBytes[layerIndex] = null!; + } + } + + + /*Parallel.For(0, LayerCount, CoreSettings.ParallelOptions, + //new ParallelOptions{MaxDegreeOfParallelism = 1}, + layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + //List layerLines = new(); + var layer = this[layerIndex]; + using var mat = layer.LayerMat; + var span = mat.GetDataByteSpan(); + + layerBytes[layerIndex] = new(); + + for (int x = layer.BoundingRectangle.X; x < layer.BoundingRectangle.Right; x++) + { + int y = layer.BoundingRectangle.Y; + int startY = -1; + byte lastColor = 0; + for (; y < layer.BoundingRectangle.Bottom; y++) + { + int pos = mat.GetPixelPos(x, y); + byte color = span[pos]; + + if (lastColor == color && color != 0) continue; + if (startY >= 0) { layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); //layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); //Debug.WriteLine(layerLines[^1]); } + + startY = color == 0 ? -1 : y; + + lastColor = color; } - //layerDefs[layerIndex] = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); - //var layerDef = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); - //layersStreams[layerIndex] = new MemoryStream(); - //Helpers.Serializer.Serialize(layersStreams[layerIndex], layerDef); + if (startY >= 0) + { + layerBytes[layerIndex].AddRange(LayerLine.GetBytes((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + //layerLines.Add(new LayerLine((ushort)startY, (ushort)(y - 1), (ushort)x, lastColor)); + //Debug.WriteLine(layerLines[^1]); + } + } - //layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes(layer.NonZeroPixelCount, (uint) layerBytes[layerIndex].Count)); - //layerBytes[layerIndex].AddRange(PageBreak.Bytes); + //layerDefs[layerIndex] = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); + //var layerDef = new LayerDef(layer.NonZeroPixelCount, (uint)layerLines.Count, layerLines.ToArray()); + //layersStreams[layerIndex] = new MemoryStream(); + //Helpers.Serializer.Serialize(layersStreams[layerIndex], layerDef); - progress.LockAndIncrement(); - }); + //layerBytes[layerIndex].InsertRange(0, LayerDef.GetHeaderBytes(layer.NonZeroPixelCount, (uint) layerBytes[layerIndex].Count)); + //layerBytes[layerIndex].AddRange(PageBreak.Bytes); - progress.Reset(OperationProgress.StatusWritingFile, LayerCount); - for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) + progress.LockAndIncrement(); + }); + + progress.Reset(OperationProgress.StatusWritingFile, LayerCount); + for (int layerIndex = 0; layerIndex < LayerCount; layerIndex++) + { + progress.Token.ThrowIfCancellationRequested(); + //Helpers.SerializeWriteFileStream(outputFile, layerDefs[layerIndex]); + //outputFile.WriteStream(layersStreams[layerIndex]); + //layersStreams[layerIndex].Dispose(); + outputFile.WriteBytes(LayerDef.GetHeaderBytes(this[layerIndex].NonZeroPixelCount, (uint)layerBytes[layerIndex].Count)); + outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); + outputFile.WriteBytes(pageBreak); + progress++; + }*/ + + Helpers.SerializeWriteFileStream(outputFile, FooterSettings); + + progress.Reset("Calculating checksum"); + uint checkSum = CalculateCheckSum(outputFile, false); + + outputFile.Write(BitExtensions.ToBytesBigEndian(checkSum)); + + Debug.WriteLine("Encode Results:"); + Debug.WriteLine(HeaderSettings); + Debug.WriteLine("-End-"); + } + + protected override void DecodeInternally(OperationProgress progress) + { + using var inputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Read); + + inputFile.Seek(0, SeekOrigin.Begin); + + HeaderSettings = Helpers.Deserialize
(inputFile); + Debug.WriteLine(HeaderSettings); + HeaderSettings.Validate(); + + var position = inputFile.Position; + + progress.Reset("Validating checksum"); + var expectedCheckSum = CalculateCheckSum(inputFile, false, -4); + uint checkSum; + if (HeaderSettings.Version <= 2) + { + inputFile.Seek(3, SeekOrigin.Current); + checkSum = (uint) inputFile.ReadByte(); + } + else + { + checkSum = inputFile.ReadUIntBigEndian(); + } + + if (expectedCheckSum != checkSum) + { + throw new FileLoadException($"Checksum fails, expecting: {expectedCheckSum} but got: {checkSum}.\n" + + $"Try to reslice the file.", FileFullPath); + } + + inputFile.Seek(position, SeekOrigin.Begin); + var previews = new byte[ThumbnailsOriginalSize!.Length][]; + for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) + { + previews[i] = new byte[ThumbnailsOriginalSize[i].Area() * 2]; + inputFile.ReadBytes(previews[i]); + inputFile.Seek(2, SeekOrigin.Current); + } + + Parallel.For(0, previews.Length, CoreSettings.ParallelOptions, previewIndex => + { + Thumbnails[previewIndex] = DecodeImage(DATATYPE_RGB565_BE, previews[previewIndex], ThumbnailsOriginalSize[previewIndex]); + previews[previewIndex] = null!; + }); + + + SlicerInfoSettings = Helpers.Deserialize(inputFile); + Debug.WriteLine(SlicerInfoSettings); + + Init(HeaderSettings.LayerCount, DecodeType == FileDecodeType.Partial); + inputFile.Seek(LayerCount * 4 + 2, SeekOrigin.Current); // Skip pre layers + + if (HeaderSettings.Version >= 3) // New informative header v3 + { + SlicerInfoV3Settings = Helpers.Deserialize(inputFile); + Debug.WriteLine(SlicerInfoV3Settings); + } + + + if (DecodeType == FileDecodeType.Full) + { + progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); + + var linesBytes = new byte[LayerCount][]; + foreach (var batch in BatchLayersIndexes()) { progress.Token.ThrowIfCancellationRequested(); - //Helpers.SerializeWriteFileStream(outputFile, layerDefs[layerIndex]); - //outputFile.WriteStream(layersStreams[layerIndex]); - //layersStreams[layerIndex].Dispose(); - outputFile.WriteBytes(LayerDef.GetHeaderBytes(this[layerIndex].NonZeroPixelCount, (uint)layerBytes[layerIndex].Count)); - outputFile.WriteBytes(layerBytes[layerIndex].ToArray()); - outputFile.WriteBytes(pageBreak); - progress++; - }*/ - Helpers.SerializeWriteFileStream(outputFile, FooterSettings); - - progress.Reset("Calculating checksum"); - uint checkSum = CalculateCheckSum(outputFile, false); - - outputFile.Write(BitExtensions.ToBytesBigEndian(checkSum)); - - Debug.WriteLine("Encode Results:"); - Debug.WriteLine(HeaderSettings); - Debug.WriteLine("-End-"); - } - - protected override void DecodeInternally(OperationProgress progress) - { - using var inputFile = new FileStream(FileFullPath, FileMode.Open, FileAccess.Read); - - inputFile.Seek(0, SeekOrigin.Begin); - - HeaderSettings = Helpers.Deserialize
(inputFile); - Debug.WriteLine(HeaderSettings); - HeaderSettings.Validate(); - - var position = inputFile.Position; - - progress.Reset("Validating checksum"); - var expectedCheckSum = CalculateCheckSum(inputFile, false, -4); - uint checkSum; - if (HeaderSettings.Version <= 2) - { - inputFile.Seek(3, SeekOrigin.Current); - checkSum = (uint) inputFile.ReadByte(); - } - else - { - checkSum = inputFile.ReadUIntBigEndian(); - } - - if (expectedCheckSum != checkSum) - { - throw new FileLoadException($"Checksum fails, expecting: {expectedCheckSum} but got: {checkSum}.\n" + - $"Try to reslice the file.", FileFullPath); - } - - inputFile.Seek(position, SeekOrigin.Begin); - var previews = new byte[ThumbnailsOriginalSize.Length][]; - for (int i = 0; i < ThumbnailsOriginalSize.Length; i++) - { - previews[i] = new byte[ThumbnailsOriginalSize[i].Area() * 2]; - inputFile.ReadBytes(previews[i]); - inputFile.Seek(2, SeekOrigin.Current); - } - - Parallel.For(0, previews.Length, CoreSettings.ParallelOptions, previewIndex => - { - Thumbnails[previewIndex] = DecodeImage(DATATYPE_RGB565_BE, previews[previewIndex], ThumbnailsOriginalSize[previewIndex]); - previews[previewIndex] = null; - }); - - - SlicerInfoSettings = Helpers.Deserialize(inputFile); - Debug.WriteLine(SlicerInfoSettings); - - LayerManager.Init(HeaderSettings.LayerCount, DecodeType == FileDecodeType.Partial); - inputFile.Seek(LayerCount * 4 + 2, SeekOrigin.Current); // Skip pre layers - - if (HeaderSettings.Version >= 3) // New informative header v3 - { - SlicerInfoV3Settings = Helpers.Deserialize(inputFile); - Debug.WriteLine(SlicerInfoV3Settings); - } - - - if (DecodeType == FileDecodeType.Full) - { - progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); - - var linesBytes = new byte[LayerCount][]; - foreach (var batch in BatchLayersIndexes()) + foreach (var layerIndex in batch) { + inputFile.Seek(4, SeekOrigin.Current); + var lineCount = BitExtensions.ToUIntBigEndian(inputFile.ReadBytes(4)); + + linesBytes[layerIndex] = new byte[lineCount * 6]; + inputFile.ReadBytes(linesBytes[layerIndex]); + inputFile.Seek(2, SeekOrigin.Current); + progress.Token.ThrowIfCancellationRequested(); + } - foreach (var layerIndex in batch) + Parallel.ForEach(batch, CoreSettings.ParallelOptions, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + using (var mat = EmguExtensions.InitMat(Resolution)) { - inputFile.Seek(4, SeekOrigin.Current); - var lineCount = BitExtensions.ToUIntBigEndian(inputFile.ReadBytes(4)); - linesBytes[layerIndex] = new byte[lineCount * 6]; - inputFile.ReadBytes(linesBytes[layerIndex]); - inputFile.Seek(2, SeekOrigin.Current); - - progress.Token.ThrowIfCancellationRequested(); - } - - Parallel.ForEach(batch, CoreSettings.ParallelOptions, layerIndex => - { - if (progress.Token.IsCancellationRequested) return; - using (var mat = EmguExtensions.InitMat(Resolution)) + for (int i = 0; i < linesBytes[layerIndex].Length; i++) { - - for (int i = 0; i < linesBytes[layerIndex].Length; i++) + LayerLine line = new() { - LayerLine line = new() + Coordinates = { - Coordinates = - { - [0] = linesBytes[layerIndex][i++], - [1] = linesBytes[layerIndex][i++], - [2] = linesBytes[layerIndex][i++], - [3] = linesBytes[layerIndex][i++], - [4] = linesBytes[layerIndex][i++] - }, - Gray = linesBytes[layerIndex][i] - }; + [0] = linesBytes[layerIndex][i++], + [1] = linesBytes[layerIndex][i++], + [2] = linesBytes[layerIndex][i++], + [3] = linesBytes[layerIndex][i++], + [4] = linesBytes[layerIndex][i++] + }, + Gray = linesBytes[layerIndex][i] + }; - CvInvoke.Line(mat, new Point(line.StartX, line.StartY), - new Point(line.StartX, line.EndY), - new MCvScalar(line.Gray)); - } - - linesBytes[layerIndex] = null; - - this[layerIndex] = new Layer((uint)layerIndex, mat, this); + CvInvoke.Line(mat, new Point(line.StartX, line.StartY), + new Point(line.StartX, line.EndY), + new MCvScalar(line.Gray)); } - progress.LockAndIncrement(); - }); - } + linesBytes[layerIndex] = null!; + + this[layerIndex] = new Layer((uint)layerIndex, mat, this); + } + + progress.LockAndIncrement(); + }); } - else // Partial read - { - inputFile.Seek(-Helpers.Serializer.SizeOf(FooterSettings), SeekOrigin.End); - } - - progress.Token.ThrowIfCancellationRequested(); - - - - FooterSettings = Helpers.Deserialize