diff --git a/PrusaSlicer/printer/Anet N4.ini b/PrusaSlicer/printer/Anet N4.ini new file mode 100644 index 0000000..c657ea3 --- /dev/null +++ b/PrusaSlicer/printer/Anet N4.ini @@ -0,0 +1,42 @@ +# generated by PrusaSlicer 2.5.0+win64 on 2022-12-31 at 19:07:52 UTC +absolute_correction = 0 +area_fill = 50 +bed_custom_model = +bed_custom_texture = +bed_shape = 0x0,68.04x0,68.04x120.96,0x120.96 +default_sla_material_profile = Prusa Orange Tough 0.05 +default_sla_print_profile = 0.05 Normal +display_height = 120.96 +display_mirror_x = 1 +display_mirror_y = 0 +display_orientation = landscape +display_pixels_x = 1440 +display_pixels_y = 2560 +display_width = 68.04 +elefant_foot_compensation = 0.2 +elefant_foot_min_width = 0.2 +fast_tilt_time = 5 +gamma_correction = 0 +high_viscosity_tilt_time = 10 +host_type = octoprint +inherits = Original Prusa SL1 +max_exposure_time = 120 +max_initial_exposure_time = 300 +max_print_height = 135 +min_exposure_time = 1 +min_initial_exposure_time = 1 +print_host = +printer_model = SL1 +printer_notes = Don't remove the following keywords! These keywords are used in the "compatible printer" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_VENDOR_PRUSA3D\nPRINTER_MODEL_SL1\nPRINTER_VENDOR_ANET\nPRINTER_MODEL_N4\nFILEFORMAT_N4\n\nSTART_CUSTOM_VALUES\nLiftHeight_6\nLiftSpeed_60\nEND_CUSTOM_VALUES +printer_settings_id = +printer_technology = SLA +printer_variant = default +printer_vendor = +printhost_apikey = +printhost_cafile = +relative_correction = 1,1 +relative_correction_x = 1 +relative_correction_y = 1 +relative_correction_z = 1 +slow_tilt_time = 8 +thumbnails = 400x400,800x480 diff --git a/README.md b/README.md index 8ea3d67..6271a78 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ But also, I need victims for test subject. Proceed at your own risk! - LGS120 (Longer Orange 120) - LGS4K (Longer Orange 4K & mono) - Flashforge SVGX +- Anet N4 - ZIP (Generic / Phrozen Zip) - VDA.ZIP (Voxeldance Additive) - VDT (Voxeldance Tango) diff --git a/Scripts/010 Editor/FileFormats.1pj b/Scripts/010 Editor/FileFormats.1pj index a4c81c5..a482fe8 100644 --- a/Scripts/010 Editor/FileFormats.1pj +++ b/Scripts/010 Editor/FileFormats.1pj @@ -12,6 +12,7 @@ gr1.bt lgs.bt mdlp.bt + n4.bt osf.bt osla.bt photons.bt diff --git a/Scripts/010 Editor/n4.bt b/Scripts/010 Editor/n4.bt new file mode 100644 index 0000000..6268843 --- /dev/null +++ b/Scripts/010 Editor/n4.bt @@ -0,0 +1,57 @@ +//------------------------------------------------ +//--- 010 Editor v8.0.1 Binary Template +// +// File: Anet N4 +// Authors: Tiago Conceição +//------------------------------------------------ + +BigEndian(); + +typedef struct() { + uint32 WhitePixelsCount ; // White pixels region (border including corner pixels) + uint32 XMin ; // Bounding rectangle X + uint32 YMin ; // Bounding rectangle Y + uint32 XMax ; // Bounding rectangle Right + uint32 YMax ; // Bounding rectangle Bottom + uint32 BitsCount ; + ubyte LayerRLE[(BitsCount + 7) >> 3] ; +} layerData; + +struct HEADER { + uint32 VersionLen ; // 2 + wchar_t Version[VersionLen/2] ; // 3 + uint32 NameLength ; + wchar_t FileName[NameLength/2] ; // File name without extension + uint32 DescriptionLength ; + wchar_t Description[DescriptionLength/2] ; // File name without extension + + double XYPixelSize ; // mm + double LayerHeight ; // mm; from 0.03 to 0.08 + + uint32 BaseLayersCount ; // Number of extent filled additional first layers; do not use! + uint32 FilledBaseLayersCount ; // Number of fully filled first layers inside BaseLayersCount; do not use! + + uint ExposureTime ; // from 3 to 25 + uint BottomExposureTime ; // from 60 to 120 + uint32 BottomLayerCount ; // from 2 to 10 + uint32 LiftSpeed ; // mm/s, from 1 to 10 + uint32 LiftHeight ; // mm, from 3 to 10 + + uint32 PreviewResolutionX ; // 260 + uint32 PreviewResolutionY ; // 140 + uint32 PreviewSize ; // 72866 + ubyte PreviewContent[PreviewSize] ; // BMP image, BGR565 + double VolumeMicroL ; // µl + uint32 EncodedPrintTime ; // s; for unknown reason always broken in original slicer + uint32 LayerCount ; +} header; + + +struct LAYERS { + local int i; + for( i = 0; i < header.LayerCount; i++ ){ + layerData lD(); + } +} layers; + +uint32 SupportsCount ; // Supports count, always 0 \ No newline at end of file diff --git a/UVtools.Core/FileFormats/AnetN4File.cs b/UVtools.Core/FileFormats/AnetN4File.cs new file mode 100644 index 0000000..e5c4315 --- /dev/null +++ b/UVtools.Core/FileFormats/AnetN4File.cs @@ -0,0 +1,548 @@ +/* + * GNU AFFERO GENERAL PUBLIC LICENSE + * Version 3, 19 November 2007 + * Copyright (C) 2007 Free Software Foundation, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +using BinarySerialization; +using Emgu.CV; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Threading.Tasks; +using UVtools.Core.Converters; +using UVtools.Core.Extensions; +using UVtools.Core.Layers; +using UVtools.Core.Operations; + +namespace UVtools.Core.FileFormats; + +/// +/// This file format is based on B9Creator job file (.b9j) with defined version = 3 +/// and added several new fields (as example, preview image). +/// Some of the format features are not recommended to use (BaseLayersCount and FilledBaseLayersCount). +/// +public sealed class AnetN4File : FileFormat +{ + #region Constants + + public const ushort RESOLUTION_X = 1440; + public const ushort RESOLUTION_Y = 2560; + + public const float DISPLAY_WIDTH = 68.04f; + 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 + + private uint _resolutionX = RESOLUTION_X; + private uint _resolutionY = RESOLUTION_Y; + + private float _displayWidth = DISPLAY_WIDTH; + private float _displayHeight = DISPLAY_HEIGHT; + + #endregion + + #region Sub Classes + + #region Header + + public class Header + { + [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; } // 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; } = 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 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(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}"; + } + } + + #endregion + + #region LayerDef + + public class LayerDef + { + /// + /// White pixels region (border including corner pixels) + /// + [FieldOrder(0)][FieldEndianness(Endianness.Big)] public uint WhitePixelsCount { get; set; } + [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; } + [Ignore] + public uint RleBytesCount + { + get => (BitsCount + 7) >> 3; + set => BitsCount = value << 3; + } + + [Ignore] public byte[] EncodedRle { get; set; } = null!; + + public LayerDef() + { + } + + public void SetFrom(Layer layer) + { + 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() + { + 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 byte[] Encode(Mat mat) + { + uint ComputeRepeatsSize(uint repeats) + { + return (uint)Math.Ceiling(Math.Log2(repeats)); + } + + void SetBits(List data, uint pos, uint value, uint count = 1) + { + if (data.Count * 8 < pos + count) + { + data.AddRange(new byte[(pos + count + 7 - data.Count * 8) / 8]); + } + + for (int off = (int)(pos + count - 1); off + 1 > pos; --off) + { + 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; + } + } + + if (mat.Width != RESOLUTION_X || mat.Height != RESOLUTION_Y) + { + throw new ArgumentException($"Anet N4 support only {RESOLUTION_X}x{RESOLUTION_Y} image, got {mat.Width}x{mat.Height}"); + } + + WhitePixelsCount = 0; + uint singleColorLength = 0; + uint compressedPos = 33; + + 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); + + for (int i = 0; i < spanMat.Length; i++) + { + bool isWhiteCurrent = spanMat[i] > 127; // No AA + + if (isWhiteCurrent) + { + WhitePixelsCount++; + } + + if (isWhiteCurrent == isWhitePrev) + { + singleColorLength++; + } + + if (isWhiteCurrent != isWhitePrev || i == spanMat.Length - 1) + { + isWhitePrev = isWhiteCurrent; + var repeatsSize = ComputeRepeatsSize(singleColorLength); + SetBits(rawData, compressedPos, repeatsSize, 5); + SetBits(rawData, compressedPos + 5, singleColorLength, repeatsSize + 1); + compressedPos += 6 + repeatsSize; + singleColorLength = 1; + } + } + + EncodedRle = rawData.ToArray(); + RleBytesCount = (uint)EncodedRle.Length; + BitsCount = compressedPos; + + return EncodedRle; + } + + public Mat Decode(bool consumeRle = true) + { + uint GetBits(uint pos, uint count = 1) + { + if (pos + count > BitsCount) + { + throw new IndexOutOfRangeException($"Trying to read {count} bits from pos {pos}, but total size is {BitsCount} bits."); + } + + uint res = 0; + for (uint i = pos; i < pos + count; i++) + { + res <<= 1; + if ((EncodedRle[i >> 0x3] & (byte)(0x1u << (int)(i & 0x7u))) != 0) + { + res |= 1; + } + } + return res; + } + + if ((BitsCount + 7) / 8 != EncodedRle.Length) + { + throw new IndexOutOfRangeException($"Incorrect RLE data size {EncodedRle.Length * 8}, except {BitsCount} bits."); + } + + uint rleWidth = GetBits(0, 16); + uint rleHeight = GetBits(16, 16); + + var mat = EmguExtensions.InitMat(new Size((int)rleWidth, (int)rleHeight)); + var imageLength = mat.GetLength(); + + byte brightness = (byte)((GetBits(32) == 1) ? 0xff : 0x0); + + int pixelPos = 0; + uint bitPos = 33; + while (pixelPos < imageLength) + { + uint keySize = GetBits(bitPos, 5); + uint stripSize = GetBits(bitPos + 5, keySize + 1); + bitPos += keySize + 6; + mat.FillSpan(ref pixelPos, (int)stripSize, brightness); + brightness = (byte)~brightness; + } + + if (consumeRle) EncodedRle = null!; + + return mat; + } + } + #endregion + + #endregion + + #region Properties + + public Header HeaderSettings { get; private set; } = new(); + public override FileFormatType FileType => FileFormatType.Binary; + + public override FileExtension[] FileExtensions { get; } = { + new(typeof(AnetN4File), "n4", "Anet N4"), + }; + + public override SpeedUnit FormatSpeedUnit => SpeedUnit.MillimetersPerSecond; + + public override byte AntiAliasing => 1; // Format does not support anti-aliasing + + public override PrintParameterModifier[]? PrintParameterModifiers { get; } = + { + PrintParameterModifier.ExposureTime, + PrintParameterModifier.BottomExposureTime, + PrintParameterModifier.BottomLayerCount, + PrintParameterModifier.LiftSpeed, + PrintParameterModifier.LiftHeight, + }; + + public override Size[]? ThumbnailsOriginalSize { get; } = { new(260, 140) }; + + public override uint ResolutionX + { + get => _resolutionX; + 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; + } + } + + public override uint ResolutionY + { + get => _resolutionY; + 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 => _displayWidth; + set + { + if (!RaiseAndSetIfChanged(ref _displayWidth, value)) return; + HeaderSettings.XYPixelSize = PixelSizeMax; + } + } + + public override float DisplayHeight + { + get => _displayHeight; + set + { + if (!RaiseAndSetIfChanged(ref _displayHeight, value)) return; + HeaderSettings.XYPixelSize = PixelSizeMax; + } + } + + public override FlipDirection DisplayMirror + { + get => FlipDirection.Horizontally; + set { } + } + + public override float LayerHeight + { + get => Layer.RoundHeight((float)HeaderSettings.LayerHeight); + set + { + HeaderSettings.LayerHeight = Layer.RoundHeight((double)value); + RaisePropertyChanged(); + } + } + + public override uint LayerCount + { + get => base.LayerCount; + set => base.LayerCount = HeaderSettings.LayersCount = LayerCount; + } + + public override ushort BottomLayerCount + { + get => (ushort)HeaderSettings.BottomLayerCount; + set => base.BottomLayerCount = (ushort)(HeaderSettings.BottomLayerCount = value); + } + + public override float BottomExposureTime + { + get => HeaderSettings.BottomExposureTime; + set => base.BottomExposureTime = HeaderSettings.BottomExposureTime = (uint)Math.Round(value); + } + + public override float ExposureTime + { + get => HeaderSettings.ExposureTime; + set => base.ExposureTime = HeaderSettings.ExposureTime = (uint)Math.Round(value); + } + + public override float BottomLiftHeight => LiftHeight; + + public override float LiftHeight + { + get => HeaderSettings.LiftHeight; + set => base.LiftHeight = HeaderSettings.LiftHeight = (uint)Math.Round(value); + } + + public override float BottomLiftSpeed => LiftSpeed; + + public override float LiftSpeed + { + get => SpeedConverter.Convert(HeaderSettings.LiftSpeed, SpeedUnit.MillimetersPerSecond, CoreSpeedUnit); + set + { + HeaderSettings.LiftSpeed = (uint)Math.Round(SpeedConverter.Convert(value, CoreSpeedUnit, SpeedUnit.MillimetersPerSecond)); + base.LiftSpeed = SpeedConverter.Convert(HeaderSettings.LiftSpeed, SpeedUnit.MillimetersPerSecond, CoreSpeedUnit); + } + } + + public override float BottomRetractSpeed => LiftSpeed; + + public override float RetractSpeed => LiftSpeed; + + public override float PrintTime + { + get => base.PrintTime; + set + { + base.PrintTime = value; + HeaderSettings.EncodedPrintTime = (uint)base.PrintTime; + } + } + + public override float MaterialMilliliters + { + get => base.MaterialMilliliters; + set + { + base.MaterialMilliliters = value; + HeaderSettings.VolumeMicroL = Math.Round(base.MaterialMilliliters * 1000.0, 3); + } + } + + public override string MachineName => "Anet N4"; + + public override object[] Configs => new object[] { HeaderSettings }; + + #endregion + + #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); + + 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); + + progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount); + var layerData = new LayerDef[LayerCount]; + + foreach (var batch in BatchLayersIndexes()) + { + Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => + { + var layer = this[layerIndex]; + using var mat = layer.LayerMat; + layerData[layerIndex] = new LayerDef(); + layerData[layerIndex].SetFrom(layer); + layerData[layerIndex].Encode(mat); + progress.LockAndIncrement(); + }); + + foreach (var layerIndex in batch) + { + progress.ThrowIfCancellationRequested(); + + outputFile.WriteSerialize(layerData[layerIndex]); + outputFile.WriteBytes(layerData[layerIndex].EncodedRle); + + layerData[layerIndex].EncodedRle = null!; // Free + } + } + + outputFile.WriteBytes(new byte[4]); // Supports count, always 0 + + Debug.WriteLine("Encode Results:"); + Debug.WriteLine(HeaderSettings); + Debug.WriteLine("-End-"); + } + + protected override void DecodeInternally(OperationProgress progress) + { + using var inputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Read); + HeaderSettings = Helpers.Deserialize
(inputFile); + if (HeaderSettings.Version is not "3") + { + 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 is null) + { + throw new FileLoadException($"Not a valid N4 file: incorrect preview format ({HeaderSettings.PreviewSize})", FileFullPath); + } + + Thumbnails[0] = DecodeImage(DATATYPE_BGR565, HeaderSettings.PreviewContent[BmpHeader.Length..], ThumbnailsOriginalSize![0]); + + Debug.WriteLine(HeaderSettings); + + Init(HeaderSettings.LayersCount, DecodeType == FileDecodeType.Partial); + var layersDefinitions = new LayerDef[HeaderSettings.LayersCount]; + + progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount); + foreach (var batch in BatchLayersIndexes()) + { + foreach (var layerIndex in batch) + { + progress.ThrowIfCancellationRequested(); + + var layerDef = Helpers.Deserialize(inputFile); + layersDefinitions[layerIndex] = layerDef; + + if (DecodeType == FileDecodeType.Full) + { + layerDef.EncodedRle = inputFile.ReadBytes(layerDef.RleBytesCount); + } + else + { + inputFile.Seek(layerDef.RleBytesCount, SeekOrigin.Current); + } + + Debug.Write($"LAYER {layerIndex} -> "); + Debug.WriteLine(layerDef); + } + + if (DecodeType == FileDecodeType.Full) + { + Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex => + { + using var mat = layersDefinitions[layerIndex].Decode(); + _layers[layerIndex] = new Layer((uint)layerIndex, mat, this); + progress.LockAndIncrement(); + }); + } + } + + RebuildLayersProperties(); + } + + protected override void PartialSaveInternally(OperationProgress progress) + { + using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write); + outputFile.Seek(0, SeekOrigin.Begin); + outputFile.WriteSerialize(HeaderSettings); + } + + #endregion +} diff --git a/UVtools.Core/FileFormats/FileFormat.cs b/UVtools.Core/FileFormats/FileFormat.cs index 8185462..597b8bd 100644 --- a/UVtools.Core/FileFormats/FileFormat.cs +++ b/UVtools.Core/FileFormats/FileFormat.cs @@ -385,6 +385,7 @@ public abstract class FileFormat : BindableBase, IDisposable, IEquatable Created + + + This file format is based on B9Creator job file (.b9j) with defined version = 3 + and added several new fields (as example, preview image). + Some of the format features are not recommended to use (BaseLayersCount and FilledBaseLayersCount). + + + + + Printer uses incorrect BMP header for preview image so we need to use it as-is instead of generating. + + + + + White pixels region (border including corner pixels) + + Gets a magic number identifying the file type.