Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- 'ACadSharp.Image.Tests/**'
- 'Directory.Packages.props'
- 'Directory.Build.props'
- 'sixlabors.lic'
- '*.sln'
- '.github/workflows/ci.yml'
push:
Expand All @@ -19,6 +20,7 @@ on:
- 'ACadSharp.Image.Tests/**'
- 'Directory.Packages.props'
- 'Directory.Build.props'
- 'sixlabors.lic'
- '*.sln'
- '.github/workflows/ci.yml'

Expand Down
Binary file modified ACadSharp.Image.Tests/Baselines/6-57-1119.model.01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ACadSharp.Image.Tests/Baselines/HSK80AHCP16190M_BMG.model.01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ACadSharp.Image.Tests/Baselines/entities.model.01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ACadSharp.Image.Tests/Baselines/features.model.01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions ACadSharp.Image.Tests/Baselines/features.model.01.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ACadSharp.Image.Tests/Baselines/fidelity.model.01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ACadSharp.Image.Tests/Baselines/viewport-sheet.paper.01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions ACadSharp.Image.Tests/ColorResolutionTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ACadSharp.Image.Extensions;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace ACadSharp.Image.Tests;

Expand All @@ -11,7 +12,7 @@ public sealed class ColorResolutionTests
public void Index7IsBlackOnLightAndWhiteOnDarkBackgrounds()
{
ImageConfiguration light = new();
ImageConfiguration dark = new() { BackgroundColor = Color.FromRgb(20, 20, 40) };
ImageConfiguration dark = new() { BackgroundColor = Color.FromPixel(new Rgba32(20, 20, 40)) };
ImageConfiguration transparent = new() { BackgroundColor = Color.Transparent };

Assert.Equal(Color.Black, light.ResolveForegroundColor());
Expand All @@ -26,6 +27,6 @@ public void ExplicitForegroundWins()

Assert.Equal(Color.Yellow, configuration.ResolveForegroundColor());
Assert.Equal(Color.Yellow, new ACadSharp.Color(7).ToImageColor(configuration.ResolveForegroundColor()));
Assert.Equal(Color.FromRgb(255, 0, 0), new ACadSharp.Color(1).ToImageColor(configuration.ResolveForegroundColor()));
Assert.Equal(Color.FromPixel(new Rgba32(255, 0, 0)), new ACadSharp.Color(1).ToImageColor(configuration.ResolveForegroundColor()));
}
}
82 changes: 44 additions & 38 deletions ACadSharp.Image.Tests/EntityRenderDispatcherTests.cs

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion ACadSharp.Image.Tests/FeatureGoldenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public void FeatureSvgMatchesGoldenAndContainsEveryPrimitive()
exporter.Configuration.OnNotification += (_, e) => notifications.Add(e);

RenderedSvgPage page = Assert.IsType<RenderedSvgPage>(Assert.Single(exporter.Render(ImageExportFormat.Svg)));
GoldenAssert.Svg("features.model.01", page.Content);
// ACadSharp's pattern-hatch intersections can land on either side of a two-decimal half-way value depending on
// platform floating point, so allow one printed unit of drift for this golden.
GoldenAssert.Svg("features.model.01", page.Content, 0.011);

Assert.DoesNotContain(notifications, n => n.NotificationType is NotificationType.Warning or NotificationType.NotImplemented);
XDocument document = XDocument.Parse(page.Content);
Expand Down
66 changes: 64 additions & 2 deletions ACadSharp.Image.Tests/GoldenAssert.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Globalization;
using System.Text.RegularExpressions;
using ACadSharp.Image.Rendering;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
Expand All @@ -13,6 +15,8 @@ internal static class GoldenAssert
{
public static bool Updating => Environment.GetEnvironmentVariable("ACADSHARP_IMAGE_UPDATE_BASELINES") == "1";

private static readonly Regex SvgNumber = new(@"[-+]?(?:\d+\.\d+|\d+)", RegexOptions.CultureInvariant);

private static string BaselineDirectory
{
get
Expand All @@ -37,7 +41,13 @@ public static void Png(string baseName, Image<Rgba32> actual)
SampleParityTests.AssertPixelsEqual(expected, actual, path);
}

public static void Svg(string baseName, string actual)
/// <param name="baseName">Baseline file name without the <c>.svg</c> extension.</param>
/// <param name="actual">The rendered SVG.</param>
/// <param name="numberTolerance">
/// Absolute tolerance for numeric tokens. Zero keeps the comparison exact; a small value absorbs platform floating-point
/// drift in values that are otherwise printed at the same precision.
/// </param>
public static void Svg(string baseName, string actual, double numberTolerance = 0d)
{
string path = Path.Combine(BaselineDirectory, baseName + ".svg");
string normalized = actual.Replace("\r\n", "\n");
Expand All @@ -50,7 +60,59 @@ public static void Svg(string baseName, string actual)
}

Assert.True(File.Exists(path), $"Missing golden {path}. Run with ACADSHARP_IMAGE_UPDATE_BASELINES=1 to create it.");
Assert.Equal(File.ReadAllText(path).Replace("\r\n", "\n"), normalized);
string expected = File.ReadAllText(path).Replace("\r\n", "\n");
if (numberTolerance <= 0d)
{
Assert.Equal(expected, normalized);
return;
}

AssertSvgNumbersEqual(expected, normalized, path, numberTolerance);
}

private static void AssertSvgNumbersEqual(string expected, string actual, string path, double tolerance)
{
List<(bool IsNumber, string Text)> expectedTokens = TokenizeSvg(expected);
List<(bool IsNumber, string Text)> actualTokens = TokenizeSvg(actual);
Assert.Equal(expectedTokens.Count, actualTokens.Count);
for (int i = 0; i < expectedTokens.Count; i++)
{
Assert.Equal(expectedTokens[i].IsNumber, actualTokens[i].IsNumber);
if (!expectedTokens[i].IsNumber)
{
Assert.Equal(expectedTokens[i].Text, actualTokens[i].Text);
continue;
}

double expectedNumber = double.Parse(expectedTokens[i].Text, CultureInfo.InvariantCulture);
double actualNumber = double.Parse(actualTokens[i].Text, CultureInfo.InvariantCulture);
Assert.True(
Math.Abs(expectedNumber - actualNumber) <= tolerance,
$"SVG number differs in {path} at token {i}: expected {expectedNumber.ToString(CultureInfo.InvariantCulture)}, actual {actualNumber.ToString(CultureInfo.InvariantCulture)}.");
}
}

private static List<(bool IsNumber, string Text)> TokenizeSvg(string svg)
{
List<(bool IsNumber, string Text)> tokens = [];
int position = 0;
foreach (Match match in SvgNumber.Matches(svg))
{
if (match.Index > position)
{
tokens.Add((false, svg[position..match.Index]));
}

tokens.Add((true, match.Value));
position = match.Index + match.Length;
}

if (position < svg.Length)
{
tokens.Add((false, svg[position..]));
}

return tokens;
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions ACadSharp.Image.Tests/ImageExporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public void RenderSplineBlockDoesNotReportNotImplemented()
Assert.NotNull(page.Canvas);
Assert.DoesNotContain(notifications, n => n.NotificationType == NotificationType.NotImplemented && n.Message.Contains("Spline", StringComparison.OrdinalIgnoreCase));

// Silence is not enough: the spline must actually reach the surface. ACadSharp 3.7.1's UpdateFromFitPoints
// Silence is not enough: the spline must actually reach the surface. ACadSharp 3.8.0's UpdateFromFitPoints
// produces no control points, so this one is drawn through its fit points, with a warning saying so.
RecordingDrawingSurface surface = new();
new ImagePageRenderer(exporter.Configuration).RenderTo(surface, exporter.Pages[0]);
Expand All @@ -129,7 +129,7 @@ public void RenderSplineBlockDoesNotReportNotImplemented()
[Fact]
public void RenderClosedPeriodicSplineDoesNotDrawSpokeToOrigin()
{
using Image<Rgba32> canvas = new(100, 100, SixLabors.ImageSharp.Color.White);
using Image<Rgba32> canvas = new(100, 100, SixLabors.ImageSharp.Color.White.ToPixel<Rgba32>());
ImageConfiguration configuration = new()
{
Width = 100,
Expand Down
13 changes: 6 additions & 7 deletions ACadSharp.Image.Tests/ImagePageRendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ public void ModelSpaceWipeoutMappedRegionDecidesViewportInclusionNotTheRawVertex
VVector = new XYZ(-5, 0, 0),
Size = new XY(1, 1),
ClippingState = true,
ClipType = ClipType.Rectangular,
};
wipeout.ClipBoundaryVertices.AddRange([new XY(-0.5, -0.5), new XY(0.5, 0.5)]);
document.Entities.Add(wipeout);
Expand Down Expand Up @@ -309,7 +308,6 @@ public void InvertedClipModelSpaceWipeoutInAViewportMasksTheSameAsThePageLevelDr
VVector = new XYZ(0, 5, 0),
Size = new XY(1, 1),
ClippingState = true,
ClipType = ClipType.Rectangular,
ClipMode = ClipMode.Inside,
};
// Strictly inside the frame, not equal to it (this wipeout's own default frame is (-0.5,-0.5)..(0.5,0.5)),
Expand Down Expand Up @@ -461,6 +459,7 @@ public void AddingAnMLineToAPreviouslyCleanBlockIsPickedUpByTheNextRenderOnTheSa
renderer.RenderTo(surface, page);

Assert.Equal(2, mline.Vertices.Count);
Assert.All(mline.Vertices, vertex => Assert.Equal(2, vertex.Segments.Count));
Assert.Equal(2, surface.Polylines.Count);
}

Expand All @@ -470,11 +469,11 @@ public void AddingALeaderToAPreviouslyCleanBlockIsPickedUpByTheNextRenderOnTheSa
// Same staleness hazard as the MLINE case above, for a LEADER: Insert.Clone() shares a LEADER's vertex list
// with its source too (see EntityRenderDispatcher remarks on DrawBlockContents), so it must be found by the
// same cached subtree scan and must not be missed once it is added after the block was first seen clean.
// The insert carries a non-zero placement deliberately: a LEADER's shared list is not emptied by Clone() the
// way an MLINE's is, it is overwritten in place by Explode()'s ApplyTransform, so under an identity
// placement that in-place write puts back the very same coordinates and the assertions below would hold
// whether or not the stale-cache bug is present. A real translation makes the un-healed path write world
// coordinates into the shared list instead, so both assertions genuinely discriminate.
// The insert carries a non-zero placement deliberately: a LEADER's shared vertex list is not emptied by
// Clone() the way an MLINE's segment lists are; it is overwritten in place by Explode()'s ApplyTransform, so
// under an identity placement that in-place write puts back the very same coordinates and the assertions
// below would hold whether or not the stale-cache bug is present. A real translation makes the un-healed path
// write world coordinates into the shared list instead, so both assertions genuinely discriminate.
BlockRecord wall = new("WALLL");
wall.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0)));
BlockRecord plan = new("PLANL");
Expand Down
2 changes: 0 additions & 2 deletions ACadSharp.Image.Tests/ImagePageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ public void FrameUsesTheMappedWipeoutRegionNotTheRawPixelVertices()
VVector = new XYZ(-5, 0, 0),
Size = new XY(1, 1),
ClippingState = true,
ClipType = ClipType.Rectangular,
};
wipeout.ClipBoundaryVertices.AddRange([new XY(-0.5, -0.5), new XY(0.5, 0.5)]);
ImagePage page = new();
Expand Down Expand Up @@ -165,7 +164,6 @@ public void TheFrameOfAnInvertedWipeoutIsItsWholeImageFootprint()
VVector = new XYZ(0, 10, 0),
Size = new XY(1, 1),
ClippingState = true,
ClipType = ClipType.Rectangular,
ClipMode = ClipMode.Inside,
};
wipeout.ClipBoundaryVertices.Add(new XY(-0.25, -0.25));
Expand Down
22 changes: 11 additions & 11 deletions ACadSharp.Image.Tests/RasterDrawingSurfaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public sealed class RasterDrawingSurfaceTests
[Fact]
public void DrawLinePaintsPixelsAlongTheLine()
{
using Image<Rgba32> canvas = new(20, 20, ImageColor.White);
using Image<Rgba32> canvas = new(20, 20, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);

surface.DrawLine(new ImageStyle(ImageColor.Black, 2f), new SurfacePoint(2, 10), new SurfacePoint(18, 10));
Expand All @@ -27,7 +27,7 @@ public void DrawLinePaintsPixelsAlongTheLine()
[Fact]
public void DrawPolylineClosedConnectsLastPointToFirst()
{
using Image<Rgba32> canvas = new(20, 20, ImageColor.White);
using Image<Rgba32> canvas = new(20, 20, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);
SurfacePoint[] points = [new(2, 2), new(18, 2), new(18, 18)];

Expand All @@ -40,7 +40,7 @@ public void DrawPolylineClosedConnectsLastPointToFirst()
[Fact]
public void FillPathUsesEvenOddRuleForHoles()
{
using Image<Rgba32> canvas = new(40, 40, ImageColor.White);
using Image<Rgba32> canvas = new(40, 40, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);
SurfacePoint[] outer = [new(2, 2), new(38, 2), new(38, 38), new(2, 38)];
SurfacePoint[] hole = [new(15, 15), new(25, 15), new(25, 25), new(15, 25)];
Expand All @@ -54,7 +54,7 @@ public void FillPathUsesEvenOddRuleForHoles()
[Fact]
public void OpacityBlendsWithBackground()
{
using Image<Rgba32> canvas = new(10, 10, ImageColor.White);
using Image<Rgba32> canvas = new(10, 10, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);

surface.FillPolygon(new ImageStyle(ImageColor.Black, 1f, null, 0.5f), [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]);
Expand All @@ -68,7 +68,7 @@ public void OpacityBlendsWithBackground()
[Fact]
public void DashPatternLeavesGaps()
{
using Image<Rgba32> canvas = new(60, 10, ImageColor.White);
using Image<Rgba32> canvas = new(60, 10, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);

// 10 px dash, 10 px gap.
Expand All @@ -82,7 +82,7 @@ public void DashPatternLeavesGaps()
[Fact]
public void ViewportDrawsIntoChildAndCompositesAtBounds()
{
using Image<Rgba32> canvas = new(40, 40, ImageColor.White);
using Image<Rgba32> canvas = new(40, 40, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);

ViewportSurface viewport = surface.BeginViewport(new SurfaceRect(20, 20, 10, 10));
Expand Down Expand Up @@ -141,7 +141,7 @@ public void PositiveBulgeBendsTowardPositiveYInSurfaceSpace()
[Fact]
public void ViewportFlipOriginIsTheExactHeightNotTheRoundedImageHeight()
{
using Image<Rgba32> canvas = new(20, 20, ImageColor.White);
using Image<Rgba32> canvas = new(20, 20, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);

// A viewport 9.3 px tall gets a 10-row image; its content must still be placed against 9.3, not 10.
Expand All @@ -160,7 +160,7 @@ public void ViewportFlipOriginIsTheExactHeightNotTheRoundedImageHeight()
[Fact]
public void ViewportFractionalPositionIsCarriedIntoTheChildOffsets()
{
using Image<Rgba32> canvas = new(20, 20, ImageColor.White);
using Image<Rgba32> canvas = new(20, 20, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);

// Viewport at (3.6, 2.4): the image is pasted at (3, 2) and the child draws 0.6 / 0.4 px further in.
Expand Down Expand Up @@ -240,7 +240,7 @@ public void RotatedTextAnchorsItsFirstLineAlongItsOwnUpAxis()
/// <summary>Draws one text run of height 10 at the canvas centre and returns the canvas.</summary>
private static Image<Rgba32> DrawnText(string value, SurfaceTextBaseline baseline, double lineSpacingFactor, double rotation, float dpi = 96f, double widthScale = 1d)
{
Image<Rgba32> canvas = new(200, 200, ImageColor.White);
Image<Rgba32> canvas = new(200, 200, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration { Dpi = dpi }, ownsCanvas: false);
surface.DrawText(
new ImageStyle(ImageColor.Black, 1f),
Expand Down Expand Up @@ -299,7 +299,7 @@ private static int[] InkColumnStarts(Image<Rgba32> canvas)
[Fact]
public void FillsDropNonFiniteGeometryInsteadOfThrowing()
{
using Image<Rgba32> canvas = new(20, 20, ImageColor.White);
using Image<Rgba32> canvas = new(20, 20, White);
using RasterDrawingSurface surface = new(canvas, new ImageConfiguration(), ownsCanvas: false);
ImageStyle style = new(ImageColor.Black, 1f);
SurfacePoint[] ring = [new(2, 2), new(18, 2), new(double.PositiveInfinity, 18)];
Expand Down Expand Up @@ -440,7 +440,7 @@ public void DrawTextRejectsANonFiniteOrNonPositiveWidthScale(double widthScale)
// Matches SvgDrawingSurface.DrawText's guard: Matrix3x2.CreateScale(0f, 1f, pivot) would otherwise collapse
// the run to nothing silently instead of being rejected the same way a direct caller cannot reach through
// TextRenderer (both Place overloads guard length >= 1e-12).
using Image<Rgba32> canvas = new(20, 20, ImageColor.White);
using Image<Rgba32> canvas = new(20, 20, White);
ImageConfiguration configuration = new();
List<NotificationEventArgs> notifications = new();
configuration.OnNotification += (_, e) => notifications.Add(e);
Expand Down
2 changes: 1 addition & 1 deletion ACadSharp.Image.Tests/RenderedPageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public sealed class RenderedPageTests
[InlineData(ImageExportFormat.Webp, new byte[] { 0x52, 0x49, 0x46, 0x46 }, "WEBP")]
public void RasterPagesEncodeInTheirFormat(ImageExportFormat format, byte[] signature, string? fourcc)
{
using Image<Rgba32> canvas = new(8, 8, SixLabors.ImageSharp.Color.White);
using Image<Rgba32> canvas = new(8, 8, SixLabors.ImageSharp.Color.White.ToPixel<Rgba32>());
using RenderedImagePage page = new("p", canvas, format, 80);
using MemoryStream stream = new();

Expand Down
2 changes: 1 addition & 1 deletion ACadSharp.Image.Tests/SplineRendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public void InconsistentKnotSplineFallsBackToAcadSharpTessellation()

dispatcher.Draw(Context(surface, configuration), spline);

// ACadSharp 3.7.1's TryPolygonalVertexes tessellates this malformed spline anyway (via its own
// ACadSharp 3.8.0's TryPolygonalVertexes tessellates this malformed spline anyway (via its own
// fallback), so the renderer's last-resort path succeeds and draws a polyline instead of warning.
Assert.Single(surface.Polylines);
Assert.DoesNotContain(notifications, n => n.NotificationType == NotificationType.Warning);
Expand Down
2 changes: 1 addition & 1 deletion ACadSharp.Image.Tests/StyleResolutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public sealed class StyleResolutionTests
[Fact]
public void OpacityMapping()
{
Assert.Equal(1f, ImageStyleResolver.ResolveOpacity(new Line(), 1f)); // ByLayer default -> opaque (Layer has no transparency in ACadSharp 3.7.1)
Assert.Equal(1f, ImageStyleResolver.ResolveOpacity(new Line(), 1f)); // ByLayer default -> opaque (Layer has no transparency in ACadSharp 3.8.0)
Assert.Equal(0.3f, ImageStyleResolver.ResolveOpacity(new Line { Transparency = new Transparency(70) }, 1f), 3);
Assert.Equal(0.5f, ImageStyleResolver.ResolveOpacity(new Line { Transparency = Transparency.ByBlock }, 0.5f));
Assert.Equal(1f, ImageStyleResolver.ResolveOpacity(new Line { Transparency = Transparency.ByBlock }, 1f));
Expand Down
Loading
Loading