Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/sn4k3/UVtools.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTiago Conceição <Tiago_caza@hotmail.com>2020-06-21 19:06:52 +0300
committerTiago Conceição <Tiago_caza@hotmail.com>2020-06-21 19:06:52 +0300
commit043770ca12943756b0ffdb64224096ef8ad33fdb (patch)
treed904f514b12f6e1b5bb55f2ca5e941ea3cda1cfb
parentc10bf6cef3511241c31c1240c3717d3c2764092e (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.md4
-rw-r--r--UVtools.Core/About.cs19
-rw-r--r--UVtools.Core/Extensions/EmguExtensions.cs28
-rw-r--r--UVtools.Core/Extensions/FileStreamExtensions.cs31
-rw-r--r--UVtools.Core/Extensions/StreamExtensions.cs29
-rw-r--r--UVtools.Core/Extensions/StringExtensions.cs49
-rw-r--r--UVtools.Core/Extensions/ZipArchiveExtensions.cs333
-rw-r--r--UVtools.Core/FileExtension.cs89
-rw-r--r--UVtools.Core/FileFormat.cs697
-rw-r--r--UVtools.Core/Helpers.cs101
-rw-r--r--UVtools.Core/IFileFormat.cs357
-rw-r--r--UVtools.Core/LayerManager.cs853
-rw-r--r--UVtools.Core/UVtools.Core.csproj28
-rw-r--r--UVtools.GUI/Extensions/ImageSharpExtensions.cs11
-rw-r--r--UVtools.GUI/FrmMain.cs19
-rw-r--r--UVtools.GUI/Properties/AssemblyInfo.cs4
-rw-r--r--UVtools.Parser/LayerManager.cs1
-rw-r--r--UVtools.Parser/PHZFile.cs29
-rw-r--r--UVtools.Parser/UVtools.Parser.csproj11
-rw-r--r--UVtools.sln10
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