diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2020-06-21 19:06:52 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2020-06-21 19:06:52 +0300 |
commit | 043770ca12943756b0ffdb64224096ef8ad33fdb (patch) | |
tree | d904f514b12f6e1b5bb55f2ca5e941ea3cda1cfb | |
parent | c10bf6cef3511241c31c1240c3717d3c2764092e (diff) |
v0.5.2.2
* (Fix) phz: Files with encryption or sliced by chitubox produced black images after save due not setting the image address nor size (Spotted by Burak Cezairli)
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | UVtools.Core/About.cs | 19 | ||||
-rw-r--r-- | UVtools.Core/Extensions/EmguExtensions.cs | 28 | ||||
-rw-r--r-- | UVtools.Core/Extensions/FileStreamExtensions.cs | 31 | ||||
-rw-r--r-- | UVtools.Core/Extensions/StreamExtensions.cs | 29 | ||||
-rw-r--r-- | UVtools.Core/Extensions/StringExtensions.cs | 49 | ||||
-rw-r--r-- | UVtools.Core/Extensions/ZipArchiveExtensions.cs | 333 | ||||
-rw-r--r-- | UVtools.Core/FileExtension.cs | 89 | ||||
-rw-r--r-- | UVtools.Core/FileFormat.cs | 697 | ||||
-rw-r--r-- | UVtools.Core/Helpers.cs | 101 | ||||
-rw-r--r-- | UVtools.Core/IFileFormat.cs | 357 | ||||
-rw-r--r-- | UVtools.Core/LayerManager.cs | 853 | ||||
-rw-r--r-- | UVtools.Core/UVtools.Core.csproj | 28 | ||||
-rw-r--r-- | UVtools.GUI/Extensions/ImageSharpExtensions.cs | 11 | ||||
-rw-r--r-- | UVtools.GUI/FrmMain.cs | 19 | ||||
-rw-r--r-- | UVtools.GUI/Properties/AssemblyInfo.cs | 4 | ||||
-rw-r--r-- | UVtools.Parser/LayerManager.cs | 1 | ||||
-rw-r--r-- | UVtools.Parser/PHZFile.cs | 29 | ||||
-rw-r--r-- | UVtools.Parser/UVtools.Parser.csproj | 11 | ||||
-rw-r--r-- | UVtools.sln | 10 |
20 files changed, 2677 insertions, 26 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb7ced..51b3743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 21/06/2020 - v0.5.2.2 + +* (Fix) phz: Files with encryption or sliced by chitubox produced black images after save due not setting the image address nor size (Spotted by Burak Cezairli) + ## 20/06/2020 - v0.5.2.1 * (Add) cws: Allow change layer PWM value diff --git a/UVtools.Core/About.cs b/UVtools.Core/About.cs new file mode 100644 index 0000000..423aae4 --- /dev/null +++ b/UVtools.Core/About.cs @@ -0,0 +1,19 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +namespace UVtools.Core +{ + public static class About + { + public static string Software = "UVtools"; + public static string Author = "Tiago Conceição"; + public static string Company = "PTRTECH"; + public static string Website = "https://github.com/sn4k3/UVtools"; + public static string Donate = "https://paypal.me/SkillTournament"; + } +} diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs new file mode 100644 index 0000000..fbf8374 --- /dev/null +++ b/UVtools.Core/Extensions/EmguExtensions.cs @@ -0,0 +1,28 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.Runtime.InteropServices; +using Emgu.CV; +using Emgu.CV.CvEnum; + +namespace UVtools.Core.Extensions +{ + public static class EmguExtensions + { + public static byte[] GetBytes(this Mat mat) + { + byte[] data = new byte[mat.Width * mat.Height * mat.NumberOfChannels]; + + GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned); + using (Mat m2 = new Mat(mat.Size, DepthType.Cv8U, 3, handle.AddrOfPinnedObject(), mat.Width * mat.NumberOfChannels)) { } + handle.Free(); + + return data; + } + } +} diff --git a/UVtools.Core/Extensions/FileStreamExtensions.cs b/UVtools.Core/Extensions/FileStreamExtensions.cs new file mode 100644 index 0000000..bc5fb29 --- /dev/null +++ b/UVtools.Core/Extensions/FileStreamExtensions.cs @@ -0,0 +1,31 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.IO; + +namespace UVtools.Core.Extensions +{ + public static class FileStreamExtensions + { + public static uint ReadBytes(this FileStream fs, byte[] bytes, int offset = 0) + { + return (uint)fs.Read(bytes, offset, bytes.Length); + } + + 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; + } + } +} diff --git a/UVtools.Core/Extensions/StreamExtensions.cs b/UVtools.Core/Extensions/StreamExtensions.cs new file mode 100644 index 0000000..f85f917 --- /dev/null +++ b/UVtools.Core/Extensions/StreamExtensions.cs @@ -0,0 +1,29 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System.IO; + +namespace UVtools.Core.Extensions +{ + public static class StreamExtensions + { + /// <summary> + /// Converts stream into byte array + /// </summary> + /// <param name="stream">Input</param> + /// <returns>Byte array data</returns> + public static byte[] ToArray(this Stream stream) + { + using (var memoryStream = new MemoryStream()) + { + stream.CopyTo(memoryStream); + return memoryStream.ToArray(); + } + } + } +} diff --git a/UVtools.Core/Extensions/StringExtensions.cs b/UVtools.Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000..29f576f --- /dev/null +++ b/UVtools.Core/Extensions/StringExtensions.cs @@ -0,0 +1,49 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.ComponentModel; +using System.Linq; + +namespace UVtools.Core.Extensions +{ + public static class StringExtensions + { + /// <summary> + /// Upper the first character in a string + /// </summary> + /// <param name="input">Input string</param> + /// <returns>Modified string with fist character upper</returns> + 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 input.First().ToString().ToUpper() + input.Substring(1); + } + } + + /// <summary> + /// Converts a string into a target type + /// </summary> + /// <typeparam name="T">Target type to convert into</typeparam> + /// <param name="input">Value</param> + /// <returns>Converted value into target type</returns> + public static T Convert<T>(this string input) + { + var converter = TypeDescriptor.GetConverter(typeof(T)); + if (converter != null) + { + //Cast ConvertFromString(string text) : object to (T) + return (T)converter.ConvertFromString(input); + } + return default(T); + } + } +} diff --git a/UVtools.Core/Extensions/ZipArchiveExtensions.cs b/UVtools.Core/Extensions/ZipArchiveExtensions.cs new file mode 100644 index 0000000..6ba71f2 --- /dev/null +++ b/UVtools.Core/Extensions/ZipArchiveExtensions.cs @@ -0,0 +1,333 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace UVtools.Core.Extensions +{ + public static class ZipArchiveExtensions + { + /// <summary> + /// Used to specify what our overwrite policy + /// is for files we are extracting. + /// </summary> + public enum Overwrite + { + Always, + IfNewer, + Never + } + + /// <summary> + /// Used to identify what we will do if we are + /// trying to create a zip file and it already + /// exists. + /// </summary> + public enum ArchiveAction + { + Merge, + Replace, + Error, + Ignore + } + + /// <summary> + /// Unzips the specified file to the given folder in a safe + /// manner. This plans for missing paths and existing files + /// and handles them gracefully. + /// </summary> + /// <param name="sourceArchiveFileName"> + /// The name of the zip file to be extracted + /// </param> + /// <param name="destinationDirectoryName"> + /// The directory to extract the zip file to + /// </param> + /// <param name="overwriteMethod"> + /// Specifies how we are going to handle an existing file. + /// The default is IfNewer. + /// </param> + public static void ImprovedExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer) + { + //Opens the zip file up to be read + using (ZipArchive archive = ZipFile.OpenRead(sourceArchiveFileName)) + { + archive.ImprovedExtractToDirectory(sourceArchiveFileName, destinationDirectoryName, overwriteMethod); + } + } + + /// <summary> + /// Unzips the specified file to the given folder in a safe + /// manner. This plans for missing paths and existing files + /// and handles them gracefully. + /// </summary> + /// <param name="sourceArchiveFileName"> + /// The name of the zip file to be extracted + /// </param> + /// <param name="destinationDirectoryName"> + /// The directory to extract the zip file to + /// </param> + /// <param name="overwriteMethod"> + /// Specifies how we are going to handle an existing file. + /// The default is IfNewer. + /// </param> + public static void ImprovedExtractToDirectory(this ZipArchive archive, string sourceArchiveFileName, string destinationDirectoryName, Overwrite overwriteMethod = Overwrite.IfNewer) + { + //Loops through each file in the zip file + foreach (ZipArchiveEntry file in archive.Entries) + { + file.ImprovedExtractToFile(destinationDirectoryName, overwriteMethod); + } + } + + /// <summary> + /// Safely extracts a single file from a zip file + /// </summary> + /// <param name="file"> + /// The zip entry we are pulling the file from + /// </param> + /// <param name="destinationPath"> + /// The root of where the file is going + /// </param> + /// <param name="overwriteMethod"> + /// Specifies how we are going to handle an existing file. + /// The default is Overwrite.IfNewer. + /// </param> + 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; + } + } + + /// <summary> + /// Allows you to add files to an archive, whether the archive + /// already exists or not + /// </summary> + /// <param name="archiveFullName"> + /// The name of the archive to you want to add your files to + /// </param> + /// <param name="files"> + /// A set of file names that are to be added + /// </param> + /// <param name="action"> + /// Specifies how we are going to handle an existing archive + /// </param> + /// <param name="compression"> + /// Specifies what type of compression to use - defaults to Optimal + /// </param> + public static void AddToArchive(string archiveFullName, + List<string> 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; + } + } + } + } + } + + /// <summary> + /// Get or put a file into archive + /// </summary> + /// <param name="input"><see cref="ZipArchive"/></param> + /// <param name="filename">Filename to create</param> + /// <returns>Created <see cref="ZipArchiveEntry"/></returns> + public static ZipArchiveEntry GetPutFile(this ZipArchive input, string filename) + { + return input.GetEntry(filename) ?? input.CreateEntry(filename); + } + + /// <summary> + /// Create or update a file into archive and write content to it + /// </summary> + /// <param name="input"><see cref="ZipArchive"/></param> + /// <param name="filename">Filename to create</param> + /// <param name="content">Content to write</param> + /// <returns>Created <see cref="ZipArchiveEntry"/></returns> + public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, string content) + { + ZipArchiveEntry entry = input.GetEntry(filename) ?? input.CreateEntry(filename); + + if (string.IsNullOrEmpty(content)) return entry; + Stream stream = entry.Open(); + stream.SetLength(0); + using (TextWriter tw = new StreamWriter(stream)) + { + tw.Write(content); + tw.Close(); + } + return entry; + } + + /// <summary> + /// Create or update a file into archive and write content to it + /// </summary> + /// <param name="input"><see cref="ZipArchive"/></param> + /// <param name="filename">Filename to create</param> + /// <param name="content">Content to write</param> + /// <returns>Created <see cref="ZipArchiveEntry"/></returns> + public static ZipArchiveEntry PutFileContent(this ZipArchive input, string filename, byte[] content) + { + ZipArchiveEntry entry = input.GetEntry(filename) ?? input.CreateEntry(filename); + + if (ReferenceEquals(content, null)) return entry; + Stream stream = entry.Open(); + stream.SetLength(0); + stream.Write(content, 0, content.Length); + return entry; + } + } +} diff --git a/UVtools.Core/FileExtension.cs b/UVtools.Core/FileExtension.cs new file mode 100644 index 0000000..cb56563 --- /dev/null +++ b/UVtools.Core/FileExtension.cs @@ -0,0 +1,89 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +using System.Collections.Generic; + +namespace UVtools.Parser +{ + /// <summary> + /// Represents a file extension for slicer file formats + /// </summary> + public sealed class FileExtension + { + #region Properties + /// <summary> + /// Gets the extension name without the dot (.) + /// </summary> + public string Extension { get; } + + /// <summary> + /// Gets the extension description + /// </summary> + public string Description { get; } + + /// <summary> + /// Gets the file filter for open and save dialogs + /// </summary> + public string Filter => $@"{Description} (*.{Extension})|*.{Extension}"; + #endregion + + #region Constructor + /// <summary> + /// Constructor + /// </summary> + /// <param name="extension">The extension name without the dot (.)</param> + /// <param name="description">The extension description</param> + public FileExtension(string extension, string description) + { + Extension = extension; + Description = description; + } + #endregion + + #region Overrides + + public override string ToString() + { + return $"{nameof(Extension)}: {Extension}, {nameof(Description)}: {Description}"; + } + + private bool Equals(FileExtension other) + { + return Extension == other.Extension; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is FileExtension other && Equals(other); + } + + public override int GetHashCode() + { + return (Extension != null ? Extension.GetHashCode() : 0); + } + + private sealed class ExtensionEqualityComparer : IEqualityComparer<FileExtension> + { + public bool Equals(FileExtension x, FileExtension y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return x.Extension == y.Extension; + } + + public int GetHashCode(FileExtension obj) + { + return (obj.Extension != null ? obj.Extension.GetHashCode() : 0); + } + } + + public static IEqualityComparer<FileExtension> ExtensionComparer { get; } = new ExtensionEqualityComparer(); + #endregion + } +} diff --git a/UVtools.Core/FileFormat.cs b/UVtools.Core/FileFormat.cs new file mode 100644 index 0000000..ab88980 --- /dev/null +++ b/UVtools.Core/FileFormat.cs @@ -0,0 +1,697 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Emgu.CV; +using UVtools.Core.Extensions; +using UVtools.Parser; + +namespace UVtools.Core +{ + /// <summary> + /// Slicer <see cref="FileFormat"/> representation + /// </summary> + public abstract class FileFormat : IFileFormat, IDisposable, IEquatable<FileFormat>, IEnumerable<Layer> + { + #region Enums + + /// <summary> + /// Enumeration of file format types + /// </summary> + public enum FileFormatType : byte + { + Archive, + Binary + } + + /// <summary> + /// Enumeration of file thumbnail size types + /// </summary> + public enum FileThumbnailSize : byte + { + Small = 0, + Large + } + #endregion + + #region Sub Classes + /// <summary> + /// Available Print Parameters to modify + /// </summary> + public class PrintParameterModifier + { + + #region Instances + public static PrintParameterModifier InitialLayerCount { get; } = new PrintParameterModifier("Initial Layer Count", @"Modify 'Initial Layer Count' value", null,0, ushort.MaxValue); + public static PrintParameterModifier InitialExposureSeconds { get; } = new PrintParameterModifier("Initial Exposure Time", @"Modify 'Initial Exposure Time' seconds", "s", 0.1M, byte.MaxValue); + public static PrintParameterModifier ExposureSeconds { get; } = new PrintParameterModifier("Exposure Time", @"Modify 'Exposure Time' seconds", "s", 0.1M, byte.MaxValue); + + public static PrintParameterModifier BottomLayerOffTime { get; } = new PrintParameterModifier("Bottom Layer Off Time", @"Modify 'Bottom Layer Off Time' seconds", "s"); + public static PrintParameterModifier LayerOffTime { get; } = new PrintParameterModifier("Layer Off Time", @"Modify 'Layer Off Time' seconds", "s"); + public static PrintParameterModifier BottomLiftHeight { get; } = new PrintParameterModifier("Bottom Lift Height", @"Modify 'Bottom Lift Height' millimeters between bottom layers", "mm"); + public static PrintParameterModifier BottomLiftSpeed { get; } = new PrintParameterModifier("Bottom Lift Speed", @"Modify 'Bottom Lift Speed' mm/min between bottom layers", "mm/min"); + public static PrintParameterModifier LiftHeight { get; } = new PrintParameterModifier("Lift Height", @"Modify 'Lift Height' millimeters between layers", "mm"); + public static PrintParameterModifier LiftSpeed { get; } = new PrintParameterModifier("Lift Speed", @"Modify 'Lift Speed' mm/min between layers", "mm/min", 10, 5000); + public static PrintParameterModifier RetractSpeed { get; } = new PrintParameterModifier("Retract Speed", @"Modify 'Retract Speed' mm/min between layers", "mm/min", 10, 5000); + + public static PrintParameterModifier BottomLightPWM { get; } = new PrintParameterModifier("Bottom Light PWM", @"Modify 'Bottom Light PWM' value", null, 50, byte.MaxValue); + public static PrintParameterModifier LightPWM { get; } = new PrintParameterModifier("Light PWM", @"Modify 'Light PWM' value", null, 50, byte.MaxValue); + #endregion + + #region Properties + + /// <summary> + /// Gets the name + /// </summary> + public string Name { get; } + + /// <summary> + /// Gets the description + /// </summary> + public string Description { get; } + + /// <summary> + /// Gets the value unit + /// </summary> + public string ValueUnit { get; } + + /// <summary> + /// Gets the minimum value + /// </summary> + public decimal Minimum { get; } + + /// <summary> + /// Gets the maximum value + /// </summary> + public decimal Maximum { get; } + #endregion + + #region Constructor + public PrintParameterModifier(string name, string description, string valueUnit = null, decimal minimum = 0, decimal maximum = 1000) + { + Name = name; + Description = description; + ValueUnit = valueUnit ?? string.Empty; + Minimum = minimum; + Maximum = maximum; + } + #endregion + + #region Overrides + public override string ToString() + { + return $"{nameof(Name)}: {Name}, {nameof(Description)}: {Description}, {nameof(ValueUnit)}: {ValueUnit}, {nameof(Minimum)}: {Minimum}, {nameof(Maximum)}: {Maximum}"; + } + #endregion + } + #endregion + + #region Constants + private const string ExtractConfigFileName = "Configuration"; + private const string ExtractConfigFileExtension = "ini"; + #endregion + + #region Static Methods + /// <summary> + /// Gets the available formats to process + /// </summary> + public static FileFormat[] AvaliableFormats { get; } = + { + new SL1File(), // Prusa SL1 + new ChituboxZipFile(), // Zip + new ChituboxFile(), // cbddlp, cbt, photon + new PHZFile(), // phz + new PWSFile(), // PSW + new ZCodexFile(), // zcodex + new CWSFile(), // CWS + new ImageFile(), // images + }; + + /// <summary> + /// Gets all filters for open and save file dialogs + /// </summary> + public static string AllFileFilters => + AvaliableFormats.Aggregate(string.Empty, + (current, fileFormat) => string.IsNullOrEmpty(current) + ? fileFormat.FileFilter + : $"{current}|" + fileFormat.FileFilter) + + + AvaliableFormats.Aggregate("|All slicer files|", + (current, fileFormat) => current.EndsWith("|") + ? $"{current}{fileFormat.FileFilterExtensionsOnly}" + : $"{current};{fileFormat.FileFilterExtensionsOnly}"); + + /// <summary> + /// Gets the count of available file extensions + /// </summary> + public static byte FileExtensionsCount + { + get + { + return AvaliableFormats.Aggregate<FileFormat, byte>(0, (current, fileFormat) => (byte) (current + fileFormat.FileExtensions.Length)); + } + } + + /// <summary> + /// Find <see cref="FileFormat"/> by an extension + /// </summary> + /// <param name="extension">Extension name to find</param> + /// <param name="isFilePath">True if <see cref="extension"/> is a file path rather than only a extension name</param> + /// <param name="createNewInstance">True to create a new instance of found file format, otherwise will return a pre created one which should be used for read-only purpose</param> + /// <returns><see cref="FileFormat"/> object or null if not found</returns> + public static FileFormat FindByExtension(string extension, bool isFilePath = false, bool createNewInstance = false) + { + return (from fileFormat in AvaliableFormats where fileFormat.IsExtensionValid(extension, isFilePath) select createNewInstance ? (FileFormat) Activator.CreateInstance(fileFormat.GetType()) : fileFormat).FirstOrDefault(); + } + + /// <summary> + /// Find <see cref="FileFormat"/> by an type + /// </summary> + /// <param name="type">Type to find</param> + /// <param name="createNewInstance">True to create a new instance of found file format, otherwise will return a pre created one which should be used for read-only purpose</param> + /// <returns><see cref="FileFormat"/> object or null if not found</returns> + public static FileFormat FindByType(Type type, bool createNewInstance = false) + { + return (from t in AvaliableFormats where type == t.GetType() select createNewInstance ? (FileFormat) Activator.CreateInstance(type) : t).FirstOrDefault(); + } + #endregion + + #region Properties + + public abstract FileFormatType FileType { get; } + + public abstract FileExtension[] FileExtensions { get; } + public abstract Type[] ConvertToFormats { get; } + + public abstract PrintParameterModifier[] PrintParameterModifiers { get; } + + public string FileFilter { + get + { + var result = string.Empty; + + foreach (var fileExt in FileExtensions) + { + if (!ReferenceEquals(result, string.Empty)) + { + result += '|'; + } + result += fileExt.Filter; + } + + return result; + } + } + + public string FileFilterExtensionsOnly + { + get + { + var result = string.Empty; + + foreach (var fileExt in FileExtensions) + { + if (!ReferenceEquals(result, string.Empty)) + { + result += ';'; + } + result += $"*.{fileExt.Extension}"; + } + + return result; + } + } + + public string FileFullPath { get; set; } + + public abstract byte ThumbnailsCount { get; } + + public byte CreatedThumbnailsCount { + get + { + if (ReferenceEquals(Thumbnails, null)) return 0; + byte count = 0; + + foreach (var thumbnail in Thumbnails) + { + if (ReferenceEquals(thumbnail, null)) continue; + count++; + } + + return count; + } + } + + public abstract Size[] ThumbnailsOriginalSize { get; } + + public Mat[] Thumbnails { get; set; } + public LayerManager LayerManager { get; set; } + + /// <summary> + /// Gets if any layer got modified + /// </summary> + public bool ModifiedLayers => LayerManager.IsModified; + + public abstract uint ResolutionX { get; } + + public abstract uint ResolutionY { get; } + public bool HaveAntiAliasing => AntiAliasing > 1; + public abstract byte AntiAliasing { get; } + + public abstract float LayerHeight { get; } + + public float TotalHeight => (float)Math.Round(LayerCount * LayerHeight, 2); + + public uint LayerCount => LayerManager.Count; + + public abstract ushort InitialLayerCount { get; } + + public abstract float InitialExposureTime { get; } + + public abstract float LayerExposureTime { get; } + + public abstract float LiftHeight { get; } + + public abstract float RetractSpeed { get; } + + public abstract float LiftSpeed { get; } + + public abstract float PrintTime { get; } + + public abstract float UsedMaterial { get; } + + public abstract float MaterialCost { get; } + + public abstract string MaterialName { get; } + + public abstract string MachineName { get; } + + public StringBuilder GCode { get; set; } + + public abstract object[] Configs { get; } + + public bool IsValid => !ReferenceEquals(FileFullPath, null); + #endregion + + #region Constructor + protected FileFormat() + { + Thumbnails = new Mat[ThumbnailsCount]; + } + #endregion + + #region Indexers + public Layer this[int index] + { + get => LayerManager[index]; + set => LayerManager[index] = value; + } + + public Layer this[uint index] + { + get => LayerManager[index]; + set => LayerManager[index] = value; + } + + public Layer this[long index] + { + get => LayerManager[index]; + set => LayerManager[index] = value; + } + #endregion + + #region Numerators + public IEnumerator<Layer> GetEnumerator() + { + return ((IEnumerable<Layer>)LayerManager.Layers).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + #endregion + + #region Overrides + public override bool Equals(object obj) + { + return Equals(obj as FileFormat); + } + + public bool Equals(FileFormat other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return FileFullPath.Equals(other.FileFullPath); + } + + public override int GetHashCode() + { + return (FileFullPath != null ? FileFullPath.GetHashCode() : 0); + } + + public void Dispose() + { + Clear(); + } + + #endregion + + #region Methods + public virtual void Clear() + { + FileFullPath = null; + LayerManager = null; + GCode = null; + + if (!ReferenceEquals(Thumbnails, null)) + { + for (int i = 0; i < ThumbnailsCount; i++) + { + Thumbnails[i]?.Dispose(); + } + } + + + } + + public void FileValidation(string fileFullPath) + { + if (ReferenceEquals(fileFullPath, null)) throw new ArgumentNullException(nameof(FileFullPath), "fullFilePath can't be null."); + if (!File.Exists(fileFullPath)) throw new FileNotFoundException("The specified file does not exists.", fileFullPath); + + if (IsExtensionValid(fileFullPath, true)) + { + return; + } + + throw new FileLoadException($"The specified file is not valid.", fileFullPath); + } + + public bool IsExtensionValid(string extension, bool isFilePath = false) + { + extension = isFilePath ? Path.GetExtension(extension)?.Remove(0, 1) : extension; + return FileExtensions.Any(fileExtension => fileExtension.Extension.Equals(extension, StringComparison.InvariantCultureIgnoreCase)); + } + + public string GetFileExtensions(string prepend = ".", string separator = ", ") + { + var result = string.Empty; + + foreach (var fileExt in FileExtensions) + { + if (!ReferenceEquals(result, string.Empty)) + { + result += separator; + } + result += $"{prepend}{fileExt.Extension}"; + } + + return result; + } + + public Mat GetThumbnail(uint maxHeight = 400) + { + for (int i = 0; i < ThumbnailsCount; i++) + { + if(ReferenceEquals(Thumbnails[i], null)) continue; + if (Thumbnails[i].Height <= maxHeight) return Thumbnails[i]; + } + + return null; + } + + public void SetThumbnails(Mat[] images) + { + for (var i = 0; i < ThumbnailsCount; i++) + { + Thumbnails[i] = images[Math.Min(i, images.Length - 1)].Clone(); + } + } + + public void SetThumbnails(Mat image) + { + for (var i = 0; i < ThumbnailsCount; i++) + { + Thumbnails[i] = image.Clone(); + } + } + + public virtual void Encode(string fileFullPath) + { + FileFullPath = fileFullPath; + + if (File.Exists(fileFullPath)) + { + File.Delete(fileFullPath); + } + + for (var i = 0; i < Thumbnails.Length; i++) + { + if (ReferenceEquals(Thumbnails[i], null)) continue; + Mat output = new Mat(); + CvInvoke.Resize(Thumbnails[i], output, new Size(ThumbnailsOriginalSize[i].Width, ThumbnailsOriginalSize[i].Height)); + Thumbnails[i] = output; + } + } + + /*public virtual void BeginEncode(string fileFullPath) + { + } + + + public abstract void InsertLayerImageEncode(Image<L8> image, uint layerIndex); + + public abstract void EndEncode();*/ + + public virtual void Decode(string fileFullPath) + { + Clear(); + FileValidation(fileFullPath); + FileFullPath = fileFullPath; + } + + public virtual void Extract(string path, bool genericConfigExtract = true, bool genericLayersExtract = true) + { + /*if (emptyFirst) + { + if (Directory.Exists(path)) + { + DirectoryInfo di = new DirectoryInfo(path); + + foreach (FileInfo file in di.GetFiles()) + { + file.Delete(); + } + foreach (DirectoryInfo dir in di.GetDirectories()) + { + dir.Delete(true); + } + } + }*/ + + //if (!Directory.Exists(path)) + //{ + Directory.CreateDirectory(path); + //} + + + if (FileType == FileFormatType.Archive) + { + //ZipFile.ExtractToDirectory(FileFullPath, path); + ZipArchiveExtensions.ImprovedExtractToDirectory(FileFullPath, path, ZipArchiveExtensions.Overwrite.Always); + return; + } + + if (genericConfigExtract) + { + if (!ReferenceEquals(Configs, null)) + { + using (TextWriter tw = new StreamWriter(Path.Combine(path, $"{ExtractConfigFileName}.{ExtractConfigFileExtension}"), false)) + { + foreach (var config in Configs) + { + var type = config.GetType(); + tw.WriteLine($"[{type.Name}]"); + foreach (var property in type.GetProperties()) + { + tw.WriteLine($"{property.Name} = {property.GetValue(config)}"); + } + + tw.WriteLine(); + } + + tw.Close(); + } + } + } + + if (genericLayersExtract) + { + uint i = 0; + if (!ReferenceEquals(Thumbnails, null)) + { + foreach (var thumbnail in Thumbnails) + { + if (ReferenceEquals(thumbnail, null)) + { + continue; + } + + thumbnail.Save(Path.Combine(path, $"Thumbnail{i}.png")); + i++; + } + } + + if (LayerCount > 0) + { + Parallel.ForEach(this, (layer) => + { + var byteArr = layer.CompressedBytes; + using (FileStream stream = File.Create(Path.Combine(path, $"Layer{layer.Index}.png"), + byteArr.Length)) + { + stream.Write(byteArr, 0, byteArr.Length); + stream.Close(); + } + }); + } + + /* Parallel.For(0, LayerCount, layerIndex => { + var byteArr = this[layerIndex].RawData; + using (FileStream stream = File.Create(Path.Combine(path, $"Layer{layerIndex}.png"), byteArr.Length)) + { + stream.Write(byteArr, 0, byteArr.Length); + stream.Close(); + } + });*/ + /*for (i = 0; i < LayerCount; i++) + { + var byteArr = GetLayer(i); + using (FileStream stream = File.Create(Path.Combine(path, $"Layer{i}.png"), byteArr.Length)) + { + stream.Write(byteArr, 0, byteArr.Length); + stream.Close(); + } + }*/ + } + } + + public virtual float GetHeightFromLayer(uint layerIndex, bool realHeight = true) + { + return (float)Math.Round((layerIndex+(realHeight ? 1 : 0)) * LayerHeight, 2); + } + + public T GetInitialLayerValueOrNormal<T>(uint layerIndex, T initialLayerValue, T normalLayerValue) + { + return layerIndex < InitialLayerCount ? initialLayerValue : normalLayerValue; + } + + public virtual object GetValueFromPrintParameterModifier(PrintParameterModifier modifier) + { + if (ReferenceEquals(modifier, PrintParameterModifier.InitialLayerCount)) + return InitialLayerCount; + if (ReferenceEquals(modifier, PrintParameterModifier.InitialExposureSeconds)) + return InitialExposureTime; + if (ReferenceEquals(modifier, PrintParameterModifier.ExposureSeconds)) + return LayerExposureTime; + + if (ReferenceEquals(modifier, PrintParameterModifier.LiftHeight)) + return LiftHeight; + if (ReferenceEquals(modifier, PrintParameterModifier.LiftSpeed)) + return LiftSpeed; + if (ReferenceEquals(modifier, PrintParameterModifier.RetractSpeed)) + return RetractSpeed; + + + + return null; + } + + public virtual bool SetValueFromPrintParameterModifier(PrintParameterModifier modifier, object value) + { + return SetValueFromPrintParameterModifier(modifier, value.ToString()); + } + + public abstract bool SetValueFromPrintParameterModifier(PrintParameterModifier modifier, string value); + + public void Save() + { + SaveAs(); + } + + public abstract void SaveAs(string filePath = null); + + public abstract bool Convert(Type to, string fileFullPath); + public bool Convert(FileFormat to, string fileFullPath) + { + return Convert(to.GetType(), fileFullPath); + } + + public void Resize(uint startLayerIndex, uint endLayerIndex, float x, float y, bool fade) + { + if (x == 1f && y == 1f) return; + + Parallel.For(startLayerIndex, endLayerIndex+1, /*new ParallelOptions { MaxDegreeOfParallelism = 1 },*/ layerIndex => + { + var newX = x; + var newY = y; + if (fade) + { + if (newX != 1f) + { + double steps = Math.Abs(newX - 1.0) / (endLayerIndex - startLayerIndex); + //maxIteration = Math.Max(iterationsStart, iterationsEnd); + + newX = (float) (newX < 1f + ? newX + (layerIndex - startLayerIndex) * steps + : newX - (layerIndex - startLayerIndex) * steps); + + // constrain + //iterations = Math.Min(Math.Max(1, iterations), maxIteration); + } + + if (y != 1f) + { + double steps = Math.Abs(newY - 1.0) / (endLayerIndex - startLayerIndex); + //maxIteration = Math.Max(iterationsStart, iterationsEnd); + + newY = (float) (newY < 1f + ? newY + (layerIndex - startLayerIndex) * steps + : newY - (layerIndex - startLayerIndex) * steps); + + // constrain + //iterations = Math.Min(Math.Max(1, iterations), maxIteration); + } + } + + if (newX == 1f && newY == 1f) return; + + + + this[layerIndex].Resize(newX, newY); + }); + } + + public byte ValidateAntiAliasingLevel() + { + if (AntiAliasing < 2) return 1; + if(AntiAliasing % 2 != 0) throw new ArgumentException("AntiAliasing must be multiples of 2, otherwise use 0 or 1 to disable it", nameof(AntiAliasing)); + return AntiAliasing; + } + + #endregion + } +} diff --git a/UVtools.Core/Helpers.cs b/UVtools.Core/Helpers.cs new file mode 100644 index 0000000..8c7941e --- /dev/null +++ b/UVtools.Core/Helpers.cs @@ -0,0 +1,101 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Globalization; +using System.IO; +using System.Reflection; +using System.Security.Cryptography; +using BinarySerialization; +using Newtonsoft.Json; +using UVtools.Core.Extensions; + +namespace UVtools.Core +{ + /// <summary> + /// A helper class with utilities + /// </summary> + public static class Helpers + { + /// <summary> + /// Gets the <see cref="BinarySerializer"/> instance + /// </summary> + public static BinarySerializer Serializer { get; } = new BinarySerializer {Endianness = Endianness.Little }; + + public static MemoryStream Serialize(object value) + { + MemoryStream stream = new MemoryStream(); + Serializer.Serialize(stream, value); + return stream; + } + + public static T Deserialize<T>(Stream stream) + { + return Serializer.Deserialize<T>(stream); + } + + public static uint SerializeWriteFileStream(FileStream fs, object value, int offset = 0) + { + using (MemoryStream stream = Helpers.Serialize(value)) + { + return fs.WriteStream(stream, offset); + } + } + + public static T JsonDeserializeObject<T>(Stream stream) + { + using (TextReader tr = new StreamReader(stream)) + { + return JsonConvert.DeserializeObject<T>(tr.ReadToEnd()); + } + } + + public static SHA1CryptoServiceProvider SHA1 { get; } = new SHA1CryptoServiceProvider(); + public static string ComputeSHA1Hash(byte[] input) + { + return Convert.ToBase64String(SHA1.ComputeHash(input)); + } + + public static bool SetPropertyValue(PropertyInfo attribute, object obj, string value) + { + var name = attribute.PropertyType.Name.ToLower(); + switch (name) + { + case "string": + attribute.SetValue(obj, value.Convert<string>()); + return true; + case "boolean": + if(char.IsDigit(value[0])) + attribute.SetValue(obj, !value.Equals(0)); + else + attribute.SetValue(obj, value.Equals("True", StringComparison.InvariantCultureIgnoreCase)); + return true; + case "byte": + attribute.SetValue(obj, value.Convert<byte>()); + return true; + case "uint16": + attribute.SetValue(obj, value.Convert<ushort>()); + return true; + case "uint32": + attribute.SetValue(obj, value.Convert<uint>()); + return true; + case "single": + attribute.SetValue(obj, (float)Math.Round(float.Parse(value, CultureInfo.InvariantCulture.NumberFormat), 3)); + return true; + case "double": + attribute.SetValue(obj, Math.Round(double.Parse(value, CultureInfo.InvariantCulture.NumberFormat), 3)); + return true; + case "decimal": + attribute.SetValue(obj, Math.Round(decimal.Parse(value, CultureInfo.InvariantCulture.NumberFormat), 3)); + return true; + default: + throw new Exception($"Data type '{name}' not recognized, contact developer."); + } + } + } +} diff --git a/UVtools.Core/IFileFormat.cs b/UVtools.Core/IFileFormat.cs new file mode 100644 index 0000000..a42ca1b --- /dev/null +++ b/UVtools.Core/IFileFormat.cs @@ -0,0 +1,357 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using System; +using System.Text; +using Emgu.CV; +using UVtools.Parser; + +namespace UVtools.Core +{ + /// <summary> + /// Slicer file format representation interface + /// </summary> + public interface IFileFormat + { + #region Properties + /// <summary> + /// Gets the file format type + /// </summary> + FileFormat.FileFormatType FileType { get; } + + /// <summary> + /// Gets the valid file extensions for this <see cref="FileFormat"/> + /// </summary> + FileExtension[] FileExtensions { get; } + + /// <summary> + /// Gets the implemented file formats able to convert to + /// </summary> + Type[] ConvertToFormats { get; } + + /// <summary> + /// Gets the available <see cref="FileFormat.PrintParameterModifier"/> + /// </summary> + FileFormat.PrintParameterModifier[] PrintParameterModifiers { get; } + + /// <summary> + /// Gets the file filter for open and save dialogs + /// </summary> + string FileFilter { get; } + + /// <summary> + /// Gets all valid file extensions in "*.extension1;*.extension2" format + /// </summary> + + string FileFilterExtensionsOnly { get; } + + /// <summary> + /// Gets the input file path loaded into this <see cref="FileFormat"/> + /// </summary> + string FileFullPath { get; set; } + + /// <summary> + /// Gets the thumbnails count present in this file format + /// </summary> + byte ThumbnailsCount { get; } + + /// <summary> + /// Gets the number of created thumbnails + /// </summary> + byte CreatedThumbnailsCount { get; } + + /// <summary> + /// Gets the original thumbnail sizes + /// </summary> + System.Drawing.Size[] ThumbnailsOriginalSize { get; } + + /// <summary> + /// Gets the thumbnails for this <see cref="FileFormat"/> + /// </summary> + Mat Thumbnails { get; set; } + + /// <summary> + /// Gets the cached layers into compressed bytes + /// </summary> + LayerManager LayerManager { get; set; } + + /// <summary> + /// Gets the image width resolution + /// </summary> + uint ResolutionX { get; } + + /// <summary> + /// Gets the image height resolution + /// </summary> + uint ResolutionY { get; } + + bool HaveAntiAliasing { get; } + + /// <summary> + /// Gets the AntiAliasing level + /// </summary> + byte AntiAliasing { get; } + + /// <summary> + /// Gets Layer Height in mm + /// </summary> + float LayerHeight { get; } + + /// <summary> + /// Gets Total Height in mm + /// </summary> + float TotalHeight { get; } + + /// <summary> + /// Gets the number of layers present in this file + /// </summary> + uint LayerCount { get; } + + /// <summary> + /// Gets the number of initial layer count + /// </summary> + /// </summary> + ushort InitialLayerCount { get; } + + /// <summary> + /// Gets the initial exposure time for <see cref="InitialLayerCount"/> + /// </summary> + float InitialExposureTime { get; } + + /// <summary> + /// Gets the normal layer exposure time + /// </summary> + float LayerExposureTime { get; } + + /// <summary> + /// Gets the speed in mm/min for the detracts + /// </summary> + float LiftSpeed { get; } + + /// <summary> + /// Gets the height in mm to retract between layers + /// </summary> + float LiftHeight { get; } + + /// <summary> + /// Gets the speed in mm/min for the retracts + /// </summary> + float RetractSpeed { get; } + + /// <summary> + /// Gets the estimate print time in seconds + /// </summary> + float PrintTime { get; } + + /// <summary> + /// Gets the estimate used material in ml + /// </summary> + float UsedMaterial { get; } + + /// <summary> + /// Gets the estimate material cost + /// </summary> + float MaterialCost { get; } + + /// <summary> + /// Gets the material name + /// </summary> + string MaterialName { get; } + + /// <summary> + /// Gets the machine name + /// </summary> + string MachineName { get; } + + /// <summary> + /// Gets the GCode, returns null if not supported + /// </summary> + StringBuilder GCode { get; set; } + + /// <summary> + /// Get all configuration objects with properties and values + /// </summary> + object[] Configs { get; } + + /// <summary> + /// Gets if this file is valid to read + /// </summary> + bool IsValid { get; } + + #endregion + + #region Methods + /// <summary> + /// Clears all definitions and properties, it also dispose valid candidates + /// </summary> + void Clear(); + + /// <summary> + /// Validate if a file is a valid <see cref="FileFormat"/> + /// </summary> + /// <param name="fileFullPath">Full file path</param> + void FileValidation(string fileFullPath); + + /// <summary> + /// Checks if a extension is valid under the <see cref="FileFormat"/> + /// </summary> + /// <param name="extension">Extension to check</param> + /// <param name="isFilePath">True if <see cref="extension"/> is a full file path, otherwise false for extension only</param> + /// <returns>True if valid, otherwise false</returns> + bool IsExtensionValid(string extension, bool isFilePath = false); + + /// <summary> + /// Gets all valid file extensions in a specified format + /// </summary> + + string GetFileExtensions(string prepend = ".", string separator = ", "); + + /// <summary> + /// Gets a thumbnail by it height or lower + /// </summary> + /// <param name="maxHeight">Max height allowed</param> + /// <returns></returns> + Mat GetThumbnail(uint maxHeight = 400); + + /// <summary> + /// Sets thumbnails from a list of thumbnails and clone them + /// </summary> + /// <param name="images"></param> + void SetThumbnails(Mat[] images); + + /// <summary> + /// Sets all thumbnails the same image + /// </summary> + /// <param name="images">Image to set</param> + void SetThumbnails(Mat images); + + /// <summary> + /// Encode to an output file + /// </summary> + /// <param name="fileFullPath">Output file</param> + void Encode(string fileFullPath); + + /* + /// <summary> + /// Begin encode to an output file + /// </summary> + /// <param name="fileFullPath">Output file</param> + //void BeginEncode(string fileFullPath); + + /// <summary> + /// Insert a layer image to be encoded + /// </summary> + /// <param name="image"></param> + /// <param name="layerIndex"></param> + //void InsertLayerImageEncode(Image<L8> image, uint layerIndex); + + /// <summary> + /// Finish the encoding procedure + /// </summary> + //void EndEncode();*/ + + /// <summary> + /// Decode a slicer file + /// </summary> + /// <param name="fileFullPath"></param> + void Decode(string fileFullPath); + + /// <summary> + /// Extract contents to a folder + /// </summary> + /// <param name="path">Path to folder where content will be extracted</param> + /// <param name="genericConfigExtract"></param> + /// <param name="genericLayersExtract"></param> + void Extract(string path, bool genericConfigExtract = true, bool genericLayersExtract = true); + + /// <summary> + /// Get height in mm from layer height + /// </summary> + /// <param name="layerIndex"></param> + /// <param name="realHeight"></param> + /// <returns>The height in mm</returns> + float GetHeightFromLayer(uint layerIndex, bool realHeight = true); + + /// <summary> + /// Gets the value for initial layer or normal layers based on layer index + /// </summary> + /// <typeparam name="T">Type of value</typeparam> + /// <param name="layerIndex">Layer index</param> + /// <param name="initialLayerValue">Initial value</param> + /// <param name="normalLayerValue">Normal value</param> + /// <returns></returns> + T GetInitialLayerValueOrNormal<T>(uint layerIndex, T initialLayerValue, T normalLayerValue); + + /// <summary> + /// Gets the value attributed to <see cref="FileFormat.PrintParameterModifier"/> + /// </summary> + /// <param name="modifier">Modifier to use</param> + /// <returns>A value</returns> + object GetValueFromPrintParameterModifier(FileFormat.PrintParameterModifier modifier); + + /// <summary> + /// Sets a property value attributed to <see cref="modifier"/> + /// </summary> + /// <param name="modifier">Modifier to use</param> + /// <param name="value">Value to set</param> + /// <returns>True if set, otherwise false = <see cref="modifier"/> not found</returns> + bool SetValueFromPrintParameterModifier(FileFormat.PrintParameterModifier modifier, object value); + + /// <summary> + /// Sets a property value attributed to <see cref="modifier"/> + /// </summary> + /// <param name="modifier">Modifier to use</param> + /// <param name="value">Value to set</param> + /// <returns>True if set, otherwise false = <see cref="modifier"/> not found</returns> + bool SetValueFromPrintParameterModifier(FileFormat.PrintParameterModifier modifier, string value); + + /// <summary> + /// Saves current configuration on input file + /// </summary> + void Save(); + + /// <summary> + /// Saves current configuration on a copy + /// </summary> + /// <param name="filePath">File path to save copy as, use null to overwrite active file (Same as <see cref="Save"/>)</param> + void SaveAs(string filePath = null); + + /// <summary> + /// Converts this file type to another file type + /// </summary> + /// <param name="to">Target file format</param> + /// <param name="fileFullPath">Output path file</param> + /// <returns>True if convert succeed, otherwise false</returns> + bool Convert(Type to, string fileFullPath); + + /// <summary> + /// Converts this file type to another file type + /// </summary> + /// <param name="to">Target file format</param> + /// <param name="fileFullPath">Output path file</param> + /// <returns>True if convert succeed, otherwise false</returns> + bool Convert(FileFormat to, string fileFullPath); + + /// <summary> + /// Resizes layer images in x and y factor, starting at 1 = 100% + /// </summary> + /// <param name="startLayerIndex">Layer index to start</param> + /// <param name="endLayerIndex">Layer index to end</param> + /// <param name="x">X factor, starts at 1</param> + /// <param name="y">Y factor, starts at 1</param> + /// <param name="fade">Fade X/Y towards 100%</param> + void Resize(uint startLayerIndex, uint endLayerIndex, float x, float y, bool fade); + + /// <summary> + /// Validate AntiAlias Level + /// </summary> + byte ValidateAntiAliasingLevel(); + + #endregion + } +} diff --git a/UVtools.Core/LayerManager.cs b/UVtools.Core/LayerManager.cs new file mode 100644 index 0000000..0da5348 --- /dev/null +++ b/UVtools.Core/LayerManager.cs @@ -0,0 +1,853 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Emgu.CV; +using Emgu.CV.CvEnum; +using Emgu.CV.Structure; +using Emgu.CV.Util; +using UVtools.Core.Extensions; + +namespace UVtools.Parser +{ + #region LayerIssue Class + + public class LayerIssue : IEnumerable<Point> + { + public enum IssueType : byte + { + Island, + ResinTrap, + TouchingBound, + //HoleSandwich, + } + + /// <summary> + /// Gets the parent layer + /// </summary> + public Layer Layer { get; } + + /// <summary> + /// Gets the issue type associated + /// </summary> + public IssueType Type { get; } + + /// <summary> + /// Gets the pixels containing the issue + /// </summary> + public Point[] Pixels { get; } + + /// <summary> + /// Gets the bounding rectangle of the pixel area + /// </summary> + public Rectangle BoundingRectangle { get; } + + /// <summary> + /// Gets the X coordinate for the first point, -1 if doesn't exists + /// </summary> + public int X => HaveValidPoint ? Pixels[0].X : -1; + + /// <summary> + /// Gets the Y coordinate for the first point, -1 if doesn't exists + /// </summary> + public int Y => HaveValidPoint ? Pixels[0].Y : -1; + + /// <summary> + /// Gets the XY point for first point + /// </summary> + public Point Point => HaveValidPoint ? Pixels[0] : new Point(-1, -1); + + /// <summary> + /// Gets the number of pixels on this issue + /// </summary> + public uint Size { + get + { + if (Type == IssueType.ResinTrap && !BoundingRectangle.IsEmpty) + { + return (uint) (BoundingRectangle.Width * BoundingRectangle.Height); + } + + if (ReferenceEquals(Pixels, null)) return 0; + return (uint) Pixels.Length; + } + } + + /// <summary> + /// Check if this issue have a valid start point to show + /// </summary> + public bool HaveValidPoint => !ReferenceEquals(Pixels, null) && Pixels.Length > 0; + + public LayerIssue(Layer layer, IssueType type, Point[] pixels = null, Rectangle boundingRectangle = new Rectangle()) + { + Layer = layer; + Type = type; + Pixels = pixels; + BoundingRectangle = boundingRectangle; + } + + public Point this[uint index] => Pixels[index]; + + public Point this[int index] => Pixels[index]; + + public IEnumerator<Point> GetEnumerator() + { + return ((IEnumerable<Point>)Pixels).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override string ToString() + { + return $"{nameof(Type)}: {Type}"; + } + } + #endregion + + #region LayerHollowArea + + public class LayerHollowArea : IEnumerable<Point> + { + public enum AreaType : byte + { + Unknown = 0, + Trap, + Drain + } + /// <summary> + /// Gets area pixels + /// </summary> + public Point[] Contour { get; } + + public System.Drawing.Rectangle BoundingRectangle { get; } + + public AreaType Type { get; set; } = AreaType.Unknown; + + public bool Processed { get; set; } + + #region Indexers + public Point this[uint index] + { + get => index < Contour.Length ? Contour[index] : Point.Empty; + set => Contour[index] = value; + } + + public Point this[int index] + { + get => index < Contour.Length ? Contour[index] : Point.Empty; + set => Contour[index] = value; + } + + public Point this[uint x, uint y] + { + get + { + for (uint i = 0; i < Contour.Length; i++) + { + if (Contour[i].X == x && Contour[i].Y == y) return Contour[i]; + } + return Point.Empty; + } + } + + public Point this[int x, int y] => this[(uint) x, (uint)y]; + + public Point this[Point point] => this[point.X, point.Y]; + + #endregion + + public IEnumerator<Point> GetEnumerator() + { + return ((IEnumerable<Point>)Contour).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public LayerHollowArea() + { + } + + public LayerHollowArea(Point[] contour, System.Drawing.Rectangle boundingRectangle, AreaType type = AreaType.Unknown) + { + Contour = contour; + BoundingRectangle = boundingRectangle; + Type = type; + } + } + #endregion + + #region Layer Class + /// <summary> + /// Represent a Layer + /// </summary> + public class Layer : IEquatable<Layer>, IEquatable<uint> + { + #region Properties + + /// <summary> + /// Gets the parent layer manager + /// </summary> + public LayerManager ParentLayerManager { get; set; } + + /// <summary> + /// Gets the layer index + /// </summary> + public uint Index { get; } + + private byte[] _compressedBytes; + /// <summary> + /// Gets or sets layer image compressed data + /// </summary> + public byte[] CompressedBytes + { + get => LayerManager.DecompressLayer(_compressedBytes); + set + { + _compressedBytes = LayerManager.CompressLayer(value); + IsModified = true; + } + } + + /// <summary> + /// Gets the original filename, null if no filename attached with layer + /// </summary> + public string Filename { get; set; } + + /// <summary> + /// Gets if layer has been modified + /// </summary> + public bool IsModified { get; set; } + + /// <summary> + /// Gets or sets a new image instance + /// </summary> + public Mat LayerMat + { + get + { + Mat mat = new Mat(); + CvInvoke.Imdecode(CompressedBytes, ImreadModes.Grayscale, mat); + return mat; + } + set + { + using (var vector = new VectorOfByte()) + { + CvInvoke.Imencode(".png", value, vector); + CompressedBytes = vector.ToArray(); + } + } + } + + /// <summary> + /// Gets a new Brg image instance + /// </summary> + public Mat BrgMat + { + get { + using (Mat image = LayerMat) + { + Mat mat = new Mat(); + CvInvoke.CvtColor(image, mat, ColorConversion.Gray2Bgr); + return mat; + } + } + } + + #endregion + + #region Constructor + public Layer(uint index, byte[] compressedBytes, string filename = null, LayerManager pararentLayerManager = null) + { + Index = index; + CompressedBytes = compressedBytes; + Filename = filename ?? $"Layer{index}.png"; + IsModified = false; + ParentLayerManager = pararentLayerManager; + } + + public Layer(uint index, Mat layerMat, string filename = null, LayerManager pararentLayerManager = null) : this(index, new byte[0], filename, pararentLayerManager) + { + LayerMat = layerMat; + IsModified = false; + } + + + public Layer(uint index, Stream stream, string filename = null, LayerManager pararentLayerManager = null) : this(index, stream.ToArray(), filename, pararentLayerManager) + { } + #endregion + + #region Equatables + + public static bool operator ==(Layer obj1, Layer obj2) + { + return obj1.Equals(obj2); + } + + public static bool operator !=(Layer obj1, Layer obj2) + { + return !obj1.Equals(obj2); + } + + public static bool operator >(Layer obj1, Layer obj2) + { + return obj1.Index > obj2.Index; + } + + public static bool operator <(Layer obj1, Layer obj2) + { + return obj1.Index < obj2.Index; + } + + public static bool operator >=(Layer obj1, Layer obj2) + { + return obj1.Index >= obj2.Index; + } + + public static bool operator <=(Layer obj1, Layer obj2) + { + return obj1.Index <= obj2.Index; + } + + public bool Equals(uint other) + { + return Index == other; + } + + public bool Equals(Layer other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(_compressedBytes, other._compressedBytes); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Layer)obj); + } + + public override int GetHashCode() + { + return (_compressedBytes != null ? _compressedBytes.GetHashCode() : 0); + } + + private sealed class IndexRelationalComparer : IComparer<Layer> + { + public int Compare(Layer x, Layer y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; + return x.Index.CompareTo(y.Index); + } + } + + public static IComparer<Layer> IndexComparer { get; } = new IndexRelationalComparer(); + #endregion + + #region Formaters + public override string ToString() + { + return $"{nameof(Index)}: {Index}, {nameof(Filename)}: {Filename}, {nameof(IsModified)}: {IsModified}"; + } + #endregion + + #region Methods + + public Layer PreviousLayer() + { + if (ReferenceEquals(ParentLayerManager, null) || Index == 0) + return null; + + return ParentLayerManager[Index - 1]; + } + + public Layer NextLayer() + { + if (ReferenceEquals(ParentLayerManager, null) || Index >= ParentLayerManager.Count - 1) + return null; + + return ParentLayerManager[Index + 1]; + } + + /// <summary> + /// Gets all islands start pixel location for this layer + /// https://www.geeksforgeeks.org/find-number-of-islands/ + /// </summary> + /// <returns><see cref="List{T}"/> holding all islands coordinates</returns> + public List<LayerIssue> GetIssues(uint requiredPixelsToSupportIsland = 5) + { + if (requiredPixelsToSupportIsland == 0) + requiredPixelsToSupportIsland = 1; + + // These arrays are used to + // get row and column numbers + // of 8 neighbors of a given cell + List<LayerIssue> result = new List<LayerIssue>(); + List<Point> pixels = new List<Point>(); + + + + var mat = LayerMat; + var bytes = mat.GetBytes(); + + + + var previousLayerImage = PreviousLayer()?.LayerMat; + byte[] previousBytes = previousLayerImage?.GetBytes(); + + + /*var nextLayerImage = NextLayer()?.Image; + byte[] nextBytes = null; + if (!ReferenceEquals(nextLayerImage, null)) + { + if (nextLayerImage.TryGetSinglePixelSpan(out var nextPixelSpan)) + { + nextBytes = MemoryMarshal.AsBytes(nextPixelSpan).ToArray(); + } + }*/ + + // Make a bool array to + // mark visited cells. + // Initially all cells + // are unvisited + bool[,] visited = new bool[mat.Width, mat.Height]; + + // Initialize count as 0 and + // traverse through the all + // cells of given matrix + //uint count = 0; + + // Island checker + sbyte[] rowNbr = { -1, -1, -1, 0, 0, 1, 1, 1 }; + sbyte[] colNbr = { -1, 0, 1, -1, 1, -1, 0, 1 }; + const uint minPixel = 10; + const uint minPixelForSupportIsland = 200; + int pixelIndex; + uint islandSupportingPixels; + if (Index > 0) + { + for (int y = 0; y < mat.Height; y++) + { + for (int x = 0; x < mat.Width; x++) + { + pixelIndex = y * mat.Width + x; + + /*if (bytes[pixelIndex] == 0 && previousBytes?[pixelIndex] == byte.MaxValue && + nextBytes?[pixelIndex] == byte.MaxValue) + { + result.Add(new LayerIssue(this, LayerIssue.IssueType.HoleSandwich, new []{new Point(x, y)})); + }*/ + + if (bytes[pixelIndex] > minPixel && !visited[x, y]) + { + // If a cell with value 1 is not + // visited yet, then new island + // found, Visit all cells in this + // island and increment island count + pixels.Clear(); + pixels.Add(new Point(x, y)); + islandSupportingPixels = previousBytes[pixelIndex] >= minPixelForSupportIsland ? 1u : 0; + + int minX = x; + int maxX = x; + int minY = y; + int maxY = y; + + int x2; + int y2; + + + Queue<Point> queue = new Queue<Point>(); + queue.Enqueue(new Point(x, y)); + // Mark this cell as visited + visited[x, y] = true; + + while (queue.Count > 0) + { + var point = queue.Dequeue(); + y2 = point.Y; + x2 = point.X; + for (byte k = 0; k < 8; k++) + { + //if (isSafe(y2 + rowNbr[k], x2 + colNbr[k])) + var tempy2 = y2 + rowNbr[k]; + var tempx2 = x2 + colNbr[k]; + pixelIndex = tempy2 * mat.Width + tempx2; + if (tempy2 >= 0 && + tempy2 < mat.Height && + tempx2 >= 0 && tempx2 < mat.Width && + bytes[pixelIndex] >= minPixel && + !visited[tempx2, tempy2]) + { + visited[tempx2, tempy2] = true; + point = new Point(tempx2, tempy2); + pixels.Add(point); + queue.Enqueue(point); + + minX = Math.Min(minX, tempx2); + maxX = Math.Max(maxX, tempx2); + minY = Math.Min(minY, tempy2); + maxY = Math.Max(maxY, tempy2); + + islandSupportingPixels += previousBytes[pixelIndex] >= minPixelForSupportIsland ? 1u : 0; + } + } + } + //count++; + + if (islandSupportingPixels >= requiredPixelsToSupportIsland) + continue; // Not a island, bounding is strong + if (islandSupportingPixels > 0 && pixels.Count < requiredPixelsToSupportIsland && + islandSupportingPixels >= Math.Max(1, pixels.Count / 2)) continue; // Not a island + result.Add(new LayerIssue(this, LayerIssue.IssueType.Island, pixels.ToArray(), new Rectangle(minX, minY, maxX-minX, maxY-minY))); + } + } + } + } + + pixels.Clear(); + + // TouchingBounds Checker + for (int x = 0; x < mat.Width; x++) // Check Top and Bottom bounds + { + if (bytes[x] >= 200) // Top + { + pixels.Add(new Point(x, 0)); + } + + if (bytes[mat.Width * mat.Height - mat.Width + x] >= 200) // Bottom + { + pixels.Add(new Point(x, mat.Height-1)); + } + } + + for (int y = 0; y < mat.Height; y++) // Check Left and Right bounds + { + if (bytes[y * mat.Width] >= 200) // Left + { + pixels.Add(new Point(0, y)); + } + + if (bytes[y * mat.Width + mat.Width - 1] >= 200) // Right + { + pixels.Add(new Point(mat.Width-1, y)); + } + } + + if (pixels.Count > 0) + { + result.Add(new LayerIssue(this, LayerIssue.IssueType.TouchingBound, pixels.ToArray())); + } + + pixels.Clear(); + + return result; + } + + public Layer Clone() + { + return new Layer(Index, CompressedBytes, Filename, ParentLayerManager); + } + #endregion + + public void Resize(float newWidth, float newHeight) + { + using (var mat = LayerMat) + { + using (var resizedMat = new Mat()) + { + int width = (int)(mat.Width * newWidth); + int height = (int)(mat.Height * newHeight); + Point location = new Point(mat.Width / 2 - width / 2, mat.Height / 2 - height / 2); + + CvInvoke.Resize(mat, resizedMat, new Size(width, height)); + mat.SetTo(new MCvScalar(0)); + resizedMat.CopyTo(mat); + LayerMat = mat; + } + } + } + } + #endregion + + #region LayerManager Class + public class LayerManager : IEnumerable<Layer> + { + #region Properties + /// <summary> + /// Layers List + /// </summary> + public Layer[] Layers { get; } + + /// <summary> + /// Gets the layers count + /// </summary> + public uint Count => (uint) Layers.Length; + + /// <summary> + /// Gets if any layer got modified, otherwise false + /// </summary> + public bool IsModified + { + get + { + for (uint i = 0; i < Count; i++) + { + if (Layers[i].IsModified) return true; + } + return false; + } + } + + + #endregion + + #region Constructors + public LayerManager(uint layerCount) + { + Layers = new Layer[layerCount]; + } + #endregion + + #region Indexers + public Layer this[uint index] + { + get => Layers[index]; + set => AddLayer(index, value); + } + + public Layer this[int index] + { + get => Layers[index]; + set => AddLayer((uint) index, value); + } + + public Layer this[long index] + { + get => Layers[index]; + set => AddLayer((uint) index, value); + } + + #endregion + + #region Numerators + public IEnumerator<Layer> GetEnumerator() + { + return ((IEnumerable<Layer>)Layers).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + #endregion + + #region Static Methods + /// <summary> + /// Compress a layer from a <see cref="Stream"/> + /// </summary> + /// <param name="input"><see cref="Stream"/> to compress</param> + /// <returns>Compressed byte array</returns> + public static byte[] CompressLayer(Stream input) + { + return CompressLayer(input.ToArray()); + } + + /// <summary> + /// Compress a layer from a byte array + /// </summary> + /// <param name="input">byte array to compress</param> + /// <returns>Compressed byte array</returns> + public static byte[] CompressLayer(byte[] input) + { + return input; + /*using (MemoryStream output = new MemoryStream()) + { + using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Optimal)) + { + dstream.Write(input, 0, input.Length); + } + return output.ToArray(); + }*/ + } + + /// <summary> + /// Decompress a layer from a byte array + /// </summary> + /// <param name="input">byte array to decompress</param> + /// <returns>Decompressed byte array</returns> + public static byte[] DecompressLayer(byte[] input) + { + return input; + /*using (MemoryStream ms = new MemoryStream(input)) + { + using (MemoryStream output = new MemoryStream()) + { + using (DeflateStream dstream = new DeflateStream(ms, CompressionMode.Decompress)) + { + dstream.CopyTo(output); + } + return output.ToArray(); + } + }*/ + } + #endregion + + #region Methods + + /// <summary> + /// Add a layer + /// </summary> + /// <param name="index">Layer index</param> + /// <param name="layer">Layer to add</param> + public void AddLayer(uint index, Layer layer) + { + Layers[index] = layer; + layer.ParentLayerManager = this; + } + + /// <summary> + /// Get layer given index + /// </summary> + /// <param name="index">Layer index</param> + /// <returns></returns> + public Layer GetLayer(uint index) + { + return Layers[index]; + } + + public ConcurrentDictionary<uint, List<LayerIssue>> GetAllIssues() + { + var result = new ConcurrentDictionary<uint, List<LayerIssue>>(); + + Parallel.ForEach(this, layer => + { + var issues = layer.GetIssues(); + if (issues.Count > 0) + { + if (!result.TryAdd(layer.Index, issues)) + { + throw new AccessViolationException("Error while trying to add an issue to the dictionary, please try again."); + } + } + }); + + /*const byte minPixel = 50; + sbyte[] rowNbr = { -1, -1, -1, 0, 0, 1, 1, 1 }; + sbyte[] colNbr = { -1, 0, 1, -1, 1, -1, 0, 1 }; + + int pixelIndex; + for (uint layerindex = 0; layerindex < Count; layerindex++) + { + var image = this[layerindex].Image; + byte[] bytes = null; + if (image.TryGetSinglePixelSpan(out var pixelSpan)) + { + bytes = MemoryMarshal.AsBytes(pixelSpan).ToArray(); + } + + bool[,] visited = new bool[image.Width, image.Height]; + + for (int y = 0; y < image.Height; y++) + { + for (int x = 0; x < image.Width; x++) + { + pixelIndex = y * image.Width + x; + if (bytes[pixelIndex] > minPixel && !visited[y, x]) + { + Queue<Point> queue = new Queue<Point>(); + queue.Enqueue(new Point(x, y)); + // Mark this cell as visited + visited[x, y] = true; + + var x2 = x; + var y2 = y; + + while (queue.Count > 0) + { + var point = queue.Dequeue(); + y2 = point.Y; + x2 = point.X; + for (byte k = 0; k < 8; k++) + { + //if (isSafe(y2 + rowNbr[k], x2 + colNbr[k])) + var tempy2 = y2 + rowNbr[k]; + var tempx2 = x2 + colNbr[k]; + pixelIndex = tempy2 * image.Width + tempx2; + if (tempy2 >= 0 && + tempy2 < image.Height && + tempx2 >= 0 && tempx2 < image.Width && + bytes[pixelIndex] >= minPixel && + !visited[tempx2, tempy2]) + { + visited[tempx2, tempy2] = true; + point = new Point(tempx2, tempy2); + pixels.Add(point); + queue.Enqueue(point); + + islandSupportingPixels += previousBytes[pixelIndex] >= minPixelForSupportIsland ? 1u : 0; + } + } + } + } + } + } + }*/ + + return result; + } + + /// <summary> + /// Desmodify all layers + /// </summary> + public void Desmodify() + { + for (uint i = 0; i < Count; i++) + { + Layers[i].IsModified = false; + } + } + + /// <summary> + /// Clone this object + /// </summary> + /// <returns></returns> + public LayerManager Clone() + { + LayerManager layerManager = new LayerManager(Count); + foreach (var layer in this) + { + layerManager[layer.Index] = layer.Clone(); + } + + return layerManager; + } + + + #endregion + + } + #endregion +} diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj new file mode 100644 index 0000000..946aed5 --- /dev/null +++ b/UVtools.Core/UVtools.Core.csproj @@ -0,0 +1,28 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> + <PackageLicenseFile>LICENSE</PackageLicenseFile> + <Company>PTRTECH</Company> + <Authors>Tiago Conceição</Authors> + <RepositoryType>Git</RepositoryType> + <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> + <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> + <Description>MSLA/DLP, file analysis, repair, conversion and manipulation</Description> + </PropertyGroup> + + <ItemGroup> + <None Include="..\LICENSE"> + <Pack>True</Pack> + <PackagePath></PackagePath> + </None> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="BinarySerializer" Version="8.5.1" /> + <PackageReference Include="Emgu.CV" Version="4.3.0.3890" /> + <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> + </ItemGroup> + +</Project> diff --git a/UVtools.GUI/Extensions/ImageSharpExtensions.cs b/UVtools.GUI/Extensions/ImageSharpExtensions.cs index bc38051..e3bc01b 100644 --- a/UVtools.GUI/Extensions/ImageSharpExtensions.cs +++ b/UVtools.GUI/Extensions/ImageSharpExtensions.cs @@ -6,12 +6,16 @@ * of this license document, but changing it is not allowed. */ +using System; +using System.Drawing; using System.IO; using Emgu.CV; +using Emgu.CV.CvEnum; using Emgu.CV.Structure; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using UVtools.Parser; +using Image = SixLabors.ImageSharp.Image; namespace UVtools.GUI.Extensions { @@ -40,6 +44,13 @@ namespace UVtools.GUI.Extensions }; } + public static Mat ToEmguMat(this Image<L8> image) + { + var mat = new Mat(new System.Drawing.Size(image.Width, image.Height), DepthType.Cv8U, 1); + mat.SetTo(Helpers.ImageL8ToBytes(image)); + return mat; + } + /*public static Image<TPixel> ToImageSharpImage<TPixel>(this System.Drawing.Bitmap bitmap) where TPixel : struct, IPixel<TPixel> { using (var memoryStream = new MemoryStream()) diff --git a/UVtools.GUI/FrmMain.cs b/UVtools.GUI/FrmMain.cs index feb9a91..26fdc2e 100644 --- a/UVtools.GUI/FrmMain.cs +++ b/UVtools.GUI/FrmMain.cs @@ -1715,13 +1715,19 @@ namespace UVtools.GUI if (tsLayerImageLayerOutline.Checked) { - Image<Gray, byte> greyscale = ActualLayerImage.ToEmguImage(); - + Image<Gray, byte> greyscale = ActualLayerImage.ToEmguImage(); // 10ms + SlicerFile[ActualLayer].Image = ActualLayerImage; // 127ms + var mat = ActualLayerImage.ToEmguMat(); // 4ms + + var bytes = new VectorOfByte(); + CvInvoke.Imencode(".png", mat, bytes); // 9ms + CvInvoke.Imdecode(bytes, ImreadModes.Grayscale, mat); // 10ms + Debug.WriteLine(CvInvoke.UseOpenCL); #if DEBUG greyscale = greyscale.ThresholdBinary(new Gray(254), new Gray(255)); VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint(); Mat hierarchy = new Mat(); - + CvInvoke.FindContours(greyscale, contours, hierarchy, RetrType.Ccomp, ChainApproxMethod.ChainApproxSimple); /* @@ -1735,10 +1741,9 @@ namespace UVtools.GUI { if ((int)arr.GetValue(0, i, 3) >= 0) continue; var r = CvInvoke.BoundingRectangle(contours[i]); - CvInvoke.Rectangle(greyscale, r, new MCvScalar(80), 2); - - greyscale.Draw(contours, i, new Gray(125), -1); - + //CvInvoke.Rectangle(greyscale, r, new MCvScalar(80), 2); + greyscale.Draw(contours, -1, new Gray(125), -1); + //CvInvoke.DrawContours(mat, contours, -1, new MCvScalar(255), -1); } #else diff --git a/UVtools.GUI/Properties/AssemblyInfo.cs b/UVtools.GUI/Properties/AssemblyInfo.cs index 638ed25..4ac8551 100644 --- a/UVtools.GUI/Properties/AssemblyInfo.cs +++ b/UVtools.GUI/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.5.2.1")] -[assembly: AssemblyFileVersion("0.5.2.1")] +[assembly: AssemblyVersion("0.5.2.2")] +[assembly: AssemblyFileVersion("0.5.2.2")] diff --git a/UVtools.Parser/LayerManager.cs b/UVtools.Parser/LayerManager.cs index 9f84489..fa03636 100644 --- a/UVtools.Parser/LayerManager.cs +++ b/UVtools.Parser/LayerManager.cs @@ -9,7 +9,6 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Dynamic; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; diff --git a/UVtools.Parser/PHZFile.cs b/UVtools.Parser/PHZFile.cs index cdab3f2..c14efbe 100644 --- a/UVtools.Parser/PHZFile.cs +++ b/UVtools.Parser/PHZFile.cs @@ -586,10 +586,13 @@ namespace UVtools.Parser { KeyRing kr = new KeyRing(Parent.HeaderSettings.EncryptionKey, layerIndex); EncodedRle = kr.Read(rawData).ToArray(); - return; + } + else + { + EncodedRle = rawData.ToArray(); } - EncodedRle = rawData.ToArray(); + DataSize = (uint) EncodedRle.Length; } public override string ToString() @@ -603,8 +606,8 @@ namespace UVtools.Parser public class KeyRing { - public ulong Init { get; } - public ulong Key { get; private set; } + public uint Init { get; } + public uint Key { get; private set; } public uint Index { get; private set; } public KeyRing(uint seed, uint layerIndex) @@ -819,11 +822,12 @@ namespace UVtools.Parser for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { LayerData layerData = LayersDefinitions[layerIndex]; + LayerData layerDataHash = null; if (HeaderSettings.EncryptionKey == 0) { string hash = Helpers.ComputeSHA1Hash(layerData.EncodedRle); - if (LayersHash.TryGetValue(hash, out var layerDataHash)) + if (LayersHash.TryGetValue(hash, out layerDataHash)) { layerData.DataAddress = layerDataHash.DataAddress; layerData.DataSize = layerDataHash.DataSize; @@ -831,15 +835,18 @@ namespace UVtools.Parser else { LayersHash.Add(hash, layerData); - layerData.DataAddress = layerDataCurrentOffset; - layerData.DataSize = (uint)layerData.EncodedRle.Length; - - outputFile.Seek(layerDataCurrentOffset, SeekOrigin.Begin); - layerDataCurrentOffset += outputFile.WriteBytes(layerData.EncodedRle); } } - + if (ReferenceEquals(layerDataHash, null)) + { + layerData.DataAddress = layerDataCurrentOffset; + + outputFile.Seek(layerDataCurrentOffset, SeekOrigin.Begin); + layerDataCurrentOffset += outputFile.WriteBytes(layerData.EncodedRle); + } + + LayersDefinitions[layerIndex] = layerData; outputFile.Seek(currentOffset, SeekOrigin.Begin); diff --git a/UVtools.Parser/UVtools.Parser.csproj b/UVtools.Parser/UVtools.Parser.csproj index 9dd52d9..63e325a 100644 --- a/UVtools.Parser/UVtools.Parser.csproj +++ b/UVtools.Parser/UVtools.Parser.csproj @@ -7,13 +7,14 @@ <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <PackageIcon></PackageIcon> <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> - <AssemblyVersion>0.5.2.1</AssemblyVersion> - <FileVersion>0.5.2.1</FileVersion> - <Version>0.5.2.1</Version> + <AssemblyVersion>0.5.2.2</AssemblyVersion> + <FileVersion>0.5.2.2</FileVersion> + <Version>0.5.2.2</Version> <Description>MSLA/DLP, file analysis, repair, conversion and manipulation</Description> - <PackageId>UVtoolsParser</PackageId> + <PackageId>UVtools.Parser</PackageId> <PackageLicenseFile>LICENSE</PackageLicenseFile> - <Product>UVtoolsParser</Product> + <Product>UVtools.Parser</Product> + <PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance> </PropertyGroup> <ItemGroup> diff --git a/UVtools.sln b/UVtools.sln index 1ac1866..71043cc 100644 --- a/UVtools.sln +++ b/UVtools.sln @@ -14,6 +14,8 @@ Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "UVtools.InstallerMM", "UVto EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "UVtools.Installer", "UVtools.Installer\UVtools.Installer.wixproj", "{41224896-08E9-4F22-9E56-5F9D46DEC7D2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UVtools.Core", "UVtools.Core\UVtools.Core.csproj", "{7C9927F8-132E-4A37-B894-440E0FD5AA3D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -50,6 +52,14 @@ Global {41224896-08E9-4F22-9E56-5F9D46DEC7D2}.Release|Any CPU.ActiveCfg = Release|x86 {41224896-08E9-4F22-9E56-5F9D46DEC7D2}.Release|x86.ActiveCfg = Release|x86 {41224896-08E9-4F22-9E56-5F9D46DEC7D2}.Release|x86.Build.0 = Release|x86 + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Debug|x86.Build.0 = Debug|Any CPU + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Release|Any CPU.Build.0 = Release|Any CPU + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Release|x86.ActiveCfg = Release|Any CPU + {7C9927F8-132E-4A37-B894-440E0FD5AA3D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE |