Skip to content

Commit c2d984c

Browse files
sn4k3claude
andcommitted
v7.1.2
- (Improvement) Linux: Present better error messages when libcvextern fails to load, and add a check for missing dependencies - (Fix) Linux: libcvextern no longer requires the system libtiff.so.5 and libsqlite3, fixing startup on Debian 13 and Ubuntu 24.04+ (#1161) - (Fix) macOS: The missing dependencies window never listed the libcvextern dependencies - (Fix) Repair layers: Attach islands could hang forever on files with cloned layers, such as from timelapse, clone layers, double exposure, lithophane, stir resin and the calibration tests, as the clones shared the same lock - (Fix) Issues: Resin traps, suction cups and print height issues on a single layer had their area calculated as a volume, multiplied by the layer height in pixels - (Fix) Issues: Ignoring an island also ignored the islands with the same pixels on other layers - (Fix) ReverseLineReader: 4 bytes UTF-8 characters and UTF-16 surrogate pairs across the read buffer boundary, and the UTF-8 byte order mark on the first line - (Upgrade) DotNext 6.11.0, System.CommandLine 2.0.14, Microsoft.SourceLink.GitHub 10.0.403 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent de8580d commit c2d984c

18 files changed

Lines changed: 482 additions & 89 deletions

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 25/10/2026 - v7.1.2
4+
5+
- (Improvement) Linux: Present better error messages when libcvextern fails to load, and add a check for missing dependencies
6+
- (Fix) Linux: libcvextern no longer requires the system libtiff.so.5 and libsqlite3, fixing startup on Debian 13 and Ubuntu 24.04+ (#1161)
7+
- (Fix) macOS: The missing dependencies window never listed the libcvextern dependencies
8+
- (Fix) Repair layers: Attach islands could hang forever on files with cloned layers, such as from timelapse, clone
9+
layers, double exposure, lithophane, stir resin and the calibration tests, as the clones shared the same lock
10+
- (Fix) Issues: Resin traps, suction cups and print height issues on a single layer had their area calculated as a
11+
volume, multiplied by the layer height in pixels
12+
- (Fix) Issues: Ignoring an island also ignored the islands with the same pixels on other layers
13+
314
## 25/10/2026 - v7.1.1
415

516
- **PCB exposure:**

‎Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<UseRIDGraph>true</UseRIDGraph>
4444

4545
<!-- Versions -->
46-
<UVtoolsVersion>7.1.1</UVtoolsVersion>
46+
<UVtoolsVersion>7.1.2</UVtoolsVersion>
4747
<AvaloniaVersion>12.1.4</AvaloniaVersion>
4848
<EmguCvVersion>5.0.0.6584</EmguCvVersion>
4949

‎UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<ItemGroup>
3232
<PackageReference
3333
Include="Microsoft.SourceLink.GitHub"
34-
Version="10.0.402"
34+
Version="10.0.403"
3535
PrivateAssets="All"
3636
/>
3737
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />

‎UVtools.Cmd/UVtools.Cmd.csproj‎

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<AssemblyName>UVtoolsCmd</AssemblyName>
5+
<Description>Commandline utility for UVtools</Description>
6+
<Version>2.2.0</Version>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
29

3-
<PropertyGroup>
4-
<OutputType>Exe</OutputType>
5-
<AssemblyName>UVtoolsCmd</AssemblyName>
6-
<Description>Commandline utility for UVtools</Description>
7-
<Version>2.2.0</Version>
8-
<IsPackable>false</IsPackable>
9-
</PropertyGroup>
10-
11-
<ItemGroup>
12-
<PackageReference Include="System.CommandLine" Version="2.0.13" />
13-
</ItemGroup>
14-
15-
<ItemGroup>
16-
<ProjectReference Include="..\UVtools.Core\UVtools.Core.csproj"/>
17-
</ItemGroup>
10+
<ItemGroup>
11+
<PackageReference Include="System.CommandLine" Version="2.0.14" />
12+
</ItemGroup>
1813

14+
<ItemGroup>
15+
<ProjectReference Include="..\UVtools.Core\UVtools.Core.csproj" />
16+
</ItemGroup>
1917
</Project>

‎UVtools.Core/IO/ReverseLineReader.cs‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ public sealed class ReverseLineReader : IEnumerable<string>
1919
/// </summary>
2020
private const int DefaultBufferSize = 4096;
2121

22+
/// <summary>
23+
/// Maximum bytes at the start of a read that belong to a character started before it: the 3 continuation bytes of a
24+
/// 4 bytes UTF-8 character, or the 2 bytes of the low surrogate of an UTF-16 pair.
25+
/// </summary>
26+
private const int MaxLeftOverBytes = 3;
27+
2228
/// <summary>
2329
/// Means of creating a Stream to read from.
2430
/// </summary>
@@ -42,6 +48,11 @@ public sealed class ReverseLineReader : IEnumerable<string>
4248
/// </summary>
4349
private readonly Func<long, byte, bool> _characterStartDetector;
4450

51+
/// <summary>
52+
/// For UTF-16, gets if the code units are big endian, otherwise little endian. <see langword="null"/> for others.
53+
/// </summary>
54+
private readonly bool? _utf16BigEndian;
55+
4556
/// <summary>
4657
/// Creates a LineReader from a stream source. The delegate is only
4758
/// called when the enumerator is fetched. UTF-8 is used to decode
@@ -98,10 +109,10 @@ internal ReverseLineReader(Func<Stream> streamSource, Encoding encoding, int buf
98109
}
99110
else if (encoding is UnicodeEncoding)
100111
{
101-
// For UTF-16, even-numbered positions are the start of a character.
102-
// TODO: This assumes no surrogate pairs. More work required
103-
// to handle that.
112+
// For UTF-16, even-numbered positions are the start of a code unit. A low surrogate is skipped later, as it
113+
// continues the pair started by the high surrogate before it.
104114
_characterStartDetector = (pos, data) => (pos & 1) == 0;
115+
_utf16BigEndian = encoding.CodePage == Encoding.BigEndianUnicode.CodePage;
105116
}
106117
else if (encoding is UTF8Encoding)
107118
{
@@ -146,9 +157,9 @@ private IEnumerator<string> GetEnumeratorImpl(Stream stream)
146157
throw new InvalidDataException("UTF-16 encoding provided, but stream has odd length.");
147158
}
148159

149-
// Allow up to two bytes for data from the start of the previous
160+
// Allow room for data from the start of the previous
150161
// read which didn't quite make it as full characters
151-
byte[] buffer = new byte[_bufferSize + 2];
162+
byte[] buffer = new byte[_bufferSize + MaxLeftOverBytes];
152163
char[] charBuffer = new char[_encoding.GetMaxCharCount(buffer.Length)];
153164
int leftOverData = 0;
154165
string? previousEnd = null;
@@ -186,14 +197,24 @@ private IEnumerator<string> GetEnumeratorImpl(Stream stream)
186197
{
187198
firstCharPosition++;
188199
// Bad UTF-8 sequences could trigger this. For UTF-8 we should always
189-
// see a valid character start in every 3 bytes, and if this is the start of the file
200+
// see a valid character start in every 4 bytes, and if this is the start of the file
190201
// so we've done a short read, we should have the character start
191202
// somewhere in the usable buffer.
192-
if (firstCharPosition == 3 || firstCharPosition == bytesToRead)
203+
if (firstCharPosition > MaxLeftOverBytes || firstCharPosition == bytesToRead)
193204
{
194-
throw new InvalidDataException("Invalid UTF-8 data");
205+
throw new InvalidDataException($"Invalid {_encoding.WebName} data");
195206
}
196207
}
208+
209+
if (_utf16BigEndian is { } bigEndian && firstCharPosition + 1 < bytesToRead)
210+
{
211+
var codeUnit = bigEndian
212+
? (char)(buffer[firstCharPosition] << 8 | buffer[firstCharPosition + 1])
213+
: (char)(buffer[firstCharPosition + 1] << 8 | buffer[firstCharPosition]);
214+
// Decoded alone it would turn into U+FFFD, decode it with its high surrogate on the next read
215+
if (char.IsLowSurrogate(codeUnit)) firstCharPosition += 2;
216+
}
217+
197218
leftOverData = firstCharPosition;
198219

199220
int charsRead = _encoding.GetChars(buffer, firstCharPosition, bytesToRead - firstCharPosition, charBuffer, 0);
@@ -244,7 +265,13 @@ private IEnumerator<string> GetEnumeratorImpl(Stream stream)
244265
if (leftOverData != 0)
245266
{
246267
// At the start of the final buffer, we had the end of another character.
247-
throw new InvalidDataException("Invalid UTF-8 data at start of stream");
268+
throw new InvalidDataException($"Invalid {_encoding.WebName} data at start of stream");
269+
}
270+
271+
// The first line of the stream is the last returned, without the byte order mark like a StreamReader
272+
if (previousEnd is { Length: > 0 } && previousEnd[0] == '\uFEFF')
273+
{
274+
previousEnd = previousEnd[1..];
248275
}
249276
if (firstYield && string.IsNullOrEmpty(previousEnd))
250277
{

‎UVtools.Core/Layers/Issue.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public override bool Equals(object? obj)
8686

8787
public override int GetHashCode()
8888
{
89-
return HashCode.Combine(Layer, BoundingRectangle, PixelsCount, Area);
89+
// Matches Equals, the layer hash would hash its image
90+
return HashCode.Combine(LayerIndex, BoundingRectangle, PixelsCount, Area);
9091
}
9192
}

‎UVtools.Core/Layers/IssueOfContours.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,6 @@ public override bool Equals(object? obj)
5252

5353
public override int GetHashCode()
5454
{
55-
return HashCode.Combine(Contours);
55+
return base.GetHashCode();
5656
}
5757
}

‎UVtools.Core/Layers/IssueOfPoints.cs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public IssueOfPoints(Layer layer, IEnumerable<Point> points, Rectangle boundingR
3838

3939
private bool Equals(IssueOfPoints other)
4040
{
41-
return Points.AsValueEnumerable().SequenceEqual(other.Points);
41+
// Same footprint on another layer is another issue
42+
return base.Equals(other) && Points.AsValueEnumerable().SequenceEqual(other.Points);
4243
}
4344

4445
public override bool Equals(object? obj)
@@ -48,6 +49,6 @@ public override bool Equals(object? obj)
4849

4950
public override int GetHashCode()
5051
{
51-
return HashCode.Combine(Points);
52+
return base.GetHashCode();
5253
}
5354
}

‎UVtools.Core/Layers/Layer.cs‎

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ public double NonZeroPixelPercentage
225225
public bool IsEmpty => _nonZeroPixelCount == 0;
226226

227227
/// <summary>
228-
/// Gets if this layer is a dummy layer to bypass a firmware constrain, that is contained at most one pixel and exposure time no more than 0.01s
228+
/// Gets if this layer is a dummy layer, like the ones to bypass a firmware constrain, that is: contains at most one
229+
/// pixel or is exposed no more than 0.01s
229230
/// </summary>
230231
public bool IsDummy => _nonZeroPixelCount <= 1 || _exposureTime <= 0.01;
231232

@@ -272,8 +273,9 @@ public RectangleF BoundingRectangleMillimeters
272273
/// <summary>
273274
/// Gets the last pixel index on the <see cref="BoundingRectangle"/>
274275
/// </summary>
275-
public uint BoundingRectangleLastPixelIndex =>
276-
(uint)(BoundingRectangle.Bottom * ResolutionX + BoundingRectangle.Right);
276+
public uint BoundingRectangleLastPixelIndex => BoundingRectangle.IsEmpty
277+
? 0
278+
: (uint)((BoundingRectangle.Bottom - 1) * ResolutionX + BoundingRectangle.Right - 1); // Right/Bottom are exclusive
277279

278280
/// <summary>
279281
/// Gets the first pixel <see cref="Point"/> on the <see cref="BoundingRectangle"/>
@@ -283,7 +285,9 @@ public RectangleF BoundingRectangleMillimeters
283285
/// <summary>
284286
/// Gets the last pixel <see cref="Point"/> on the <see cref="BoundingRectangle"/>
285287
/// </summary>
286-
public Point BoundingRectangleLastPixelPosition => new(BoundingRectangle.Right, BoundingRectangle.Bottom);
288+
public Point BoundingRectangleLastPixelPosition => BoundingRectangle.IsEmpty
289+
? BoundingRectangle.Location
290+
: new(BoundingRectangle.Right - 1, BoundingRectangle.Bottom - 1);
287291

288292
/// <summary>
289293
/// Gets the first pixel index on this layer
@@ -863,12 +867,10 @@ public float LayerHeight
863867
get
864868
{
865869
if (IsFirstLayer) return _positionZ;
866-
var previousLayer = this;
867-
868-
while
869-
((previousLayer =
870-
previousLayer.PreviousLayer) is not
871-
null) // This cycle returns the correct layer height if two or more layers have the same position z
870+
// This cycle returns the correct layer height if two or more layers have the same position z
871+
for (var previousLayer = PreviousLayer;
872+
previousLayer is not null;
873+
previousLayer = previousLayer.PreviousLayer)
872874
{
873875
var layerHeight = RoundHeight(_positionZ - previousLayer.PositionZ);
874876
//Debug.WriteLine($"Layer {_index}-{previousLayer.Index}: {_positionZ} - {previousLayer.PositionZ}: {layerHeight}");
@@ -1057,12 +1059,14 @@ public Mat LayerMat
10571059
get => SlicerFile.DecodeType != FileFormat.FileDecodeType.Full ? null! : _compressedMat.Mat;
10581060
set
10591061
{
1060-
GetBoundingRectangle(value, true);
10611062
if (value is null)
10621063
{
1064+
// GetBoundingRectangle would measure the image being replaced
1065+
BoundingRectangle = Rectangle.Empty;
1066+
NonZeroPixelCount = 0;
10631067
_compressedMat.SetEmptyCompressedBytes();
10641068
}
1065-
else if (_nonZeroPixelCount == 0)
1069+
else if (GetBoundingRectangle(value, true).IsEmpty)
10661070
{
10671071
_compressedMat.SetEmptyCompressedBytes(value, true);
10681072
}
@@ -1393,14 +1397,14 @@ public Layer(Mat layerMat, FileFormat slicerFile, LayerCompressionCodec? compres
13931397

13941398
#region Equatables
13951399

1396-
public static bool operator ==(Layer obj1, Layer obj2)
1400+
public static bool operator ==(Layer? obj1, Layer? obj2)
13971401
{
1398-
return obj1.Equals(obj2);
1402+
return obj1 is null ? obj2 is null : obj1.Equals(obj2);
13991403
}
14001404

1401-
public static bool operator !=(Layer obj1, Layer obj2)
1405+
public static bool operator !=(Layer? obj1, Layer? obj2)
14021406
{
1403-
return !obj1.Equals(obj2);
1407+
return !(obj1 == obj2);
14041408
}
14051409

14061410
public static bool operator >(Layer obj1, Layer obj2)
@@ -1966,19 +1970,22 @@ public void CopyLiftTo(Layer layer)
19661970
public void CopyImageTo(Layer layer)
19671971
{
19681972
_compressedMat.CopyTo(layer._compressedMat);
1973+
// Disposes the contours of the replaced image, then takes the ones of the copied image
1974+
layer.InvalidateImage();
19691975
layer._contours = _contours?.Clone();
19701976
layer.BoundingRectangle = BoundingRectangle;
19711977
layer.NonZeroPixelCount = _nonZeroPixelCount;
19721978
layer.FirstPixelIndex = FirstPixelIndex;
19731979
layer.FirstPixelPosition = FirstPixelPosition;
19741980
layer.LastPixelIndex = LastPixelIndex;
19751981
layer.LastPixelPosition = LastPixelPosition;
1976-
layer.InvalidateImage();
19771982
}
19781983

19791984
public Layer Clone()
19801985
{
19811986
var layer = (Layer)MemberwiseClone();
1987+
// A shared lock would let threads locking layers in order (repair islands) deadlock when a file holds clones
1988+
layer.Mutex = new Lock();
19821989
layer._compressedMat = _compressedMat.Clone();
19831990
layer._contours = _contours?.Clone();
19841991
//Debug.WriteLine(ReferenceEquals(_compressedBytes, layer.CompressedBytes));

‎UVtools.Core/Layers/MainIssue.cs‎

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ public MainIssue(IssueType type, Issue issue) : this(type, issue.BoundingRectang
133133

134134
public MainIssue(IssueType type, IEnumerable<Issue> issues) : this(type)
135135
{
136-
foreach (var issue in issues)
136+
Childs = issues.AsValueEnumerable().OrderBy(issue => issue.LayerIndex).ToArray();
137+
foreach (var issue in Childs)
137138
{
138139
issue.Parent = this;
139140
var layerHeightInPixels = issue.Layer.SlicerFile.MillimetersToPixelsF(issue.Layer.LayerHeight, 20);
@@ -149,25 +150,12 @@ public MainIssue(IssueType type, IEnumerable<Issue> issues) : this(type)
149150
BoundingRectangle = Rectangle.Union(BoundingRectangle, issue.BoundingRectangle);
150151
}
151152

152-
if (Childs.Length == 1)
153-
{
154-
Area = Childs[0].Area;
155-
Childs = issues.AsValueEnumerable().ToArray();
156-
}
157-
else
158-
{
159-
Childs = issues.AsValueEnumerable().OrderBy(issue => issue.LayerIndex).ToArray();
160-
}
153+
// A single layer has an area, not a volume
154+
if (Childs.Length == 1) Area = Childs[0].Area;
161155

162156
Area = Math.Round(Area, 3);
163157
}
164158

165-
private void Sort()
166-
{
167-
if (Childs.Length < 1) return;
168-
Array.Sort(Childs, (issue, issue1) => issue.LayerIndex.CompareTo(issue1.LayerIndex));
169-
}
170-
171159
public bool IsIssueInBetween(int layerIndex) => layerIndex >= StartLayerIndex && layerIndex <= EndLayerIndex;
172160
public bool IsIssueInBetween(uint layerIndex) => layerIndex >= StartLayerIndex && layerIndex <= EndLayerIndex;
173161
public bool IsIssueInBetween(Layer layer) => IsIssueInBetween(layer.Index);
@@ -202,6 +190,7 @@ public override bool Equals(object? obj)
202190

203191
public override int GetHashCode()
204192
{
205-
return HashCode.Combine((int)Type, BoundingRectangle, PixelCount, Area, Childs);
193+
// The children are compared by content in Equals, hashing the array reference would break the contract
194+
return HashCode.Combine((int)Type, BoundingRectangle, PixelCount, Area);
206195
}
207196
}

0 commit comments

Comments
 (0)