Files
UVtools/UVtools.Core/FileFormats/PhotonSFile.cs
T
Tiago Conceição 3f838001c7 v2.7.0
* **Core:**
   * Write "Unhandled Exceptions" to an "errors.log" file at user settings directory
   * Increase layer height max precision from 2 to 3 decimals
* **Settings - Layer Preview:**
   * Allow to set hollow line thickness to -1 to fill the area
   * Add tooltip for "Auto rotate on load": Auto rotate the layer preview on file load for a landscape viewport
   * Add option to masks outline color and thickness
   * Add option to clear ROI when adding masks
   * Add option "Auto flip on load": Auto flip the layer preview on file load if the file is marked to print mirrored on the printer LCD
* **Layer preview:**
   * Add selectable rotation directions 90º (CW and CCW)
   * Add preview flip (CTRL+F) horizontally and/or vertically
   * Add maskable regions to process on a layer (SHIFT + Alt + Click) on a area
   * ROI: Shortcut "Shift + left click" now also selects hollow black areas inside a white perimeter
   * ROI: Shortcut "ESC + Shift" to clear only the ROI and leave masks in
   * Fix a crash when using the pixel picker tool outside image bounds
* **Pixel editor:**
   * Change drawings brush diameter limit from 255 to 4000 maximum
   * When using layers below go lower than layer 0 it no longer apply the modifications
* **File formats:**
   * Add an internal GCodeBuilder to generate identical gcode within formats with auto convertion, managing features and parsing information
   * Internally rearrange formats properties and pass values to the base class
   * Fix "Save As" filename when using formats with dual extensions
   * CBDDLP and CTB: "LightPWM" was setting "BottomLightPWM" instead
   * CWS: Fix problem with filenames with dots (.) and ending with numbers (#171)
   * CWS: Improved the enconding and decoding performance
   * CWS: Implement all available print paramenters and per layer, missing values are got from gcode interpretation
   * CWS: Use set "light off delay" layer value instead of calculating it
   * CWS: Get light off delay per layer parsed from gcode
   * CWS - RGB flavour (Bene4 Mono): Warn about wrong resolution if width not multiples of 3 (#171)
   * ZCode: Allow to set Bottom and Light intensity (LightPWM) on paramters and per layer
   * ZCode: Allow to change bottom light pwm independent from normal light pwm
   * LGS: Light off and bottom light off was setting the value on the wrong units
   * UVJ: Unable to set per layer parameters
* **Issues:**
   * When computing islands and resin traps together, they will not compute in parallel anymore to prevent CPU and tasks exaustion, it will calculate islands first and then resin traps, this should also speed up the process on weaker machines
   * Gather resin trap areas together when computing for other issues to spare a decoding cycle latter
   * When using a threshold for islands detection it was also appling it to the overhangs
   * Fix the spare decoding conditional cycle for partial scans
   * Change resin trap search from parallel to sync to prevent fake detections and missing joints at cost of speed (#13)
* **Tools:**
   * Add layer selector: 'From first to current layer' and 'From current to last layer'
   * I printed this file: Multiplier - Number of time(s) the file has been printed. Half numbers can be used to consume from a failed print. Example: 0.5x if a print canceled at 50% progress
   * Pixel dimming: Increase wall thickness default from 5px to 10px
   * Import layers: Importing layers was not marking layers as modified, then the save file won't save the new images in, to prevent other similar bugs, all layers that got replaced will be auto marked as modified
2021-03-19 04:48:45 +00:00

545 lines
20 KiB
C#

/*
* GNU AFFERO GENERAL PUBLIC LICENSE
* Version 3, 19 November 2007
* Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
* Everyone is permitted to copy and distribute verbatim copies
* of this license document, but changing it is not allowed.
*/
// https://github.com/cbiffle/catibo/blob/master/doc/cbddlp-ctb.adoc
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using BinarySerialization;
using Emgu.CV;
using Emgu.CV.CvEnum;
using UVtools.Core.Extensions;
using UVtools.Core.Operations;
namespace UVtools.Core.FileFormats
{
public class PhotonSFile : FileFormat
{
public const byte RLEEncodingLimit = 0x7f - 2; // 128;
#region Sub Classes
#region Header
public class Header
{
public const uint ResolutionX = 1440;
public const uint ResolutionY = 2560;
public const float DisplayWidth = 68.04f;
public const float DisplayHeight = 120.96f;
public const float BuildZ = 150f;
public const uint TAG1 = 2;
public const ushort TAG2 = 49;
[FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint Tag1 { get; set; } = TAG1; // 2
[FieldOrder(1)] [FieldEndianness(Endianness.Big)] public ushort Tag2 { get; set; } = TAG2; // 49
[FieldOrder(2)] [FieldEndianness(Endianness.Big)] public double XYPixelSize { get; set; } = 0.04725; // 0.04725
[FieldOrder(3)] [FieldEndianness(Endianness.Big)] public double LayerHeight { get; set; }
[FieldOrder(4)] [FieldEndianness(Endianness.Big)] public double ExposureSeconds { get; set; }
[FieldOrder(5)] [FieldEndianness(Endianness.Big)] public double LightOffDelay { get; set; }
[FieldOrder(6)] [FieldEndianness(Endianness.Big)] public double BottomExposureSeconds { get; set; }
[FieldOrder(7)] [FieldEndianness(Endianness.Big)] public uint BottomLayerCount { get; set; }
[FieldOrder(8)] [FieldEndianness(Endianness.Big)] public double LiftHeight { get; set; } // mm
[FieldOrder(9)] [FieldEndianness(Endianness.Big)] public double LiftSpeed { get; set; } // mm/s
[FieldOrder(10)] [FieldEndianness(Endianness.Big)] public double RetractSpeed { get; set; } // mm/s
[FieldOrder(11)] [FieldEndianness(Endianness.Big)] public double VolumeMl { get; set; } // ml
[FieldOrder(12)] [FieldEndianness(Endianness.Big)] public uint PreviewResolutionX { get; set; } = 225;
[FieldOrder(13)] [FieldEndianness(Endianness.Big)] public uint Unknown2 { get; set; } = 42;
[FieldOrder(14)] [FieldEndianness(Endianness.Big)] public uint PreviewResolutionY { get; set; } = 168;
[FieldOrder(15)] [FieldEndianness(Endianness.Big)] public uint Unknown4 { get; set; } = 10;
public override string ToString()
{
return $"{nameof(Tag1)}: {Tag1}, {nameof(Tag2)}: {Tag2}, {nameof(XYPixelSize)}: {XYPixelSize}, {nameof(LayerHeight)}: {LayerHeight}, {nameof(ExposureSeconds)}: {ExposureSeconds}, {nameof(LightOffDelay)}: {LightOffDelay}, {nameof(BottomExposureSeconds)}: {BottomExposureSeconds}, {nameof(BottomLayerCount)}: {BottomLayerCount}, {nameof(LiftHeight)}: {LiftHeight}, {nameof(LiftSpeed)}: {LiftSpeed}, {nameof(RetractSpeed)}: {RetractSpeed}, {nameof(VolumeMl)}: {VolumeMl}, {nameof(PreviewResolutionX)}: {PreviewResolutionX}, {nameof(Unknown2)}: {Unknown2}, {nameof(PreviewResolutionY)}: {PreviewResolutionY}, {nameof(Unknown4)}: {Unknown4}";
}
}
public class LayerHeader
{
[FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint LayerCount { get; set; }
public override string ToString()
{
return $"{nameof(LayerCount)}: {LayerCount}";
}
}
#endregion
#region LayerDef
public class LayerData
{
[FieldOrder(0)] [FieldEndianness(Endianness.Big)] public uint Unknown1 { get; set; } = 44944;
[FieldOrder(1)] [FieldEndianness(Endianness.Big)] public uint Unknown2 { get; set; } = 0;
[FieldOrder(2)] [FieldEndianness(Endianness.Big)] public uint Unknown3 { get; set; } = 0;
[FieldOrder(3)] [FieldEndianness(Endianness.Big)] public uint ResolutionX { get; set; } = 1440;
[FieldOrder(4)] [FieldEndianness(Endianness.Big)] public uint ResolutionY { get; set; } = 2560;
[FieldOrder(5)] [FieldEndianness(Endianness.Big)] public uint DataSize { get; set; }
[Ignore] public uint RleDataSize => (DataSize >> 3) - 4;
[FieldOrder(6)] [FieldEndianness(Endianness.Big)] public uint Unknown5 { get; set; } = 2684702720;
[Ignore] public byte[] EncodedRle { get; set; }
public override string ToString()
{
return $"{nameof(Unknown1)}: {Unknown1}, {nameof(Unknown2)}: {Unknown2}, {nameof(Unknown3)}: {Unknown3}, {nameof(ResolutionX)}: {ResolutionX}, {nameof(ResolutionY)}: {ResolutionY}, {nameof(DataSize)}: {DataSize}, {nameof(RleDataSize)}: {RleDataSize}, {nameof(Unknown5)}: {Unknown5}, {nameof(EncodedRle)}: {EncodedRle.Length}";
}
public unsafe byte[] Encode(Mat mat)
{
List<byte> rawData = new List<byte>();
var spanMat = mat.GetBytePointer();
var imageLength = mat.GetLength();
int rep = 0;
byte color = 0;
void AddRep()
{
if (rep <= 0) return;
rep--;
byte rle =
(byte) (((rep & 1) > 0 ? 128 : 0) |
((rep & 2) > 0 ? 64 : 0) |
((rep & 4) > 0 ? 32 : 0) |
((rep & 8) > 0 ? 16 : 0) |
((rep & 16) > 0 ? 8 : 0) |
((rep & 32) > 0 ? 4 : 0) |
((rep & 64) > 0 ? 2 : 0) | color);
rawData.Add(rle);
}
for (int i = 0; i < imageLength; i++)
{
//color = color <= 127 ? 0 : 255; // Sanitize no AA
byte thisColor = spanMat[i] <= 127 ? byte.MinValue : byte.MaxValue; // Sanitize no AA
if (thisColor != color)
{
AddRep();
color = thisColor; // Sanitize no AA
rep = 1;
}
else
{
rep++;
if (rep == RLEEncodingLimit)
{
AddRep();
rep = 0;
}
}
}
AddRep();
EncodedRle = rawData.ToArray();
DataSize = (uint)(EncodedRle.Length * 8 + 4);
return EncodedRle;
}
public unsafe Mat Decode(bool consumeRle = true)
{
var mat = EmguExtensions.InitMat(new Size((int) ResolutionX, (int) ResolutionY));
var matSpan = mat.GetBytePointer();
var imageLength = mat.GetLength();
int pixel = 0;
foreach (var run in EncodedRle)
{
byte col = (byte) ((run & 0x01) * 255);
var numPixelsInRun =
(((run & 128) > 0 ? 1 : 0) |
((run & 64) > 0 ? 2 : 0) |
((run & 32) > 0 ? 4 : 0) |
((run & 16) > 0 ? 8 : 0) |
((run & 8) > 0 ? 16 : 0) |
((run & 4) > 0 ? 32 : 0) |
((run & 2) > 0 ? 64 : 0)) + 1;
for (; numPixelsInRun > 0; numPixelsInRun--)
{
if (pixel > imageLength)
{
mat.Dispose();
throw new FileLoadException($"Error image ran off the end, expecting {imageLength} pixels");
}
matSpan[pixel++] = col;
}
}
// Not required as mat is all black by default
//for (;pixel < imageLength; pixel++) matSpan[pixel] = 0;
if (consumeRle)
EncodedRle = null;
return mat;
}
}
#endregion
#endregion
#region Properties
public Header HeaderSettings { get; protected internal set; } = new Header();
public LayerHeader LayerSettings { get; protected internal set; } = new LayerHeader();
public override FileFormatType FileType => FileFormatType.Binary;
public override FileExtension[] FileExtensions { get; } = {
new("photons", "Chitubox PhotonS"),
};
public override PrintParameterModifier[] PrintParameterModifiers { get; } =
{
PrintParameterModifier.BottomLayerCount,
PrintParameterModifier.BottomExposureSeconds,
PrintParameterModifier.ExposureSeconds,
//PrintParameterModifier.BottomLightOffDelay,
PrintParameterModifier.LightOffDelay,
//PrintParameterModifier.BottomLiftHeight,
//PrintParameterModifier.BottomLiftSpeed,
PrintParameterModifier.LiftHeight,
PrintParameterModifier.LiftSpeed,
PrintParameterModifier.RetractSpeed,
};
public override byte ThumbnailsCount { get; } = 1;
public override Size[] ThumbnailsOriginalSize { get; } = {new Size(225, 168) };
public override uint ResolutionX
{
get => Header.ResolutionX;
set { }
}
public override uint ResolutionY
{
get => Header.ResolutionY;
set { }
}
public override float DisplayWidth
{
get => Header.DisplayWidth;
set { }
}
public override float DisplayHeight
{
get => Header.DisplayHeight;
set { }
}
public override bool MirrorDisplay
{
get => true;
set { }
}
public override byte AntiAliasing
{
get => 1;
set { }
}
public override float LayerHeight
{
get => (float) Math.Round(HeaderSettings.LayerHeight);
set
{
HeaderSettings.LayerHeight = Layer.RoundHeight(value);
RaisePropertyChanged();
}
}
public override uint LayerCount
{
get => base.LayerCount;
set => base.LayerCount = LayerSettings.LayerCount = LayerCount;
}
public override ushort BottomLayerCount
{
get => (ushort) HeaderSettings.BottomLayerCount;
set => base.BottomLayerCount = (ushort) (HeaderSettings.BottomLayerCount = value);
}
public override float BottomExposureTime
{
get => (float) HeaderSettings.BottomExposureSeconds;
set => base.BottomExposureTime = (float) (HeaderSettings.BottomExposureSeconds = value);
}
public override float ExposureTime
{
get => (float) HeaderSettings.ExposureSeconds;
set => base.ExposureTime = (float) (HeaderSettings.ExposureSeconds = Math.Round(value, 2));
}
public override float BottomLiftHeight => LightOffDelay;
public override float LiftHeight
{
get => (float) HeaderSettings.LiftHeight;
set => base.LiftHeight = (float) (HeaderSettings.LiftHeight = Math.Round(value, 2));
}
public override float BottomLiftSpeed => LiftSpeed;
public override float LiftSpeed
{
get => (float) Math.Round(HeaderSettings.LiftSpeed * 60.0, 2);
set
{
HeaderSettings.LiftSpeed = Math.Round(value / 60.0, 2);
base.LiftSpeed = value;
}
}
public override float RetractSpeed
{
get => (float)Math.Round(HeaderSettings.RetractSpeed * 60.0, 2);
set
{
HeaderSettings.RetractSpeed = (float) Math.Round(value / 60.0, 2);
base.RetractSpeed = value;
}
}
public override float BottomLightOffDelay => LightOffDelay;
public override float LightOffDelay
{
get => (float)HeaderSettings.LightOffDelay;
set => base.LightOffDelay = (float)(HeaderSettings.LightOffDelay = Math.Round(value, 2));
}
public override float MaterialMilliliters
{
get => base.MaterialMilliliters;
set
{
base.MaterialMilliliters = value;
HeaderSettings.VolumeMl = base.MaterialMilliliters;
}
}
public override string MachineName => "Anycubic Photon S";
public override object[] Configs => new object[] { HeaderSettings };
#endregion
#region Constructors
public PhotonSFile()
{
}
#endregion
#region Methods
public unsafe byte[] PreviewEncode(Mat mat)
{
byte[] bytes = new byte[mat.Width * mat.Height * 2];
var span = mat.GetBytePointer();
var imageLength = mat.GetLength();
int index = 0;
for (int i = 0; i < imageLength; i+=3)
{
byte b = span[i];
byte g = span[i+1];
byte r = span[i+2];
ushort rgb15 = (ushort) (((r >> 3) << 11) | ((g >> 3) << 6) | ((b >> 3) << 0));
bytes[index++] = (byte) (rgb15 >> 8);
bytes[index++] = (byte) (rgb15 & 0xff);
}
if (index != bytes.Length)
{
throw new FileLoadException($"Preview encode incomplete encode, expected: {bytes.Length}, encoded: {index}");
}
return bytes;
}
protected override void EncodeInternally(string fileFullPath, OperationProgress progress)
{
throw new NotSupportedException("PhotonS is read-only format, please use pws instead!");
//uint currentOffset = (uint)Helpers.Serializer.SizeOf(HeaderSettings);
using (var outputFile = new FileStream(fileFullPath, FileMode.Create, FileAccess.Write))
{
outputFile.WriteSerialize(HeaderSettings);
outputFile.WriteBytes(PreviewEncode(Thumbnails[0]));
LayerSettings.LayerCount = LayerCount;
outputFile.WriteSerialize(LayerSettings);
LayerData[] layerData = new LayerData[LayerCount];
Parallel.For(0, LayerCount, layerIndex =>
{
if (progress.Token.IsCancellationRequested) return;
using (var mat = this[layerIndex].LayerMat)
{
layerData[layerIndex] = new LayerData();
layerData[layerIndex].Encode(mat);
}
lock (progress.Mutex)
{
progress++;
}
});
progress.ItemName = "Saving layers";
progress.ProcessedItems = 0;
for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++)
{
progress.Token.ThrowIfCancellationRequested();
outputFile.WriteSerialize(layerData[layerIndex]);
outputFile.WriteBytes(layerData[layerIndex].EncodedRle);
progress++;
}
}
Debug.WriteLine("Encode Results:");
Debug.WriteLine(HeaderSettings);
Debug.WriteLine("-End-");
}
public unsafe Mat PreviewDecode(byte []data)
{
Mat mat = new Mat((int) HeaderSettings.PreviewResolutionX, (int)HeaderSettings.PreviewResolutionY, DepthType.Cv8U, 3);
var span = mat.GetBytePointer();
int spanIndex = 0;
for (int i = 0; i < data.Length; i+=2)
{
ushort color16 = (ushort)(data[i] + (data[i + 1] << 8));
var r = (color16 >> 11) & 0x1F;
var g = (color16 >> 5) & 0x3F;
var b = (color16 >> 0) & 0x1F;
/*span[spanIndex++] = (byte)(b << 3);
span[spanIndex++] = (byte)(g << 2);
span[spanIndex++] = (byte)(r << 3);*/
span[spanIndex++] = (byte)((b << 3) | (b & 0x7));
span[spanIndex++] = (byte)((g << 2) | (g & 0x3));
span[spanIndex++] = (byte)((r << 3) | (r & 0x7));
}
return mat;
}
protected override void DecodeInternally(string fileFullPath, OperationProgress progress)
{
using (var inputFile = new FileStream(fileFullPath, FileMode.Open, FileAccess.Read))
{
HeaderSettings = Helpers.Deserialize<Header>(inputFile);
if (HeaderSettings.Tag1 != Header.TAG1 || HeaderSettings.Tag2 != Header.TAG2)
{
throw new FileLoadException("Not a valid PHOTONS file! TAGs doesn't match", fileFullPath);
}
int previewSize = (int) (HeaderSettings.PreviewResolutionX * HeaderSettings.PreviewResolutionY * 2);
byte[] previewData = new byte[previewSize];
uint currentOffset = (uint) Helpers.Serializer.SizeOf(HeaderSettings);
currentOffset += inputFile.ReadBytes(previewData);
Thumbnails[0] = PreviewDecode(previewData);
LayerSettings = Helpers.Deserialize<LayerHeader>(inputFile);
currentOffset += (uint)Helpers.Serializer.SizeOf(LayerSettings);
Debug.WriteLine(HeaderSettings);
Debug.WriteLine(LayerSettings);
LayerData[] layerData = new LayerData[LayerSettings.LayerCount];
progress.Reset(OperationProgress.StatusGatherLayers, LayerSettings.LayerCount);
for (int layerIndex = 0; layerIndex < LayerSettings.LayerCount; layerIndex++)
{
progress.Token.ThrowIfCancellationRequested();
layerData[layerIndex] = Helpers.Deserialize<LayerData>(inputFile);
layerData[layerIndex].EncodedRle = new byte[layerData[layerIndex].RleDataSize];
currentOffset += inputFile.ReadBytes(layerData[layerIndex].EncodedRle);
Debug.WriteLine($"Layer {layerIndex} -> {layerData[layerIndex]}");
}
LayerManager = new LayerManager(LayerSettings.LayerCount, this);
progress.Reset(OperationProgress.StatusDecodeLayers, LayerCount);
Parallel.For(0, LayerCount,
//new ParallelOptions{MaxDegreeOfParallelism = 1},
layerIndex =>
{
if (progress.Token.IsCancellationRequested) return;
using var image = layerData[layerIndex].Decode();
this[layerIndex] = new Layer((uint) layerIndex, image, LayerManager);
lock (progress.Mutex)
{
progress++;
}
});
LayerManager.RebuildLayersProperties();
}
}
public override void SaveAs(string filePath = null, OperationProgress progress = null)
{
if (RequireFullEncode)
{
if (!string.IsNullOrEmpty(filePath))
{
FileFullPath = filePath;
}
Encode(FileFullPath, progress);
return;
}
if (!string.IsNullOrEmpty(filePath))
{
File.Copy(FileFullPath, filePath, true);
FileFullPath = filePath;
}
using (var outputFile = new FileStream(FileFullPath, FileMode.Open, FileAccess.Write))
{
outputFile.Seek(0, SeekOrigin.Begin);
Helpers.SerializeWriteFileStream(outputFile, HeaderSettings);
}
}
#endregion
}
}