/*
* 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.
*/
// https://github.com/cbiffle/catibo/blob/master/doc/cbddlp-ctb.adoc
using BinarySerialization;
using Emgu.CV;
using Emgu.CV.CvEnum;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UVtools.Core.Extensions;
using UVtools.Core.Layers;
using UVtools.Core.Operations;
namespace UVtools.Core.FileFormats;
public sealed class FDGFile : FileFormat
{
#region Constants
private const uint MAGIC = 0xBD3C7AC8; // 3174857416
private const ushort REPEATRGB15MASK = 0x20;
private const ushort RLE16EncodingLimit = 0x1000;
#endregion
#region Sub Classes
#region Header
public class Header
{
private string _machineName = DefaultMachineName;
///
/// Gets a magic number identifying the file type.
/// 0xBD3C7AC8 for fdg
///
[FieldOrder(0)] public uint Magic { get; set; } = MAGIC;
///
/// Gets the software version
///
[FieldOrder(1)] public uint Version { get; set; } = 2;
///
/// Gets the number of records in the layer table
///
[FieldOrder(2)] public uint LayerCount { get; set; }
///
/// Gets number of layers configured as "bottom." Note that this field appears in both the file header and ExtConfig..
///
[FieldOrder(3)] public uint BottomLayersCount { get; set; } = 10;
///
/// Gets the records whether this file was generated assuming normal (0) or mirrored (1) image projection. LCD printers are "mirrored" for this purpose.
///
[FieldOrder(4)] public uint ProjectorType { get; set; }
[FieldOrder(5)] public uint BottomLayersCount2 { get; set; } = 10; // ???
///
/// Gets the printer resolution along X axis, in pixels. This information is critical to correctly decoding layer images.
///
[FieldOrder(6)] public uint ResolutionX { get; set; }
///
/// Gets the printer resolution along Y axis, in pixels. This information is critical to correctly decoding layer images.
///
[FieldOrder(7)] public uint ResolutionY { get; set; }
///
/// Gets the layer height setting used at slicing, in millimeters. Actual height used by the machine is in the layer table.
///
[FieldOrder(8)] public float LayerHeightMilimeter { get; set; }
///
/// Gets the exposure time setting used at slicing, in seconds, for normal (non-bottom) layers, respectively. Actual time used by the machine is in the layer table.
///
[FieldOrder(9)] public float LayerExposureSeconds { get; set; }
///
/// Gets the exposure time setting used at slicing, in seconds, for bottom layers. Actual time used by the machine is in the layer table.
///
[FieldOrder(10)] public float BottomExposureSeconds { get; set; }
///
/// Gets the file offsets of ImageHeader records describing the larger preview images.
///
[FieldOrder(11)] public uint PreviewLargeOffsetAddress { get; set; }
///
/// Gets the file offsets of ImageHeader records describing the smaller preview images.
///
[FieldOrder(12)] public uint PreviewSmallOffsetAddress { get; set; }
///
/// Gets the file offset of a table of LayerHeader records giving parameters for each printed layer.
///
[FieldOrder(13)] public uint LayersDefinitionOffsetAddress { get; set; }
///
/// Gets the estimated duration of print, in seconds.
///
[FieldOrder(14)] public uint PrintTime { get; set; }
///
/// ?
///
[FieldOrder(15)] public uint AntiAliasLevel { get; set; } = 1;
///
/// Gets the PWM duty cycle for the UV illumination source on normal levels, respectively.
/// This appears to be an 8-bit quantity where 0xFF is fully on and 0x00 is fully off.
///
[FieldOrder(16)] public ushort LightPWM { get; set; } = 255;
///
/// Gets the PWM duty cycle for the UV illumination source on bottom levels, respectively.
/// This appears to be an 8-bit quantity where 0xFF is fully on and 0x00 is fully off.
///
[FieldOrder(17)] public ushort BottomLightPWM { get; set; } = 255;
[FieldOrder(18)] public uint Padding1 { get; set; }
[FieldOrder(19)] public uint Padding2 { get; set; }
///
/// Gets the height of the model described by this file, in millimeters.
///
[FieldOrder(20)] public float OverallHeightMilimeter { get; set; }
///
/// Gets dimensions of the printer’s X output volume, in millimeters.
///
[FieldOrder(21)] public float BedSizeX { get; set; }
///
/// Gets dimensions of the printer’s Y output volume, in millimeters.
///
[FieldOrder(22)] public float BedSizeY { get; set; }
///
/// Gets dimensions of the printer’s Z output volume, in millimeters.
///
[FieldOrder(23)] public float BedSizeZ { get; set; }
///
/// Gets the key used to encrypt layer data, or 0 if encryption is not used.
///
[FieldOrder(24)] public uint EncryptionKey { get; set; }
[FieldOrder(25)] public uint AntiAliasLevelInfo { get; set; }
[FieldOrder(26)] public uint EncryptionMode { get; set; } = 0x4c;
///
/// Gets the estimated required resin, measured in milliliters. The volume number is derived from the model.
///
[FieldOrder(27)] public float VolumeMl { get; set; }
///
/// Gets the estimated grams, derived from volume using configured factors for density.
///
[FieldOrder(28)] public float WeightG { get; set; }
///
/// Gets the estimated cost based on currency unit the user had configured. Derived from volume using configured factors for density and cost.
///
[FieldOrder(29)] public float CostDollars { get; set; }
///
/// Gets the machine name offset to a string naming the machine type, and its length in bytes.
///
[FieldOrder(30)] public uint MachineNameAddress { get; set; }
///
/// Gets the machine size in bytes
///
[FieldOrder(31)] public uint MachineNameSize { get; set; } = (uint)(string.IsNullOrEmpty(DefaultMachineName) ? 0 : DefaultMachineName.Length);
///
/// Gets the machine name. string is not nul-terminated.
/// The character encoding is currently unknown — all observed files in the wild use 7-bit ASCII characters only.
/// Note that the machine type here is set in the software profile, and is not the name the user assigned to the machine.
///
[Ignore]
public string MachineName
{
get => _machineName;
set
{
if (string.IsNullOrEmpty(value)) value = DefaultMachineName;
_machineName = value;
MachineNameSize = string.IsNullOrEmpty(_machineName) ? 0 : (uint)_machineName.Length;
}
}
///
/// Gets the light off time setting used at slicing, for bottom layers, in seconds. Actual time used by the machine is in the layer table. Note that light_off_time_s appears in both the file header and ExtConfig.
///
[FieldOrder(32)] public float BottomLightOffDelay { get; set; } = 1;
///
/// Gets the light off time setting used at slicing, for normal layers, in seconds. Actual time used by the machine is in the layer table. Note that light_off_time_s appears in both the file header and ExtConfig.
///
[FieldOrder(33)] public float LightOffDelay { get; set; } = 1;
[FieldOrder(34)] public uint Padding4 { get; set; }
///
/// Gets the distance to lift the build platform away from the vat after bottom layers, in millimeters.
///
[FieldOrder(35)] public float BottomLiftHeight { get; set; } = 5;
///
/// Gets the speed at which to lift the build platform away from the vat after bottom layers, in millimeters per minute.
///
[FieldOrder(36)] public float BottomLiftSpeed { get; set; } = 300;
///
/// Gets the distance to lift the build platform away from the vat after normal layers, in millimeters.
///
[FieldOrder(37)] public float LiftHeight { get; set; } = 5;
///
/// Gets the speed at which to lift the build platform away from the vat after normal layers, in millimeters per minute.
///
[FieldOrder(38)] public float LiftSpeed { get; set; } = 300;
///
/// Gets the speed to use when the build platform re-approaches the vat after lift, in millimeters per minute.
///
[FieldOrder(39)] public float RetractSpeed { get; set; } = 300;
[FieldOrder(40)] public uint Padding5 { get; set; }
[FieldOrder(41)] public uint Padding6 { get; set; }
[FieldOrder(42)] public uint Padding7 { get; set; }
[FieldOrder(43)] public uint Padding8 { get; set; }
[FieldOrder(44)] public uint Padding9 { get; set; }
[FieldOrder(45)] public uint Padding10 { get; set; }
[FieldOrder(46)] public uint Padding11 { get; set; }
///
/// Gets the minutes since Jan 1, 1970 UTC
///
[FieldOrder(47)] public uint ModifiedTimestampMinutes { get; set; } = (uint) DateTimeExtensions.Timestamp.TotalMinutes;
[Ignore] public string ModifiedDate => DateTimeExtensions.GetDateTimeFromTimestampMinutes(ModifiedTimestampMinutes).ToString("dd/MM/yyyy HH:mm");
[FieldOrder(48)] public uint SoftwareVersion { get; set; } = 0x01060300;
[FieldOrder(49)] public uint Padding12 { get; set; }
[FieldOrder(50)] public uint Padding13 { get; set; }
[FieldOrder(51)] public uint Padding14 { get; set; }
[FieldOrder(52)] public uint Padding15 { get; set; }
[FieldOrder(53)] public uint Padding16 { get; set; }
[FieldOrder(54)] public uint Padding17 { get; set; }
public override string ToString()
{
return $"{nameof(_machineName)}: {_machineName}, {nameof(Magic)}: {Magic}, {nameof(Version)}: {Version}, {nameof(LayerCount)}: {LayerCount}, {nameof(BottomLayersCount)}: {BottomLayersCount}, {nameof(ProjectorType)}: {ProjectorType}, {nameof(BottomLayersCount2)}: {BottomLayersCount2}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(LayerHeightMilimeter)}: {LayerHeightMilimeter}, {nameof(LayerExposureSeconds)}: {LayerExposureSeconds}, {nameof(BottomExposureSeconds)}: {BottomExposureSeconds}, {nameof(PreviewLargeOffsetAddress)}: {PreviewLargeOffsetAddress}, {nameof(PreviewSmallOffsetAddress)}: {PreviewSmallOffsetAddress}, {nameof(LayersDefinitionOffsetAddress)}: {LayersDefinitionOffsetAddress}, {nameof(PrintTime)}: {PrintTime}, {nameof(AntiAliasLevel)}: {AntiAliasLevel}, {nameof(LightPWM)}: {LightPWM}, {nameof(BottomLightPWM)}: {BottomLightPWM}, {nameof(Padding1)}: {Padding1}, {nameof(Padding2)}: {Padding2}, {nameof(OverallHeightMilimeter)}: {OverallHeightMilimeter}, {nameof(BedSizeX)}: {BedSizeX}, {nameof(BedSizeY)}: {BedSizeY}, {nameof(BedSizeZ)}: {BedSizeZ}, {nameof(EncryptionKey)}: {EncryptionKey}, {nameof(AntiAliasLevelInfo)}: {AntiAliasLevelInfo}, {nameof(EncryptionMode)}: {EncryptionMode}, {nameof(VolumeMl)}: {VolumeMl}, {nameof(WeightG)}: {WeightG}, {nameof(CostDollars)}: {CostDollars}, {nameof(MachineNameAddress)}: {MachineNameAddress}, {nameof(MachineNameSize)}: {MachineNameSize}, {nameof(MachineName)}: {MachineName}, {nameof(BottomLightOffDelay)}: {BottomLightOffDelay}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(Padding4)}: {Padding4}, {nameof(BottomLiftHeight)}: {BottomLiftHeight}, {nameof(BottomLiftSpeed)}: {BottomLiftSpeed}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(Padding5)}: {Padding5}, {nameof(Padding6)}: {Padding6}, {nameof(Padding7)}: {Padding7}, {nameof(Padding8)}: {Padding8}, {nameof(Padding9)}: {Padding9}, {nameof(Padding10)}: {Padding10}, {nameof(Padding11)}: {Padding11}, {nameof(ModifiedTimestampMinutes)}: {ModifiedTimestampMinutes}, {nameof(ModifiedDate)}: {ModifiedDate}, {nameof(SoftwareVersion)}: {SoftwareVersion}, {nameof(Padding12)}: {Padding12}, {nameof(Padding13)}: {Padding13}, {nameof(Padding14)}: {Padding14}, {nameof(Padding15)}: {Padding15}, {nameof(Padding16)}: {Padding16}, {nameof(Padding17)}: {Padding17}";
}
}
#endregion
#region Preview
///
/// The files contain two preview images.
/// These are shown on the printer display when choosing which file to print, sparing the poor printer from needing to render a 3D image from scratch.
///
public class Preview
{
///
/// Gets the X dimension of the preview image, in pixels.
///
[FieldOrder(0)] public uint ResolutionX { get; set; }
///
/// Gets the Y dimension of the preview image, in pixels.
///
[FieldOrder(1)] public uint ResolutionY { get; set; }
///
/// Gets the image offset of the encoded data blob.
///
[FieldOrder(2)] public uint ImageOffset { get; set; }
///
/// Gets the image length in bytes.
///
[FieldOrder(3)] public uint ImageLength { get; set; }
[FieldOrder(4)] public uint Unknown1 { get; set; }
[FieldOrder(5)] public uint Unknown2 { get; set; }
[FieldOrder(6)] public uint Unknown3 { get; set; }
[FieldOrder(7)] public uint Unknown4 { get; set; }
public unsafe Mat Decode(byte[] rawImageData)
{
var image = new Mat(new Size((int)ResolutionX, (int)ResolutionY), DepthType.Cv8U, 3);
var span = image.GetBytePointer();
int pixel = 0;
for (uint n = 0; n < ImageLength; n++)
{
uint dot = (uint)(rawImageData[n] & 0xFF | ((rawImageData[++n] & 0xFF) << 8));
//uint color = ((dot & 0xF800) << 8) | ((dot & 0x07C0) << 5) | ((dot & 0x001F) << 3);
byte red = (byte)(((dot >> 11) & 0x1F) << 3);
byte green = (byte)(((dot >> 6) & 0x1F) << 3);
byte blue = (byte)((dot & 0x1F) << 3);
int repeat = 1;
if ((dot & 0x0020) == 0x0020)
{
repeat += rawImageData[++n] & 0xFF | ((rawImageData[++n] & 0x0F) << 8);
}
for (int j = 0; j < repeat; j++)
{
span[pixel++] = blue;
span[pixel++] = green;
span[pixel++] = red;
//span[pixel] = new Rgba32(red, green, blue, byte.MaxValue);
}
}
return image;
}
public static unsafe byte[] Encode(Mat image)
{
List rawData = new();
var span = image.GetBytePointer();
var imageLength = image.GetLength();
ushort color15 = 0;
uint rep = 0;
void RleRGB15()
{
switch (rep)
{
case 0:
return;
case 1:
rawData.Add((byte)(color15 & ~REPEATRGB15MASK));
rawData.Add((byte)((color15 & ~REPEATRGB15MASK) >> 8));
break;
case 2:
for (int i = 0; i < 2; i++)
{
rawData.Add((byte)(color15 & ~REPEATRGB15MASK));
rawData.Add((byte)((color15 & ~REPEATRGB15MASK) >> 8));
}
break;
default:
rawData.Add((byte)(color15 | REPEATRGB15MASK));
rawData.Add((byte)((color15 | REPEATRGB15MASK) >> 8));
rawData.Add((byte)((rep - 1) | 0x3000));
rawData.Add((byte)(((rep - 1) | 0x3000) >> 8));
break;
}
}
for (int pixel = 0; pixel < imageLength; pixel += image.NumberOfChannels)
{
var ncolor15 =
(span[pixel] >> 3)
| ((span[pixel+1] >> 2) << 5)
| ((span[pixel+2] >> 3) << 11);
if (ncolor15 == color15)
{
rep++;
if (rep == RLE16EncodingLimit)
{
RleRGB15();
rep = 0;
}
}
else
{
RleRGB15();
color15 = (ushort) ncolor15;
rep = 1;
}
}
RleRGB15();
return rawData.ToArray();
}
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}";
}
}
#endregion
#region Layer
public class LayerDef
{
///
/// Gets the build platform Z position for this layer, measured in millimeters.
///
[FieldOrder(0)] public float LayerPositionZ { get; set; }
///
/// Gets the exposure time for this layer, in seconds.
///
[FieldOrder(1)] public float LayerExposure { get; set; }
///
/// Gets how long to keep the light off after exposing this layer, in seconds.
///
[FieldOrder(2)] public float LightOffDelay { get; set; }
///
/// Gets the layer image offset to encoded layer data, and its length in bytes.
///
[FieldOrder(3)] public uint DataAddress { get; set; }
///
/// Gets the layer image length in bytes.
///
[FieldOrder(4)] public uint DataSize { get; set; }
[FieldOrder(5)] public uint PageNumber { get; set; }
[FieldOrder(6)] public uint Unknown2 { get; set; } = 84;
[FieldOrder(7)] public uint Unknown3 { get; set; }
[FieldOrder(8)] public uint Unknown4 { get; set; }
[Ignore] public byte[]? EncodedRle { get; set; }
[Ignore] public FDGFile? Parent { get; set; }
public LayerDef()
{
}
public LayerDef(FDGFile parent, Layer layer)
{
Parent = parent;
SetFrom(layer);
}
public void SetFrom(Layer layer)
{
LayerPositionZ = layer.PositionZ;
LayerExposure = layer.ExposureTime;
LightOffDelay = layer.LightOffDelay;
}
public void CopyTo(Layer layer)
{
layer.PositionZ = LayerPositionZ;
layer.ExposureTime = LayerExposure;
layer.LightOffDelay = LightOffDelay;
}
public unsafe Mat Decode(uint layerIndex, bool consumeData = true)
{
var image = EmguExtensions.InitMat(Parent!.Resolution);
var span = image.GetBytePointer();
if (Parent.HeaderSettings.EncryptionKey > 0)
{
LayerRleCryptBuffer(Parent.HeaderSettings.EncryptionKey, layerIndex, EncodedRle!);
}
int limit = image.Width * image.Height;
int index = 0;
byte lastColor = 0;
foreach (var code in EncodedRle!)
{
if ((code & 0x80) == 0x80)
{
//lastColor = (byte) (code << 1);
// // Convert from 7bpp to 8bpp (extending the last bit)
lastColor = (byte)(((code & 0x7f) << 1) | (code & 1));
if (lastColor >= 0xfc)
{
// Make 'white' actually white
lastColor = 0xff;
}
if (index < limit)
{
span[index] = lastColor;
}
else
{
image.Dispose();
throw new FileLoadException("Corrupted RLE data.");
}
index++;
}
else
{
for (uint i = 0; i < code; i++)
{
if (index < limit)
{
span[index] = lastColor;
}
else
{
image.Dispose();
throw new FileLoadException("Corrupted RLE data.");
}
index++;
}
}
}
if (consumeData)
EncodedRle = null;
return image;
}
public void Encode(Mat mat, uint layerIndex)
{
List rawData = new();
//byte color = byte.MaxValue >> 1;
byte color = byte.MaxValue;
uint stride = 0;
void AddRep()
{
rawData.Add((byte)(color | 0x80));
stride--;
int done = 0;
while (done < stride)
{
int todo = 0x7d;
if (stride - done < todo)
{
todo = (int)(stride - done);
}
rawData.Add((byte)(todo));
done += todo;
}
}
int halfWidth = mat.Width / 2;
//int pixel = 0;
for (int y = 0; y < mat.Height; y++)
{
var span = mat.GetRowByteSpan(y);
for (int x = 0; x < span.Length; x++)
{
var grey7 = (byte)((span[x] >> 1) & 0x7f);
if (grey7 > 0x7c)
{
grey7 = 0x7c;
}
if (color == byte.MaxValue)
{
color = grey7;
stride = 1;
}
else if (grey7 != color || x == halfWidth)
{
AddRep();
color = grey7;
stride = 1;
}
else
{
stride++;
}
}
AddRep();
color = byte.MaxValue;
}
EncodedRle = Parent!.HeaderSettings.EncryptionKey > 0
? LayerRleCrypt(Parent.HeaderSettings.EncryptionKey, layerIndex, rawData)
: rawData.ToArray();
DataSize = (uint) EncodedRle.Length;
}
public override string ToString()
{
return $"{nameof(LayerPositionZ)}: {LayerPositionZ}, {nameof(LayerExposure)}: {LayerExposure}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(DataAddress)}: {DataAddress}, {nameof(DataSize)}: {DataSize}, {nameof(PageNumber)}: {PageNumber}, {nameof(Unknown2)}: {Unknown2}, {nameof(Unknown3)}: {Unknown3}, {nameof(Unknown4)}: {Unknown4}";
}
}
#endregion
#endregion
#region Properties
public Header HeaderSettings { get; private set; } = new();
public Preview[] Previews { get; private set; }
public LayerDef[] LayersDefinitions { get; private set; } = null!;
public override FileFormatType FileType => FileFormatType.Binary;
public override string ConvertMenuGroup => "Chitubox";
public override FileExtension[] FileExtensions { get; } = {
new(typeof(FDGFile), "fdg", "Voxelab FDG"),
};
public override PrintParameterModifier[]? PrintParameterModifiers { get; } =
{
PrintParameterModifier.BottomLayerCount,
PrintParameterModifier.BottomExposureTime,
PrintParameterModifier.ExposureTime,
PrintParameterModifier.BottomLightOffDelay,
PrintParameterModifier.LightOffDelay,
PrintParameterModifier.BottomLiftHeight,
PrintParameterModifier.BottomLiftSpeed,
PrintParameterModifier.LiftHeight,
PrintParameterModifier.LiftSpeed,
PrintParameterModifier.RetractSpeed,
PrintParameterModifier.BottomLightPWM,
PrintParameterModifier.LightPWM,
};
public override PrintParameterModifier[]? PrintParameterPerLayerModifiers { get; } = {
PrintParameterModifier.LightOffDelay,
PrintParameterModifier.ExposureTime,
};
public override Size[]? ThumbnailsOriginalSize { get; } =
{
new(400, 300),
new(200, 125)
};
public override uint[] AvailableVersions { get; } = { 2 };
public override uint DefaultVersion => 2;
public override uint Version
{
get => HeaderSettings.Version;
set
{
base.Version = value;
HeaderSettings.Version = (ushort)base.Version;
}
}
public override uint ResolutionX
{
get => HeaderSettings.ResolutionX;
set => base.ResolutionX = HeaderSettings.ResolutionX = value;
}
public override uint ResolutionY
{
get => HeaderSettings.ResolutionY;
set => base.ResolutionY = HeaderSettings.ResolutionY = value;
}
public override float DisplayWidth
{
get => HeaderSettings.BedSizeX;
set => base.DisplayWidth = HeaderSettings.BedSizeX = RoundDisplaySize(value);
}
public override float DisplayHeight
{
get => HeaderSettings.BedSizeY;
set => base.DisplayHeight = HeaderSettings.BedSizeY = RoundDisplaySize(value);
}
public override float MachineZ
{
get => HeaderSettings.BedSizeZ > 0 ? HeaderSettings.BedSizeZ : base.MachineZ;
set => base.MachineZ = HeaderSettings.BedSizeZ = (float)Math.Round(value, 2);
}
public override FlipDirection DisplayMirror
{
get => HeaderSettings.ProjectorType == 0 ? FlipDirection.None : FlipDirection.Horizontally;
set
{
HeaderSettings.ProjectorType = value == FlipDirection.None ? 0u : 1;
RaisePropertyChanged();
}
}
public override byte AntiAliasing
{
get => (byte) HeaderSettings.AntiAliasLevelInfo;
set => base.AntiAliasing = (byte)(HeaderSettings.AntiAliasLevelInfo = Math.Clamp(value, 1u, 16u));
}
public override float LayerHeight
{
get => HeaderSettings.LayerHeightMilimeter;
set
{
HeaderSettings.LayerHeightMilimeter = Layer.RoundHeight(value);
RaisePropertyChanged();
}
}
public override float PrintHeight
{
get => HeaderSettings.OverallHeightMilimeter;
set => base.PrintHeight = HeaderSettings.OverallHeightMilimeter = base.PrintHeight;
}
public override uint LayerCount
{
get => base.LayerCount;
set => base.LayerCount = HeaderSettings.LayerCount = base.LayerCount;
}
public override ushort BottomLayerCount
{
get => (ushort) HeaderSettings.BottomLayersCount;
set => base.BottomLayerCount = (ushort) (HeaderSettings.BottomLayersCount2 = HeaderSettings.BottomLayersCount = value);
}
public override float BottomLightOffDelay
{
get => HeaderSettings.BottomLightOffDelay;
set => base.BottomLightOffDelay = HeaderSettings.BottomLightOffDelay = (float)Math.Round(value, 2);
}
public override float LightOffDelay
{
get => HeaderSettings.LightOffDelay;
set => base.LightOffDelay = HeaderSettings.LightOffDelay = (float)Math.Round(value, 2);
}
public override float BottomWaitTimeBeforeCure
{
get => base.BottomWaitTimeBeforeCure;
set
{
SetBottomLightOffDelay(value);
base.BottomWaitTimeBeforeCure = value;
}
}
public override float WaitTimeBeforeCure
{
get => base.WaitTimeBeforeCure;
set
{
SetNormalLightOffDelay(value);
base.WaitTimeBeforeCure = value;
}
}
public override float BottomExposureTime
{
get => HeaderSettings.BottomExposureSeconds;
set => base.BottomExposureTime = HeaderSettings.BottomExposureSeconds = value;
}
public override float ExposureTime
{
get => HeaderSettings.LayerExposureSeconds;
set => base.ExposureTime = HeaderSettings.LayerExposureSeconds = (float)Math.Round(value, 2);
}
public override float BottomLiftHeight
{
get => HeaderSettings.BottomLiftHeight;
set => base.BottomLiftHeight = HeaderSettings.BottomLiftHeight = (float)Math.Round(value, 2);
}
public override float LiftHeight
{
get => HeaderSettings.LiftHeight;
set => base.LiftHeight = HeaderSettings.LiftHeight = (float)Math.Round(value, 2);
}
public override float BottomLiftSpeed
{
get => HeaderSettings.BottomLiftSpeed;
set => base.BottomLiftSpeed = HeaderSettings.BottomLiftSpeed = (float)Math.Round(value, 2);
}
public override float LiftSpeed
{
get => HeaderSettings.LiftSpeed;
set => base.LiftSpeed = HeaderSettings.LiftSpeed = (float)Math.Round(value, 2);
}
public override float BottomRetractSpeed => RetractSpeed;
public override float RetractSpeed
{
get => HeaderSettings.RetractSpeed;
set => base.RetractSpeed = HeaderSettings.RetractSpeed = (float)Math.Round(value, 2);
}
public override byte BottomLightPWM
{
get => (byte) HeaderSettings.BottomLightPWM;
set => base.BottomLightPWM = (byte) (HeaderSettings.BottomLightPWM = value);
}
public override byte LightPWM
{
get => (byte) HeaderSettings.BottomLightPWM;
set => base.LightPWM = (byte) (HeaderSettings.BottomLightPWM = value);
}
public override float PrintTime
{
get => base.PrintTime;
set
{
base.PrintTime = value;
HeaderSettings.PrintTime = (uint) base.PrintTime;
}
}
public override float MaterialMilliliters
{
get => base.MaterialMilliliters;
set
{
base.MaterialMilliliters = value;
HeaderSettings.VolumeMl = base.MaterialMilliliters;
}
}
public override float MaterialGrams
{
get => (float)Math.Round(HeaderSettings.WeightG, 3);
set => base.MaterialGrams = HeaderSettings.WeightG = (float)Math.Round(value, 3);
}
public override float MaterialCost
{
get => (float) Math.Round(HeaderSettings.CostDollars, 3);
set => base.MaterialCost = HeaderSettings.CostDollars = (float)Math.Round(value, 3);
}
public override string MachineName
{
get => HeaderSettings.MachineName;
set => base.MachineName = HeaderSettings.MachineName = value;
}
public override object[] Configs => new object[] { HeaderSettings };
#endregion
#region Constructors
public FDGFile()
{
Previews = new Preview[ThumbnailsCount];
}
#endregion
#region Methods
public override void Clear()
{
base.Clear();
for (byte i = 0; i < ThumbnailsCount; i++)
{
Previews[i] = new Preview();
}
LayersDefinitions = null!;
}
protected override void EncodeInternally(OperationProgress progress)
{
/*if (HeaderSettings.EncryptionKey == 0)
{
Random rnd = new Random();
HeaderSettings.EncryptionKey = (uint)rnd.Next(short.MaxValue, int.MaxValue);
}*/
using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Create, FileAccess.Write);
outputFile.Seek(Helpers.Serializer.SizeOf(HeaderSettings), SeekOrigin.Begin);
for (byte i = 0; i < ThumbnailsCount; i++)
{
var image = Thumbnails[i];
if(image is null) continue;
var bytes = Preview.Encode(image);
if (bytes.Length == 0) continue;
if (i == (byte) FileThumbnailSize.Small)
{
HeaderSettings.PreviewSmallOffsetAddress = (uint)outputFile.Position;
}
else
{
HeaderSettings.PreviewLargeOffsetAddress = (uint)outputFile.Position;
}
Preview preview = new()
{
ResolutionX = (uint) image.Width,
ResolutionY = (uint) image.Height,
ImageLength = (uint)bytes.Length,
};
preview.ImageOffset = (uint)(outputFile.Position + Helpers.Serializer.SizeOf(preview));
outputFile.WriteSerialize(preview);
outputFile.WriteBytes(bytes);
}
if (HeaderSettings.MachineNameSize > 0)
{
HeaderSettings.MachineNameAddress = (uint)outputFile.Position;
var machineBytes = Encoding.ASCII.GetBytes(HeaderSettings.MachineName);
outputFile.Write(machineBytes, 0, machineBytes.Length);
}
progress.Reset(OperationProgress.StatusEncodeLayers, LayerCount);
var layersHash = new Dictionary();
LayersDefinitions = new LayerDef[HeaderSettings.LayerCount];
HeaderSettings.LayersDefinitionOffsetAddress = (uint)outputFile.Position;
long layerDefCurrentOffset = HeaderSettings.LayersDefinitionOffsetAddress;
long layerDataCurrentOffset = HeaderSettings.LayersDefinitionOffsetAddress + Helpers.Serializer.SizeOf(new LayerDef()) * LayerCount;
foreach (var batch in BatchLayersIndexes())
{
Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex =>
{
progress.PauseIfRequested();
using (var mat = this[layerIndex].LayerMat)
{
LayersDefinitions[layerIndex] = new LayerDef(this, this[layerIndex]);
LayersDefinitions[layerIndex].Encode(mat, (uint)layerIndex);
}
progress.LockAndIncrement();
});
foreach (var layerIndex in batch)
{
progress.PauseOrCancelIfRequested();
var layerDef = LayersDefinitions[layerIndex];
LayerDef? layerDefHash = null;
if (HeaderSettings.EncryptionKey == 0)
{
string hash = CryptExtensions.ComputeSHA1Hash(layerDef.EncodedRle!);
if (layersHash.TryGetValue(hash, out layerDefHash))
{
layerDef.DataAddress = layerDefHash.DataAddress;
layerDef.DataSize = layerDefHash.DataSize;
}
else
{
layersHash.Add(hash, layerDef);
}
}
if (layerDefHash is null)
{
layerDef.PageNumber = (uint)(layerDataCurrentOffset / ChituboxFile.PageSize);
layerDef.DataAddress = (uint)(layerDataCurrentOffset - ChituboxFile.PageSize * layerDef.PageNumber);
outputFile.Seek(layerDataCurrentOffset, SeekOrigin.Begin);
layerDataCurrentOffset += outputFile.WriteBytes(layerDef.EncodedRle!);
}
outputFile.Seek(layerDefCurrentOffset, SeekOrigin.Begin);
layerDefCurrentOffset += outputFile.WriteSerialize(layerDef);
layerDef.EncodedRle = null; // Free
}
}
HeaderSettings.ModifiedTimestampMinutes = (uint)DateTimeExtensions.TimestampMinutes;
outputFile.Seek(0, SeekOrigin.Begin);
outputFile.WriteSerialize(HeaderSettings);
Debug.WriteLine("Encode Results:");
Debug.WriteLine(HeaderSettings);
Debug.WriteLine(Previews[0]);
Debug.WriteLine(Previews[1]);
Debug.WriteLine("-End-");
}
protected override void DecodeInternally(OperationProgress progress)
{
using var inputFile = new FileStream(FileFullPath!, FileMode.Open, FileAccess.Read);
//HeaderSettings = Helpers.ByteToType(InputFile);
//HeaderSettings = Helpers.Serializer.Deserialize(InputFile.ReadBytes(Helpers.Serializer.SizeOf(typeof(Header))));
HeaderSettings = Helpers.Deserialize(inputFile);
if (HeaderSettings.Magic != MAGIC)
{
throw new FileLoadException("Not a valid FDG file!", FileFullPath);
}
HeaderSettings.AntiAliasLevel = 1;
progress.Reset(OperationProgress.StatusDecodePreviews, ThumbnailsCount);
Debug.Write("Header -> ");
Debug.WriteLine(HeaderSettings);
for (byte i = 0; i < ThumbnailsCount; i++)
{
uint offsetAddress = i == 0
? HeaderSettings.PreviewSmallOffsetAddress
: HeaderSettings.PreviewLargeOffsetAddress;
if (offsetAddress == 0) continue;
inputFile.Seek(offsetAddress, SeekOrigin.Begin);
Previews[i] = Helpers.Deserialize(inputFile);
Debug.Write($"Preview {i} -> ");
Debug.WriteLine(Previews[i]);
inputFile.Seek(Previews[i].ImageOffset, SeekOrigin.Begin);
byte[] rawImageData = new byte[Previews[i].ImageLength];
inputFile.Read(rawImageData, 0, (int) Previews[i].ImageLength);
Thumbnails[i] = Previews[i].Decode(rawImageData);
progress++;
}
if (HeaderSettings is {MachineNameAddress: > 0, MachineNameSize: > 0})
{
inputFile.Seek(HeaderSettings.MachineNameAddress, SeekOrigin.Begin);
var buffer = new byte[HeaderSettings.MachineNameSize];
inputFile.Read(buffer, 0, (int) HeaderSettings.MachineNameSize);
HeaderSettings.MachineName = Encoding.ASCII.GetString(buffer);
}
Init(HeaderSettings.LayerCount, DecodeType == FileDecodeType.Partial);
LayersDefinitions = new LayerDef[HeaderSettings.LayerCount];
progress.Reset(OperationProgress.StatusDecodeLayers, HeaderSettings.LayerCount);
foreach (var batch in BatchLayersIndexes())
{
foreach (var layerIndex in batch)
{
progress.PauseOrCancelIfRequested();
var layerDef = Helpers.Deserialize(inputFile);
layerDef.Parent = this;
LayersDefinitions[layerIndex] = layerDef;
Debug.Write($"LAYER {layerIndex} -> ");
Debug.WriteLine(layerDef);
if (DecodeType == FileDecodeType.Full)
{
inputFile.SeekDoWorkAndRewind(layerDef.PageNumber * ChituboxFile.PageSize + layerDef.DataAddress,
() => { layerDef.EncodedRle = inputFile.ReadBytes(layerDef.DataSize); });
}
}
if (DecodeType == FileDecodeType.Full)
{
Parallel.ForEach(batch, CoreSettings.GetParallelOptions(progress), layerIndex =>
{
progress.PauseIfRequested();
if (DecodeType == FileDecodeType.Full)
{
using var mat = LayersDefinitions[layerIndex].Decode((uint)layerIndex);
_layers[layerIndex] = new Layer((uint)layerIndex, mat, this);
}
progress.LockAndIncrement();
});
}
}
for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++)
{
LayersDefinitions[layerIndex].CopyTo(this[layerIndex]);
}
}
protected override void PartialSaveInternally(OperationProgress progress)
{
HeaderSettings.ModifiedTimestampMinutes = (uint)DateTimeExtensions.TimestampMinutes;
using var outputFile = new FileStream(TemporaryOutputFileFullPath, FileMode.Open, FileAccess.Write);
outputFile.Seek(0, SeekOrigin.Begin);
outputFile.WriteSerialize(HeaderSettings);
/*if (HeaderSettings.MachineNameAddress > 0 && HeaderSettings.MachineNameSize > 0)
{
outputFile.Seek(HeaderSettings.MachineNameAddress, SeekOrigin.Begin);
byte[] buffer = new byte[HeaderSettings.MachineNameSize];
outputFile.Write(Encoding.ASCII.GetBytes(HeaderSettings.MachineName), 0, (int)HeaderSettings.MachineNameSize);
}*/
outputFile.Seek(HeaderSettings.LayersDefinitionOffsetAddress, SeekOrigin.Begin);
for (uint layerIndex = 0; layerIndex < HeaderSettings.LayerCount; layerIndex++)
{
LayersDefinitions[layerIndex].SetFrom(this[layerIndex]);
outputFile.WriteSerialize(LayersDefinitions[layerIndex]);
}
}
#endregion
#region Static Methods
public static byte[] LayerRleCrypt(uint seed, uint layerIndex, IEnumerable input)
{
var result = input.ToArray();
LayerRleCryptBuffer(seed, layerIndex, result);
return result;
}
public static void LayerRleCryptBuffer(uint seed, uint layerIndex, byte[] input)
{
if (seed == 0) return;
var init = (seed - 0x1dcb76c3) ^ 0x257e2431;
var key = init * 0x82391efd * (layerIndex ^ 0x110bdacd);
int index = 0;
for (int i = 0; i < input.Length; i++)
{
var k = (byte)(key >> 8 * index);
index++;
if ((index & 3) == 0)
{
key += init;
index = 0;
}
input[i] = (byte)(input[i] ^ k);
}
}
#endregion
}