using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
using Xunit;
namespace OpenNest.Tests.IO
{
public class SvgImportTests : IDisposable
{
private const double Precision = 0.002;
private const string SquarePath = "";
private readonly string folder = Directory.CreateTempSubdirectory("svg-tests").FullName;
public void Dispose() => Directory.Delete(folder, true);
private string Write(string content)
{
var path = Path.Combine(folder, Guid.NewGuid() + ".svg");
File.WriteAllText(path, content);
return path;
}
private static string Svg(string body, string attributes = "width='10in' height='10in' viewBox='0 0 10 10'") =>
$"";
private CadImportResult Import(string content, CadImportOptions options = null) =>
CadImporter.Import(Write(content), options ?? new CadImportOptions());
private static (double Length, double Width) Size(CadImportResult result) =>
(result.Bounds.Length, result.Bounds.Width);
[Theory]
[InlineData("width='10in' height='10in' viewBox='0 0 100 100'", 1.0)]
[InlineData("width='254mm' height='254mm' viewBox='0 0 10 10'", 10.0)]
[InlineData("width='25.4cm' height='25.4cm' viewBox='0 0 10 10'", 10.0)]
[InlineData("width='720pt' height='720pt' viewBox='0 0 10 10'", 10.0)]
[InlineData("width='60pc' height='60pc' viewBox='0 0 10 10'", 10.0)]
// Without a view box user units are CSS pixels, whatever unit the page is in.
[InlineData("width='1in' height='1in'", 10.0 / 96)]
public void DeclaredSizeGivesRealWorldScale(string attributes, double expected)
{
var (length, width) = Size(Import(Svg(SquarePath, attributes)));
Assert.Equal(expected, length, 6);
Assert.Equal(expected, width, 6);
}
[Fact]
public void ConvertsToMillimeterDrawings()
{
var result = Import(Svg(SquarePath), new CadImportOptions { DrawingUnits = Units.Millimeters });
Assert.Equal(254, result.Bounds.Length, 6);
}
[Theory]
[InlineData("viewBox='0 0 10 10'")]
[InlineData("width='100%' height='100%' viewBox='0 0 10 10'")]
[InlineData("width='100px' height='100px' viewBox='0 0 10 10'")]
[InlineData("width='100' height='100' viewBox='0 0 10 10'")]
[InlineData("")]
public void UndeclaredSizeRefusesToGuess(string attributes)
{
var path = Write(Svg(SquarePath, attributes));
var error = Assert.Throws(() => CadImporter.Import(path));
Assert.Contains("real-world size", error.Message);
Assert.Equal((10, 10), OpenNest.IO.Svg.UndeclaredSize(path));
}
[Theory]
[InlineData(96, 10.0 / 96)]
[InlineData(72, 10.0 / 72)]
[InlineData(1, 10.0)]
public void ExplicitScaleAppliesToUndeclaredFiles(double unitsPerInch, double expected)
{
var result = Import(
Svg(SquarePath, "viewBox='0 0 10 10'"),
new CadImportOptions { SvgUserUnitsPerInch = unitsPerInch }
);
Assert.Equal(expected, result.Bounds.Length, 6);
}
[Fact]
public void DeclaredSizeBeatsExplicitScale()
{
var result = Import(Svg(SquarePath), new CadImportOptions { SvgUserUnitsPerInch = 96 });
Assert.Equal(10, result.Bounds.Length, 6);
Assert.Null(OpenNest.IO.Svg.UndeclaredSize(Write(Svg(SquarePath))));
}
[Fact]
public void NonUniformPageFitsViewBoxUniformly()
{
var result = Import(Svg(SquarePath, "width='20in' height='10in' viewBox='0 0 10 10'"));
Assert.Equal((10, 10), Size(result));
}
[Fact]
public void PreserveAspectRatioNoneStretches()
{
var result = Import(Svg(
SquarePath,
"width='20in' height='10in' viewBox='0 0 10 10' preserveAspectRatio='none'"
));
Assert.Equal((20, 10), Size(result));
}
[Fact]
public void FlipsYSoTheDrawingIsNotMirrored()
{
var result = Import(Svg(""));
// The wide edge is at the top of the page, so it is the high Y edge in OpenNest.
var top = result.Entities.OfType().Single(l =>
System.Math.Abs(l.StartPoint.Y - l.EndPoint.Y) < 1e-9
);
Assert.Equal(result.Bounds.Top, top.StartPoint.Y, 6);
}
[Fact]
public void BasicShapesImport()
{
var result = Import(Svg(
""
+ ""
+ ""
+ ""
+ ""
));
Assert.Single(result.Entities.OfType());
Assert.Equal(1, result.Entities.OfType().Single().Radius, 9);
Assert.Equal(10, result.Entities.OfType().Count());
}
[Fact]
public void RoundedRectKeepsItsCorners()
{
var result = Import(Svg(""));
Assert.Equal(4, result.Entities.OfType().Count());
var corners = result.Entities.OfType().ToList();
Assert.Equal(4, corners.Count);
Assert.All(corners, a => Assert.Equal(1, a.Radius, 6));
Assert.Equal((10, 6), Size(result));
AssertClosed(result.Entities);
}
[Fact]
public void AttributeUnitsResolveAgainstCssPixels()
{
// Inside a 96-per-inch page, 1in is 96 user units: one real inch.
var result = Import(Svg(
"",
"width='100in' height='100in' viewBox='0 0 9600 9600'"
));
Assert.Equal(1, result.Entities.OfType().Single().Radius, 6);
}
[Theory]
[InlineData("M0 0 10 0 10 10z", 3)]
[InlineData("m0 0 10 0 0 10z", 3)]
[InlineData("M0,0L10,0L10,10Z", 3)]
[InlineData("M0 0h10v10h-10", 3)]
[InlineData("M0-1.5.5.5", 1)]
public void PathGrammar(string data, int lines)
{
Assert.Equal(lines, Import(Svg($"")).Entities.OfType().Count());
}
[Fact]
public void ImplicitLineAfterRelativeMoveIsRelative()
{
var result = Import(Svg(""));
Assert.Equal((2, 2), Size(result));
}
[Fact]
public void CompactArcFlagsParse()
{
var result = Import(Svg(""));
Assert.Equal(1, result.Entities.OfType().Single().Radius, 6);
}
[Theory]
[InlineData("L1 1")]
[InlineData("M0 0L1")]
[InlineData("M0 0Q1 1")]
[InlineData("M0 0A1 1 0 2 0 1 1")]
[InlineData("M0 0Y1 1")]
public void MalformedPathIsRejectedRatherThanPartlyImported(string data)
{
var path = Write(Svg($""));
Assert.Throws(() => CadImporter.Import(path));
}
[Fact]
public void CircularArcKeepsItsSweepDirection()
{
// Sweep 1 from (0,0) to (2,0) passes through y=-1 on the page, so y=+1 in OpenNest.
var result = Import(Svg(""));
var arc = result.Entities.OfType().Single();
Assert.Equal(1, arc.Radius, 6);
Assert.Equal(1, result.Bounds.Top, 6);
Assert.Equal(0, result.Bounds.Bottom, 6);
}
[Fact]
public void LargeArcFlagTakesTheLongWay()
{
var result = Import(Svg(""));
Assert.True(result.Entities.OfType().Single().SweepAngle() > System.Math.PI);
}
[Fact]
public void TooSmallArcRadiusScalesUp()
{
var result = Import(Svg(""));
Assert.Equal(5, result.Entities.OfType().Single().Radius, 6);
}
[Fact]
public void TwoHalfArcsMakeAWholeHole()
{
var result = Import(Svg(""));
Assert.Equal((10, 10), Size(result));
AssertArea(result, System.Math.PI * 25);
}
[Fact]
public void IllustratorStyleBezierCircleFitsTheCircle()
{
const double k = 0.5522847498 * 5;
var data = FormattableString.Invariant(
$"M5 0C{5 + k} 0 10 {5 - k} 10 5S{5 + k} 10 5 10S0 {5 + k} 0 5S{5 - k} 0 5 0Z"
);
var result = Import(Svg($""));
Assert.Equal(10, result.Bounds.Length, 2);
AssertClosed(result.Entities);
AssertArea(result, System.Math.PI * 25);
}
[Fact]
public void QuadraticAndSmoothQuadraticStayOnTheCurve()
{
var result = Import(Svg(""));
Assert.Equal(20, result.Bounds.Length, 3);
Assert.Equal(10, result.Bounds.Width, 2);
}
[Fact]
public void TransformsNestInOrder()
{
var result = Import(Svg(
""
+ ""
));
Assert.Equal(102, result.Bounds.Left, 6);
Assert.Equal(2, result.Bounds.Length, 6);
}
[Fact]
public void RotateAboutPointKeepsThePoint()
{
var result = Import(Svg(""));
Assert.Equal(0, result.Bounds.Left, 6);
Assert.Equal(2, result.Bounds.Right, 6);
}
[Fact]
public void UniformScaleKeepsACircle()
{
var result = Import(Svg(""));
Assert.Equal(6, result.Entities.OfType().Single().Radius, 6);
}
[Fact]
public void NonUniformScaleMakesACircleAnEllipse()
{
var result = Import(Svg(""));
Assert.Empty(result.Entities.OfType());
Assert.Equal(20, result.Bounds.Length, 2);
Assert.Equal(10, result.Bounds.Width, 2);
AssertArea(result, System.Math.PI * 50);
}
[Fact]
public void RotatedEllipseKeepsItsAxes()
{
var result = Import(Svg(""));
Assert.Equal(6, result.Bounds.Length, 2);
Assert.Equal(12, result.Bounds.Width, 2);
}
[Fact]
public void EllipticalArcEndsWhereItsNeighborsStart()
{
var result = Import(Svg(""));
AssertClosed(result.Entities);
}
[Fact]
public void HiddenAndDefinitionContentIsIgnored()
{
var result = Import(Svg(
""
+ ""
+ ""
+ ""
+ ""
+ SquarePath
));
Assert.Equal((10, 10), Size(result));
Assert.Empty(result.Warnings);
}
[Fact]
public void UseCopiesTheTargetAtItsOffset()
{
var result = Import(Svg(
""
+ ""
));
Assert.Equal(5, result.Bounds.Left, 6);
Assert.Equal(12, result.Bounds.Right, 6);
}
[Fact]
public void SelfReferencingUseDoesNotHang()
{
var result = Import(Svg("" + SquarePath));
Assert.Equal((10, 10), Size(result));
Assert.Contains(result.Warnings, w => w.Contains("