diff --git a/UVtools.Core/FileFormats/AnetN4File.cs b/UVtools.Core/FileFormats/AnetN4File.cs index 787efbe..e5c4315 100644 --- a/UVtools.Core/FileFormats/AnetN4File.cs +++ b/UVtools.Core/FileFormats/AnetN4File.cs @@ -26,7 +26,7 @@ namespace UVtools.Core.FileFormats; /// and added several new fields (as example, preview image). /// Some of the format features are not recommended to use (BaseLayersCount and FilledBaseLayersCount). /// -public class AnetN4File : FileFormat +public sealed class AnetN4File : FileFormat { #region Constants @@ -37,6 +37,11 @@ public class AnetN4File : FileFormat public const float DISPLAY_HEIGHT = 120.96f; public const float MACHINE_Z = 135f; + /// + /// Printer uses incorrect BMP header for preview image so we need to use it as-is instead of generating. + /// + private static readonly byte[] BmpHeader = { 66, 77, 162, 4, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0, 40, 0, 0, 0, 4, 1, 0, 0, 140, 0, 0, 0, 1, 0, 16, 0, 3, 0, 0, 0, 96, 4, 0, 0, 18, 11, 0, 0, 18, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 224, 7, 0, 0, 31, 0, 0, 0 }; + #endregion #region Members @@ -44,8 +49,8 @@ public class AnetN4File : FileFormat private uint _resolutionX = RESOLUTION_X; private uint _resolutionY = RESOLUTION_Y; - // Printer uses incorrect BMP header for preview image so we need to use it as-is instead of generating. - private byte[] _bmpHeader = { 66, 77, 162, 4, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0, 40, 0, 0, 0, 4, 1, 0, 0, 140, 0, 0, 0, 1, 0, 16, 0, 3, 0, 0, 0, 96, 4, 0, 0, 18, 11, 0, 0, 18, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 224, 7, 0, 0, 31, 0, 0, 0 }; + private float _displayWidth = DISPLAY_WIDTH; + private float _displayHeight = DISPLAY_HEIGHT; #endregion @@ -55,33 +60,33 @@ public class AnetN4File : FileFormat public class Header { - [FieldOrder(0)][FieldEndianness(Endianness.Big)] public int VersionLen { get; set; } - [FieldOrder(1)][FieldEncoding("UTF-16BE")][FieldLength(nameof(VersionLen))][SerializeAs(SerializedType.SizedString)] public string? Version { get; set; } = "3"; - [FieldOrder(2)][FieldEndianness(Endianness.Big)] public int NameLength { get; set; } - [FieldOrder(3)][FieldEncoding("UTF-16BE")][FieldLength(nameof(NameLength))][SerializeAs(SerializedType.SizedString)] public string? Name { get; set; } - [FieldOrder(4)][FieldEndianness(Endianness.Big)] public int DescriptionLength { get; set; } - [FieldOrder(5)][FieldEncoding("UTF-16BE")][FieldLength(nameof(DescriptionLength))][SerializeAs(SerializedType.SizedString)] public string? Description { get; set; } + [FieldOrder(0)][FieldEndianness(Endianness.Big)] public uint VersionLength { get; set; } = 2; + [FieldOrder(1)][FieldEncoding("UTF-16BE")][FieldLength(nameof(VersionLength))] public string Version { get; set; } = "3"; + [FieldOrder(2)][FieldEndianness(Endianness.Big)] public uint NameLength { get; set; } = (uint)About.SoftwareWithVersion.Length * 2; + [FieldOrder(3)][FieldEncoding("UTF-16BE")][FieldLength(nameof(NameLength))] public string Name { get; set; } = About.SoftwareWithVersion; + [FieldOrder(4)][FieldEndianness(Endianness.Big)] public uint DescriptionLength { get; set; } = (uint)About.SoftwareWithVersion.Length * 2; + [FieldOrder(5)][FieldEncoding("UTF-16BE")][FieldLength(nameof(DescriptionLength))] public string Description { get; set; } = About.SoftwareWithVersion; [FieldOrder(6)][FieldEndianness(Endianness.Big)] public double XYPixelSize { get; set; } = 0.04725; // mm [FieldOrder(7)][FieldEndianness(Endianness.Big)] public double LayerHeight { get; set; } // mm; from 0.03 to 0.08 - [FieldOrder(8)][FieldEndianness(Endianness.Big)] public uint BaseLayersCount { get; set; } = 0; // Number of extent filled additional first layers; do not use! - [FieldOrder(9)][FieldEndianness(Endianness.Big)] public uint FilledBaseLayersCount { get; set; } = 0; // Number of fully filled first layers inside BaseLayersCount; do not use! - [FieldOrder(10)][FieldEndianness(Endianness.Big)] public uint ExposureSeconds { get; set; } // from 3 to 25 - [FieldOrder(11)][FieldEndianness(Endianness.Big)] public uint BottomExposureSeconds { get; set; } // from 60 to 120 - [FieldOrder(12)][FieldEndianness(Endianness.Big)] public uint BottomLayerCount { get; set; } // from 2 to 10 + [FieldOrder(8)][FieldEndianness(Endianness.Big)] public uint BaseLayersCount { get; set; } // Number of extent filled additional first layers; do not use! + [FieldOrder(9)][FieldEndianness(Endianness.Big)] public uint FilledBaseLayersCount { get; set; } // Number of fully filled first layers inside BaseLayersCount; do not use! + [FieldOrder(10)][FieldEndianness(Endianness.Big)] public uint ExposureTime { get; set; } = 6; // from 3 to 25 + [FieldOrder(11)][FieldEndianness(Endianness.Big)] public uint BottomExposureTime { get; set; } = 90; // from 60 to 120 + [FieldOrder(12)][FieldEndianness(Endianness.Big)] public uint BottomLayerCount { get; set; } = DefaultBottomLayerCount; // from 2 to 10 [FieldOrder(13)][FieldEndianness(Endianness.Big)] public uint LiftSpeed { get; set; } = (uint)Math.Ceiling(SpeedConverter.Convert(DefaultLiftSpeed, CoreSpeedUnit, SpeedUnit.MillimetersPerSecond)); // mm/s, from 1 to 10 [FieldOrder(14)][FieldEndianness(Endianness.Big)] public uint LiftHeight { get; set; } = (uint)DefaultLiftHeight; // mm, from 3 to 10 [FieldOrder(15)][FieldEndianness(Endianness.Big)] public uint PreviewResolutionX { get; set; } = 260; [FieldOrder(16)][FieldEndianness(Endianness.Big)] public uint PreviewResolutionY { get; set; } = 140; - [FieldOrder(17)][FieldEndianness(Endianness.Big)] public uint PreviewSize { get; set; } - [FieldOrder(18)][FieldEndianness(Endianness.Big)][FieldLength(nameof(PreviewSize))] public byte[]? PreviewContent { get; set; } // BMP image, BGR565 + [FieldOrder(17)][FieldEndianness(Endianness.Big)] public uint PreviewSize { get; set; } = 72866; + [FieldOrder(18)][FieldEndianness(Endianness.Big)][FieldLength(nameof(PreviewSize))] public byte[] PreviewContent { get; set; } = Array.Empty(); // BMP image, BGR565 [FieldOrder(19)][FieldEndianness(Endianness.Big)] public double VolumeMicroL { get; set; } // µl - [FieldOrder(20)][FieldEndianness(Endianness.Big)] public int EncodedPrintTime { get; set; } = 0; // s; for unknown reason always broken in original slicer + [FieldOrder(20)][FieldEndianness(Endianness.Big)] public uint EncodedPrintTime { get; set; } // s; for unknown reason always broken in original slicer [FieldOrder(21)][FieldEndianness(Endianness.Big)] public uint LayersCount { get; set; } // Not include BaseLayers public override string ToString() { - return $"{nameof(Version)}: {Version}, {nameof(Name)}: {Name}, {nameof(Description)}: {Description}, {nameof(XYPixelSize)}: {XYPixelSize}, {nameof(LayerHeight)}: {LayerHeight}, {nameof(ExposureSeconds)}: {ExposureSeconds}, {nameof(BottomExposureSeconds)}: {BottomExposureSeconds}, {nameof(BottomLayerCount)}: {BottomLayerCount}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(VolumeMicroL)}: {VolumeMicroL}, {nameof(PreviewResolutionX)}: {PreviewResolutionX}, {nameof(PreviewResolutionY)}: {PreviewResolutionY}, {nameof(LayersCount)}: {LayersCount}"; + return $"{nameof(VersionLength)}: {VersionLength}, {nameof(Version)}: {Version}, {nameof(NameLength)}: {NameLength}, {nameof(Name)}: {Name}, {nameof(DescriptionLength)}: {DescriptionLength}, {nameof(Description)}: {Description}, {nameof(XYPixelSize)}: {XYPixelSize}, {nameof(LayerHeight)}: {LayerHeight}, {nameof(BaseLayersCount)}: {BaseLayersCount}, {nameof(FilledBaseLayersCount)}: {FilledBaseLayersCount}, {nameof(ExposureTime)}: {ExposureTime}, {nameof(BottomExposureTime)}: {BottomExposureTime}, {nameof(BottomLayerCount)}: {BottomLayerCount}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(PreviewResolutionX)}: {PreviewResolutionX}, {nameof(PreviewResolutionY)}: {PreviewResolutionY}, {nameof(PreviewSize)}: {PreviewSize}, {nameof(PreviewContent)}: {PreviewContent}, {nameof(VolumeMicroL)}: {VolumeMicroL}, {nameof(EncodedPrintTime)}: {EncodedPrintTime}, {nameof(LayersCount)}: {LayersCount}"; } } @@ -91,10 +96,12 @@ public class AnetN4File : FileFormat public class LayerDef { + /// + /// White pixels region (border including corner pixels) + /// [FieldOrder(0)][FieldEndianness(Endianness.Big)] public uint WhitePixelsCount { get; set; } - // White pixels region (border including corner pixels) - [FieldOrder(1)][FieldEndianness(Endianness.Big)] public int XMin { get; set; } = 0; - [FieldOrder(2)][FieldEndianness(Endianness.Big)] public int YMin { get; set; } = 0; + [FieldOrder(1)][FieldEndianness(Endianness.Big)] public int XMin { get; set; } + [FieldOrder(2)][FieldEndianness(Endianness.Big)] public int YMin { get; set; } [FieldOrder(3)][FieldEndianness(Endianness.Big)] public int XMax { get; set; } = RESOLUTION_X - 1; [FieldOrder(4)][FieldEndianness(Endianness.Big)] public int YMax { get; set; } = RESOLUTION_Y - 1; [FieldOrder(5)][FieldEndianness(Endianness.Big)] public uint BitsCount { get; set; } @@ -110,11 +117,14 @@ public class AnetN4File : FileFormat public LayerDef() { } - - public LayerDef(Mat mat) + + public void SetFrom(Layer layer) { - XMax = mat.Width - 1; - YMax = mat.Height - 1; + WhitePixelsCount = layer.NonZeroPixelCount; // To be re-set latter while encoding + XMin = layer.BoundingRectangle.X; + YMin = layer.BoundingRectangle.Y; + XMax = layer.BoundingRectangle.Right; + YMax = layer.BoundingRectangle.Bottom; } public override string ToString() @@ -122,14 +132,14 @@ public class AnetN4File : FileFormat return $"{nameof(WhitePixelsCount)}: {WhitePixelsCount}, {nameof(XMin)}: {XMin}, {nameof(YMin)}: {YMin}, {nameof(XMax)}: {XMax}, {nameof(YMax)}: {YMax}, {nameof(BitsCount)}: {BitsCount}, {nameof(RleBytesCount)}: {RleBytesCount}, {nameof(EncodedRle)}: {EncodedRle?.Length}"; } - public unsafe byte[] Encode(Mat mat) + public byte[] Encode(Mat mat) { - uint computeRepeatsSize(uint repeats) + uint ComputeRepeatsSize(uint repeats) { return (uint)Math.Ceiling(Math.Log2(repeats)); } - void setBits(List data, uint pos, uint value, uint count = 1) + void SetBits(List data, uint pos, uint value, uint count = 1) { if (data.Count * 8 < pos + count) { @@ -138,9 +148,9 @@ public class AnetN4File : FileFormat for (int off = (int)(pos + count - 1); off + 1 > pos; --off) { - byte tmp = data[(int)off / 8]; - byte mask = (byte)(1 << ((int)off % 8)); - data[(int)off / 8] = ((int)value & 1) == 1 ? (byte)(tmp | mask) : (byte)(tmp & ~mask); + byte tmp = data[off / 8]; + byte mask = (byte)(1 << (off % 8)); + data[off / 8] = ((int)value & 1) == 1 ? (byte)(tmp | mask) : (byte)(tmp & ~mask); value >>= 1; } } @@ -150,29 +160,22 @@ public class AnetN4File : FileFormat throw new ArgumentException($"Anet N4 support only {RESOLUTION_X}x{RESOLUTION_Y} image, got {mat.Width}x{mat.Height}"); } - Rectangle rec = CvInvoke.BoundingRectangle(mat); - XMin = rec.Left; - XMax = rec.Right - 1; - YMin = rec.Top; - YMax = rec.Bottom - 1; - WhitePixelsCount = 0; - - uint singleColorLenght = 0; + uint singleColorLength = 0; uint compressedPos = 33; - List rawData = new(); - var spanMat = mat.GetBytePointer(); + var rawData = new List(); + var spanMat = mat.GetDataByteSpan(); bool isWhitePrev = spanMat[0] > 127; - setBits(rawData, 0, (uint)mat.Width, 16); - setBits(rawData, 16, (uint)mat.Height, 16); - setBits(rawData, 32, isWhitePrev ? 1u : 0u); + SetBits(rawData, 0, (uint)mat.Width, 16); + SetBits(rawData, 16, (uint)mat.Height, 16); + SetBits(rawData, 32, isWhitePrev ? 1u : 0u); - for (int pixel_ind = 0; pixel_ind < mat.GetLength(); ++pixel_ind) + for (int i = 0; i < spanMat.Length; i++) { - bool isWhiteCurrent = spanMat[pixel_ind] > 127; // No AA + bool isWhiteCurrent = spanMat[i] > 127; // No AA if (isWhiteCurrent) { @@ -181,17 +184,17 @@ public class AnetN4File : FileFormat if (isWhiteCurrent == isWhitePrev) { - singleColorLenght++; + singleColorLength++; } - if (isWhiteCurrent != isWhitePrev || pixel_ind == mat.GetLength() - 1) + if (isWhiteCurrent != isWhitePrev || i == spanMat.Length - 1) { isWhitePrev = isWhiteCurrent; - var repeatsSize = computeRepeatsSize(singleColorLenght); - setBits(rawData, compressedPos, repeatsSize, 5); - setBits(rawData, compressedPos + 5, singleColorLenght, repeatsSize + 1); + var repeatsSize = ComputeRepeatsSize(singleColorLength); + SetBits(rawData, compressedPos, repeatsSize, 5); + SetBits(rawData, compressedPos + 5, singleColorLength, repeatsSize + 1); compressedPos += 6 + repeatsSize; - singleColorLenght = 1; + singleColorLength = 1; } } @@ -212,7 +215,7 @@ public class AnetN4File : FileFormat } uint res = 0; - for (uint i = pos; i < pos + count; ++i) + for (uint i = pos; i < pos + count; i++) { res <<= 1; if ((EncodedRle[i >> 0x3] & (byte)(0x1u << (int)(i & 0x7u))) != 0) @@ -228,10 +231,10 @@ public class AnetN4File : FileFormat throw new IndexOutOfRangeException($"Incorrect RLE data size {EncodedRle.Length * 8}, except {BitsCount} bits."); } - uint RleWidth = GetBits(0, 16); - uint RleHeight = GetBits(16, 16); + uint rleWidth = GetBits(0, 16); + uint rleHeight = GetBits(16, 16); - var mat = EmguExtensions.InitMat(new Size((int)RleWidth, (int)RleHeight)); + var mat = EmguExtensions.InitMat(new Size((int)rleWidth, (int)rleHeight)); var imageLength = mat.GetLength(); byte brightness = (byte)((GetBits(32) == 1) ? 0xff : 0x0); @@ -247,8 +250,7 @@ public class AnetN4File : FileFormat brightness = (byte)~brightness; } - if (consumeRle) - EncodedRle = null!; + if (consumeRle) EncodedRle = null!; return mat; } @@ -259,18 +261,16 @@ public class AnetN4File : FileFormat #region Properties - public Header HeaderSettings { get; protected internal set; } = new(); + public Header HeaderSettings { get; private set; } = new(); public override FileFormatType FileType => FileFormatType.Binary; - public override string ConvertMenuGroup => "Anet"; - public override FileExtension[] FileExtensions { get; } = { - new(typeof(AnetN4File), "N4", "Anet N4"), + new(typeof(AnetN4File), "n4", "Anet N4"), }; public override SpeedUnit FormatSpeedUnit => SpeedUnit.MillimetersPerSecond; - public override byte AntiAliasing => 1; // Format does not support antialiasing + public override byte AntiAliasing => 1; // Format does not support anti-aliasing public override PrintParameterModifier[]? PrintParameterModifiers { get; } = { @@ -289,6 +289,7 @@ public class AnetN4File : FileFormat set { if (!RaiseAndSetIfChanged(ref _resolutionX, value)) return; + if(value != RESOLUTION_X) throw new ArgumentException($"Anet N4 support only {RESOLUTION_X}x{RESOLUTION_Y} image, got {ResolutionX}x{ResolutionY}"); HeaderSettings.XYPixelSize = PixelSizeMax; } } @@ -299,20 +300,29 @@ public class AnetN4File : FileFormat set { if (!RaiseAndSetIfChanged(ref _resolutionY, value)) return; + if (value != RESOLUTION_Y) throw new ArgumentException($"Anet N4 support only {RESOLUTION_X}x{RESOLUTION_Y} image, got {ResolutionX}x{ResolutionY}"); HeaderSettings.XYPixelSize = PixelSizeMax; } } public override float DisplayWidth { - get => DISPLAY_WIDTH; - set { } + get => _displayWidth; + set + { + if (!RaiseAndSetIfChanged(ref _displayWidth, value)) return; + HeaderSettings.XYPixelSize = PixelSizeMax; + } } public override float DisplayHeight { - get => DISPLAY_HEIGHT; - set { } + get => _displayHeight; + set + { + if (!RaiseAndSetIfChanged(ref _displayHeight, value)) return; + HeaderSettings.XYPixelSize = PixelSizeMax; + } } public override FlipDirection DisplayMirror @@ -323,20 +333,14 @@ public class AnetN4File : FileFormat public override float LayerHeight { - get => (float)Layer.RoundHeight(HeaderSettings.LayerHeight); + get => Layer.RoundHeight((float)HeaderSettings.LayerHeight); set { - HeaderSettings.LayerHeight = Layer.RoundHeight(value); + HeaderSettings.LayerHeight = Layer.RoundHeight((double)value); RaisePropertyChanged(); } } - public override float MachineZ - { - get => MACHINE_Z; - set { } - } - public override uint LayerCount { get => base.LayerCount; @@ -349,36 +353,16 @@ public class AnetN4File : FileFormat set => base.BottomLayerCount = (ushort)(HeaderSettings.BottomLayerCount = value); } - public override float BottomWaitTimeBeforeCure - { - get => 0; - set - { - SetBottomLightOffDelay(0); - base.BottomWaitTimeBeforeCure = 0; - } - } - - public override float WaitTimeBeforeCure - { - get => 0; - set - { - SetNormalLightOffDelay(0); - base.WaitTimeBeforeCure = 0; - } - } - public override float BottomExposureTime { - get => HeaderSettings.BottomExposureSeconds; - set => base.BottomExposureTime = HeaderSettings.BottomExposureSeconds = (uint)Math.Round(value); + get => HeaderSettings.BottomExposureTime; + set => base.BottomExposureTime = HeaderSettings.BottomExposureTime = (uint)Math.Round(value); } public override float ExposureTime { - get => HeaderSettings.ExposureSeconds; - set => base.ExposureTime = HeaderSettings.ExposureSeconds = (uint)Math.Round(value); + get => HeaderSettings.ExposureTime; + set => base.ExposureTime = HeaderSettings.ExposureTime = (uint)Math.Round(value); } public override float BottomLiftHeight => LiftHeight; @@ -411,7 +395,7 @@ public class AnetN4File : FileFormat set { base.PrintTime = value; - HeaderSettings.EncodedPrintTime = (int)base.PrintTime; + HeaderSettings.EncodedPrintTime = (uint)base.PrintTime; } } @@ -421,7 +405,7 @@ public class AnetN4File : FileFormat set { base.MaterialMilliliters = value; - HeaderSettings.VolumeMicroL = base.MaterialMilliliters * 1000.0; + HeaderSettings.VolumeMicroL = Math.Round(base.MaterialMilliliters * 1000.0, 3); } } @@ -434,18 +418,30 @@ public class AnetN4File : FileFormat #region Constructors public AnetN4File() { + MachineZ = MACHINE_Z; } #endregion #region Methods + protected override void OnBeforeEncode(bool isPartialEncode) + { + HeaderSettings.Name = FilenameNoExt!; + HeaderSettings.Description = $"{About.SoftwareWithVersion} @ {DateTime.Now}"; + } + protected override void EncodeInternally(OperationProgress progress) { using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write); - byte[] previewBuffer = new byte[72866]; - _bmpHeader.CopyTo(previewBuffer, 0); - EncodeImage(DATATYPE_BGR565, Thumbnails[0]!).CopyTo(previewBuffer, 66); + var previewBuffer = new byte[72866]; // 72866 + BmpHeader.CopyTo(previewBuffer, 0); + + if (CreatedThumbnailsCount > 0) + { + EncodeImage(DATATYPE_BGR565, Thumbnails[0]!).CopyTo(previewBuffer, BmpHeader.Length); + } + HeaderSettings.PreviewContent = previewBuffer; outputFile.WriteSerialize(HeaderSettings); @@ -457,11 +453,11 @@ public class AnetN4File : FileFormat { Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => { - using (var mat = this[layerIndex].LayerMat) - { - layerData[layerIndex] = new LayerDef(mat); - layerData[layerIndex].Encode(mat); - } + var layer = this[layerIndex]; + using var mat = layer.LayerMat; + layerData[layerIndex] = new LayerDef(); + layerData[layerIndex].SetFrom(layer); + layerData[layerIndex].Encode(mat); progress.LockAndIncrement(); }); @@ -476,7 +472,7 @@ public class AnetN4File : FileFormat } } - outputFile.WriteSerialize(new UInt32()); // Supports count, always 0 + outputFile.WriteBytes(new byte[4]); // Supports count, always 0 Debug.WriteLine("Encode Results:"); Debug.WriteLine(HeaderSettings); @@ -487,17 +483,17 @@ public class AnetN4File : FileFormat { using var inputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Read); HeaderSettings = Helpers.Deserialize
(inputFile); - if (HeaderSettings.Version == null || !HeaderSettings.Version.Equals("3")) + if (HeaderSettings.Version is not "3") { - throw new FileLoadException("Not a valid N4 file: Version doesn't match", FileFullPath); + throw new FileLoadException($"Not a valid N4 file: Version doesn't match, got {HeaderSettings.Version} instead of 3)", FileFullPath); } - if (HeaderSettings.PreviewSize != 72866 || HeaderSettings.PreviewContent == null) + if (HeaderSettings.PreviewSize != 72866 || HeaderSettings.PreviewContent is null) { - throw new FileLoadException("Not a valid N4 file: incorrect preview format", FileFullPath); + throw new FileLoadException($"Not a valid N4 file: incorrect preview format ({HeaderSettings.PreviewSize})", FileFullPath); } - Thumbnails[0] = DecodeImage(DATATYPE_BGR565, HeaderSettings.PreviewContent[66..], ThumbnailsOriginalSize![0]); + Thumbnails[0] = DecodeImage(DATATYPE_BGR565, HeaderSettings.PreviewContent[BmpHeader.Length..], ThumbnailsOriginalSize![0]); Debug.WriteLine(HeaderSettings);