diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b28cbfb..afff5fa1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## 19/04/2024 - v4.3.1
+
+- **Tool - Change resolution:**
+ - (Add) 12K resolution profile
+ - (Improvement) Do not mark the option "Resize layers with the proposed ratio" when a machine preset results in the same pixel size / ratio of (1.0x, 1.0x)
+ - (Fix) Wait time before cure suggestion for GOO and PRZ file formats, it now allows the use of the create first empty layer (#864)
+- **Thumbnail sanitizer:**
+ - (Add) Check if the thumbnails have the correct number of channels, if not it will throw an error
+ - (Improvement) When full encode a file, strip all extra thumbnails that are not used by the file format if they are not an archive
+ - (Improvement) Resize all thumbnails to the same size as the original from file format even when there are more than file format requires, this fixes a problem when converting from zip that have many thumbnails but file format selects the larger and the smallest, leading to encode the wrong size
+ - (Improvement) Convert thumbnails to BGR and strip the alpha channel when required, this fixes the issue where format conversion from zip such as sl1 and vdt where corrupting the final file with invalid thumbnail rle
+- (Add) Tool - Light bleed compensation: Add "Dim subject" option to select from different subjects to dim, "Bridges" was added (#868)
+- (Add) Settings - Notifications: Allow to define beeps sounds to play after ran long operations (#863)
+- (Improvement) CTB, FDG, PHZ: Make possible to encode a thumbnail using 1 and 4 channels, this fixes the issue where the file format could encode invalid thumbnail data
+- (Improvement) Add empty layer for Goo file format when running the wait time before cure suggestion
+- (Fix) Terminal: The run globals was lost from the previous version update
+- (Upgrade) .NET from 6.0.28 to 6.0.29
+
## 06/04/2024 - v4.3.0
- **File formats:**
diff --git a/CREDITS.md b/CREDITS.md
index 6d36b84b..b9af963e 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -84,4 +84,12 @@
- Mark Johnston
- Ivan Ivanov
- Simon Christ
-- Leonardo Farenzena Felin
\ No newline at end of file
+- Leonardo Farenzena Felin
+- Sylvain Chartrand
+- Sason Simpson
+- Starbase3D
+- Robert Blackett
+- Michael Pullen
+- Landry David
+- Jeremy Conoley
+- Brady George
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
index 5b0bb0d4..58f8819e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -31,7 +31,7 @@
$(MSBuildThisFileDirectory)publish
- 4.3.0
+ 4.3.1
11.0.7
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 6727d3dc..b6169d9a 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,24 +1,16 @@
-- **File formats:**
- - (Add) nanoDLP file format
- - (Add) SL1 printer note keyword: `LAYERIMAGEFORMAT_xxx` sets the layer image format required for the converted file if the format have multiple options (For Archives with PNG's)
- - (Fix) Anycubic file format: Model Min/Max(X/Y) was not properly calculated
- - (Fix) Photon Mono M5s Pro incorrect display height and width (fixes #854)
-- **PrusaSlicer:**
- - (Add) Concepts3D Athena 8K & 12K
- - (Change) Wanhao D7: Add `LAYERIMAGEFORMAT_Png32` to printer notes
- - (Change) Nova3D Bene4 Mono, Bene5, Elfin2 Mono SE, Whale, Whale2: Add `LAYERIMAGEFORMAT_Png24BgrAA` to printer notes
-- **UI:**
- - (Add) Settings - Automations - Events: After file load, before file save and after file save. Events are fired upon an action and execute a defined script.
- If the script is written with the UVtools scripting structure, it will run under an operation and within the Core context.
- Otherwise, if plain C# code is used, it will run under the Terminal and in the UI context.
- - (Add) Show a message of congratulations on the software birthday (Trigger only once per year)
- - (Add) Menu - Help: Add "Community forums" submenu, move Facebook group into it and add GitHub, Reddit, Twitter and Youtube
- - (Change) Window title: Move version near software name and add the system arch to it
- - (Change) About window: Move version near software name and add "Age" label
- - (Change) Benchmark tool: Add a thin border to the result panels
- - (Improvement) On the status bar, hide the " @ mm/min" from lift speed label if file is not able to use lift speed parameters
- - (Improvement) Re-style the new version button
- - (Fix) Show print times correctly when larger than a day (#854)
-- (Upgrade) .NET from 6.0.27 to 6.0.28
-- (Upgrade) Wix from 4.0.4 to 5.0.0
+- **Tool - Change resolution:**
+ - (Add) 12K resolution profile
+ - (Improvement) Do not mark the option "Resize layers with the proposed ratio" when a machine preset results in the same pixel size / ratio of (1.0x, 1.0x)
+ - (Fix) Wait time before cure suggestion for GOO and PRZ file formats, it now allows the use of the create first empty layer (#864)
+- **Thumbnail sanitizer:**
+ - (Add) Check if the thumbnails have the correct number of channels, if not it will throw an error
+ - (Improvement) When full encode a file, strip all extra thumbnails that are not used by the file format if they are not an archive
+ - (Improvement) Resize all thumbnails to the same size as the original from file format even when there are more than file format requires, this fixes a problem when converting from zip that have many thumbnails but file format selects the larger and the smallest, leading to encode the wrong size
+ - (Improvement) Convert thumbnails to BGR and strip the alpha channel when required, this fixes the issue where format conversion from zip such as sl1 and vdt where corrupting the final file with invalid thumbnail rle
+- (Add) Tool - Light bleed compensation: Add "Dim subject" option to select from different subjects to dim, "Bridges" was added (#868)
+- (Add) Settings - Notifications: Allow to define beeps sounds to play after ran long operations (#863)
+- (Improvement) CTB, FDG, PHZ: Make possible to encode a thumbnail using 1 and 4 channels, this fixes the issue where the file format could encode invalid thumbnail data
+- (Improvement) Add empty layer for Goo file format when running the wait time before cure suggestion
+- (Fix) Terminal: The run globals was lost from the previous version update
+- (Upgrade) .NET from 6.0.28 to 6.0.29
diff --git a/UVtools.Core/Extensions/EmguExtensions.cs b/UVtools.Core/Extensions/EmguExtensions.cs
index 5293e6d1..144f57c0 100644
--- a/UVtools.Core/Extensions/EmguExtensions.cs
+++ b/UVtools.Core/Extensions/EmguExtensions.cs
@@ -188,10 +188,13 @@ public static GpuMat ToGpuMat(this Mat mat)
///
///
///
- public static unsafe UnmanagedMemoryStream GetUnmanagedMemoryStream(this Mat mat, FileAccess accessMode)
+ public static UnmanagedMemoryStream GetUnmanagedMemoryStream(this Mat mat, FileAccess accessMode)
{
var length = mat.GetLength();
- return new UnmanagedMemoryStream(mat.GetBytePointer(), length, length, accessMode);
+ unsafe
+ {
+ return new UnmanagedMemoryStream(mat.GetBytePointer(), length, length, accessMode);
+ }
}
///
@@ -200,17 +203,22 @@ public static unsafe UnmanagedMemoryStream GetUnmanagedMemoryStream(this Mat mat
///
///
public static unsafe byte* GetBytePointer(this Mat mat)
- => (byte*)mat.DataPointer.ToPointer();
+ {
+ return (byte*)mat.DataPointer.ToPointer();
+ }
///
/// Gets the whole data span to manipulate or read pixels, use this when possibly using ROI
///
///
- public static unsafe Span2D GetDataSpan2D(this Mat mat)
+ public static Span2D GetDataSpan2D(this Mat mat) where T : struct
{
var step = mat.GetRealStep();
- if (mat.IsContinuous) return new(mat.DataPointer.ToPointer(), mat.Height, step, 0);
- return new(mat.DataPointer.ToPointer(), mat.Height, step, mat.Step / mat.DepthToByteCount() - step);
+ unsafe
+ {
+ if (mat.IsContinuous) return new(mat.DataPointer.ToPointer(), mat.Height, step, 0);
+ return new(mat.DataPointer.ToPointer(), mat.Height, step, mat.Step / mat.DepthToByteCount() - step);
+ }
}
///
@@ -240,9 +248,34 @@ public static unsafe Span2D GetDataSpan2D(this Mat mat)
///
///
///
- public static unsafe Span GetDataSpan(this Mat mat, int length = 0, int offset = 0)
+ public static Span GetDataSpan(this Mat mat, int length = 0, int offset = 0) where T : struct
{
+ if (!mat.IsContinuous)
+ throw new NotSupportedException("To create a Span, the Mat's memory must be continuous. This Mat does not use continuous memory. Use Span2D instead.");
+
+ if (offset < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be a positive value.");
+
+ var maxLength = mat.GetLength() / Marshal.SizeOf() - offset;
+
+ if (maxLength < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset value overflow this Mat size.");
+
if (length <= 0)
+ {
+ length = maxLength;
+ }
+ else if (length > maxLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length), length, $"The maximum size allowed for this Mat with an offset of {offset} is {maxLength}.");
+ }
+
+ unsafe
+ {
+ return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length);
+ }
+
+ /*if (length <= 0)
{
if (mat.IsContinuous)
{
@@ -253,7 +286,7 @@ public static unsafe Span GetDataSpan(this Mat mat, int length = 0, int of
length = mat.Step / mat.DepthToByteCount() * (mat.Height - 1) + mat.GetRealStep();
}
}
- return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length);
+ return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length);*/
}
///
@@ -264,7 +297,7 @@ public static unsafe Span GetDataSpan(this Mat mat, int length = 0, int of
///
///
///
- public static Span GetPixelSpan(this Mat mat, int x, int y)
+ public static Span GetPixelSpan(this Mat mat, int x, int y) where T : struct
=> mat.GetDataSpan(mat.NumberOfChannels, mat.GetPixelPos(x, y));
///
@@ -274,7 +307,7 @@ public static Span GetPixelSpan(this Mat mat, int x, int y)
///
///
///
- public static Span GetPixelSpan(this Mat mat, int pos)
+ public static Span GetPixelSpan(this Mat mat, int pos) where T : struct
=> mat.GetDataSpan(mat.NumberOfChannels, pos);
///
@@ -286,11 +319,29 @@ public static Span GetPixelSpan(this Mat mat, int pos)
///
///
///
- public static unsafe Span GetRowSpan(this Mat mat, int y, int length = 0, int offset = 0)
+ public static Span GetRowSpan(this Mat mat, int y, int length = 0, int offset = 0) where T : struct
{
- var originalStep = mat.Step;
- if(length <= 0) length = mat.GetRealStep();
- return new(IntPtr.Add(mat.DataPointer, y * originalStep + offset).ToPointer(), length);
+ if (offset < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be a positive value.");
+
+ var maxLength = mat.GetRealStep() / Marshal.SizeOf() - offset;
+
+ if (maxLength < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset value overflow this Mat row size.");
+
+ if (length <= 0)
+ {
+ length = maxLength;
+ }
+ else if (length > maxLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length), length, $"The maximum size allowed for this Mat row with an offset of {offset} is {maxLength}.");
+ }
+
+ unsafe
+ {
+ return new(IntPtr.Add(mat.DataPointer, y * mat.Step + offset).ToPointer(), length);
+ }
}
///
@@ -330,20 +381,32 @@ public static unsafe Span GetColSpan(this Mat mat, int x, int length = 0,
///
///
///
- public static unsafe ReadOnlySpan GetDataReadOnlySpan(this Mat mat, int length = 0, int offset = 0)
+ public static ReadOnlySpan GetDataReadOnlySpan(this Mat mat, int length = 0, int offset = 0) where T : struct
{
+ if (!mat.IsContinuous)
+ throw new NotSupportedException("To create a Span, the Mat's memory must be continuous. This Mat does not use continuous memory. Use Span2D instead.");
+
+ if (offset < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be a positive value.");
+
+ int maxLength = mat.GetLength() / Marshal.SizeOf() - offset;
+
+ if (maxLength < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset value overflow this Mat size.");
+
if (length <= 0)
{
- if (mat.IsContinuous)
- {
- length = mat.GetLength();
- }
- else
- {
- length = mat.Step / mat.DepthToByteCount() * (mat.Height - 1) + mat.GetRealStep();
- }
+ length = maxLength;
+ }
+ else if (length > maxLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length), length, $"The maximum size allowed for this Mat with an offset of {offset} is {maxLength}.");
+ }
+
+ unsafe
+ {
+ return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length);
}
- return new(IntPtr.Add(mat.DataPointer, offset).ToPointer(), length);
}
///
@@ -362,11 +425,29 @@ public static unsafe ReadOnlySpan GetDataReadOnlySpan(this Mat mat, int le
///
///
///
- public static unsafe ReadOnlySpan GetRowReadOnlySpan(this Mat mat, int y, int length = 0, int offset = 0)
+ public static ReadOnlySpan GetRowReadOnlySpan(this Mat mat, int y, int length = 0, int offset = 0) where T : struct
{
- var originalStep = mat.Step;
- if (length <= 0) length = mat.GetRealStep();
- return new(IntPtr.Add(mat.DataPointer, y * originalStep + offset).ToPointer(), length);
+ if (offset < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be a positive value.");
+
+ var maxLength = mat.GetRealStep() / Marshal.SizeOf() - offset;
+
+ if (maxLength < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset value overflow this Mat row size.");
+
+ if (length <= 0)
+ {
+ length = maxLength;
+ }
+ else if (length > maxLength)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length), length, $"The maximum size allowed for this Mat row with an offset of {offset} is {maxLength}.");
+ }
+
+ unsafe
+ {
+ return new(IntPtr.Add(mat.DataPointer, y * mat.Step + offset).ToPointer(), length);
+ }
}
///
diff --git a/UVtools.Core/FileFormats/CTBEncryptedFile.cs b/UVtools.Core/FileFormats/CTBEncryptedFile.cs
index 89b67edf..f3ab44da 100644
--- a/UVtools.Core/FileFormats/CTBEncryptedFile.cs
+++ b/UVtools.Core/FileFormats/CTBEncryptedFile.cs
@@ -540,10 +540,10 @@ public class Preview
[FieldOrder(3)] public uint ImageLength { get; set; }
- public unsafe Mat Decode(byte[] rawImageData)
+ public Mat Decode(byte[] rawImageData)
{
var image = new Mat(new Size((int)ResolutionX, (int)ResolutionY), DepthType.Cv8U, 3);
- var span = image.GetBytePointer();
+ var span = image.GetDataByteSpan();
int pixel = 0;
for (int n = 0; n < rawImageData.Length; n++)
@@ -574,14 +574,13 @@ public override string ToString()
return $"{nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(ImageOffset)}: {ImageOffset}, {nameof(ImageLength)}: {ImageLength}";
}
- public unsafe byte[] Encode(Mat image)
+ public byte[] Encode(Mat image)
{
List rawData = new();
ushort color15 = 0;
uint rep = 0;
- var span = image.GetBytePointer();
- var imageLength = image.GetLength();
+ var span = image.GetDataByteReadOnlySpan();
void RleRGB15()
{
@@ -611,11 +610,27 @@ void RleRGB15()
}
int pixel = 0;
- while (pixel < imageLength)
+ while (pixel < span.Length)
{
+ byte b = span[pixel++];
+ byte g;
+ byte r;
+
+ if (image.NumberOfChannels == 1) // 8 bit safe-guard
+ {
+ r = g = b;
+ }
+ else
+ {
+ g = span[pixel++];
+ r = span[pixel++];
+ }
+
+ if (image.NumberOfChannels == 4) pixel++; // skip alpha
+
var ncolor15 =
// bgr
- (span[pixel++] >> 3) | ((span[pixel++] >> 2) << 5) | ((span[pixel++] >> 3) << 11);
+ (b >> 3) | ((g >> 2) << 5) | ((r >> 3) << 11);
if (ncolor15 == color15)
{
diff --git a/UVtools.Core/FileFormats/ChituboxFile.cs b/UVtools.Core/FileFormats/ChituboxFile.cs
index 890f911c..d0779e0c 100644
--- a/UVtools.Core/FileFormats/ChituboxFile.cs
+++ b/UVtools.Core/FileFormats/ChituboxFile.cs
@@ -535,10 +535,10 @@ public class Preview
[FieldOrder(6)] public uint Unknown3 { get; set; }
[FieldOrder(7)] public uint Unknown4 { get; set; }
- public unsafe Mat Decode(byte[] rawImageData)
+ public Mat Decode(byte[] rawImageData)
{
var image = new Mat(new Size((int) ResolutionX, (int) ResolutionY), DepthType.Cv8U, 3);
- var span = image.GetBytePointer();
+ var span = image.GetDataByteSpan();
/*var previewSize = ResolutionX * ResolutionY * 2;
if (previewSize != rawImageData.Length)
@@ -578,14 +578,13 @@ public override string ToString()
return $"{nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(ImageOffset)}: {ImageOffset}, {nameof(ImageLength)}: {ImageLength}, {nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(Unknown3)}: {Unknown3}, {nameof(Unknown4)}: {Unknown4}";
}
- public unsafe byte[] Encode(Mat image)
+ public byte[] Encode(Mat image)
{
List rawData = new();
ushort color15 = 0;
uint rep = 0;
- var span = image.GetBytePointer();
- var imageLength = image.GetLength();
+ var span = image.GetDataByteReadOnlySpan();
void RleRGB15()
{
@@ -615,11 +614,27 @@ void RleRGB15()
}
int pixel = 0;
- while (pixel < imageLength)
+ while (pixel < span.Length)
{
+ byte b = span[pixel++];
+ byte g;
+ byte r;
+
+ if (image.NumberOfChannels == 1) // 8 bit safe-guard
+ {
+ r = g = b;
+ }
+ else
+ {
+ g = span[pixel++];
+ r = span[pixel++];
+ }
+
+ if (image.NumberOfChannels == 4) pixel++; // skip alpha
+
var ncolor15 =
// bgr
- (span[pixel++] >> 3) | ((span[pixel++] >> 2) << 5) | ((span[pixel++] >> 3) << 11);
+ (b >> 3) | ((g >> 2) << 5) | ((r >> 3) << 11);
if (ncolor15 == color15)
{
diff --git a/UVtools.Core/FileFormats/FDGFile.cs b/UVtools.Core/FileFormats/FDGFile.cs
index ae5c24c8..8d85f710 100644
--- a/UVtools.Core/FileFormats/FDGFile.cs
+++ b/UVtools.Core/FileFormats/FDGFile.cs
@@ -303,10 +303,10 @@ public class Preview
[FieldOrder(6)] public uint Unknown3 { get; set; }
[FieldOrder(7)] public uint Unknown4 { get; set; }
- public unsafe Mat Decode(byte[] rawImageData)
+ public Mat Decode(byte[] rawImageData)
{
var image = new Mat(new Size((int)ResolutionX, (int)ResolutionY), DepthType.Cv8U, 3);
- var span = image.GetBytePointer();
+ var span = image.GetDataByteSpan();
int pixel = 0;
for (uint n = 0; n < ImageLength; n++)
@@ -335,11 +335,10 @@ public unsafe Mat Decode(byte[] rawImageData)
return image;
}
- public static unsafe byte[] Encode(Mat image)
+ public static byte[] Encode(Mat image)
{
List rawData = new();
- var span = image.GetBytePointer();
- var imageLength = image.GetLength();
+ var span = image.GetDataByteReadOnlySpan();
ushort color15 = 0;
uint rep = 0;
@@ -371,12 +370,26 @@ void RleRGB15()
}
}
- for (int pixel = 0; pixel < imageLength; pixel += image.NumberOfChannels)
+ int pixel = 0;
+ while (pixel < span.Length)
{
- var ncolor15 =
- (span[pixel] >> 3)
- | ((span[pixel+1] >> 2) << 5)
- | ((span[pixel+2] >> 3) << 11);
+ byte b = span[pixel++];
+ byte g;
+ byte r;
+
+ if (image.NumberOfChannels == 1) // 8 bit safe-guard
+ {
+ r = g = b;
+ }
+ else
+ {
+ g = span[pixel++];
+ r = span[pixel++];
+ }
+
+ if (image.NumberOfChannels == 4) pixel++; // skip alpha
+
+ var ncolor15 = (b >> 3) | ((g >> 2) << 5) | ((r >> 3) << 11);
if (ncolor15 == color15)
{
diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs
index e60d5e77..5c81a2ee 100644
--- a/UVtools.Core/FileFormats/FileFormat.cs
+++ b/UVtools.Core/FileFormats/FileFormat.cs
@@ -34,6 +34,7 @@
using UVtools.Core.Operations;
using UVtools.Core.PixelEditor;
using UVtools.Core.Exceptions;
+using System.Threading.Channels;
namespace UVtools.Core.FileFormats;
@@ -781,7 +782,7 @@ or DATATYPE_BGR888
var bytesPerPixel = dataType is "RGB888" or "BGR888" ? 3 : 2;
var bytes = new byte[mat.Width * mat.Height * bytesPerPixel];
uint index = 0;
- var span = mat.GetDataByteSpan();
+ var span = mat.GetDataByteReadOnlySpan();
for (int i = 0; i < span.Length;)
{
byte b = span[i++];
@@ -3837,8 +3838,10 @@ public bool RenameFile(string newFileName, bool overwrite = false)
/// Sanitizes the thumbnails to respect the file specification:
/// - Remove empty thumbnails
/// - Remove the excess thumbnails up to spec, if requested
- /// - Resize thumbnails to the spec
- /// - Creates missing thumbnails required by the file spec
+ /// - Check if the thumbnails have the correct number of channels
+ /// - Force BGR color space and strip alpha channel if required by the format
+ /// - Resize thumbnails to the spec
+ /// - Creates missing thumbnails required by the file spec
///
/// True to trim the excess thumbnails, otherwise false to not trim
/// True if anything changed, otherwise false
@@ -3861,13 +3864,55 @@ public bool SanitizeThumbnails(bool trimToFileSpec = false)
changed = true;
}
+ // Check if the thumbnails have the correct number of channels
+ for (var i = 0; i < ThumbnailsCount; i++)
+ {
+ var numberOfChannels = Thumbnails[i].NumberOfChannels;
+ var validNumberOfChannels = new[] { 1, 3, 4 };
+
+
+ if (!validNumberOfChannels.Contains(numberOfChannels))
+ {
+ throw new InvalidDataException($"The thumbnail {i} holds an invalid number of channels ({numberOfChannels}). To be valid should have: <{string.Join(", ", validNumberOfChannels)}>.");
+ }
+ }
+
+ // Force BGR color space and strip alpha channel if required by the format
+ if (FileType != FileFormatType.Archive)
+ {
+ for (var i = 0; i < ThumbnailsCount; i++)
+ {
+ switch (Thumbnails[i].NumberOfChannels)
+ {
+ case 1:
+ CvInvoke.CvtColor(Thumbnails[i], Thumbnails[i], ColorConversion.Gray2Bgr);
+ changed = true;
+ break;
+ case 4:
+ CvInvoke.CvtColor(Thumbnails[i], Thumbnails[i], ColorConversion.Bgra2Bgr);
+ changed = true;
+ break;
+ }
+ }
+ }
+
// Resize thumbnails to the spec
- for (var i = 0; i < ThumbnailsCount && i < ThumbnailsOriginalSize.Length; i++)
+ if (ThumbnailsCount > 0 && ThumbnailsOriginalSize.Length > 0)
{
- if (Thumbnails[i].Size != ThumbnailsOriginalSize[i])
+ int originalThumbnailSize = 0;
+ for (var i = 0; i < ThumbnailsCount; i++)
{
- CvInvoke.Resize(Thumbnails[i], Thumbnails[i], ThumbnailsOriginalSize[i]);
- changed = true;
+ if (Thumbnails[i].Size != ThumbnailsOriginalSize[originalThumbnailSize])
+ {
+ CvInvoke.Resize(Thumbnails[i], Thumbnails[i], ThumbnailsOriginalSize[originalThumbnailSize]);
+ changed = true;
+ }
+
+ originalThumbnailSize++;
+ if (originalThumbnailSize >= ThumbnailsOriginalSize.Length)
+ {
+ originalThumbnailSize = 0;
+ }
}
}
@@ -4117,7 +4162,7 @@ public void Encode(string? fileFullPath, OperationProgress? progress = null)
}
// Make sure thumbnails are all set, otherwise clone/create them
- SanitizeThumbnails();
+ SanitizeThumbnails(FileType != FileFormatType.Archive);
OnBeforeEncode(false);
BeforeEncode(false);
@@ -4367,7 +4412,6 @@ public void DecodeThumbnailsFromZip(ZipArchive zipArchive, OperationProgress? pr
var mat = new Mat();
CvInvoke.Imdecode(stream.ToArray(), ImreadModes.Unchanged, mat);
Thumbnails.Add(mat);
- stream.Close();
progress++;
}
}
@@ -4384,7 +4428,6 @@ public void DecodeAllThumbnailsFromZip(ZipArchive zipArchive, OperationProgress?
Mat mat = new();
CvInvoke.Imdecode(stream.ToArray(), ImreadModes.Unchanged, mat);
Thumbnails.Add(mat);
- stream.Close();
progress++;
}
}
diff --git a/UVtools.Core/FileFormats/PHZFile.cs b/UVtools.Core/FileFormats/PHZFile.cs
index b9c4aa8d..a9c41d2e 100644
--- a/UVtools.Core/FileFormats/PHZFile.cs
+++ b/UVtools.Core/FileFormats/PHZFile.cs
@@ -1,4 +1,4 @@
-/*
+ /*
* GNU AFFERO GENERAL PUBLIC LICENSE
* Version 3, 19 November 2007
* Copyright (C) 2007 Free Software Foundation, Inc.
@@ -317,10 +317,10 @@ public class Preview
[FieldOrder(6)] public uint Unknown3 { get; set; }
[FieldOrder(7)] public uint Unknown4 { get; set; }
- public unsafe Mat Decode(byte[] rawImageData)
+ public Mat Decode(byte[] rawImageData)
{
var image = new Mat(new Size((int)ResolutionX, (int)ResolutionY), DepthType.Cv8U, 3);
- var span = image.GetBytePointer();
+ var span = image.GetDataByteSpan();
int pixel = 0;
for (uint n = 0; n < ImageLength; n++)
@@ -349,11 +349,10 @@ public unsafe Mat Decode(byte[] rawImageData)
return image;
}
- public static unsafe byte[] Encode(Mat image)
+ public static byte[] Encode(Mat image)
{
List rawData = new();
- var span = image.GetBytePointer();
- var imageLength = image.GetLength();
+ var span = image.GetDataByteReadOnlySpan();
ushort color15 = 0;
uint rep = 0;
@@ -385,12 +384,26 @@ void RleRGB15()
}
}
- for (int pixel = 0; pixel < imageLength; pixel += image.NumberOfChannels)
+ int pixel = 0;
+ while (pixel < span.Length)
{
- var ncolor15 =
- (span[pixel] >> 3)
- | ((span[pixel+1] >> 2) << 5)
- | ((span[pixel+2] >> 3) << 11);
+ byte b = span[pixel++];
+ byte g;
+ byte r;
+
+ if (image.NumberOfChannels == 1) // 8 bit safe-guard
+ {
+ r = g = b;
+ }
+ else
+ {
+ g = span[pixel++];
+ r = span[pixel++];
+ }
+
+ if (image.NumberOfChannels == 4) pixel++; // skip alpha
+
+ var ncolor15 = (b >> 3) | ((g >> 2) << 5) | ((r >> 3) << 11);
if (ncolor15 == color15)
{
diff --git a/UVtools.Core/Layers/Layer.cs b/UVtools.Core/Layers/Layer.cs
index fd1d0379..d08b6d2e 100644
--- a/UVtools.Core/Layers/Layer.cs
+++ b/UVtools.Core/Layers/Layer.cs
@@ -1469,8 +1469,9 @@ public Rectangle GetBoundingRectangle(Mat? mat = null, bool reCalculate = false)
NonZeroPixelCount = (uint)CvInvoke.CountNonZero(roiMat);
// Compute first and last pixel
- var span = roiMat.GetDataByteReadOnlySpan();
- var yOffset = mat.GetRealStep() * BoundingRectangle.Y;
+ var span = roiMat.GetRowByteReadOnlySpan(0);
+ var step = mat.GetRealStep();
+ var yOffset = step * BoundingRectangle.Y;
for (var i = 0; i < span.Length; i++)
{
if (span[i] == 0) continue;
@@ -1479,6 +1480,9 @@ public Rectangle GetBoundingRectangle(Mat? mat = null, bool reCalculate = false)
FirstPixelPosition = new Point(xOffset, BoundingRectangle.Y);
break;
}
+
+ span = roiMat.GetRowByteReadOnlySpan(roiMat.Height - 1);
+ yOffset = step * BoundingRectangle.Bottom;
for (var i = span.Length - 1; i >= 0; i--)
{
if (span[i] == 0) continue;
diff --git a/UVtools.Core/Operations/OperationChangeResolution.cs b/UVtools.Core/Operations/OperationChangeResolution.cs
index 4534cbf3..0c91c6e9 100644
--- a/UVtools.Core/Operations/OperationChangeResolution.cs
+++ b/UVtools.Core/Operations/OperationChangeResolution.cs
@@ -239,6 +239,7 @@ public static Resolution[] GetResolutions()
new Resolution(4920, 2880, "5K UHD"),
new Resolution(5448, 3064, "6K"),
new Resolution(7680, 4320, "8K UHD"),
+ new Resolution(11520, 5120, "12K"),
};
}
diff --git a/UVtools.Core/Operations/OperationLightBleedCompensation.cs b/UVtools.Core/Operations/OperationLightBleedCompensation.cs
index dd731c0b..65c5061f 100644
--- a/UVtools.Core/Operations/OperationLightBleedCompensation.cs
+++ b/UVtools.Core/Operations/OperationLightBleedCompensation.cs
@@ -20,12 +20,24 @@
namespace UVtools.Core.Operations;
-#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
+#pragma warning disable CS0660, CS0661
public class OperationLightBleedCompensation : Operation
-#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
+#pragma warning restore CS0660, CS0661
{
#region Enums
+ public enum LightBleedCompensationSubject : byte
+ {
+ [Description("Similarities: Dims sequential pixels in the model")]
+ Similarities,
+
+ [Description("Bridges: Dims sequential pixels that are bridges")]
+ Bridges,
+
+ [Description("Both: Similarities and bridges")]
+ Both
+ }
+
public enum LightBleedCompensationLookupMode : byte
{
[Description("Previous: Look for sequential pixels relative to the previous layers")]
@@ -44,6 +56,7 @@ public enum LightBleedCompensationLookupMode : byte
private LightBleedCompensationLookupMode _lookupMode = LightBleedCompensationLookupMode.Next;
private string _dimBy = "25,15,10,5";
+ private LightBleedCompensationSubject _subject;
#endregion
@@ -53,7 +66,7 @@ public enum LightBleedCompensationLookupMode : byte
public override string IconClass => "mdi-lightbulb-on";
public override string Title => "Light bleed compensation";
public override string Description =>
- "Compensate the over-curing and light bleed from clear resins by dimming the sequential pixels.\n" +
+ "Compensate the over-curing and light bleed from clear resins by dimming the sequential pixels in the Z axis.\n" +
"Note: You need to find the optimal minimum pixel brightness that such resin can print in order to optimize this process.\n" +
"With more translucent resins you can go with lower brightness but stick to a limit that can form the layer without loss." +
" Tiny details can be lost when using low brightness level.\n" +
@@ -86,7 +99,8 @@ public enum LightBleedCompensationLookupMode : byte
public override string ToString()
{
- var result = $"[Lookup: {_lookupMode}]" +
+ var result = $"[Subject: {_subject}]" +
+ $" [Lookup: {_lookupMode}]" +
$" [Dim by: {_dimBy}]" + LayerRangeString;
if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}";
return result;
@@ -103,6 +117,12 @@ public OperationLightBleedCompensation(FileFormat slicerFile) : base(slicerFile)
#region Properties
+ public LightBleedCompensationSubject Subject
+ {
+ get => _subject;
+ set => RaiseAndSetIfChanged(ref _subject, value);
+ }
+
public LightBleedCompensationLookupMode LookupMode
{
get => _lookupMode;
@@ -116,11 +136,13 @@ public string DimBy
{
if(!RaiseAndSetIfChanged(ref _dimBy, value)) return;
RaisePropertyChanged(nameof(MinimumBrightness));
+ RaisePropertyChanged(nameof(MinimumBrightnessPercentage));
RaisePropertyChanged(nameof(MaximumSubtraction));
}
}
public int MinimumBrightness => 255 - MaximumSubtraction;
+ public float MinimumBrightnessPercentage => (float)Math.Round(MinimumBrightness * 100.0 / 255.0, 2);
public int MaximumSubtraction => DimByArray.Aggregate(0, (current, dim) => current + dim);
public byte[] DimByArray
@@ -163,7 +185,7 @@ public MCvScalar[] DimByMCvScalar
protected bool Equals(OperationLightBleedCompensation other)
{
- return _lookupMode == other._lookupMode && _dimBy == other._dimBy;
+ return _lookupMode == other._lookupMode && _dimBy == other._dimBy && _subject == other._subject;
}
public override bool Equals(object? obj)
@@ -171,9 +193,18 @@ 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((OperationLightBleedCompensation) obj);
+ return Equals((OperationLightBleedCompensation)obj);
+ }
+
+ public static bool operator ==(OperationLightBleedCompensation? left, OperationLightBleedCompensation? right)
+ {
+ return Equals(left, right);
}
+ public static bool operator !=(OperationLightBleedCompensation? left, OperationLightBleedCompensation? right)
+ {
+ return !Equals(left, right);
+ }
#endregion
@@ -237,19 +268,38 @@ protected override bool ExecuteInternally(OperationProgress progress)
}
}
- if (previousMat is null && nextMat is null) break; // Nothing more to do
+ if (mask is null || (previousMat is null && nextMat is null)) break; // Nothing more to do
if (previousMat is not null && nextMat is not null) // both, need to merge previous with next layer
{
CvInvoke.Add(previousMatRoi, nextMatRoi, previousMatRoi);
mask = previousMatRoi;
}
- CvInvoke.Subtract(target, dimMats[i], target, mask);
+ switch (_subject)
+ {
+ case LightBleedCompensationSubject.Similarities:
+ CvInvoke.Subtract(target, dimMats[i], target, mask);
+ break;
+ case LightBleedCompensationSubject.Bridges:
+ mask!.SetTo(EmguExtensions.WhiteColor, mask);
+ CvInvoke.BitwiseNot(mask, mask);
+ CvInvoke.Subtract(target, dimMats[i], target, mask);
+ break;
+ case LightBleedCompensationSubject.Both:
+ CvInvoke.Subtract(target, dimMats[i], target, mask);
+ mask!.SetTo(EmguExtensions.WhiteColor, mask);
+ CvInvoke.BitwiseNot(mask, mask);
+ CvInvoke.Subtract(target, dimMats[i], target, mask);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(Subject), _subject, null);
+ }
previousMat?.Dispose();
nextMat?.Dispose();
previousMatRoi?.Dispose();
nextMatRoi?.Dispose();
+ mask?.Dispose();
}
// Apply the results only to the selected masked area, if user selected one
diff --git a/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs b/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs
index e3c17153..4073869d 100644
--- a/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs
+++ b/UVtools.Core/Suggestions/SuggestionWaitTimeBeforeCure.cs
@@ -468,7 +468,7 @@ protected override bool ExecuteInternally(OperationProgress progress)
}
}
- if (_createEmptyFirstLayer && SlicerFile is (ChituboxFile or CTBEncryptedFile) and { CanUseLayerPositionZ: true, SupportGCode: false})
+ if (_createEmptyFirstLayer && SlicerFile is (ChituboxFile or CTBEncryptedFile or GooFile) and { CanUseLayerPositionZ: true, SupportGCode: false})
{
var firstLayer = SlicerFile.FirstLayer!;
if (!firstLayer.IsDummy) // First layer is not blank as it seems, lets create one
diff --git a/UVtools.Core/SystemOS/SystemAware.cs b/UVtools.Core/SystemOS/SystemAware.cs
index 3148f0be..3e781391 100644
--- a/UVtools.Core/SystemOS/SystemAware.cs
+++ b/UVtools.Core/SystemOS/SystemAware.cs
@@ -14,6 +14,7 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
+using System.Threading.Tasks;
using UVtools.Core.Extensions;
namespace UVtools.Core.SystemOS;
@@ -428,6 +429,33 @@ void ProcessOnOutputDataReceived(object sender, DataReceivedEventArgs e)
return stringBuilder.ToString();
}
+ ///
+ /// Produces a beep tone from speaker
+ ///
+ /// The sound frequency
+ /// The duration of the beep in milliseconds
+ /// True to wait for beep ends to continue, otherwise run in background
+ public static void Beep(int frequency = 800, int duration = 150, bool waitForCompletion = false)
+ {
+ frequency = Math.Clamp(frequency, 20, 20_000);
+ duration = Math.Max(40, duration);
+
+ if (OperatingSystem.IsWindows())
+ {
+#pragma warning disable CA1416 // Validate platform compatibility
+ if (waitForCompletion) { Console.Beep(frequency, duration); }
+ else { Task.Run(() => Console.Beep(frequency, duration)); }
+#pragma warning restore CA1416 // Validate platform compatibility
+
+ }
+ else
+ {
+ var seconds = Math.Round(duration / 1000.0, 3, MidpointRounding.AwayFromZero);
+ StartProcess("bash", $"-c \"if [ '$(command -v speaker-test)' ]; then speaker-test -t sine -f {frequency} -l 1 > /dev/null & sleep {seconds} && kill -9 $!; else echo -e '\\a'; fi\"",
+ waitForCompletion);
+ }
+ }
+
///
/// Gets if is running under Linux and under AppImage format
///
diff --git a/UVtools.UI/App.axaml.cs b/UVtools.UI/App.axaml.cs
index 73b6b4b4..0d9cffb0 100644
--- a/UVtools.UI/App.axaml.cs
+++ b/UVtools.UI/App.axaml.cs
@@ -20,6 +20,8 @@
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
using System.Web;
using Avalonia.Data.Core.Plugins;
using Avalonia.Media;
@@ -31,6 +33,7 @@
using UVtools.UI.Windows;
using Bitmap = Avalonia.Media.Imaging.Bitmap;
using UVtools.UI.Extensions;
+using static UVtools.Core.FileFormats.UVJFile;
namespace UVtools.UI;
@@ -401,6 +404,23 @@ public static Stream GetAsset(string url)
return null;
}
+ public static void BeepIfAble()
+ {
+ if (!UserSettings.Instance.General.NotificationBeep || UserSettings.Instance.General.NotificationBeepCount == 0) return;
+ Task.Run(() =>
+ {
+ int frequency = UserSettings.Instance.General.NotificationBeepFrequency;
+
+ for (int i = 0; i < UserSettings.Instance.General.NotificationBeepCount; i++)
+ {
+ SystemAware.Beep(frequency, UserSettings.Instance.General.NotificationBeepDuration, true);
+ frequency += UserSettings.Instance.General.NotificationBeepRepeatFrequencyOffset;
+ Thread.Sleep(UserSettings.Instance.General.NotificationBeepRepeatDelay);
+ }
+
+ });
+ }
+
#endregion
#region Assembly properties
diff --git a/UVtools.UI/Assets/Styles/Styles.axaml b/UVtools.UI/Assets/Styles/Styles.axaml
index 0aede129..5fe44da9 100644
--- a/UVtools.UI/Assets/Styles/Styles.axaml
+++ b/UVtools.UI/Assets/Styles/Styles.axaml
@@ -328,6 +328,14 @@
+
+
+
@@ -22,9 +33,9 @@
-
-
-
+
+
+
diff --git a/UVtools.UI/UserSettings.cs b/UVtools.UI/UserSettings.cs
index fa144aa1..92e093f8 100644
--- a/UVtools.UI/UserSettings.cs
+++ b/UVtools.UI/UserSettings.cs
@@ -63,11 +63,19 @@ public sealed class GeneralUserSettings : BindableBase
private bool _fileSavePromptOverwrite = true;
private string? _fileSaveAsDefaultName = "{0}_{PrintTimeString}_{MaterialMillilitersInteger}ml_copy";
private string? _fileSaveAsDefaultNameCleanUpRegex = @"_?[0-9]+h[0-9]+m([0-9]+s)?|_?(([0-9]*[.])?[0-9]+)ml|_copy([0-9]*)?";
+ private bool _notificationBeep = true;
+ private byte _notificationBeepCount = 1;
+ private ushort _notificationBeepActivateAboveTime = 20;
+ private ushort _notificationBeepFrequency = 600;
+ private ushort _notificationBeepDuration = 300;
+ private int _notificationBeepRepeatFrequencyOffset = 50;
+ private ushort _notificationBeepRepeatDelay;
private bool _sendToPromptForRemovableDeviceEject = true;
private RangeObservableCollection _sendToCustomLocations = new();
private RangeObservableCollection _sendToProcess = new();
private ushort _lockedFilesOpenCounter;
private bool _fileSaveUpdateNameWithNewInformation = true;
+
public const byte LockedFilesMaxOpenCounter = 10;
@@ -231,6 +239,48 @@ public string? FileSaveAsDefaultNameCleanUpRegex
set => RaiseAndSetIfChanged(ref _fileSaveAsDefaultNameCleanUpRegex, value);
}
+ public bool NotificationBeep
+ {
+ get => _notificationBeep;
+ set => RaiseAndSetIfChanged(ref _notificationBeep, value);
+ }
+
+ public byte NotificationBeepCount
+ {
+ get => _notificationBeepCount;
+ set => RaiseAndSetIfChanged(ref _notificationBeepCount, value);
+ }
+
+ public ushort NotificationBeepActivateAboveTime
+ {
+ get => _notificationBeepActivateAboveTime;
+ set => RaiseAndSetIfChanged(ref _notificationBeepActivateAboveTime, value);
+ }
+
+ public ushort NotificationBeepFrequency
+ {
+ get => _notificationBeepFrequency;
+ set => RaiseAndSetIfChanged(ref _notificationBeepFrequency, value);
+ }
+
+ public ushort NotificationBeepDuration
+ {
+ get => _notificationBeepDuration;
+ set => RaiseAndSetIfChanged(ref _notificationBeepDuration, value);
+ }
+
+ public int NotificationBeepRepeatFrequencyOffset
+ {
+ get => _notificationBeepRepeatFrequencyOffset;
+ set => RaiseAndSetIfChanged(ref _notificationBeepRepeatFrequencyOffset, value);
+ }
+
+ public ushort NotificationBeepRepeatDelay
+ {
+ get => _notificationBeepRepeatDelay;
+ set => RaiseAndSetIfChanged(ref _notificationBeepRepeatDelay, value);
+ }
+
public bool SendToPromptForRemovableDeviceEject
{
get => _sendToPromptForRemovableDeviceEject;
diff --git a/UVtools.UI/Windows/SettingsWindow.axaml b/UVtools.UI/Windows/SettingsWindow.axaml
index 9d5bbb7a..a92dfc75 100644
--- a/UVtools.UI/Windows/SettingsWindow.axaml
+++ b/UVtools.UI/Windows/SettingsWindow.axaml
@@ -390,6 +390,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UVtools.UI/Windows/SettingsWindow.axaml.cs b/UVtools.UI/Windows/SettingsWindow.axaml.cs
index 734603ab..13288b56 100644
--- a/UVtools.UI/Windows/SettingsWindow.axaml.cs
+++ b/UVtools.UI/Windows/SettingsWindow.axaml.cs
@@ -155,7 +155,11 @@ public void AutomationsClearField(object fieldObj)
propertyInfo.SetValue(Settings.Automations, null);
return;
}
-
+ }
+
+ public void PlayBeep()
+ {
+ App.BeepIfAble();
}
public async void SendToAddCustomLocation()
diff --git a/UVtools.UI/Windows/TerminalWindow.axaml.cs b/UVtools.UI/Windows/TerminalWindow.axaml.cs
index 81b7adf1..5ec178e4 100644
--- a/UVtools.UI/Windows/TerminalWindow.axaml.cs
+++ b/UVtools.UI/Windows/TerminalWindow.axaml.cs
@@ -139,7 +139,7 @@ public async void SendCommand()
{
if (_scriptState is null)
{
- _scriptState = await Scripter.RunScript(_commandText);
+ _scriptState = await Scripter.RunScript(_commandText, App.MainWindow);
}
else
{
diff --git a/documentation/UVtools.Core.xml b/documentation/UVtools.Core.xml
index e5287a45..e21a3949 100644
--- a/documentation/UVtools.Core.xml
+++ b/documentation/UVtools.Core.xml
@@ -4550,8 +4550,10 @@
Sanitizes the thumbnails to respect the file specification:
- Remove empty thumbnails
- Remove the excess thumbnails up to spec, if requested
- - Resize thumbnails to the spec
- - Creates missing thumbnails required by the file spec
+ - Check if the thumbnails have the correct number of channels
+ - Force BGR color space and strip alpha channel if required by the format
+ - Resize thumbnails to the spec
+ - Creates missing thumbnails required by the file spec
True to trim the excess thumbnails, otherwise false to not trim
True if anything changed, otherwise false
@@ -9204,6 +9206,14 @@
Gets the name of the running .app. Returns null or empty if not running an macOS .app
+
+
+ Produces a beep tone from speaker
+
+ The sound frequency
+ The duration of the beep in milliseconds
+ True to wait for beep ends to continue, otherwise run in background
+
Gets if is running under Linux and under AppImage format