From 4561a3c19e33785422c69545b98806ea3fed4d27 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 27 Jan 2024 21:18:13 +1000 Subject: [PATCH 01/33] Add hacked AGG stroker --- .editorconfig | 2 + .gitattributes | 3 + .../DrawShapesWithImageSharp.csproj | 4 +- shared-infrastructure | 2 +- .../ImageSharp.Drawing.csproj | 16 +- .../Processing/PathGradientBrush.cs | 42 +- src/ImageSharp.Drawing/Shapes/EndCapStyle.cs | 7 + src/ImageSharp.Drawing/Shapes/JointStyle.cs | 17 + .../Shapes/OutlinePathExtensions.cs | 20 +- .../Shapes/PolygonClipper/ArrayBuilder{T}.cs | 135 ++++ .../Shapes/PolygonClipper/PolygonStroker.cs | 743 ++++++++++++++++++ .../Shapes/PolygonClipper/VertexDistance.cs | 237 ++++++ .../Rasterization/ScanEdgeCollection.Build.cs | 3 + tests/Directory.Build.targets | 13 +- .../ImageSharp.Drawing.Benchmarks.csproj | 14 +- .../ImageSharp.Drawing.Tests.csproj | 4 +- 16 files changed, 1199 insertions(+), 63 deletions(-) create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs diff --git a/.editorconfig b/.editorconfig index 2e3045fb..c28089d7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -172,6 +172,8 @@ dotnet_diagnostic.IDE0063.severity = suggestion csharp_using_directive_placement = outside_namespace:warning # Modifier preferences csharp_prefer_static_local_function = true:warning +# Primary constructor preferences +csharp_style_prefer_primary_constructors = false:none ########################################## # Unnecessary Code Rules diff --git a/.gitattributes b/.gitattributes index 3647a706..b5f742ab 100644 --- a/.gitattributes +++ b/.gitattributes @@ -133,3 +133,6 @@ *.pnm filter=lfs diff=lfs merge=lfs -text *.wbmp filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text +*.ico filter=lfs diff=lfs merge=lfs -text +*.cur filter=lfs diff=lfs merge=lfs -text +*.ani filter=lfs diff=lfs merge=lfs -text diff --git a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj index c9b0a852..274215c8 100644 --- a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj +++ b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj @@ -8,12 +8,12 @@ - net7.0;net6.0 + net8.0 - net6.0 + net8.0 diff --git a/shared-infrastructure b/shared-infrastructure index 353b9afe..1dbfb576 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 353b9afe32a8000410312d17263407cd7bb82d19 +Subproject commit 1dbfb576c83507645265c79e03369b66cdc0379f diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index f98d9ad1..644a96f9 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -14,27 +14,27 @@ Debug;Release - - - 2.0 - - enable Nullable + + + + 3.0 + - net7.0;net6.0 + net8.0 true - net6.0 + net8.0 true @@ -46,7 +46,7 @@ - + \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index da640936..e697db3f 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -22,29 +22,11 @@ public sealed class PathGradientBrush : Brush /// Array of colors that correspond to each point in the polygon. public PathGradientBrush(PointF[] points, Color[] colors) { - if (points == null) - { - throw new ArgumentNullException(nameof(points)); - } - - if (points.Length < 3) - { - throw new ArgumentOutOfRangeException( - nameof(points), - "There must be at least 3 lines to construct a path gradient brush."); - } + Guard.NotNull(points, nameof(points)); + Guard.MustBeGreaterThanOrEqualTo(points.Length, 3, nameof(points)); - if (colors == null) - { - throw new ArgumentNullException(nameof(colors)); - } - - if (colors.Length == 0) - { - throw new ArgumentOutOfRangeException( - nameof(colors), - "One or more color is needed to construct a path gradient brush."); - } + Guard.NotNull(colors, nameof(colors)); + Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors)); int size = points.Length; @@ -105,21 +87,7 @@ public override BrushApplicator CreateApplicator( this.hasSpecialCenterColor); private static Color CalculateCenterColor(Color[] colors) - { - if (colors == null) - { - throw new ArgumentNullException(nameof(colors)); - } - - if (colors.Length == 0) - { - throw new ArgumentOutOfRangeException( - nameof(colors), - "One or more color is needed to construct a path gradient brush."); - } - - return new Color(colors.Select(c => (Vector4)c).Aggregate((p1, p2) => p1 + p2) / colors.Length); - } + => new(colors.Select(c => (Vector4)c).Aggregate((p1, p2) => p1 + p2) / colors.Length); private static float DistanceBetween(Vector2 p1, Vector2 p2) => (p2 - p1).Length(); diff --git a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs index bd670956..50607e20 100644 --- a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs +++ b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs @@ -33,3 +33,10 @@ public enum EndCapStyle /// Joined = 4 } + +internal enum LineCap +{ + Butt, + Square, + Round +} diff --git a/src/ImageSharp.Drawing/Shapes/JointStyle.cs b/src/ImageSharp.Drawing/Shapes/JointStyle.cs index cf683a45..ca8cb115 100644 --- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs +++ b/src/ImageSharp.Drawing/Shapes/JointStyle.cs @@ -23,3 +23,20 @@ public enum JointStyle /// Miter = 2 } + +internal enum LineJoin +{ + MiterJoin = 0, + MiterJoinRevert = 1, + RoundJoin = 2, + BevelJoin = 3, + MiterJoinRound = 4 +} + +internal enum InnerJoin +{ + InnerBevel, + InnerMiter, + InnerJag, + InnerRound +} diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index c76b7811..e8de488f 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -34,17 +34,31 @@ public static IPath GenerateOutline(this IPath path, float width) /// The style to apply to the end caps. /// A new representing the outline. /// Thrown when an offset cannot be calculated. +#pragma warning disable RCS1163 // Unused parameter +#pragma warning disable IDE0060 // Remove unused parameter public static IPath GenerateOutline(this IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) +#pragma warning restore IDE0060 // Remove unused parameter +#pragma warning restore RCS1163 // Unused parameter { if (width <= 0) { return Path.Empty; } - ClipperOffset offset = new(MiterOffsetDelta); - offset.AddPath(path, jointStyle, endCapStyle); + List polygons = []; + foreach (ISimplePath simplePath in path.Flatten()) + { + PolygonStroker stroker = new(); + Polygon polygon = stroker.ProcessPath(simplePath.Points.Span); + polygons.Add(polygon); + } - return offset.Execute(width); + return new ComplexPolygon(polygons); + + // ClipperOffset offset = new(MiterOffsetDelta); + // offset.AddPath(path, jointStyle, endCapStyle); + + // return offset.Execute(width); } /// diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs new file mode 100644 index 00000000..8513ac09 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; + +/// +/// A helper type for avoiding allocations while building arrays. +/// +/// The type of item contained in the array. +internal struct ArrayBuilder + where T : struct +{ + private const int DefaultCapacity = 4; + private const int MaxCoreClrArrayLength = 0x7FeFFFFF; + + // Starts out null, initialized on first Add. + private T[]? data; + private int size; + + /// + /// Initializes a new instance of the struct. + /// + /// The initial capacity of the array. + public ArrayBuilder(int capacity) + : this() + { + Guard.MustBeGreaterThanOrEqualTo(capacity, 0, nameof(capacity)); + + this.data = new T[capacity]; + this.size = capacity; + } + + /// + /// Gets or sets the number of items in the array. + /// + public int Length + { + readonly get => this.size; + + set + { + if (value != this.size) + { + if (value > 0) + { + this.EnsureCapacity(value); + this.size = value; + } + else + { + this.size = 0; + } + } + } + } + + /// + /// Returns a reference to specified element of the array. + /// + /// The index of the element to return. + /// The . + /// + /// Thrown when index less than 0 or index greater than or equal to . + /// + public readonly ref T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.size, nameof(index)); + return ref this.data![index]; + } + } + + /// + /// Adds the given item to the array. + /// + /// The item to add. + public void Add(T item) + { + int position = this.size; + + // Expand the array. + this.Length++; + this.data![position] = item; + } + + /// + /// Remove the last item from the array. + /// + public void RemoveLast() + { + DebugGuard.MustBeGreaterThan(this.size, 0, nameof(this.size)); + this.size--; + } + + /// + /// Clears the array. + /// Allocated memory is left intact for future usage. + /// + public void Clear() => + + // No need to actually clear since we're not allowing reference types. + this.size = 0; + + private void EnsureCapacity(int min) + { + int length = this.data?.Length ?? 0; + if (length < min) + { + // Same expansion algorithm as List. + uint newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; + if (newCapacity > MaxCoreClrArrayLength) + { + newCapacity = MaxCoreClrArrayLength; + } + + if (newCapacity < min) + { + newCapacity = (uint)min; + } + + T[] array = new T[newCapacity]; + + if (this.size > 0) + { + Array.Copy(this.data!, array, this.size); + } + + this.data = array; + } + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs new file mode 100644 index 00000000..2f41fab4 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -0,0 +1,743 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; + +#pragma warning disable SA1201 // Elements should appear in the correct order +internal sealed class PolygonStroker +{ + private ArrayBuilder outVertices = new(0); + private ArrayBuilder srcVertices = new(0); + private int closed; + private int outVertex; + private Status prevStatus; + private int srcVertex; + private Status status; + private double strokeWidth = 0.5; + private double widthAbs = 0.5; + private double widthEps = 0.5 / 1024.0; + private int widthSign = 1; + + public double Shorten { get; set; } + + public double InnerMiterLimit { get; set; } = 1.01; + + public double ApproximationScale { get; set; } = 1.0; + + public LineJoin LineJoin { get; set; } = LineJoin.MiterJoin; + + public LineCap LineCap { get; set; } = LineCap.Butt; + + public InnerJoin InnerJoin { get; set; } = InnerJoin.InnerMiter; + + public double Width + { + get => this.strokeWidth * 2.0; + set + { + this.strokeWidth = value * 0.5; + if (this.strokeWidth < 0) + { + this.widthAbs = -this.strokeWidth; + this.widthSign = -1; + } + else + { + this.widthAbs = this.strokeWidth; + this.widthSign = 1; + } + + this.widthEps = this.strokeWidth / 1024.0; + } + } + + public double MiterLimitTheta + { + set => this.MiterLimit = 1.0 / Math.Sin(value * 0.5); + } + + public double MiterLimit { get; set; } = 4; + + public Polygon ProcessPath(ReadOnlySpan pathPoints) + { + for (int i = 0; i < pathPoints.Length - 2; i++) + { + PointF point = pathPoints[i]; + this.AddVertex(point.X, point.Y, PathCommand.LineTo); + } + + this.AddVertex(pathPoints[^1].X, pathPoints[^1].Y, PathCommand.EndPoly); + + double x = 0; + double y = 0; + List results = new(pathPoints.Length); + while (!(_ = this.Vertex(ref x, ref y)).Stop()) + { + results.Add(new PointF((float)x, (float)y)); + } + + return new Polygon(results.ToArray()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveAll() + { + this.srcVertices.Clear(); + this.closed = 0; + this.status = Status.Initial; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddVertex(double x, double y, PathCommand cmd) + { + this.status = Status.Initial; + if (cmd.MoveTo()) + { + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + this.AddVertex(x, y); + } + else if (cmd.Vertex()) + { + this.AddVertex(x, y); + } + else + { + this.closed = cmd.GetCloseFlag(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Rewind() + { + if (this.status == Status.Initial) + { + this.CloseVertexPath(this.closed != 0); + if (this.Shorten > 0.0 && this.srcVertices.Length > 1) + { + double d; + int n = this.srcVertices.Length - 2; + while (n != 0) + { + d = this.srcVertices[n].Distance; + + if (d > this.Shorten) + { + break; + } + + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + this.Shorten -= d; + --n; + } + + if (this.srcVertices.Length < 2) + { + this.srcVertices.RemoveLast(); + } + else + { + n = this.srcVertices.Length - 1; + ref VertexDistance prev = ref this.srcVertices[n - 1]; + ref VertexDistance last = ref this.srcVertices[n]; + d = (prev.Distance - this.Shorten) / prev.Distance; + double x = prev.X + ((last.X - prev.X) * d); + double y = prev.Y + ((last.Y - prev.Y) * d); + last.X = x; + last.Y = y; + this.srcVertices[n] = last; + this.srcVertices[n - 1] = prev; + if (!prev.Measure(last) && this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + this.CloseVertexPath(this.closed != 0); + } + } + + if (this.srcVertices.Length < 3) + { + this.closed = 0; + } + } + + this.status = Status.Ready; + this.srcVertex = 0; + this.outVertex = 0; + } + + public PathCommand Vertex(ref double x, ref double y) + { + PathCommand cmd = PathCommand.LineTo; + while (!cmd.Stop()) + { + switch (this.status) + { + case Status.Initial: + this.Rewind(); + this.status = Status.Ready; + + break; + + case Status.Ready: + if (this.srcVertices.Length < 2 + (this.closed != 0 ? 1 : 0)) + { + cmd = PathCommand.Stop; + + break; + } + + this.status = this.closed != 0 ? Status.Outline1 : Status.Cap1; + cmd = PathCommand.MoveTo; + this.srcVertex = 0; + this.outVertex = 0; + + break; + + case Status.Cap1: + this.CalcCap(ref this.srcVertices[0], ref this.srcVertices[1], this.srcVertices[0].Distance); + this.srcVertex = 1; + this.prevStatus = Status.Outline1; + this.status = Status.OutVertices; + this.outVertex = 0; + + break; + + case Status.Cap2: + this.CalcCap(ref this.srcVertices[^1], ref this.srcVertices[^2], this.srcVertices[^2].Distance); + this.prevStatus = Status.Outline2; + this.status = Status.OutVertices; + this.outVertex = 0; + + break; + + case Status.Outline1: + if (this.closed != 0) + { + if (this.srcVertex >= this.srcVertices.Length) + { + this.prevStatus = Status.CloseFirst; + this.status = Status.EndPoly1; + + break; + } + } + else if (this.srcVertex >= this.srcVertices.Length - 1) + { + this.status = Status.Cap2; + + break; + } + + this.CalcJoin(ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], ref this.srcVertices[this.srcVertex], ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance, this.srcVertices[this.srcVertex].Distance); + ++this.srcVertex; + this.prevStatus = this.status; + this.status = Status.OutVertices; + this.outVertex = 0; + + break; + + case Status.CloseFirst: + this.status = Status.Outline2; + cmd = PathCommand.MoveTo; + this.status = Status.Outline2; + + break; + + case Status.Outline2: + if (this.srcVertex <= (this.closed == 0 ? 1 : 0)) + { + this.status = Status.EndPoly2; + this.prevStatus = Status.Stop; + + break; + } + + --this.srcVertex; + this.CalcJoin(ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], ref this.srcVertices[this.srcVertex], ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], this.srcVertices[this.srcVertex].Distance, this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance); + + this.prevStatus = this.status; + this.status = Status.OutVertices; + this.outVertex = 0; + + break; + + case Status.OutVertices: + if (this.outVertex >= this.outVertices.Length) + { + this.status = this.prevStatus; + } + else + { + PointF c = this.outVertices[this.outVertex++]; + x = c.X; + y = c.Y; + + return cmd; + } + + break; + + case Status.EndPoly1: + this.status = this.prevStatus; + + return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Ccw); + + case Status.EndPoly2: + this.status = this.prevStatus; + + return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Cw); + + case Status.Stop: + cmd = PathCommand.Stop; + + break; + } + } + + return cmd; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddVertex(double x, double y, double dist = 0) + { + if (this.srcVertices.Length > 1) + { + ref VertexDistance vd1 = ref this.srcVertices[^2]; + ref VertexDistance vd2 = ref this.srcVertices[^1]; + bool ret = vd1.Measure(vd2); + if (!ret && this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + } + + this.srcVertices.Add(new VertexDistance(x, y, dist)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CloseVertexPath(bool closed) + { + while (this.srcVertices.Length > 1) + { + ref VertexDistance vd1 = ref this.srcVertices[^2]; + ref VertexDistance vd2 = ref this.srcVertices[^1]; + bool ret = vd1.Measure(vd2); + + if (ret) + { + break; + } + + VertexDistance t = this.srcVertices[^1]; + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + + this.AddVertex(t.X, t.Y, t.Distance); + } + + if (!closed) + { + return; + } + + while (this.srcVertices.Length > 1) + { + ref VertexDistance vd1 = ref this.srcVertices[^1]; + ref VertexDistance vd2 = ref this.srcVertices[0]; + bool ret = vd1.Measure(vd2); + + if (ret) + { + break; + } + + if (this.srcVertices.Length != 0) + { + this.srcVertices.RemoveLast(); + } + } + } + + private void CalcArc(double x, double y, double dx1, double dy1, double dx2, double dy2) + { + double a1 = Math.Atan2(dy1 * this.widthSign, dx1 * this.widthSign); + double a2 = Math.Atan2(dy2 * this.widthSign, dx2 * this.widthSign); + int i, n; + + double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125 / this.ApproximationScale))) * 2; + + this.AddPoint(x + dx1, y + dy1); + if (this.widthSign > 0) + { + if (a1 > a2) + { + a2 += Constants.Misc.PiMul2; + } + + n = (int)((a2 - a1) / da); + da = (a2 - a1) / (n + 1); + a1 += da; + for (i = 0; i < n; i++) + { + this.AddPoint(x + (Math.Cos(a1) * this.strokeWidth), y + (Math.Sin(a1) * this.strokeWidth)); + a1 += da; + } + } + else + { + if (a1 < a2) + { + a2 -= Constants.Misc.PiMul2; + } + + n = (int)((a1 - a2) / da); + da = (a1 - a2) / (n + 1); + a1 -= da; + for (i = 0; i < n; i++) + { + this.AddPoint(x + (Math.Cos(a1) * this.strokeWidth), y + (Math.Sin(a1) * this.strokeWidth)); + a1 -= da; + } + } + + this.AddPoint(x + dx2, y + dy2); + } + + private void CalcMiter(ref VertexDistance v0, ref VertexDistance v1, ref VertexDistance v2, double dx1, double dy1, double dx2, double dy2, LineJoin lj, double mlimit, double dbevel) + { + double xi = v1.X; + double yi = v1.Y; + double di = 1.0; + double lim = this.widthAbs * mlimit; + bool miterLimitExceeded = true; + bool intersectionFailed = true; + + if (UtilityMethods.CalcIntersection(v0.X + dx1, v0.Y - dy1, v1.X + dx1, v1.Y - dy1, v1.X + dx2, v1.Y - dy2, v2.X + dx2, v2.Y - dy2, ref xi, ref yi)) + { + di = UtilityMethods.CalcDistance(v1.X, v1.Y, xi, yi); + if (di <= lim) + { + this.AddPoint(xi, yi); + miterLimitExceeded = false; + } + + intersectionFailed = false; + } + else + { + double x2 = v1.X + dx1; + double y2 = v1.Y - dy1; + if ((UtilityMethods.CrossProduct(v0.X, v0.Y, v1.X, v1.Y, x2, y2) < 0.0) == (UtilityMethods.CrossProduct(v1.X, v1.Y, v2.X, v2.Y, x2, y2) < 0.0)) + { + this.AddPoint(v1.X + dx1, v1.Y - dy1); + miterLimitExceeded = false; + } + } + + if (!miterLimitExceeded) + { + return; + } + + switch (lj) + { + case LineJoin.MiterJoinRevert: + + this.AddPoint(v1.X + dx1, v1.Y - dy1); + this.AddPoint(v1.X + dx2, v1.Y - dy2); + + break; + + case LineJoin.MiterJoinRound: + this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); + + break; + + default: + if (intersectionFailed) + { + mlimit *= this.widthSign; + this.AddPoint(v1.X + dx1 + (dy1 * mlimit), v1.Y - dy1 + (dx1 * mlimit)); + this.AddPoint(v1.X + dx2 - (dy2 * mlimit), v1.Y - dy2 - (dx2 * mlimit)); + } + else + { + double x1 = v1.X + dx1; + double y1 = v1.Y - dy1; + double x2 = v1.X + dx2; + double y2 = v1.Y - dy2; + di = (lim - dbevel) / (di - dbevel); + this.AddPoint(x1 + ((xi - x1) * di), y1 + ((yi - y1) * di)); + this.AddPoint(x2 + ((xi - x2) * di), y2 + ((yi - y2) * di)); + } + + break; + } + } + + private void CalcCap(ref VertexDistance v0, ref VertexDistance v1, double len) + { + this.outVertices.Clear(); + + double dx1 = (v1.Y - v0.Y) / len; + double dy1 = (v1.X - v0.X) / len; + double dx2 = 0; + double dy2 = 0; + + dx1 *= this.strokeWidth; + dy1 *= this.strokeWidth; + + if (this.LineCap != LineCap.Round) + { + if (this.LineCap == LineCap.Square) + { + dx2 = dy1 * this.widthSign; + dy2 = dx1 * this.widthSign; + } + + this.AddPoint(v0.X - dx1 - dx2, v0.Y + dy1 - dy2); + this.AddPoint(v0.X + dx1 - dx2, v0.Y - dy1 - dy2); + } + else + { + double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125 / this.ApproximationScale))) * 2; + double a1; + int i; + int n = (int)(Constants.Misc.Pi / da); + + da = Constants.Misc.Pi / (n + 1); + this.AddPoint(v0.X - dx1, v0.Y + dy1); + if (this.widthSign > 0) + { + a1 = Math.Atan2(dy1, -dx1); + a1 += da; + for (i = 0; i < n; i++) + { + this.AddPoint(v0.X + (Math.Cos(a1) * this.strokeWidth), v0.Y + (Math.Sin(a1) * this.strokeWidth)); + a1 += da; + } + } + else + { + a1 = Math.Atan2(-dy1, dx1); + a1 -= da; + for (i = 0; i < n; i++) + { + this.AddPoint(v0.X + (Math.Cos(a1) * this.strokeWidth), v0.Y + (Math.Sin(a1) * this.strokeWidth)); + a1 -= da; + } + } + + this.AddPoint(v0.X + dx1, v0.Y - dy1); + } + } + + private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDistance v2, double len1, double len2) + { + double dx1 = this.strokeWidth * (v1.Y - v0.Y) / len1; + double dy1 = this.strokeWidth * (v1.X - v0.X) / len1; + double dx2 = this.strokeWidth * (v2.Y - v1.Y) / len2; + double dy2 = this.strokeWidth * (v2.X - v1.X) / len2; + + this.outVertices.Clear(); + + double cp = UtilityMethods.CrossProduct(v0.X, v0.Y, v1.X, v1.Y, v2.X, v2.Y); + if (Math.Abs(cp) > double.Epsilon && (cp > 0) == (this.strokeWidth > 0)) + { + double limit = (len1 < len2 ? len1 : len2) / this.widthAbs; + if (limit < this.InnerMiterLimit) + { + limit = this.InnerMiterLimit; + } + + switch (this.InnerJoin) + { + default: // inner_bevel + this.AddPoint(v1.X + dx1, v1.Y - dy1); + this.AddPoint(v1.X + dx2, v1.Y - dy2); + + break; + + case InnerJoin.InnerMiter: + this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterJoinRevert, limit, 0); + + break; + + case InnerJoin.InnerJag: + case InnerJoin.InnerRound: + cp = ((dx1 - dx2) * (dx1 - dx2)) + ((dy1 - dy2) * (dy1 - dy2)); + if (cp < len1 * len1 && cp < len2 * len2) + { + this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterJoinRevert, limit, 0); + } + else if (this.InnerJoin == InnerJoin.InnerJag) + { + this.AddPoint(v1.X + dx1, v1.Y - dy1); + this.AddPoint(v1.X, v1.Y); + this.AddPoint(v1.X + dx2, v1.Y - dy2); + } + else + { + this.AddPoint(v1.X + dx1, v1.Y - dy1); + this.AddPoint(v1.X, v1.Y); + this.CalcArc(v1.X, v1.Y, dx2, -dy2, dx1, -dy1); + this.AddPoint(v1.X, v1.Y); + this.AddPoint(v1.X + dx2, v1.Y - dy2); + } + + break; + } + } + else + { + double dx = (dx1 + dx2) / 2; + double dy = (dy1 + dy2) / 2; + double dbevel = Math.Sqrt((dx * dx) + (dy * dy)); + + if (this.LineJoin is LineJoin.RoundJoin or LineJoin.BevelJoin && this.ApproximationScale * (this.widthAbs - dbevel) < this.widthEps) + { + if (UtilityMethods.CalcIntersection(v0.X + dx1, v0.Y - dy1, v1.X + dx1, v1.Y - dy1, v1.X + dx2, v1.Y - dy2, v2.X + dx2, v2.Y - dy2, ref dx, ref dy)) + { + this.AddPoint(dx, dy); + } + else + { + this.AddPoint(v1.X + dx1, v1.Y - dy1); + } + + return; + } + + switch (this.LineJoin) + { + case LineJoin.MiterJoin: + case LineJoin.MiterJoinRevert: + case LineJoin.MiterJoinRound: + this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, this.LineJoin, this.MiterLimit, dbevel); + + break; + + case LineJoin.RoundJoin: + this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); + + break; + + default: + this.AddPoint(v1.X + dx1, v1.Y - dy1); + this.AddPoint(v1.X + dx2, v1.Y - dy2); + + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddPoint(double x, double y) => this.outVertices.Add(new PointF((float)x, (float)y)); + + private enum Status + { + Initial, + Ready, + Cap1, + Cap2, + Outline1, + CloseFirst, + Outline2, + OutVertices, + EndPoly1, + EndPoly2, + Stop + } +} + +[Flags] +internal enum PathCommand : byte +{ + Stop = 0, + MoveTo = 1, + LineTo = 2, + Curve3 = 3, + Curve4 = 4, + CurveN = 5, + Catrom = 6, + Spline = 7, + EndPoly = 0x0F, + Mask = 0x0F +} + +[Flags] +internal enum PathFlags : byte +{ + None = 0, + Ccw = 0x10, + Cw = 0x20, + Close = 0x40, + Mask = 0xF0 +} + +internal static class PathCommandExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Vertex(this PathCommand c) => c is >= PathCommand.MoveTo and < PathCommand.EndPoly; + + public static bool Drawing(this PathCommand c) => c is >= PathCommand.LineTo and < PathCommand.EndPoly; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Stop(this PathCommand c) => c == PathCommand.Stop; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool MoveTo(this PathCommand c) => c == PathCommand.MoveTo; + + public static bool LineTo(this PathCommand c) => c == PathCommand.LineTo; + + public static bool Curve(this PathCommand c) => c is PathCommand.Curve3 or PathCommand.Curve4; + + public static bool Curve3(this PathCommand c) => c == PathCommand.Curve3; + + public static bool Curve4(this PathCommand c) => c == PathCommand.Curve4; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool EndPoly(this PathCommand c) => (c & PathCommand.Mask) == PathCommand.EndPoly; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Closed(this PathCommand c) => ((int)c & ~((int)PathFlags.Cw | (int)PathFlags.Ccw)) == ((int)PathCommand.EndPoly | (int)PathFlags.Close); + + public static bool NextPoly(this PathCommand c) => Stop(c) || MoveTo(c) || EndPoly(c); + + public static bool Oriented(int c) => (c & (int)(PathFlags.Cw | PathFlags.Ccw)) != 0; + + public static bool Cw(int c) => (c & (int)PathFlags.Cw) != 0; + + public static bool Ccw(int c) => (c & (int)PathFlags.Ccw) != 0; + + public static int CloseFlag(this PathCommand c) => (int)c & (int)PathFlags.Close; + + public static int GetOrientation(this PathCommand c) => (int)c & (int)(PathFlags.Cw | PathFlags.Ccw); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ClearOrientation(this PathCommand c) => (int)c & ~(int)(PathFlags.Cw | PathFlags.Ccw); + + public static int SetOrientation(this PathCommand c, PathFlags o) => ClearOrientation(c) | (int)o; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetCloseFlag(this PathCommand c) => (int)c & (int)PathFlags.Close; +} +#pragma warning restore SA1201 // Elements should appear in the correct order diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs new file mode 100644 index 00000000..b0151127 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs @@ -0,0 +1,237 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; + +internal struct VertexDistance +{ + private const double Dd = 1.0 / Constants.Misc.VertexDistanceEpsilon; + public double X; + public double Y; + public double Distance; + + public VertexDistance(double x, double y) + : this() + { + this.X = x; + this.Y = y; + this.Distance = 0; + } + + public VertexDistance(double x, double y, double distance) + : this() + { + this.X = x; + this.Y = y; + this.Distance = distance; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Measure(in VertexDistance vd) + { + bool ret = (this.Distance = UtilityMethods.CalcDistance(this.X, this.Y, vd.X, vd.Y)) > Constants.Misc.VertexDistanceEpsilon; + if (!ret) + { + this.Distance = Dd; + } + + return ret; + } +} + +internal static class Constants +{ + public struct Cover + { + public const int Shift = 8; + public const int Size = 1 << Shift; + public const int Mask = Size - 1; + public const int None = 0; + public const int Full = Mask; + } + + public struct PolySubpixel + { + public const int Shift = 8; + public const int ShiftMul2 = Shift << 1; + public const int Scale = 1 << Shift; + public const int Mask = Scale - 1; + } + + public struct Antialias + { + public const int Shift = 8; + public const int Scale = 0x100; + public const int Scale2 = 0x200; + public const int Mask = 0xff; + public const int Mask2 = 0x1ff; + } + + public struct GradientSubpixel + { + public const int Shift = 4; + public const int Scale = 1 << Shift; + public const int Mask = Scale - 1; + public const int DownscaleShift = 8 - Shift; + } + + public struct Misc + { + public const double BezierArcAngleEpsilon = 0.01; + public const double AffineEpsilon = 1e-14; + public const double VertexDistanceEpsilon = 1e-14; + public const double IntersectionEpsilon = 1.0e-30; + public const double Pi = 3.14159265358979323846; + public const double PiMul2 = 3.14159265358979323846 * 2; + public const double PiDiv2 = 3.14159265358979323846 * 0.5; + public const double PiDiv180 = 3.14159265358979323846 / 180.0; + public const double CurveDistanceEpsilon = 1e-30; + public const double CurveCollinearityEpsilon = 1e-30; + public const double CurveAngleToleranceEpsilon = 0.01; + public const int CurveRecursionLimit = 32; + public const int PolyMaxCoord = (1 << 30) - 1; + } +} + +internal static unsafe class UtilityMethods +{ + private static readonly Random Random = new Random(); + + private static ReadOnlySpan SqrtTable => new ushort[] { 0, 2048, 2896, 3547, 4096, 4579, 5017, 5418, 5793, 6144, 6476, 6792, 7094, 7384, 7663, 7932, 8192, 8444, 8689, 8927, 9159, 9385, 9606, 9822, 10033, 10240, 10443, 10642, 10837, 11029, 11217, 11403, 11585, 11765, 11942, 12116, 12288, 12457, 12625, 12790, 12953, 13114, 13273, 13430, 13585, 13738, 13890, 14040, 14189, 14336, 14482, 14626, 14768, 14910, 15050, 15188, 15326, 15462, 15597, 15731, 15864, 15995, 16126, 16255, 16384, 16512, 16638, 16764, 16888, 17012, 17135, 17257, 17378, 17498, 17618, 17736, 17854, 17971, 18087, 18203, 18318, 18432, 18545, 18658, 18770, 18882, 18992, 19102, 19212, 19321, 19429, 19537, 19644, 19750, 19856, 19961, 20066, 20170, 20274, 20377, 20480, 20582, 20684, 20785, 20886, 20986, 21085, 21185, 21283, 21382, 21480, 21577, 21674, 21771, 21867, 21962, 22058, 22153, 22247, 22341, 22435, 22528, 22621, 22713, 22806, 22897, 22989, 23080, 23170, 23261, 23351, 23440, 23530, 23619, 23707, 23796, 23884, 23971, 24059, 24146, 24232, 24319, 24405, 24491, 24576, 24661, 24746, 24831, 24915, 24999, 25083, 25166, 25249, 25332, 25415, 25497, 25580, 25661, 25743, 25824, 25905, 25986, 26067, 26147, 26227, 26307, 26387, 26466, 26545, 26624, 26703, 26781, 26859, 26937, 27015, 27092, 27170, 27247, 27324, 27400, 27477, 27553, 27629, 27705, 27780, 27856, 27931, 28006, 28081, 28155, 28230, 28304, 28378, 28452, 28525, 28599, 28672, 28745, 28818, 28891, 28963, 29035, 29108, 29180, 29251, 29323, 29394, 29466, 29537, 29608, 29678, 29749, 29819, 29890, 29960, 30030, 30099, 30169, 30238, 30308, 30377, 30446, 30515, 30583, 30652, 30720, 30788, 30856, 30924, 30992, 31059, 31127, 31194, 31261, 31328, 31395, 31462, 31529, 31595, 31661, 31727, 31794, 31859, 31925, 31991, 32056, 32122, 32187, 32252, 32317, 32382, 32446, 32511, 32575, 32640, 32704, 32768, 32832, 32896, 32959, 33023, 33086, 33150, 33213, 33276, 33339, 33402, 33465, 33527, 33590, 33652, 33714, 33776, 33839, 33900, 33962, 34024, 34086, 34147, 34208, 34270, 34331, 34392, 34453, 34514, 34574, 34635, 34695, 34756, 34816, 34876, 34936, 34996, 35056, 35116, 35176, 35235, 35295, 35354, 35413, 35472, 35531, 35590, 35649, 35708, 35767, 35825, 35884, 35942, 36001, 36059, 36117, 36175, 36233, 36291, 36348, 36406, 36464, 36521, 36578, 36636, 36693, 36750, 36807, 36864, 36921, 36978, 37034, 37091, 37147, 37204, 37260, 37316, 37372, 37429, 37485, 37540, 37596, 37652, 37708, 37763, 37819, 37874, 37929, 37985, 38040, 38095, 38150, 38205, 38260, 38315, 38369, 38424, 38478, 38533, 38587, 38642, 38696, 38750, 38804, 38858, 38912, 38966, 39020, 39073, 39127, 39181, 39234, 39287, 39341, 39394, 39447, 39500, 39553, 39606, 39659, 39712, 39765, 39818, 39870, 39923, 39975, 40028, 40080, 40132, 40185, 40237, 40289, 40341, 40393, 40445, 40497, 40548, 40600, 40652, 40703, 40755, 40806, 40857, 40909, 40960, 41011, 41062, 41113, 41164, 41215, 41266, 41317, 41368, 41418, 41469, 41519, 41570, 41620, 41671, 41721, 41771, 41821, 41871, 41922, 41972, 42021, 42071, 42121, 42171, 42221, 42270, 42320, 42369, 42419, 42468, 42518, 42567, 42616, 42665, 42714, 42763, 42813, 42861, 42910, 42959, 43008, 43057, 43105, 43154, 43203, 43251, 43300, 43348, 43396, 43445, 43493, 43541, 43589, 43637, 43685, 43733, 43781, 43829, 43877, 43925, 43972, 44020, 44068, 44115, 44163, 44210, 44258, 44305, 44352, 44400, 44447, 44494, 44541, 44588, 44635, 44682, 44729, 44776, 44823, 44869, 44916, 44963, 45009, 45056, 45103, 45149, 45195, 45242, 45288, 45334, 45381, 45427, 45473, 45519, 45565, 45611, 45657, 45703, 45749, 45795, 45840, 45886, 45932, 45977, 46023, 46069, 46114, 46160, 46205, 46250, 46296, 46341, 46386, 46431, 46477, 46522, 46567, 46612, 46657, 46702, 46746, 46791, 46836, 46881, 46926, 46970, 47015, 47059, 47104, 47149, 47193, 47237, 47282, 47326, 47370, 47415, 47459, 47503, 47547, 47591, 47635, 47679, 47723, 47767, 47811, 47855, 47899, 47942, 47986, 48030, 48074, 48117, 48161, 48204, 48248, 48291, 48335, 48378, 48421, 48465, 48508, 48551, 48594, 48637, 48680, 48723, 48766, 48809, 48852, 48895, 48938, 48981, 49024, 49067, 49109, 49152, 49195, 49237, 49280, 49322, 49365, 49407, 49450, 49492, 49535, 49577, 49619, 49661, 49704, 49746, 49788, 49830, 49872, 49914, 49956, 49998, 50040, 50082, 50124, 50166, 50207, 50249, 50291, 50332, 50374, 50416, 50457, 50499, 50540, 50582, 50623, 50665, 50706, 50747, 50789, 50830, 50871, 50912, 50954, 50995, 51036, 51077, 51118, 51159, 51200, 51241, 51282, 51323, 51364, 51404, 51445, 51486, 51527, 51567, 51608, 51649, 51689, 51730, 51770, 51811, 51851, 51892, 51932, 51972, 52013, 52053, 52093, 52134, 52174, 52214, 52254, 52294, 52334, 52374, 52414, 52454, 52494, 52534, 52574, 52614, 52654, 52694, 52734, 52773, 52813, 52853, 52892, 52932, 52972, 53011, 53051, 53090, 53130, 53169, 53209, 53248, 53287, 53327, 53366, 53405, 53445, 53484, 53523, 53562, 53601, 53640, 53679, 53719, 53758, 53797, 53836, 53874, 53913, 53952, 53991, 54030, 54069, 54108, 54146, 54185, 54224, 54262, 54301, 54340, 54378, 54417, 54455, 54494, 54532, 54571, 54609, 54647, 54686, 54724, 54762, 54801, 54839, 54877, 54915, 54954, 54992, 55030, 55068, 55106, 55144, 55182, 55220, 55258, 55296, 55334, 55372, 55410, 55447, 55485, 55523, 55561, 55599, 55636, 55674, 55712, 55749, 55787, 55824, 55862, 55900, 55937, 55975, 56012, 56049, 56087, 56124, 56162, 56199, 56236, 56273, 56311, 56348, 56385, 56422, 56459, 56497, 56534, 56571, 56608, 56645, 56682, 56719, 56756, 56793, 56830, 56867, 56903, 56940, 56977, 57014, 57051, 57087, 57124, 57161, 57198, 57234, 57271, 57307, 57344, 57381, 57417, 57454, 57490, 57527, 57563, 57599, 57636, 57672, 57709, 57745, 57781, 57817, 57854, 57890, 57926, 57962, 57999, 58035, 58071, 58107, 58143, 58179, 58215, 58251, 58287, 58323, 58359, 58395, 58431, 58467, 58503, 58538, 58574, 58610, 58646, 58682, 58717, 58753, 58789, 58824, 58860, 58896, 58931, 58967, 59002, 59038, 59073, 59109, 59144, 59180, 59215, 59251, 59286, 59321, 59357, 59392, 59427, 59463, 59498, 59533, 59568, 59603, 59639, 59674, 59709, 59744, 59779, 59814, 59849, 59884, 59919, 59954, 59989, 60024, 60059, 60094, 60129, 60164, 60199, 60233, 60268, 60303, 60338, 60373, 60407, 60442, 60477, 60511, 60546, 60581, 60615, 60650, 60684, 60719, 60753, 60788, 60822, 60857, 60891, 60926, 60960, 60995, 61029, 61063, 61098, 61132, 61166, 61201, 61235, 61269, 61303, 61338, 61372, 61406, 61440, 61474, 61508, 61542, 61576, 61610, 61644, 61678, 61712, 61746, 61780, 61814, 61848, 61882, 61916, 61950, 61984, 62018, 62051, 62085, 62119, 62153, 62186, 62220, 62254, 62287, 62321, 62355, 62388, 62422, 62456, 62489, 62523, 62556, 62590, 62623, 62657, 62690, 62724, 62757, 62790, 62824, 62857, 62891, 62924, 62957, 62991, 63024, 63057, 63090, 63124, 63157, 63190, 63223, 63256, 63289, 63323, 63356, 63389, 63422, 63455, 63488, 63521, 63554, 63587, 63620, 63653, 63686, 63719, 63752, 63785, 63817, 63850, 63883, 63916, 63949, 63982, 64014, 64047, 64080, 64113, 64145, 64178, 64211, 64243, 64276, 64309, 64341, 64374, 64406, 64439, 64471, 64504, 64536, 64569, 64601, 64634, 64666, 64699, 64731, 64763, 64796, 64828, 64861, 64893, 64925, 64957, 64990, 65022, 65054, 65086, 65119, 65151, 65183, 65215, 65247, 65279, 65312, 65344, 65376, 65408, 65440, 65472, 65504 }; + + private static ReadOnlySpan ElderBitTable => new byte[] { 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundToI32(double v) => (int)(v < 0.0 ? v - 0.5 : v + 0.5); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int RoundToU32(double v) => (int)(v + 0.5); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double Rand(double x) => ((((Random.Next() << 15) | Random.Next()) & 0x3FFFFFFF) % 1000000) * x / 1000000.0; + + public static double Rand(double x1, double x2) + { + int r = Random.Next() & 0x7FFF; + + return (r * (x2 - x1) / 32768.0) + x1; + } + + public static bool IsEqualEps(double v1, double v2, double epsilon) => Math.Abs(v1 - v2) <= epsilon; + + public static byte InvertByte(byte x) => (byte)~x; + + public static bool PointInTriangle(double x1, double y1, double x2, double y2, double x3, double y3, double x, double y) + { + bool cp1 = CrossProduct(x1, y1, x2, y2, x, y) < 0.0; + bool cp2 = CrossProduct(x2, y2, x3, y3, x, y) < 0.0; + bool cp3 = CrossProduct(x3, y3, x1, y1, x, y) < 0.0; + + return cp1 == cp2 && cp2 == cp3; + } + + public static double CalcLinePointDistance(double x1, double y1, double x2, double y2, double x, double y) + { + double dx = x2 - x1; + double dy = y2 - y1; + double d = Math.Sqrt((dx * dx) + (dy * dy)); + + if (d < Constants.Misc.VertexDistanceEpsilon) + { + return CalcDistance(x1, y1, x, y); + } + + return (((x - x2) * dy) - ((y - y2) * dx)) / d; + } + + public static double CalcDistance(double x1, double y1, double x2, double y2) + { + double dx = x2 - x1; + double dy = y2 - y1; + + return Math.Sqrt((dx * dx) + (dy * dy)); + } + + public static void CalcOrthogonal(double thickness, double x1, double y1, double x2, double y2, out double x, out double y) + { + double dx = x2 - x1; + double dy = y2 - y1; + double d = Math.Sqrt((dx * dx) + (dy * dy)); + x = thickness * dy / d; + y = -thickness * dx / d; + } + + public static bool CalcIntersection(double ax, double ay, double bx, double by, double cx, double cy, double dx, double dy, ref double x, ref double y) + { + double num = ((ay - cy) * (dx - cx)) - ((ax - cx) * (dy - cy)); + double den = ((bx - ax) * (dy - cy)) - ((by - ay) * (dx - cx)); + + if (Math.Abs(den) < Constants.Misc.IntersectionEpsilon) + { + return false; + } + + double r = num / den; + x = ax + (r * (bx - ax)); + y = ay + (r * (by - ay)); + + return true; + } + + public static double CalcSquareDistance(double x1, double y1, double x2, double y2) + { + double dx = x2 - x1; + double dy = y2 - y1; + + return (dx * dx) + (dy * dy); + } + + public static double CrossProduct(double x1, double y1, double x2, double y2, double x, double y) => ((x - x2) * (y2 - y1)) - ((y - y2) * (x2 - x1)); + + public static int FastSqrt(int val) + { + int t = val; + int shift = 11; + + int bit = t >> 24; + if (bit != 0) + { + bit = ElderBitTable[bit] + 24; + } + else + { + bit = (t >> 16) & 0xFF; + if (bit != 0) + { + bit = ElderBitTable[bit] + 16; + } + else + { + bit = (t >> 8) & 0xFF; + if (bit != 0) + { + bit = ElderBitTable[bit] + 8; + } + else + { + bit = ElderBitTable[t]; + } + } + } + + bit -= 9; + + if (bit <= 0) + { + return SqrtTable[val] >> shift; + } + + bit = (bit >> 1) + (bit & 1); + shift -= bit; + val >>= bit << 1; + + return SqrtTable[val] >> shift; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs index a9acab03..bb56e870 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs @@ -208,10 +208,13 @@ static void RoundY(ReadOnlySpan vertices, Span destination, float [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 AdvSimdShuffle(Vector128 a, Vector128 b, byte control) { + // TODO: Review the codegen here. Might be better just looping. +#pragma warning disable CA1857 // A constant is expected for the parameter Vector128 result = Vector128.Create(AdvSimd.Extract(a, (byte)(control & 0x3))); result = AdvSimd.Insert(result, 1, AdvSimd.Extract(a, (byte)((control >> 2) & 0x3))); result = AdvSimd.Insert(result, 2, AdvSimd.Extract(b, (byte)((control >> 4) & 0x3))); result = AdvSimd.Insert(result, 3, AdvSimd.Extract(b, (byte)((control >> 6) & 0x3))); +#pragma warning restore CA1857 // A constant is expected for the parameter return result; } diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index b0dc7c7b..095d228f 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -18,15 +18,14 @@ - - - - + + + - - + + + - diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index daef9a87..25dd0315 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -8,21 +8,29 @@ false + + + + CA1822 + + - net7.0;net6.0 + net8.0 - net6.0 + net8.0 - + + + diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index 5767ff84..02121304 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -13,12 +13,12 @@ - net7.0;net6.0 + net8.0 - net6.0 + net8.0 From 52c5312a34b25511a4976e6a37c7f31feafedf13 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 28 Jan 2024 09:06:50 +1000 Subject: [PATCH 02/33] Update PolygonStroker.cs --- src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs index 2f41fab4..1a78eda5 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -62,12 +62,13 @@ public double MiterLimitTheta public Polygon ProcessPath(ReadOnlySpan pathPoints) { - for (int i = 0; i < pathPoints.Length - 2; i++) + for (int i = 0; i < pathPoints.Length - 1; i++) { PointF point = pathPoints[i]; this.AddVertex(point.X, point.Y, PathCommand.LineTo); } + // This doesn't actually add the point. It triggers a close. this.AddVertex(pathPoints[^1].X, pathPoints[^1].Y, PathCommand.EndPoly); double x = 0; From bed37224cd282785386b11ace00b8bc50fa48c1c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 28 Jan 2024 09:18:37 +1000 Subject: [PATCH 03/33] Actually use the width --- src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index e8de488f..41eb17ee 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -48,7 +48,7 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi List polygons = []; foreach (ISimplePath simplePath in path.Flatten()) { - PolygonStroker stroker = new(); + PolygonStroker stroker = new() { Width = width }; Polygon polygon = stroker.ProcessPath(simplePath.Points.Span); polygons.Add(polygon); } From 558de975271bd50af9b93efb226d06e6dfb78d8f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 28 Jan 2024 09:48:08 +1000 Subject: [PATCH 04/33] Bad hack to attempt to close path --- .../Shapes/PolygonClipper/PolygonStroker.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs index 1a78eda5..9d5fc0e1 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -62,14 +62,15 @@ public double MiterLimitTheta public Polygon ProcessPath(ReadOnlySpan pathPoints) { - for (int i = 0; i < pathPoints.Length - 1; i++) + for (int i = 0; i < pathPoints.Length; i++) { PointF point = pathPoints[i]; this.AddVertex(point.X, point.Y, PathCommand.LineTo); } - // This doesn't actually add the point. It triggers a close. - this.AddVertex(pathPoints[^1].X, pathPoints[^1].Y, PathCommand.EndPoly); + this.AddVertex(pathPoints[^1].X, pathPoints[^1].Y, PathCommand.LineTo); + this.closed = 1; + this.Rewind(); double x = 0; double y = 0; From 7527241eb04cc2910e1a01675ddd3478f8de2b94 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 29 Jan 2024 16:17:21 +1000 Subject: [PATCH 05/33] Close the polygons on stroking. --- .../Shapes/OutlinePathExtensions.cs | 2 +- .../Shapes/PolygonClipper/PolygonStroker.cs | 35 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index 41eb17ee..0b1db2dd 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -49,7 +49,7 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi foreach (ISimplePath simplePath in path.Flatten()) { PolygonStroker stroker = new() { Width = width }; - Polygon polygon = stroker.ProcessPath(simplePath.Points.Span); + Polygon polygon = stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed); polygons.Add(polygon); } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs index 9d5fc0e1..1e93e3cf 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -60,7 +60,7 @@ public double MiterLimitTheta public double MiterLimit { get; set; } = 4; - public Polygon ProcessPath(ReadOnlySpan pathPoints) + public Polygon ProcessPath(ReadOnlySpan pathPoints, bool isClosed) { for (int i = 0; i < pathPoints.Length; i++) { @@ -68,16 +68,37 @@ public Polygon ProcessPath(ReadOnlySpan pathPoints) this.AddVertex(point.X, point.Y, PathCommand.LineTo); } - this.AddVertex(pathPoints[^1].X, pathPoints[^1].Y, PathCommand.LineTo); - this.closed = 1; - this.Rewind(); + if (isClosed) + { + this.AddVertex(0, 0, PathCommand.EndPoly | (PathCommand)PathFlags.Close); + } double x = 0; double y = 0; - List results = new(pathPoints.Length); - while (!(_ = this.Vertex(ref x, ref y)).Stop()) + List results = new(pathPoints.Length * 3); + + int startIndex = 0; + PointF? lastPoint = null; + PathCommand command; + while (!(command = this.Vertex(ref x, ref y)).Stop()) { - results.Add(new PointF((float)x, (float)y)); + PointF currentPoint; + if (command.EndPoly() && results.Count > 0) + { + PointF initial = results[startIndex]; + currentPoint = new(initial.X, initial.Y); + results.Add(currentPoint); + startIndex = results.Count; + } + else + { + currentPoint = new((float)x, (float)y); + if (currentPoint != lastPoint) + { + results.Add(currentPoint); + lastPoint = currentPoint; + } + } } return new Polygon(results.ToArray()); From 3c8b5d3b2ab45b8888dd95fd254c8af9fa3330ef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 30 Jan 2024 22:08:50 +1000 Subject: [PATCH 06/33] Clip dashed paths --- .../Processors/Drawing/FillPathProcessor.cs | 6 +- .../Drawing/FillPathProcessor{TPixel}.cs | 2 +- src/ImageSharp.Drawing/Shapes/InternalPath.cs | 23 +- .../Shapes/OutlinePathExtensions.cs | 94 +++++--- .../Shapes/PolygonClipper/ArrayBuilder{T}.cs | 8 +- .../Shapes/PolygonClipper/Clipper.cs | 2 +- .../Shapes/PolygonClipper/ClipperUtils.cs | 10 +- .../Shapes/PolygonClipper/PolygonClipper.cs | 8 +- .../Shapes/PolygonClipper/PolygonOffsetter.cs | 6 +- .../Shapes/PolygonClipper/PolygonStroker.cs | 138 +++++------- .../Shapes/PolygonClipper/VertexDistance.cs | 2 +- .../Shapes/Rasterization/ActiveEdgeList.cs | 4 +- .../Shapes/Rasterization/PolygonScanner.cs | 6 +- .../Rasterization/ScanEdgeCollection.Build.cs | 202 +++++++++--------- 14 files changed, 245 insertions(+), 266 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs index 63c11d64..477c3325 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs @@ -52,8 +52,8 @@ public IImageProcessor CreatePixelSpecificProcessor(Configuratio if (shape is RectangularPolygon rectPoly) { - var rectF = new RectangleF(rectPoly.Location, rectPoly.Size); - var rect = (Rectangle)rectF; + RectangleF rectF = new(rectPoly.Location, rectPoly.Size); + Rectangle rect = (Rectangle)rectF; if (!this.Options.GraphicsOptions.Antialias || rectF == rect) { // Cast as in and back are the same or we are using anti-aliasing @@ -63,7 +63,7 @@ public IImageProcessor CreatePixelSpecificProcessor(Configuratio } // Clone the definition so we can pass the transformed path. - var definition = new FillPathProcessor(this.Options, this.Brush, shape); + FillPathProcessor definition = new(this.Options, this.Brush, shape); return new FillPathProcessor(configuration, definition, source, sourceRectangle); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index f9426b96..c835882d 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -67,7 +67,7 @@ protected override void OnFrameApply(ImageFrame source) // We need to offset the pixel grid to account for when we outline a path. // basically if the line is [1,2] => [3,2] then when outlining at 1 we end up with a region of [0.5,1.5],[1.5, 1.5],[3.5,2.5],[2.5,2.5] - // and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the# + // and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the // region to align with the pixel grid. if (graphicsOptions.Antialias) { diff --git a/src/ImageSharp.Drawing/Shapes/InternalPath.cs b/src/ImageSharp.Drawing/Shapes/InternalPath.cs index ac19097a..d9becedd 100644 --- a/src/ImageSharp.Drawing/Shapes/InternalPath.cs +++ b/src/ImageSharp.Drawing/Shapes/InternalPath.cs @@ -200,7 +200,7 @@ internal IMemoryOwner ExtractVertices(MemoryAllocator allocator) private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointOrientation CalulateOrientation(Vector2 p, Vector2 q, Vector2 r) + private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vector2 r) { // See http://www.geeksforgeeks.org/orientation-3-ordered-points/ // for details of below formula. @@ -217,7 +217,7 @@ private static PointOrientation CalulateOrientation(Vector2 p, Vector2 q, Vector } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointOrientation CalulateOrientation(Vector2 qp, Vector2 rq) + private static PointOrientation CalculateOrientation(Vector2 qp, Vector2 rq) { // See http://www.geeksforgeeks.org/orientation-3-ordered-points/ // for details of below formula. @@ -242,7 +242,7 @@ private static PointOrientation CalulateOrientation(Vector2 qp, Vector2 rq) /// private static PointData[] Simplify(IReadOnlyList segments, bool isClosed, bool removeCloseAndCollinear) { - var simplified = new List(); + List simplified = new(segments.Count); foreach (ILineSegment seg in segments) { @@ -260,10 +260,10 @@ private static PointData[] Simplify(ReadOnlyMemory vectors, bool isClose int polyCorners = points.Length; if (polyCorners == 0) { - return Array.Empty(); + return []; } - var results = new List(); + List results = new(polyCorners); Vector2 lastPoint = points[0]; if (!isClosed) @@ -292,7 +292,7 @@ private static PointData[] Simplify(ReadOnlyMemory vectors, bool isClose Length = 0, }); - return results.ToArray(); + return [.. results]; } } while (removeCloseAndCollinear && points[0].Equivalent(points[prev], Epsilon2)); // skip points too close together @@ -304,31 +304,28 @@ private static PointData[] Simplify(ReadOnlyMemory vectors, bool isClose new PointData { Point = points[0], - Orientation = CalulateOrientation(lastPoint, points[0], points[1]), + Orientation = CalculateOrientation(lastPoint, points[0], points[1]), Length = Vector2.Distance(lastPoint, points[0]), }); lastPoint = points[0]; } - float totalDist = 0; for (int i = 1; i < polyCorners; i++) { int next = WrapArrayIndex(i + 1, polyCorners); - PointOrientation or = CalulateOrientation(lastPoint, points[i], points[next]); + PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]); if (or == PointOrientation.Collinear && next != 0) { continue; } - float dist = Vector2.Distance(lastPoint, points[i]); - totalDist += dist; results.Add( new PointData { Point = points[i], Orientation = or, - Length = dist, + Length = Vector2.Distance(lastPoint, points[i]), }); lastPoint = points[i]; } @@ -342,7 +339,7 @@ private static PointData[] Simplify(ReadOnlyMemory vectors, bool isClose } } - return results.ToArray(); + return [.. results]; } private struct PointData diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index 0b1db2dd..2cc071fd 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; namespace SixLabors.ImageSharp.Drawing; @@ -11,7 +12,6 @@ namespace SixLabors.ImageSharp.Drawing; /// public static class OutlinePathExtensions { - private const float MiterOffsetDelta = 20; private const JointStyle DefaultJointStyle = JointStyle.Square; private const EndCapStyle DefaultEndCapStyle = EndCapStyle.Butt; @@ -45,20 +45,16 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi return Path.Empty; } - List polygons = []; + List stroked = []; + + // TODO: Wire up options + PolygonStroker stroker = new() { Width = width, LineJoin = LineJoin.BevelJoin, LineCap = LineCap.Butt }; foreach (ISimplePath simplePath in path.Flatten()) { - PolygonStroker stroker = new() { Width = width }; - Polygon polygon = stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed); - polygons.Add(polygon); + stroked.Add(new Polygon(stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed).ToArray())); } - return new ComplexPolygon(polygons); - - // ClipperOffset offset = new(MiterOffsetDelta); - // offset.AddPath(path, jointStyle, endCapStyle); - - // return offset.Execute(width); + return new ComplexPolygon(stroked); } /// @@ -78,11 +74,11 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe path to outline /// The outline width. /// The pattern made of multiples of the width. - /// Whether the first item in the pattern is on or off. + /// Whether the first item in the pattern is off. /// A new representing the outline. /// Thrown when an offset cannot be calculated. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) - => GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle); + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool invert) + => GenerateOutline(path, width, pattern, invert, DefaultJointStyle, DefaultEndCapStyle); /// /// Generates an outline of the path with alternating on and off segments based on the pattern. @@ -103,12 +99,12 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe path to outline /// The outline width. /// The pattern made of multiples of the width. - /// Whether the first item in the pattern is on or off. + /// Whether the first item in the pattern is off. /// The style to apply to the joints. /// The style to apply to the end caps. /// A new representing the outline. /// Thrown when an offset cannot be calculated. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle) + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool invert, JointStyle jointStyle, EndCapStyle endCapStyle) { if (width <= 0) { @@ -122,18 +118,21 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan paths = path.Flatten(); - ClipperOffset offset = new(MiterOffsetDelta); - List buffer = new(); - foreach (ISimplePath p in paths) + // TODO: Wire up options + PolygonStroker stroker = new() { Width = width, LineJoin = LineJoin.BevelJoin, LineCap = LineCap.Butt }; + + PathsF stroked = []; + List buffer = []; + foreach (ISimplePath simplePath in paths) { - bool online = !startOff; + bool online = !invert; float targetLength = pattern[0] * width; int patternPos = 0; - ReadOnlySpan points = p.Points.Span; + ReadOnlySpan points = simplePath.Points.Span; // Create a new list of points representing the new outline int pCount = points.Length; - if (!p.IsClosed) + if (!simplePath.IsClosed) { pCount--; } @@ -145,20 +144,20 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan targetLength) + float distanceToNext = Vector2.Distance(currentPoint, targetPoint); + if (distanceToNext > targetLength) { - // find a point between the 2 - float t = targetLength / distToNext; + // Find a point between the 2 + float t = targetLength / distanceToNext; Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t); buffer.Add(currentPoint); buffer.Add(point); - // we now inset a line joining + // We now insert a line if (online) { - offset.AddPath(new ReadOnlySpan(buffer.ToArray()), jointStyle, endCapStyle); + stroked.Add(stroker.ProcessPath(CollectionsMarshal.AsSpan(buffer), false)); } online = !online; @@ -167,39 +166,64 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan 0) { - if (p.IsClosed) + if (simplePath.IsClosed) { buffer.Add(points[0]); } else { - buffer.Add(points[points.Length - 1]); + buffer.Add(points[^1]); } if (online) { - offset.AddPath(new ReadOnlySpan(buffer.ToArray()), jointStyle, endCapStyle); + stroked.Add(stroker.ProcessPath(CollectionsMarshal.AsSpan(buffer), false)); } buffer.Clear(); } } - return offset.Execute(width); + // Clean up self intersections. + PolygonClipper clipper = new() { PreserveCollinear = true }; + clipper.AddSubject(stroked); + PathsF clipped = []; + clipper.Execute(ClippingOperation.Union, FillRule.Positive, clipped); + + if (clipped.Count == 0) + { + // Cannot clip. Return the stroked path. + Polygon[] polygons = new Polygon[stroked.Count]; + for (int i = 0; i < stroked.Count; i++) + { + polygons[i] = new Polygon(stroked[i].ToArray()); + } + + return new ComplexPolygon(polygons); + } + + // Convert the clipped paths back to polygons. + Polygon[] result = new Polygon[clipped.Count]; + for (int i = 0; i < clipped.Count; i++) + { + result[i] = new Polygon(clipped[i].ToArray()); + } + + return new ComplexPolygon(result); } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs index 8513ac09..db54c89c 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs @@ -26,10 +26,10 @@ internal struct ArrayBuilder public ArrayBuilder(int capacity) : this() { - Guard.MustBeGreaterThanOrEqualTo(capacity, 0, nameof(capacity)); - - this.data = new T[capacity]; - this.size = capacity; + if (capacity > 0) + { + this.data = new T[capacity]; + } } /// diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs index d02b9d6e..a617b960 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs @@ -14,7 +14,7 @@ internal class Clipper /// Initializes a new instance of the class. /// public Clipper() - => this.polygonClipper = new PolygonClipper(); + => this.polygonClipper = new PolygonClipper() { PreserveCollinear = true }; /// /// Generates the clipped shapes from the previously provided paths. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs index 9da5b3f3..2263d042 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs @@ -46,7 +46,7 @@ public static PathF StripDuplicates(PathF path, bool isClosedPath) return result; } - Vector2 lastPt = path[0]; + PointF lastPt = path[0]; result.Add(lastPt); for (int i = 1; i < cnt; i++) { @@ -152,11 +152,9 @@ public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Ve // ensure NOT collinear return res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0; } - else - { - return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0) - && (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0); - } + + return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0) + && (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs index 0772b740..331cdcad 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs @@ -398,8 +398,8 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) { solutionClosed.Clear(); solutionOpen.Clear(); - solutionClosed.Capacity = this.outrecList.Count; - solutionOpen.Capacity = this.outrecList.Count; + solutionClosed.EnsureCapacity(this.outrecList.Count); + solutionOpen.EnsureCapacity(this.outrecList.Count); int i = 0; @@ -1122,7 +1122,7 @@ private void Reset() this.isSortedMinimaList = true; } - this.scanlineList.Capacity = this.minimaList.Count; + this.scanlineList.EnsureCapacity(this.minimaList.Count); for (int i = this.minimaList.Count - 1; i >= 0; i--) { this.scanlineList.Add(this.minimaList[i].Vertex.Point.Y); @@ -1924,7 +1924,7 @@ private void AddPathsToVertexList(PathsF paths, ClippingType polytype, bool isOp totalVertCnt += path.Count; } - this.vertexList.Capacity = this.vertexList.Count + totalVertCnt; + this.vertexList.EnsureCapacity(this.vertexList.Count + totalVertCnt); foreach (PathF path in paths) { diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs index 501ba036..084a0c43 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs @@ -301,7 +301,7 @@ private void BuildNormals(PathF path) { int cnt = path.Count; this.normals.Clear(); - this.normals.Capacity = cnt; + this.normals.EnsureCapacity(cnt); for (int i = 0; i < cnt - 1; i++) { @@ -682,13 +682,13 @@ public PathsF(int capacity) } } -internal class PathF : List +internal class PathF : List { public PathF() { } - public PathF(IEnumerable items) + public PathF(IEnumerable items) : base(items) { } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs index 1e93e3cf..8046d6a5 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -8,8 +8,8 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; #pragma warning disable SA1201 // Elements should appear in the correct order internal sealed class PolygonStroker { - private ArrayBuilder outVertices = new(0); - private ArrayBuilder srcVertices = new(0); + private ArrayBuilder outVertices = new(1); + private ArrayBuilder srcVertices = new(16); private int closed; private int outVertex; private Status prevStatus; @@ -20,7 +20,7 @@ internal sealed class PolygonStroker private double widthEps = 0.5 / 1024.0; private int widthSign = 1; - public double Shorten { get; set; } + public double MiterLimit { get; set; } = 4; public double InnerMiterLimit { get; set; } = 1.01; @@ -53,15 +53,9 @@ public double Width } } - public double MiterLimitTheta - { - set => this.MiterLimit = 1.0 / Math.Sin(value * 0.5); - } - - public double MiterLimit { get; set; } = 4; - - public Polygon ProcessPath(ReadOnlySpan pathPoints, bool isClosed) + public PathF ProcessPath(ReadOnlySpan pathPoints, bool isClosed) { + this.Reset(); for (int i = 0; i < pathPoints.Length; i++) { PointF point = pathPoints[i]; @@ -75,12 +69,12 @@ public Polygon ProcessPath(ReadOnlySpan pathPoints, bool isClosed) double x = 0; double y = 0; - List results = new(pathPoints.Length * 3); - int startIndex = 0; PointF? lastPoint = null; PathCommand command; - while (!(command = this.Vertex(ref x, ref y)).Stop()) + + PathF results = new(pathPoints.Length * 3); + while (!(command = this.Accumulate(ref x, ref y)).Stop()) { PointF currentPoint; if (command.EndPoly() && results.Count > 0) @@ -101,19 +95,22 @@ public Polygon ProcessPath(ReadOnlySpan pathPoints, bool isClosed) } } - return new Polygon(results.ToArray()); + return results; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RemoveAll() + private void Reset() { this.srcVertices.Clear(); + this.outVertices.Clear(); + this.srcVertex = 0; + this.outVertex = 0; this.closed = 0; this.status = Status.Initial; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddVertex(double x, double y, PathCommand cmd) + private void AddVertex(double x, double y, PathCommand cmd) { this.status = Status.Initial; if (cmd.MoveTo()) @@ -135,71 +132,7 @@ public void AddVertex(double x, double y, PathCommand cmd) } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Rewind() - { - if (this.status == Status.Initial) - { - this.CloseVertexPath(this.closed != 0); - if (this.Shorten > 0.0 && this.srcVertices.Length > 1) - { - double d; - int n = this.srcVertices.Length - 2; - while (n != 0) - { - d = this.srcVertices[n].Distance; - - if (d > this.Shorten) - { - break; - } - - if (this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - - this.Shorten -= d; - --n; - } - - if (this.srcVertices.Length < 2) - { - this.srcVertices.RemoveLast(); - } - else - { - n = this.srcVertices.Length - 1; - ref VertexDistance prev = ref this.srcVertices[n - 1]; - ref VertexDistance last = ref this.srcVertices[n]; - d = (prev.Distance - this.Shorten) / prev.Distance; - double x = prev.X + ((last.X - prev.X) * d); - double y = prev.Y + ((last.Y - prev.Y) * d); - last.X = x; - last.Y = y; - this.srcVertices[n] = last; - this.srcVertices[n - 1] = prev; - if (!prev.Measure(last) && this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - - this.CloseVertexPath(this.closed != 0); - } - } - - if (this.srcVertices.Length < 3) - { - this.closed = 0; - } - } - - this.status = Status.Ready; - this.srcVertex = 0; - this.outVertex = 0; - } - - public PathCommand Vertex(ref double x, ref double y) + private PathCommand Accumulate(ref double x, ref double y) { PathCommand cmd = PathCommand.LineTo; while (!cmd.Stop()) @@ -207,7 +140,13 @@ public PathCommand Vertex(ref double x, ref double y) switch (this.status) { case Status.Initial: - this.Rewind(); + this.CloseVertexPath(this.closed != 0); + + if (this.srcVertices.Length < 3) + { + this.closed = 0; + } + this.status = Status.Ready; break; @@ -262,8 +201,15 @@ public PathCommand Vertex(ref double x, ref double y) break; } - this.CalcJoin(ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], ref this.srcVertices[this.srcVertex], ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance, this.srcVertices[this.srcVertex].Distance); + this.CalcJoin( + ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], + ref this.srcVertices[this.srcVertex], + ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], + this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance, + this.srcVertices[this.srcVertex].Distance); + ++this.srcVertex; + this.prevStatus = this.status; this.status = Status.OutVertices; this.outVertex = 0; @@ -287,7 +233,13 @@ public PathCommand Vertex(ref double x, ref double y) } --this.srcVertex; - this.CalcJoin(ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], ref this.srcVertices[this.srcVertex], ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], this.srcVertices[this.srcVertex].Distance, this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance); + + this.CalcJoin( + ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], + ref this.srcVertices[this.srcVertex], + ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], + this.srcVertices[this.srcVertex].Distance, + this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance); this.prevStatus = this.status; this.status = Status.OutVertices; @@ -332,7 +284,7 @@ public PathCommand Vertex(ref double x, ref double y) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddVertex(double x, double y, double dist = 0) + private void AddVertex(double x, double y, double distance = 0) { if (this.srcVertices.Length > 1) { @@ -345,7 +297,7 @@ private void AddVertex(double x, double y, double dist = 0) } } - this.srcVertices.Add(new VertexDistance(x, y, dist)); + this.srcVertices.Add(new VertexDistance(x, y, distance)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -444,7 +396,17 @@ private void CalcArc(double x, double y, double dx1, double dy1, double dx2, dou this.AddPoint(x + dx2, y + dy2); } - private void CalcMiter(ref VertexDistance v0, ref VertexDistance v1, ref VertexDistance v2, double dx1, double dy1, double dx2, double dy2, LineJoin lj, double mlimit, double dbevel) + private void CalcMiter( + ref VertexDistance v0, + ref VertexDistance v1, + ref VertexDistance v2, + double dx1, + double dy1, + double dx2, + double dy2, + LineJoin lj, + double mlimit, + double dbevel) { double xi = v1.X; double yi = v1.Y; diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs index b0151127..f8d49b5f 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs @@ -29,7 +29,7 @@ public VertexDistance(double x, double y, double distance) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Measure(in VertexDistance vd) + public bool Measure(VertexDistance vd) { bool ret = (this.Distance = UtilityMethods.CalcDistance(this.X, this.Y, vd.X, vd.Y)) > Constants.Misc.VertexDistanceEpsilon; if (!ret) diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs index 35b75a74..a58cce99 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs @@ -39,7 +39,7 @@ public ActiveEdgeList(Span buffer) public void EnterEdge(int edgeIdx) => this.Buffer[this.count++] = edgeIdx | EnteringEdgeFlag; - public void LeaveEdge(int edgeIdx) + public readonly void LeaveEdge(int edgeIdx) { Span active = this.ActiveEdges; for (int i = 0; i < active.Length; i++) @@ -50,8 +50,6 @@ public void LeaveEdge(int edgeIdx) return; } } - - throw new ArgumentOutOfRangeException(nameof(edgeIdx)); } public void RemoveLeavingEdges() diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs index 32e4ba54..5ef039bc 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -96,9 +96,9 @@ public static PolygonScanner Create( IntersectionRule intersectionRule, MemoryAllocator allocator) { - using var multipolygon = TessellatedMultipolygon.Create(polygon, allocator); - var edges = ScanEdgeCollection.Create(multipolygon, allocator, subsampling); - var scanner = new PolygonScanner(edges, multipolygon.TotalVertexCount * 2, minY, maxY, subsampling, intersectionRule, allocator); + using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, allocator); + ScanEdgeCollection edges = ScanEdgeCollection.Create(multipolygon, allocator, subsampling); + PolygonScanner scanner = new(edges, multipolygon.TotalVertexCount * 2, minY, maxY, subsampling, intersectionRule, allocator); scanner.Init(); return scanner; } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs index bb56e870..3c6da448 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs @@ -84,125 +84,125 @@ internal static ScanEdgeCollection Create(TessellatedMultipolygon multiPolygon, walker.Move(true); // Emit last edge } - static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio) + return new ScanEdgeCollection(buffer, walker.EdgeCounter); + } + + private static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio) + { + int ri = 0; + if (Avx.IsSupported) { - int ri = 0; - if (Avx.IsSupported) + // If the length of the input buffer as a float array is a multiple of 16, we can use AVX instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector256FloatCount_x2 = Vector256.Count * 2; + int remainder = verticesLengthInFloats % vector256FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) { - // If the length of the input buffer as a float array is a multiple of 16, we can use AVX instructions: - int verticesLengthInFloats = vertices.Length * 2; - int vector256FloatCount_x2 = Vector256.Count * 2; - int remainder = verticesLengthInFloats % vector256FloatCount_x2; - int verticesLength = verticesLengthInFloats - remainder; + ri = vertices.Length - (remainder / 2); + nint maxIterations = verticesLength / (Vector256.Count * 2); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + + Vector256 ssRatio = Vector256.Create(subsamplingRatio); + Vector256 inverseSsRatio = Vector256.Create(1F / subsamplingRatio); + Vector256 half = Vector256.Create(.5F); - if (verticesLength > 0) + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) { - ri = vertices.Length - (remainder / 2); - nint maxIterations = verticesLength / (Vector256.Count * 2); - ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); - ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - - Vector256 ssRatio = Vector256.Create(subsamplingRatio); - Vector256 inverseSsRatio = Vector256.Create(1F / subsamplingRatio); - Vector256 half = Vector256.Create(.5F); - - // For every 1 vector we add to the destination we read 2 from the vertices. - for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) - { - // Load 8 PointF - Vector256 points1 = Unsafe.Add(ref sourceBase, j); - Vector256 points2 = Unsafe.Add(ref sourceBase, j + 1); - - // Shuffle the points to group the Y properties - Vector128 points1Y = Sse.Shuffle(points1.GetLower(), points1.GetUpper(), 0b11_01_11_01); - Vector128 points2Y = Sse.Shuffle(points2.GetLower(), points2.GetUpper(), 0b11_01_11_01); - Vector256 pointsY = Vector256.Create(points1Y, points2Y); - - // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. - // https://www.ocf.berkeley.edu/~horie/rounding.html - Vector256 rounded = Avx.RoundToPositiveInfinity(Avx.Subtract(Avx.Multiply(pointsY, ssRatio), half)); - Unsafe.Add(ref destinationBase, i) = Avx.Multiply(rounded, inverseSsRatio); - } + // Load 8 PointF + Vector256 points1 = Unsafe.Add(ref sourceBase, j); + Vector256 points2 = Unsafe.Add(ref sourceBase, j + 1); + + // Shuffle the points to group the Y properties + Vector128 points1Y = Sse.Shuffle(points1.GetLower(), points1.GetUpper(), 0b11_01_11_01); + Vector128 points2Y = Sse.Shuffle(points2.GetLower(), points2.GetUpper(), 0b11_01_11_01); + Vector256 pointsY = Vector256.Create(points1Y, points2Y); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + // https://www.ocf.berkeley.edu/~horie/rounding.html + Vector256 rounded = Avx.RoundToPositiveInfinity(Avx.Subtract(Avx.Multiply(pointsY, ssRatio), half)); + Unsafe.Add(ref destinationBase, i) = Avx.Multiply(rounded, inverseSsRatio); } } - else if (Sse41.IsSupported) + } + else if (Sse41.IsSupported) + { + // If the length of the input buffer as a float array is a multiple of 8, we can use Sse instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector128FloatCount_x2 = Vector128.Count * 2; + int remainder = verticesLengthInFloats % vector128FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) { - // If the length of the input buffer as a float array is a multiple of 8, we can use Sse instructions: - int verticesLengthInFloats = vertices.Length * 2; - int vector128FloatCount_x2 = Vector128.Count * 2; - int remainder = verticesLengthInFloats % vector128FloatCount_x2; - int verticesLength = verticesLengthInFloats - remainder; + ri = vertices.Length - (remainder / 2); + nint maxIterations = verticesLength / (Vector128.Count * 2); + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - if (verticesLength > 0) + Vector128 ssRatio = Vector128.Create(subsamplingRatio); + Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); + Vector128 half = Vector128.Create(.5F); + + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) { - ri = vertices.Length - (remainder / 2); - nint maxIterations = verticesLength / (Vector128.Count * 2); - ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); - ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - - Vector128 ssRatio = Vector128.Create(subsamplingRatio); - Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); - Vector128 half = Vector128.Create(.5F); - - // For every 1 vector we add to the destination we read 2 from the vertices. - for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) - { - // Load 4 PointF - Vector128 points1 = Unsafe.Add(ref sourceBase, j); - Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); - - // Shuffle the points to group the Y properties - Vector128 pointsY = Sse.Shuffle(points1, points2, 0b11_01_11_01); - - // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. - // https://www.ocf.berkeley.edu/~horie/rounding.html - Vector128 rounded = Sse41.RoundToPositiveInfinity(Sse.Subtract(Sse.Multiply(pointsY, ssRatio), half)); - Unsafe.Add(ref destinationBase, i) = Sse.Multiply(rounded, inverseSsRatio); - } + // Load 4 PointF + Vector128 points1 = Unsafe.Add(ref sourceBase, j); + Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); + + // Shuffle the points to group the Y properties + Vector128 pointsY = Sse.Shuffle(points1, points2, 0b11_01_11_01); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + // https://www.ocf.berkeley.edu/~horie/rounding.html + Vector128 rounded = Sse41.RoundToPositiveInfinity(Sse.Subtract(Sse.Multiply(pointsY, ssRatio), half)); + Unsafe.Add(ref destinationBase, i) = Sse.Multiply(rounded, inverseSsRatio); } } - else if (AdvSimd.IsSupported) + } + else if (AdvSimd.IsSupported) + { + // If the length of the input buffer as a float array is a multiple of 8, we can use AdvSimd instructions: + int verticesLengthInFloats = vertices.Length * 2; + int vector128FloatCount_x2 = Vector128.Count * 2; + int remainder = verticesLengthInFloats % vector128FloatCount_x2; + int verticesLength = verticesLengthInFloats - remainder; + + if (verticesLength > 0) { - // If the length of the input buffer as a float array is a multiple of 8, we can use AdvSimd instructions: - int verticesLengthInFloats = vertices.Length * 2; - int vector128FloatCount_x2 = Vector128.Count * 2; - int remainder = verticesLengthInFloats % vector128FloatCount_x2; - int verticesLength = verticesLengthInFloats - remainder; + ri = vertices.Length - (remainder / 2); + nint maxIterations = verticesLength / (Vector128.Count * 2); + ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - if (verticesLength > 0) + Vector128 ssRatio = Vector128.Create(subsamplingRatio); + Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); + + // For every 1 vector we add to the destination we read 2 from the vertices. + for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) { - ri = vertices.Length - (remainder / 2); - nint maxIterations = verticesLength / (Vector128.Count * 2); - ref Vector128 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(vertices)); - ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - - Vector128 ssRatio = Vector128.Create(subsamplingRatio); - Vector128 inverseSsRatio = Vector128.Create(1F / subsamplingRatio); - - // For every 1 vector we add to the destination we read 2 from the vertices. - for (nint i = 0, j = 0; i < maxIterations; i++, j += 2) - { - // Load 4 PointF - Vector128 points1 = Unsafe.Add(ref sourceBase, j); - Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); - - // Shuffle the points to group the Y - Vector128 pointsY = AdvSimdShuffle(points1, points2, 0b11_01_11_01); - - // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. - Vector128 rounded = AdvSimd.RoundAwayFromZero(AdvSimd.Multiply(pointsY, ssRatio)); - Unsafe.Add(ref destinationBase, i) = AdvSimd.Multiply(rounded, inverseSsRatio); - } - } - } + // Load 4 PointF + Vector128 points1 = Unsafe.Add(ref sourceBase, j); + Vector128 points2 = Unsafe.Add(ref sourceBase, j + 1); - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; + // Shuffle the points to group the Y + Vector128 pointsY = AdvSimdShuffle(points1, points2, 0b11_01_11_01); + + // Multiply by the subsampling ratio, round, then multiply by the inverted subsampling ratio and assign. + Vector128 rounded = AdvSimd.RoundAwayFromZero(AdvSimd.Multiply(pointsY, ssRatio)); + Unsafe.Add(ref destinationBase, i) = AdvSimd.Multiply(rounded, inverseSsRatio); + } } } - return new ScanEdgeCollection(buffer, walker.EdgeCounter); + for (; ri < vertices.Length; ri++) + { + destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 319b80d17b14f42a32e42a1256123f4b72c56f08 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 30 Jan 2024 23:04:14 +1000 Subject: [PATCH 07/33] Wire up options --- src/ImageSharp.Drawing/Shapes/JointStyle.cs | 2 +- .../Shapes/OutlinePathExtensions.cs | 34 ++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/JointStyle.cs b/src/ImageSharp.Drawing/Shapes/JointStyle.cs index ca8cb115..c1464824 100644 --- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs +++ b/src/ImageSharp.Drawing/Shapes/JointStyle.cs @@ -19,7 +19,7 @@ public enum JointStyle Round = 1, /// - /// Joints will generate to a long point unless the end of the point will exceed 20 times the width then we generate the joint using . + /// Joints will generate to a long point unless the end of the point will exceed 4 times the width then we generate the joint using . /// Miter = 2 } diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index 2cc071fd..14430f19 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -34,11 +34,7 @@ public static IPath GenerateOutline(this IPath path, float width) /// The style to apply to the end caps. /// A new representing the outline. /// Thrown when an offset cannot be calculated. -#pragma warning disable RCS1163 // Unused parameter -#pragma warning disable IDE0060 // Remove unused parameter public static IPath GenerateOutline(this IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) -#pragma warning restore IDE0060 // Remove unused parameter -#pragma warning restore RCS1163 // Unused parameter { if (width <= 0) { @@ -47,11 +43,10 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi List stroked = []; - // TODO: Wire up options - PolygonStroker stroker = new() { Width = width, LineJoin = LineJoin.BevelJoin, LineCap = LineCap.Butt }; + PolygonStroker stroker = new() { Width = width, LineJoin = GetLineJoin(jointStyle), LineCap = GetLineCap(endCapStyle) }; foreach (ISimplePath simplePath in path.Flatten()) { - stroked.Add(new Polygon(stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed).ToArray())); + stroked.Add(new Polygon(stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed || endCapStyle is EndCapStyle.Polygon or EndCapStyle.Joined).ToArray())); } return new ComplexPolygon(stroked); @@ -116,14 +111,11 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan paths = path.Flatten(); - - // TODO: Wire up options - PolygonStroker stroker = new() { Width = width, LineJoin = LineJoin.BevelJoin, LineCap = LineCap.Butt }; - + PolygonStroker stroker = new() { Width = width, LineJoin = GetLineJoin(jointStyle), LineCap = GetLineCap(endCapStyle) }; PathsF stroked = []; List buffer = []; - foreach (ISimplePath simplePath in paths) + + foreach (ISimplePath simplePath in path.Flatten()) { bool online = !invert; float targetLength = pattern[0] * width; @@ -226,4 +218,20 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan value switch + { + JointStyle.Square => LineJoin.BevelJoin, + JointStyle.Round => LineJoin.RoundJoin, + _ => LineJoin.MiterJoin, + }; + + private static LineCap GetLineCap(EndCapStyle value) + => value switch + { + EndCapStyle.Round => LineCap.Round, + EndCapStyle.Square => LineCap.Square, + _ => LineCap.Butt, + }; } From 0a70463955a7e06f4589af44a189af4b14ebaaef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 8 Feb 2024 20:36:57 +1000 Subject: [PATCH 08/33] Cleanup --- .../Shapes/PolygonClipper/VertexDistance.cs | 146 +----------------- 1 file changed, 2 insertions(+), 144 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs index f8d49b5f..89383756 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs @@ -43,40 +43,6 @@ public bool Measure(VertexDistance vd) internal static class Constants { - public struct Cover - { - public const int Shift = 8; - public const int Size = 1 << Shift; - public const int Mask = Size - 1; - public const int None = 0; - public const int Full = Mask; - } - - public struct PolySubpixel - { - public const int Shift = 8; - public const int ShiftMul2 = Shift << 1; - public const int Scale = 1 << Shift; - public const int Mask = Scale - 1; - } - - public struct Antialias - { - public const int Shift = 8; - public const int Scale = 0x100; - public const int Scale2 = 0x200; - public const int Mask = 0xff; - public const int Mask2 = 0x1ff; - } - - public struct GradientSubpixel - { - public const int Shift = 4; - public const int Scale = 1 << Shift; - public const int Mask = Scale - 1; - public const int DownscaleShift = 8 - Shift; - } - public struct Misc { public const double BezierArcAngleEpsilon = 0.01; @@ -97,55 +63,7 @@ public struct Misc internal static unsafe class UtilityMethods { - private static readonly Random Random = new Random(); - - private static ReadOnlySpan SqrtTable => new ushort[] { 0, 2048, 2896, 3547, 4096, 4579, 5017, 5418, 5793, 6144, 6476, 6792, 7094, 7384, 7663, 7932, 8192, 8444, 8689, 8927, 9159, 9385, 9606, 9822, 10033, 10240, 10443, 10642, 10837, 11029, 11217, 11403, 11585, 11765, 11942, 12116, 12288, 12457, 12625, 12790, 12953, 13114, 13273, 13430, 13585, 13738, 13890, 14040, 14189, 14336, 14482, 14626, 14768, 14910, 15050, 15188, 15326, 15462, 15597, 15731, 15864, 15995, 16126, 16255, 16384, 16512, 16638, 16764, 16888, 17012, 17135, 17257, 17378, 17498, 17618, 17736, 17854, 17971, 18087, 18203, 18318, 18432, 18545, 18658, 18770, 18882, 18992, 19102, 19212, 19321, 19429, 19537, 19644, 19750, 19856, 19961, 20066, 20170, 20274, 20377, 20480, 20582, 20684, 20785, 20886, 20986, 21085, 21185, 21283, 21382, 21480, 21577, 21674, 21771, 21867, 21962, 22058, 22153, 22247, 22341, 22435, 22528, 22621, 22713, 22806, 22897, 22989, 23080, 23170, 23261, 23351, 23440, 23530, 23619, 23707, 23796, 23884, 23971, 24059, 24146, 24232, 24319, 24405, 24491, 24576, 24661, 24746, 24831, 24915, 24999, 25083, 25166, 25249, 25332, 25415, 25497, 25580, 25661, 25743, 25824, 25905, 25986, 26067, 26147, 26227, 26307, 26387, 26466, 26545, 26624, 26703, 26781, 26859, 26937, 27015, 27092, 27170, 27247, 27324, 27400, 27477, 27553, 27629, 27705, 27780, 27856, 27931, 28006, 28081, 28155, 28230, 28304, 28378, 28452, 28525, 28599, 28672, 28745, 28818, 28891, 28963, 29035, 29108, 29180, 29251, 29323, 29394, 29466, 29537, 29608, 29678, 29749, 29819, 29890, 29960, 30030, 30099, 30169, 30238, 30308, 30377, 30446, 30515, 30583, 30652, 30720, 30788, 30856, 30924, 30992, 31059, 31127, 31194, 31261, 31328, 31395, 31462, 31529, 31595, 31661, 31727, 31794, 31859, 31925, 31991, 32056, 32122, 32187, 32252, 32317, 32382, 32446, 32511, 32575, 32640, 32704, 32768, 32832, 32896, 32959, 33023, 33086, 33150, 33213, 33276, 33339, 33402, 33465, 33527, 33590, 33652, 33714, 33776, 33839, 33900, 33962, 34024, 34086, 34147, 34208, 34270, 34331, 34392, 34453, 34514, 34574, 34635, 34695, 34756, 34816, 34876, 34936, 34996, 35056, 35116, 35176, 35235, 35295, 35354, 35413, 35472, 35531, 35590, 35649, 35708, 35767, 35825, 35884, 35942, 36001, 36059, 36117, 36175, 36233, 36291, 36348, 36406, 36464, 36521, 36578, 36636, 36693, 36750, 36807, 36864, 36921, 36978, 37034, 37091, 37147, 37204, 37260, 37316, 37372, 37429, 37485, 37540, 37596, 37652, 37708, 37763, 37819, 37874, 37929, 37985, 38040, 38095, 38150, 38205, 38260, 38315, 38369, 38424, 38478, 38533, 38587, 38642, 38696, 38750, 38804, 38858, 38912, 38966, 39020, 39073, 39127, 39181, 39234, 39287, 39341, 39394, 39447, 39500, 39553, 39606, 39659, 39712, 39765, 39818, 39870, 39923, 39975, 40028, 40080, 40132, 40185, 40237, 40289, 40341, 40393, 40445, 40497, 40548, 40600, 40652, 40703, 40755, 40806, 40857, 40909, 40960, 41011, 41062, 41113, 41164, 41215, 41266, 41317, 41368, 41418, 41469, 41519, 41570, 41620, 41671, 41721, 41771, 41821, 41871, 41922, 41972, 42021, 42071, 42121, 42171, 42221, 42270, 42320, 42369, 42419, 42468, 42518, 42567, 42616, 42665, 42714, 42763, 42813, 42861, 42910, 42959, 43008, 43057, 43105, 43154, 43203, 43251, 43300, 43348, 43396, 43445, 43493, 43541, 43589, 43637, 43685, 43733, 43781, 43829, 43877, 43925, 43972, 44020, 44068, 44115, 44163, 44210, 44258, 44305, 44352, 44400, 44447, 44494, 44541, 44588, 44635, 44682, 44729, 44776, 44823, 44869, 44916, 44963, 45009, 45056, 45103, 45149, 45195, 45242, 45288, 45334, 45381, 45427, 45473, 45519, 45565, 45611, 45657, 45703, 45749, 45795, 45840, 45886, 45932, 45977, 46023, 46069, 46114, 46160, 46205, 46250, 46296, 46341, 46386, 46431, 46477, 46522, 46567, 46612, 46657, 46702, 46746, 46791, 46836, 46881, 46926, 46970, 47015, 47059, 47104, 47149, 47193, 47237, 47282, 47326, 47370, 47415, 47459, 47503, 47547, 47591, 47635, 47679, 47723, 47767, 47811, 47855, 47899, 47942, 47986, 48030, 48074, 48117, 48161, 48204, 48248, 48291, 48335, 48378, 48421, 48465, 48508, 48551, 48594, 48637, 48680, 48723, 48766, 48809, 48852, 48895, 48938, 48981, 49024, 49067, 49109, 49152, 49195, 49237, 49280, 49322, 49365, 49407, 49450, 49492, 49535, 49577, 49619, 49661, 49704, 49746, 49788, 49830, 49872, 49914, 49956, 49998, 50040, 50082, 50124, 50166, 50207, 50249, 50291, 50332, 50374, 50416, 50457, 50499, 50540, 50582, 50623, 50665, 50706, 50747, 50789, 50830, 50871, 50912, 50954, 50995, 51036, 51077, 51118, 51159, 51200, 51241, 51282, 51323, 51364, 51404, 51445, 51486, 51527, 51567, 51608, 51649, 51689, 51730, 51770, 51811, 51851, 51892, 51932, 51972, 52013, 52053, 52093, 52134, 52174, 52214, 52254, 52294, 52334, 52374, 52414, 52454, 52494, 52534, 52574, 52614, 52654, 52694, 52734, 52773, 52813, 52853, 52892, 52932, 52972, 53011, 53051, 53090, 53130, 53169, 53209, 53248, 53287, 53327, 53366, 53405, 53445, 53484, 53523, 53562, 53601, 53640, 53679, 53719, 53758, 53797, 53836, 53874, 53913, 53952, 53991, 54030, 54069, 54108, 54146, 54185, 54224, 54262, 54301, 54340, 54378, 54417, 54455, 54494, 54532, 54571, 54609, 54647, 54686, 54724, 54762, 54801, 54839, 54877, 54915, 54954, 54992, 55030, 55068, 55106, 55144, 55182, 55220, 55258, 55296, 55334, 55372, 55410, 55447, 55485, 55523, 55561, 55599, 55636, 55674, 55712, 55749, 55787, 55824, 55862, 55900, 55937, 55975, 56012, 56049, 56087, 56124, 56162, 56199, 56236, 56273, 56311, 56348, 56385, 56422, 56459, 56497, 56534, 56571, 56608, 56645, 56682, 56719, 56756, 56793, 56830, 56867, 56903, 56940, 56977, 57014, 57051, 57087, 57124, 57161, 57198, 57234, 57271, 57307, 57344, 57381, 57417, 57454, 57490, 57527, 57563, 57599, 57636, 57672, 57709, 57745, 57781, 57817, 57854, 57890, 57926, 57962, 57999, 58035, 58071, 58107, 58143, 58179, 58215, 58251, 58287, 58323, 58359, 58395, 58431, 58467, 58503, 58538, 58574, 58610, 58646, 58682, 58717, 58753, 58789, 58824, 58860, 58896, 58931, 58967, 59002, 59038, 59073, 59109, 59144, 59180, 59215, 59251, 59286, 59321, 59357, 59392, 59427, 59463, 59498, 59533, 59568, 59603, 59639, 59674, 59709, 59744, 59779, 59814, 59849, 59884, 59919, 59954, 59989, 60024, 60059, 60094, 60129, 60164, 60199, 60233, 60268, 60303, 60338, 60373, 60407, 60442, 60477, 60511, 60546, 60581, 60615, 60650, 60684, 60719, 60753, 60788, 60822, 60857, 60891, 60926, 60960, 60995, 61029, 61063, 61098, 61132, 61166, 61201, 61235, 61269, 61303, 61338, 61372, 61406, 61440, 61474, 61508, 61542, 61576, 61610, 61644, 61678, 61712, 61746, 61780, 61814, 61848, 61882, 61916, 61950, 61984, 62018, 62051, 62085, 62119, 62153, 62186, 62220, 62254, 62287, 62321, 62355, 62388, 62422, 62456, 62489, 62523, 62556, 62590, 62623, 62657, 62690, 62724, 62757, 62790, 62824, 62857, 62891, 62924, 62957, 62991, 63024, 63057, 63090, 63124, 63157, 63190, 63223, 63256, 63289, 63323, 63356, 63389, 63422, 63455, 63488, 63521, 63554, 63587, 63620, 63653, 63686, 63719, 63752, 63785, 63817, 63850, 63883, 63916, 63949, 63982, 64014, 64047, 64080, 64113, 64145, 64178, 64211, 64243, 64276, 64309, 64341, 64374, 64406, 64439, 64471, 64504, 64536, 64569, 64601, 64634, 64666, 64699, 64731, 64763, 64796, 64828, 64861, 64893, 64925, 64957, 64990, 65022, 65054, 65086, 65119, 65151, 65183, 65215, 65247, 65279, 65312, 65344, 65376, 65408, 65440, 65472, 65504 }; - - private static ReadOnlySpan ElderBitTable => new byte[] { 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7 }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RoundToI32(double v) => (int)(v < 0.0 ? v - 0.5 : v + 0.5); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RoundToU32(double v) => (int)(v + 0.5); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double Rand(double x) => ((((Random.Next() << 15) | Random.Next()) & 0x3FFFFFFF) % 1000000) * x / 1000000.0; - - public static double Rand(double x1, double x2) - { - int r = Random.Next() & 0x7FFF; - - return (r * (x2 - x1) / 32768.0) + x1; - } - - public static bool IsEqualEps(double v1, double v2, double epsilon) => Math.Abs(v1 - v2) <= epsilon; - - public static byte InvertByte(byte x) => (byte)~x; - - public static bool PointInTriangle(double x1, double y1, double x2, double y2, double x3, double y3, double x, double y) - { - bool cp1 = CrossProduct(x1, y1, x2, y2, x, y) < 0.0; - bool cp2 = CrossProduct(x2, y2, x3, y3, x, y) < 0.0; - bool cp3 = CrossProduct(x3, y3, x1, y1, x, y) < 0.0; - - return cp1 == cp2 && cp2 == cp3; - } - - public static double CalcLinePointDistance(double x1, double y1, double x2, double y2, double x, double y) - { - double dx = x2 - x1; - double dy = y2 - y1; - double d = Math.Sqrt((dx * dx) + (dy * dy)); - - if (d < Constants.Misc.VertexDistanceEpsilon) - { - return CalcDistance(x1, y1, x, y); - } - - return (((x - x2) * dy) - ((y - y2) * dx)) / d; - } - public static double CalcDistance(double x1, double y1, double x2, double y2) { double dx = x2 - x1; @@ -154,15 +72,7 @@ public static double CalcDistance(double x1, double y1, double x2, double y2) return Math.Sqrt((dx * dx) + (dy * dy)); } - public static void CalcOrthogonal(double thickness, double x1, double y1, double x2, double y2, out double x, out double y) - { - double dx = x2 - x1; - double dy = y2 - y1; - double d = Math.Sqrt((dx * dx) + (dy * dy)); - x = thickness * dy / d; - y = -thickness * dx / d; - } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool CalcIntersection(double ax, double ay, double bx, double by, double cx, double cy, double dx, double dy, ref double x, ref double y) { double num = ((ay - cy) * (dx - cx)) - ((ax - cx) * (dy - cy)); @@ -180,58 +90,6 @@ public static bool CalcIntersection(double ax, double ay, double bx, double by, return true; } - public static double CalcSquareDistance(double x1, double y1, double x2, double y2) - { - double dx = x2 - x1; - double dy = y2 - y1; - - return (dx * dx) + (dy * dy); - } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double CrossProduct(double x1, double y1, double x2, double y2, double x, double y) => ((x - x2) * (y2 - y1)) - ((y - y2) * (x2 - x1)); - - public static int FastSqrt(int val) - { - int t = val; - int shift = 11; - - int bit = t >> 24; - if (bit != 0) - { - bit = ElderBitTable[bit] + 24; - } - else - { - bit = (t >> 16) & 0xFF; - if (bit != 0) - { - bit = ElderBitTable[bit] + 16; - } - else - { - bit = (t >> 8) & 0xFF; - if (bit != 0) - { - bit = ElderBitTable[bit] + 8; - } - else - { - bit = ElderBitTable[t]; - } - } - } - - bit -= 9; - - if (bit <= 0) - { - return SqrtTable[val] >> shift; - } - - bit = (bit >> 1) + (bit & 1); - shift -= bit; - val >>= bit << 1; - - return SqrtTable[val] >> shift; - } } From 2152cecb0f5cb86c0c54b29349252dada16a73b0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 7 Mar 2024 21:07:48 +1000 Subject: [PATCH 09/33] Use latest ImageSharp build --- .../ImageSharpLogo.cs | 2 +- .../Extensions/GraphicsOptionsExtensions.cs | 11 +- .../ImageSharp.Drawing.csproj | 4 +- .../Processing/GradientBrush.cs | 3 +- .../Processing/PathGradientBrush.cs | 16 +-- .../Processing/PatternBrush.cs | 8 +- src/ImageSharp.Drawing/Processing/Pen.cs | 5 +- .../Processors/Text/RichTextGlyphRenderer.cs | 2 +- .../Processing/RecolorBrush.cs | 17 ++- .../Drawing/EllipseStressTest.cs | 53 +++++++++ .../Drawing/DrawBezierTests.cs | 8 +- .../Drawing/DrawComplexPolygonTests.cs | 16 +-- .../Drawing/DrawPathTests.cs | 4 +- .../Drawing/DrawingRobustnessTests.cs | 43 ++++++- .../Drawing/FillComplexPolygonTests.cs | 4 +- .../Drawing/FillLinearGradientBrushTests.cs | 44 ++++--- .../Drawing/FillOutsideBoundsTests.cs | 2 +- .../Drawing/FillPathGradientBrushTests.cs | 9 +- .../Drawing/FillPatternBrushTests.cs | 107 +++++++++--------- .../Drawing/SolidBezierTests.cs | 6 +- .../Drawing/Text/DrawTextOnImageTests.cs | 2 +- .../Issues/Issue_241.cs | 2 +- .../Issues/Issue_270.cs | 4 +- .../Issues/Issue_28_108.cs | 26 ++--- .../Issues/Issues_55_59.cs | 4 +- tests/ImageSharp.Drawing.Tests/TestFormat.cs | 76 ++++--------- .../BasicTestPatternProvider.cs | 26 ++--- .../ImageProviders/SolidProvider.cs | 2 +- .../ImageProviders/TestPatternProvider.cs | 46 ++++---- .../TestUtilities/ImagingTestCaseUtility.cs | 6 +- .../TestUtilities/TestEnvironment.cs | 2 +- .../TestUtilities/TestImageExtensions.cs | 2 +- .../TestUtilities/TestPixel.cs | 8 +- .../TestUtilities/TestUtils.cs | 23 ++-- .../Tests/TestImageProviderTests.cs | 5 +- .../Tests/TestUtilityExtensionsTests.cs | 9 +- 36 files changed, 305 insertions(+), 302 deletions(-) create mode 100644 tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs diff --git a/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs b/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs index 71380b05..00bd3114 100644 --- a/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs +++ b/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs @@ -62,7 +62,7 @@ public static void SaveLogo(float size, string path) img.Mutate(i => i.Fill(colors[s], segments[s].Transform(scaler))); } - img.Mutate(i => i.Fill(new Rgba32(0, 0, 0, 170), new ComplexPolygon(new EllipsePolygon(center, 161f), new EllipsePolygon(center, 61f)).Transform(scaler))); + img.Mutate(i => i.Fill(Color.FromPixel(new Rgba32(0, 0, 0, 170)), new ComplexPolygon(new EllipsePolygon(center, 161f), new EllipsePolygon(center, 61f)).Transform(scaler))); string fullPath = System.IO.Path.GetFullPath(System.IO.Path.Combine("Output", path)); diff --git a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs index 31fcc925..299ac333 100644 --- a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs +++ b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; - namespace SixLabors.ImageSharp.Drawing; /// @@ -28,20 +26,19 @@ public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Co return false; } - if (options.AlphaCompositionMode != PixelAlphaCompositionMode.SrcOver - && options.AlphaCompositionMode != PixelAlphaCompositionMode.Src) + if (options.AlphaCompositionMode is not PixelAlphaCompositionMode.SrcOver and not PixelAlphaCompositionMode.Src) { return false; } - const float Opaque = 1F; + const float opaque = 1f; - if (options.BlendPercentage != Opaque) + if (options.BlendPercentage != opaque) { return false; } - if (((Vector4)color).W != Opaque) + if (color.ToScaledVector4().W != opaque) { return false; } diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 644a96f9..7f7a6645 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -45,8 +45,8 @@ - - + + \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 4b2364d9..789363b1 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -4,7 +4,6 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Utilities; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -138,7 +137,7 @@ protected GradientBrushApplicator( float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); // TODO: This should use premultiplied vectors to avoid bad blends e.g. red -> brown <- green. - return new Color(Vector4.Lerp((Vector4)from.Color, (Vector4)to.Color, onLocalGradient)).ToPixel(); + return Color.FromScaledVector(Vector4.Lerp(from.Color.ToScaledVector4(), to.Color.ToScaledVector4(), onLocalGradient)).ToPixel(); } } diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index e697db3f..a5d9773e 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -87,7 +87,7 @@ public override BrushApplicator CreateApplicator( this.hasSpecialCenterColor); private static Color CalculateCenterColor(Color[] colors) - => new(colors.Select(c => (Vector4)c).Aggregate((p1, p2) => p1 + p2) / colors.Length); + => Color.FromScaledVector(colors.Select(c => c.ToScaledVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length); private static float DistanceBetween(Vector2 p1, Vector2 p2) => (p2 - p1).Length(); @@ -115,8 +115,8 @@ public Edge(Vector2 start, Vector2 end, Color startColor, Color endColor) { this.Start = start; this.End = end; - this.StartColor = (Vector4)startColor; - this.EndColor = (Vector4)endColor; + this.StartColor = startColor.ToScaledVector4(); + this.EndColor = endColor.ToScaledVector4(); this.length = DistanceBetween(this.End, this.Start); } @@ -204,7 +204,7 @@ public PathGradientBrushApplicator( Vector2[] points = edges.Select(s => s.Start).ToArray(); this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count; - this.centerColor = (Vector4)centerColor; + this.centerColor = centerColor.ToScaledVector4(); this.hasSpecialCenterColor = hasSpecialCenterColor; this.centerPixel = centerColor.ToPixel(); this.maxDistance = points.Select(p => p - this.center).Max(d => d.Length()); @@ -240,9 +240,7 @@ public PathGradientBrushApplicator( + (u * this.edges[0].EndColor) + (v * this.edges[2].StartColor); - TPixel px = default; - px.FromScaledVector4(pointColor); - return px; + return TPixel.FromScaledVector4(pointColor); } Vector2 direction = Vector2.Normalize(point - this.center); @@ -263,9 +261,7 @@ public PathGradientBrushApplicator( Vector4 color = Vector4.Lerp(edgeColor, this.centerColor, ratio); - TPixel pixel = default; - pixel.FromScaledVector4(color); - return pixel; + return TPixel.FromScaledVector4(color); } } diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 1891f3ed..92bf3db8 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -54,8 +54,8 @@ public PatternBrush(Color foreColor, Color backColor, bool[,] pattern) /// The pattern. internal PatternBrush(Color foreColor, Color backColor, in DenseMatrix pattern) { - var foreColorVector = (Vector4)foreColor; - var backColorVector = (Vector4)backColor; + Vector4 foreColorVector = foreColor.ToScaledVector4(); + Vector4 backColorVector = backColor.ToScaledVector4(); this.pattern = new DenseMatrix(pattern.Columns, pattern.Rows); this.patternVector = new DenseMatrix(pattern.Columns, pattern.Rows); for (int i = 0; i < pattern.Data.Length; i++) @@ -156,8 +156,8 @@ public PatternBrushApplicator( public override void Apply(Span scanline, int x, int y) { int patternY = y % this.pattern.Rows; - Span amounts = this.blenderBuffers.AmountSpan.Slice(0, scanline.Length); - Span overlays = this.blenderBuffers.OverlaySpan.Slice(0, scanline.Length); + Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; + Span overlays = this.blenderBuffers.OverlaySpan[..scanline.Length]; for (int i = 0; i < scanline.Length; i++) { diff --git a/src/ImageSharp.Drawing/Processing/Pen.cs b/src/ImageSharp.Drawing/Processing/Pen.cs index 8e277552..b33b9070 100644 --- a/src/ImageSharp.Drawing/Processing/Pen.cs +++ b/src/ImageSharp.Drawing/Processing/Pen.cs @@ -51,11 +51,12 @@ protected Pen(Brush strokeFill, float strokeWidth) protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern) { Guard.NotNull(strokeFill, nameof(strokeFill)); - Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth)); + + // Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth)); Guard.NotNull(strokePattern, nameof(strokePattern)); this.StrokeFill = strokeFill; - this.StrokeWidth = strokeWidth; + this.StrokeWidth = strokeWidth > 0 ? strokeWidth : 1; this.pattern = strokePattern; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index b937e84a..c4942a52 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -144,7 +144,7 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara /// public void SetColor(GlyphColor color) - => this.currentColor = new Color(new Rgba32(color.Red, color.Green, color.Blue, color.Alpha)); + => this.currentColor = Color.FromPixel(new Rgba32(color.Red, color.Green, color.Blue, color.Alpha)); public override TextDecorations EnabledDecorations() { diff --git a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index 797e88d6..c881adaa 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -100,15 +100,14 @@ public RecolorBrushApplicator( float threshold) : base(configuration, options, source) { - this.sourceColor = sourceColor.ToVector4(); + this.sourceColor = sourceColor.ToScaledVector4(); this.targetColorPixel = targetColor; + // TODO: Review this. We can skip the conversion from/to Vector4. // Lets hack a min max extremes for a color space by letting the IPackedPixel clamp our values to something in the correct spaces :) - var maxColor = default(TPixel); - maxColor.FromVector4(new Vector4(float.MaxValue)); - var minColor = default(TPixel); - minColor.FromVector4(new Vector4(float.MinValue)); - this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold; + TPixel maxColor = TPixel.FromScaledVector4(Vector4.One); + TPixel minColor = TPixel.FromVector4(Vector4.Zero); + this.threshold = Vector4.DistanceSquared(maxColor.ToScaledVector4(), minColor.ToScaledVector4()) * threshold; this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width); } @@ -118,7 +117,7 @@ public RecolorBrushApplicator( { // Offset the requested pixel by the value in the rectangle (the shapes position) TPixel result = this.Target[x, y]; - var background = result.ToVector4(); + Vector4 background = result.ToScaledVector4(); float distance = Vector4.DistanceSquared(background, this.sourceColor); if (distance <= this.threshold) { @@ -136,8 +135,8 @@ public RecolorBrushApplicator( /// public override void Apply(Span scanline, int x, int y) { - Span amounts = this.blenderBuffers.AmountSpan.Slice(0, scanline.Length); - Span overlays = this.blenderBuffers.OverlaySpan.Slice(0, scanline.Length); + Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; + Span overlays = this.blenderBuffers.OverlaySpan[..scanline.Length]; for (int i = 0; i < scanline.Length; i++) { diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs new file mode 100644 index 00000000..f5c35606 --- /dev/null +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; + +[ShortRunJob] +public class EllipseStressTest +{ + private Image image; + private readonly int width = 2560; + private readonly int height = 1369; + private readonly Random random = new(); + + [GlobalSetup] + public void Setup() => this.image = new(this.width, this.height, Color.White.ToPixel()); + + [Benchmark] + public void DrawImageSharp() + { + for (int i = 0; i < 20_000; i++) + { + Color brushColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); + Color penColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); + + float r = this.Rand(20f) + 1f; + float x = this.Rand(this.width); + float y = this.Rand(this.height); + EllipsePolygon ellipse = new(new PointF(x, y), r); + this.image.Mutate( + m => + m.Fill(Brushes.Solid(brushColor), ellipse) + .Draw(Pens.Solid(penColor, this.Rand(5)), ellipse)); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.image.SaveAsPng(TestEnvironment.GetFullPath("artifacts\\ellipse-stress.png")); + this.image.Dispose(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float Rand(float x) + => ((float)(((this.random.Next() << 15) | this.random.Next()) & 0x3FFFFFFF) % 1000000) * x / 1000000f; +} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs index 9ebdf256..649f92c1 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs @@ -33,16 +33,14 @@ public void DrawBeziers(TestImageProvider provider, string color new Vector2(300, 400) }; - Rgba32 rgba = TestUtils.GetColorByName(colorName); - rgba.A = alpha; - Color color = rgba; + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha/255f); FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; provider.RunValidatingProcessorTest( x => x.DrawBeziers(color, 5f, points), testDetails, - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs index eb658a68..a13d3024 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs @@ -12,32 +12,30 @@ public class DrawComplexPolygonTests { [Theory] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, true, false, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, true, false)] + //[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, true, false, false)] + //[WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, true, false)] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, false, false, true)] public void DrawComplexPolygon(TestImageProvider provider, bool overlap, bool transparent, bool dashed) where TPixel : unmanaged, IPixel { - var simplePath = new Polygon(new LinearLineSegment( + Polygon simplePath = new(new LinearLineSegment( new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300))); - var hole1 = new Polygon(new LinearLineSegment( + Polygon hole1 = new(new LinearLineSegment( new Vector2(37, 85), overlap ? new Vector2(130, 40) : new Vector2(93, 85), new Vector2(65, 137))); IPath clipped = simplePath.Clip(hole1); - Rgba32 colorRgba = Color.White; + Color color = Color.White; if (transparent) { - colorRgba.A = 150; + color = color.WithAlpha(150 / 255f); } - Color color = colorRgba; - string testDetails = string.Empty; if (overlap) { @@ -56,6 +54,8 @@ public void DrawComplexPolygon(TestImageProvider provider, bool Pen pen = dashed ? Pens.Dash(color, 5f) : Pens.Solid(color, 5f); + // clipped = new RectangularPolygon(RectangleF.FromLTRB(60, 260, 200, 280)); + provider.RunValidatingProcessorTest( x => x.Draw(pen, clipped), testDetails, diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs index bbd196d7..8e204f48 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs @@ -41,9 +41,7 @@ public void DrawPath(TestImageProvider provider, string colorNam Path path = new(linearSegment, bezierSegment, ellipticArcSegment1, ellipticArcSegment2); - Rgba32 rgba = TestUtils.GetColorByName(colorName); - rgba.A = alpha; - Color color = rgba; + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255f); FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index fbc04393..7ffaea65 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -75,7 +75,7 @@ private static void CompareToSkiaResultsImpl(TestImageProvider provider, throw new Exception(result.DifferencePercentageString); } - [Theory(Skip = "For local testing")] + [Theory]//(Skip = "For local testing")] [WithSolidFilledImages(3600, 2400, "Black", PixelTypes.Rgba32, TestImages.GeoJson.States, 16, 30, 30)] public void LargeGeoJson_Lines(TestImageProvider provider, string geoJsonFile, int aa, float sx, float sy) { @@ -130,7 +130,7 @@ private Image FillGeoJsonPolygons(TestImageProvider provider, st { rnd.NextBytes(rgb); - var color = Color.FromRgb(rgb[0], rgb[1], rgb[2]); + var color = Color.FromPixel(new Rgba32(rgb[0], rgb[1], rgb[2])); image.Mutate(c => c.FillPolygon(options, color, loop)); } @@ -154,7 +154,6 @@ public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, i IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); using Image image = provider.GetImage(); - foreach (PointF[] loop in points) { image.Mutate(c => c.DrawLine(Color.White, 1.0f, loop)); @@ -168,7 +167,40 @@ public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, i image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } - [Theory(Skip = "For local experiments only")] + [Theory] + [WithSolidFilledImages(400 * 3, 400 * 3, "Black", PixelTypes.Rgba32, 3)] + [WithSolidFilledImages(400 * 5, 400 * 5, "Black", PixelTypes.Rgba32, 5)] + [WithSolidFilledImages(400 * 10, 400 * 10, "Black", PixelTypes.Rgba32, 10)] + public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provider, int scale) + { + string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); + + FeatureCollection features = JsonConvert.DeserializeObject(jsonContent); + + Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); + + Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) + * Matrix3x2.CreateScale(60, 60); + IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); + + using Image image = provider.GetImage(); + var pen = new SolidPen(new SolidBrush(Color.White), 1.0f); + foreach (PointF[] loop in points) + { + IPath outline = pen.GeneratePath(new Path(loop).Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); + outline = outline.Transform(Matrix3x2.CreateScale(scale, scale)); + image.Mutate(c => c.Fill(pen.StrokeFill, outline)); + } + + // Strict comparer, because the image is sparse: + ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); + + string details = $"Scale({scale})"; + image.DebugSave(provider, details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + } + + [Theory]//(Skip = "For local experiments only")] [InlineData(0)] [InlineData(5000)] [InlineData(9000)] @@ -199,7 +231,8 @@ public void Missisippi_Skia(int offset) path.LineTo(pts[0].X, pts[0].Y); } - var imageInfo = new SKImageInfo(10000, 10000); + int sze = offset == 0 ? 400 : 10000; + var imageInfo = new SKImageInfo(sze, sze); using var paint = new SKPaint { diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs index 9128d0c8..ee7272d6 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs @@ -29,10 +29,10 @@ public void ComplexPolygon_SolidFill(TestImageProvider provider, IPath clipped = simplePath.Clip(hole1); - Rgba32 colorRgba = Color.HotPink; + Color colorRgba = Color.HotPink; if (transparent) { - colorRgba.A = 150; + colorRgba = colorRgba.WithAlpha(150 / 255f); } Color color = colorRgba; diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs index 53374e39..b6205c33 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs @@ -25,7 +25,7 @@ public void WithEqualColorsReturnsUnicolorImage(TestImageProvider(TestImageProvider pro TolerantComparer, image => { - var unicolorLinearGradientBrush = new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = new( new Point(0, 0), new Point(image.Width, 0), GradientRepetitionMode.None, @@ -73,7 +73,7 @@ public void HorizontalReturnsUnicolorColumns(TestImageProvider p Color red = Color.Red; Color yellow = Color.Yellow; - var unicolorLinearGradientBrush = new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = new( new Point(0, 0), new Point(image.Width, 0), GradientRepetitionMode.None, @@ -101,7 +101,7 @@ public void HorizontalGradientWithRepMode( Color red = Color.Red; Color yellow = Color.Yellow; - var unicolorLinearGradientBrush = new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = new( new Point(0, 0), new Point(image.Width / 10, 0), repetitionMode, @@ -147,8 +147,8 @@ public void WithDoubledStopsProduceDashedPatterns( using (Image image = provider.GetImage()) { - var unicolorLinearGradientBrush = - new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = + new( new Point(0, 0), new Point(image.Width, 0), GradientRepetitionMode.None, @@ -189,7 +189,7 @@ public void VerticalBrushReturnsUnicolorRows( Color red = Color.Red; Color yellow = Color.Yellow; - var unicolorLinearGradientBrush = new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = new( new Point(0, 0), new Point(0, image.Height), GradientRepetitionMode.None, @@ -247,8 +247,8 @@ public void DiagonalReturnsCorrectImages( Color red = Color.Red; Color yellow = Color.Yellow; - var unicolorLinearGradientBrush = - new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = + new( new Point(startX, startY), new Point(endX, endY), GradientRepetitionMode.None, @@ -308,16 +308,15 @@ public void ArbitraryGradients( Color.Red }; - var coloringVariant = new StringBuilder(); - var colorStops = new ColorStop[stopPositions.Length]; + StringBuilder coloringVariant = new(); + ColorStop[] colorStops = new ColorStop[stopPositions.Length]; for (int i = 0; i < stopPositions.Length; i++) { Color color = colors[stopColorCodes[i % colors.Length]]; float position = stopPositions[i]; colorStops[i] = new ColorStop(position, color); - Rgba32 rgba = color; - coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", rgba.ToHex(), position); + coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToHex(), position); } FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; @@ -325,7 +324,7 @@ public void ArbitraryGradients( provider.VerifyOperation( image => { - var unicolorLinearGradientBrush = new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = new( new Point(startX, startY), new Point(endX, endY), GradientRepetitionMode.None, @@ -356,16 +355,15 @@ public void MultiplePointGradients( Color.White, Color.Lime }; - var coloringVariant = new StringBuilder(); - var colorStops = new ColorStop[stopPositions.Length]; + StringBuilder coloringVariant = new(); + ColorStop[] colorStops = new ColorStop[stopPositions.Length]; for (int i = 0; i < stopPositions.Length; i++) { Color color = colors[stopColorCodes[i % colors.Length]]; float position = stopPositions[i]; colorStops[i] = new ColorStop(position, color); - Rgba32 rgba = color; - coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", rgba.ToHex(), position); + coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToHex(), position); } FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; @@ -373,7 +371,7 @@ public void MultiplePointGradients( provider.VerifyOperation( image => { - var unicolorLinearGradientBrush = new LinearGradientBrush( + LinearGradientBrush unicolorLinearGradientBrush = new( new Point(startX, startY), new Point(endX, endY), GradientRepetitionMode.None, @@ -402,19 +400,19 @@ void ApplyGloss(IImageProcessingContext ctx) { Size size = ctx.GetCurrentSize(); IPathCollection glossPath = BuildGloss(size.Width, size.Height); - var graphicsOptions = new GraphicsOptions + GraphicsOptions graphicsOptions = new() { Antialias = true, ColorBlendingMode = PixelColorBlendingMode.Normal, AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop }; - var linearGradientBrush = new LinearGradientBrush(new Point(0, 0), new Point(0, size.Height / 2), GradientRepetitionMode.Repeat, new ColorStop(0, Color.White.WithAlpha(0.5f)), new ColorStop(1, Color.White.WithAlpha(0.25f))); + LinearGradientBrush linearGradientBrush = new(new Point(0, 0), new Point(0, size.Height / 2), GradientRepetitionMode.Repeat, new ColorStop(0, Color.White.WithAlpha(0.5f)), new ColorStop(1, Color.White.WithAlpha(0.25f))); ctx.SetGraphicsOptions(graphicsOptions).Fill(linearGradientBrush, glossPath); } IPathCollection BuildGloss(int imageWidth, int imageHeight) { - var pathBuilder = new PathBuilder(); + PathBuilder pathBuilder = new(); pathBuilder.AddLine(new PointF(0, 0), new PointF(imageWidth, 0)); pathBuilder.AddLine(new PointF(imageWidth, 0), new PointF(imageWidth, imageHeight * 0.4f)); pathBuilder.AddQuadraticBezier(new PointF(imageWidth, imageHeight * 0.4f), new PointF(imageWidth / 2, imageHeight * 0.6f), new PointF(0, imageHeight * 0.4f)); @@ -431,7 +429,7 @@ public void BrushApplicatorIsThreadSafeIssue1044(TestImageProvider { - var brush = new PathGradientBrush( + PathGradientBrush brush = new( new[] { new PointF(0, 0), new PointF(200, 0), new PointF(200, 200), new PointF(0, 200), new PointF(0, 0) }, new[] { Color.Red, Color.Yellow, Color.Green, Color.DarkCyan, Color.Red }); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs index 40ea2b7e..f3109f66 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs @@ -20,7 +20,7 @@ public void DrawRectactangleOutsideBoundsDrawingArea(int xpos) int width = 100; int height = 100; - using (var image = new Image(width, height, Color.Red)) + using (var image = new Image(width, height, Color.Red.ToPixel())) { var rectangle = new Rectangle(xpos, 0, width, height); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs index fac1779b..1eef7159 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs @@ -57,14 +57,7 @@ public void FillTriangleWithGreyscale(TestImageProvider provider { PointF[] points = { new PointF(10, 0), new PointF(20, 20), new PointF(0, 20) }; - var c1 = default(Rgba32); - var c2 = default(Rgba32); - var c3 = default(Rgba32); - new HalfSingle(-1).ToRgba32(ref c1); - new HalfSingle(0).ToRgba32(ref c2); - new HalfSingle(1).ToRgba32(ref c3); - - Color[] colors = { new Color(c1), new Color(c2), new Color(c3) }; + Color[] colors = { Color.FromPixel(new HalfSingle(-1)), Color.FromPixel(new HalfSingle(0)), Color.FromPixel(new HalfSingle(1)) }; var brush = new PathGradientBrush(points, colors); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs index 9391c945..5b3a791d 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs @@ -10,48 +10,47 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; public class FillPatternBrushTests { - private void Test(string name, Rgba32 background, Brush brush, Rgba32[,] expectedPattern) + private static void Test(string name, Color background, Brush brush, Color[,] expectedPattern) { string path = TestEnvironment.CreateOutputDirectory("Drawing", "FillPatternBrushTests"); - using (var image = new Image(20, 20)) - { - image.Mutate(x => x.Fill(background).Fill(brush)); - image.Save($"{path}/{name}.png"); + using Image image = new(20, 20); + image.Mutate(x => x.Fill(background).Fill(brush)); + + image.Save($"{path}/{name}.png"); - Buffer2D sourcePixels = image.GetRootFramePixelBuffer(); + Buffer2D sourcePixels = image.GetRootFramePixelBuffer(); - // lets pick random spots to start checking - var r = new Random(); - var expectedPatternFast = new DenseMatrix(expectedPattern); - int xStride = expectedPatternFast.Columns; - int yStride = expectedPatternFast.Rows; - int offsetX = r.Next(image.Width / xStride) * xStride; - int offsetY = r.Next(image.Height / yStride) * yStride; - for (int x = 0; x < xStride; x++) + // lets pick random spots to start checking + Random r = new(); + DenseMatrix expectedPatternFast = new(expectedPattern); + int xStride = expectedPatternFast.Columns; + int yStride = expectedPatternFast.Rows; + int offsetX = r.Next(image.Width / xStride) * xStride; + int offsetY = r.Next(image.Height / yStride) * yStride; + for (int x = 0; x < xStride; x++) + { + for (int y = 0; y < yStride; y++) { - for (int y = 0; y < yStride; y++) + int actualX = x + offsetX; + int actualY = y + offsetY; + Rgba32 expected = expectedPatternFast[y, x].ToPixel(); // inverted pattern + Rgba32 actual = sourcePixels[actualX, actualY]; + if (expected != actual) { - int actualX = x + offsetX; - int actualY = y + offsetY; - Rgba32 expected = expectedPatternFast[y, x]; // inverted pattern - Rgba32 actual = sourcePixels[actualX, actualY]; - if (expected != actual) - { - Assert.True(false, $"Expected {expected} but found {actual} at ({actualX},{actualY})"); - } + Assert.Fail($"Expected {expected} but found {actual} at ({actualX},{actualY})"); } } - - image.Mutate(x => x.Resize(80, 80, KnownResamplers.NearestNeighbor)); - image.Save($"{path}/{name}x4.png"); } + + image.Mutate(x => x.Resize(80, 80, KnownResamplers.NearestNeighbor)); + image.Save($"{path}/{name}x4.png"); } [Fact] public void ImageShouldBeFloodFilledWithPercent10() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, @@ -59,7 +58,7 @@ public void ImageShouldBeFloodFilledWithPercent10() { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "Percent10", Color.Blue, Brushes.Percent10(Color.HotPink, Color.LimeGreen), @@ -69,7 +68,7 @@ public void ImageShouldBeFloodFilledWithPercent10() [Fact] public void ImageShouldBeFloodFilledWithPercent10Transparent() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, @@ -77,7 +76,7 @@ public void ImageShouldBeFloodFilledWithPercent10Transparent() { Color.Blue, Color.Blue, Color.Blue, Color.Blue } }; - this.Test( + Test( "Percent10_Transparent", Color.Blue, Brushes.Percent10(Color.HotPink), @@ -87,7 +86,7 @@ public void ImageShouldBeFloodFilledWithPercent10Transparent() [Fact] public void ImageShouldBeFloodFilledWithPercent20() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, @@ -95,7 +94,7 @@ public void ImageShouldBeFloodFilledWithPercent20() { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen } }; - this.Test( + Test( "Percent20", Color.Blue, Brushes.Percent20(Color.HotPink, Color.LimeGreen), @@ -105,7 +104,7 @@ public void ImageShouldBeFloodFilledWithPercent20() [Fact] public void ImageShouldBeFloodFilledWithPercent20_transparent() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, @@ -113,7 +112,7 @@ public void ImageShouldBeFloodFilledWithPercent20_transparent() { Color.Blue, Color.Blue, Color.HotPink, Color.Blue } }; - this.Test( + Test( "Percent20_Transparent", Color.Blue, Brushes.Percent20(Color.HotPink), @@ -123,7 +122,7 @@ public void ImageShouldBeFloodFilledWithPercent20_transparent() [Fact] public void ImageShouldBeFloodFilledWithHorizontal() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, @@ -131,7 +130,7 @@ public void ImageShouldBeFloodFilledWithHorizontal() { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "Horizontal", Color.Blue, Brushes.Horizontal(Color.HotPink, Color.LimeGreen), @@ -141,7 +140,7 @@ public void ImageShouldBeFloodFilledWithHorizontal() [Fact] public void ImageShouldBeFloodFilledWithHorizontal_transparent() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, @@ -149,7 +148,7 @@ public void ImageShouldBeFloodFilledWithHorizontal_transparent() { Color.Blue, Color.Blue, Color.Blue, Color.Blue } }; - this.Test( + Test( "Horizontal_Transparent", Color.Blue, Brushes.Horizontal(Color.HotPink), @@ -159,7 +158,7 @@ public void ImageShouldBeFloodFilledWithHorizontal_transparent() [Fact] public void ImageShouldBeFloodFilledWithMin() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, @@ -167,7 +166,7 @@ public void ImageShouldBeFloodFilledWithMin() { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } }; - this.Test( + Test( "Min", Color.Blue, Brushes.Min(Color.HotPink, Color.LimeGreen), @@ -177,7 +176,7 @@ public void ImageShouldBeFloodFilledWithMin() [Fact] public void ImageShouldBeFloodFilledWithMin_transparent() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, @@ -185,7 +184,7 @@ public void ImageShouldBeFloodFilledWithMin_transparent() { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, }; - this.Test( + Test( "Min_Transparent", Color.Blue, Brushes.Min(Color.HotPink), @@ -195,7 +194,7 @@ public void ImageShouldBeFloodFilledWithMin_transparent() [Fact] public void ImageShouldBeFloodFilledWithVertical() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, @@ -203,7 +202,7 @@ public void ImageShouldBeFloodFilledWithVertical() { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "Vertical", Color.Blue, Brushes.Vertical(Color.HotPink, Color.LimeGreen), @@ -213,7 +212,7 @@ public void ImageShouldBeFloodFilledWithVertical() [Fact] public void ImageShouldBeFloodFilledWithVertical_transparent() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, @@ -221,7 +220,7 @@ public void ImageShouldBeFloodFilledWithVertical_transparent() { Color.Blue, Color.HotPink, Color.Blue, Color.Blue } }; - this.Test( + Test( "Vertical_Transparent", Color.Blue, Brushes.Vertical(Color.HotPink), @@ -231,7 +230,7 @@ public void ImageShouldBeFloodFilledWithVertical_transparent() [Fact] public void ImageShouldBeFloodFilledWithForwardDiagonal() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink }, { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, @@ -239,7 +238,7 @@ public void ImageShouldBeFloodFilledWithForwardDiagonal() { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "ForwardDiagonal", Color.Blue, Brushes.ForwardDiagonal(Color.HotPink, Color.LimeGreen), @@ -249,7 +248,7 @@ public void ImageShouldBeFloodFilledWithForwardDiagonal() [Fact] public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.Blue, Color.Blue, Color.Blue, Color.HotPink }, { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, @@ -257,7 +256,7 @@ public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() { Color.HotPink, Color.Blue, Color.Blue, Color.Blue } }; - this.Test( + Test( "ForwardDiagonal_Transparent", Color.Blue, Brushes.ForwardDiagonal(Color.HotPink), @@ -267,7 +266,7 @@ public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() [Fact] public void ImageShouldBeFloodFilledWithBackwardDiagonal() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, @@ -275,7 +274,7 @@ public void ImageShouldBeFloodFilledWithBackwardDiagonal() { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink } }; - this.Test( + Test( "BackwardDiagonal", Color.Blue, Brushes.BackwardDiagonal(Color.HotPink, Color.LimeGreen), @@ -285,7 +284,7 @@ public void ImageShouldBeFloodFilledWithBackwardDiagonal() [Fact] public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent() { - var expectedPattern = new Rgba32[,] + Color[,] expectedPattern = new Color[,] { { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, @@ -293,7 +292,7 @@ public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent() { Color.Blue, Color.Blue, Color.Blue, Color.HotPink } }; - this.Test( + Test( "BackwardDiagonal_Transparent", Color.Blue, Brushes.BackwardDiagonal(Color.HotPink), diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs index ea3fb983..e9adfcb1 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs @@ -49,10 +49,8 @@ public void OverlayByFilledPolygonOpacity(TestImageProvider prov new Vector2(300, 400) }; - Rgba32 color = Color.HotPink; - color.A = 150; - - using (var image = provider.GetImage() as Image) + Color color = Color.HotPink.WithAlpha(150/255f); + using (Image image = provider.GetImage() as Image) { image.Mutate(x => x.BackgroundColor(Color.Blue)); image.Mutate(x => x.Fill(color, new Polygon(new CubicBezierLineSegment(simplePath)))); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs index 6ab6f7f1..a357bf22 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs @@ -129,7 +129,7 @@ public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688_2(Test { Font font = CreateFont(TestFonts.OpenSans, 39); string text = new('a', 10000); - Rgba32 color = Color.Black; + Color color = Color.Black; var point = new PointF(100, 100); using Image img = provider.GetImage(); diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs index 96634e5e..1d208a0d 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs @@ -26,7 +26,7 @@ public void DoesNotThrowArgumentOutOfRangeException() }; const string content = "TEST"; - using Image image = new Image(512, 256, Color.Black); + using Image image = new Image(512, 256, Color.Black.ToPixel()); image.Mutate(x => x.DrawText(opt, content, Brushes.Horizontal(Color.Orange))); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs index ace95bbc..43ad525b 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs @@ -29,8 +29,8 @@ public void DoesNotThrowArgumentOutOfRangeException() Font font = SystemFonts.CreateFont("Arial", minimumCrashingFontSize); Pen pen = Pens.Solid(Color.Black, 1); - using Image targetImage = new(targetImageWidth, targetImageHeight, Color.Wheat); - using Image imageBrushImage = new(sourceImageWidth, sourceImageHeight, Color.Black); + using Image targetImage = new(targetImageWidth, targetImageHeight, Color.Wheat.ToPixel()); + using Image imageBrushImage = new(sourceImageWidth, sourceImageHeight, Color.Black.ToPixel()); ImageBrush imageBrush = new(imageBrushImage); targetImage.Mutate(x => x.DrawText(CreateTextOptions(font, targetImageWidth), text, imageBrush, pen)); diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs index 5dd10f95..128329bd 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs @@ -9,8 +9,6 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Issues; public class Issue_28_108 { - private Rgba32 red = Color.Red.ToRgba32(); - [Theory] [InlineData(1F)] [InlineData(1.5F)] @@ -18,17 +16,17 @@ public class Issue_28_108 [InlineData(3F)] public void DrawingLineAtTopShouldDisplay(float stroke) { - using var image = new Image(Configuration.Default, 100, 100, Color.Black); + using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); image.Mutate(x => x .SetGraphicsOptions(g => g.Antialias = false) .DrawLine( - this.red, + Color.Red, stroke, new PointF(0, 0), new PointF(100, 0))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 0)); - Assert.All(locations, l => Assert.Equal(this.red, image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } [Theory] @@ -38,17 +36,17 @@ public void DrawingLineAtTopShouldDisplay(float stroke) [InlineData(3F)] public void DrawingLineAtBottomShouldDisplay(float stroke) { - using var image = new Image(Configuration.Default, 100, 100, Color.Black); + using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); image.Mutate(x => x .SetGraphicsOptions(g => g.Antialias = false) .DrawLine( - this.red, + Color.Red, stroke, new PointF(0, 99), new PointF(100, 99))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 99)); - Assert.All(locations, l => Assert.Equal(this.red, image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } [Theory] @@ -58,17 +56,17 @@ public void DrawingLineAtBottomShouldDisplay(float stroke) [InlineData(3F)] public void DrawingLineAtLeftShouldDisplay(float stroke) { - using var image = new Image(Configuration.Default, 100, 100, Color.Black); + using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); image.Mutate(x => x .SetGraphicsOptions(g => g.Antialias = false) .DrawLine( - this.red, + Color.Red, stroke, new PointF(0, 0), new PointF(0, 99))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 0, y: i)); - Assert.All(locations, l => Assert.Equal(this.red, image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } [Theory] @@ -78,16 +76,16 @@ public void DrawingLineAtLeftShouldDisplay(float stroke) [InlineData(3F)] public void DrawingLineAtRightShouldDisplay(float stroke) { - using var image = new Image(Configuration.Default, 100, 100, Color.Black); + using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); image.Mutate(x => x .SetGraphicsOptions(g => g.Antialias = false) .DrawLine( - this.red, + Color.Red, stroke, new PointF(99, 0), new PointF(99, 99))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 99, y: i)); - Assert.All(locations, l => Assert.Equal(this.red, image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs index 07698129..4101cfda 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs @@ -24,7 +24,7 @@ public void SimplifyOutOfRangeExceptionDrawLines() }; using var image = new Image(100, 100); - image.Mutate(imageContext => imageContext.DrawLine(new Rgba32(255, 0, 0), 1, line)); + image.Mutate(imageContext => imageContext.DrawLine(Color.FromPixel(new Rgba32(255, 0, 0)), 1, line)); } [Fact] @@ -37,6 +37,6 @@ public void SimplifyOutOfRangeExceptionDraw() new LinearLineSegment(new PointF(592.916f, 1155.754f), new PointF(592.0153f, 1156.238f))); using var image = new Image(2000, 2000); - image.Mutate(imageContext => imageContext.Draw(new Rgba32(255, 0, 0), 1, path)); + image.Mutate(imageContext => imageContext.Draw(Color.FromPixel(new Rgba32(255, 0, 0)), 1, path)); } } diff --git a/tests/ImageSharp.Drawing.Tests/TestFormat.cs b/tests/ImageSharp.Drawing.Tests/TestFormat.cs index 648b0507..dddd9234 100644 --- a/tests/ImageSharp.Drawing.Tests/TestFormat.cs +++ b/tests/ImageSharp.Drawing.Tests/TestFormat.cs @@ -263,75 +263,49 @@ public Task EncodeAsync(Image image, Stream stream, Cancellation public struct TestPixelForAgnosticDecode : IPixel { - public PixelOperations CreatePixelOperations() => new(); + public readonly Rgba32 ToRgba32() => default; - public void FromScaledVector4(Vector4 vector) - { - } + public readonly Vector4 ToScaledVector4() => default; - public Vector4 ToScaledVector4() => default; + public readonly Vector4 ToVector4() => default; - public void FromVector4(Vector4 vector) - { - } + public static PixelTypeInfo GetPixelTypeInfo() + => PixelTypeInfo.Create( + PixelComponentInfo.Create(2, 8, 8), + PixelColorType.Red | PixelColorType.Green, + PixelAlphaRepresentation.None); - public Vector4 ToVector4() => default; + public static PixelOperations CreatePixelOperations() => new(); - public void FromArgb32(Argb32 source) - { - } + public static TestPixelForAgnosticDecode FromScaledVector4(Vector4 vector) => default; - public void FromBgra5551(Bgra5551 source) - { - } + public static TestPixelForAgnosticDecode FromVector4(Vector4 vector) => default; - public void FromBgr24(Bgr24 source) - { - } + public static TestPixelForAgnosticDecode FromAbgr32(Abgr32 source) => default; - public void FromBgra32(Bgra32 source) - { - } + public static TestPixelForAgnosticDecode FromArgb32(Argb32 source) => default; - public void FromAbgr32(Abgr32 source) - { - } + public static TestPixelForAgnosticDecode FromBgra5551(Bgra5551 source) => default; - public void FromL8(L8 source) - { - } + public static TestPixelForAgnosticDecode FromBgr24(Bgr24 source) => default; - public void FromL16(L16 source) - { - } + public static TestPixelForAgnosticDecode FromBgra32(Bgra32 source) => default; - public void FromLa16(La16 source) - { - } + public static TestPixelForAgnosticDecode FromL8(L8 source) => default; - public void FromLa32(La32 source) - { - } + public static TestPixelForAgnosticDecode FromL16(L16 source) => default; - public void FromRgb24(Rgb24 source) - { - } + public static TestPixelForAgnosticDecode FromLa16(La16 source) => default; - public void FromRgba32(Rgba32 source) - { - } + public static TestPixelForAgnosticDecode FromLa32(La32 source) => default; - public void ToRgba32(ref Rgba32 dest) - { - } + public static TestPixelForAgnosticDecode FromRgb24(Rgb24 source) => default; - public void FromRgb48(Rgb48 source) - { - } + public static TestPixelForAgnosticDecode FromRgba32(Rgba32 source) => default; - public void FromRgba64(Rgba64 source) - { - } + public static TestPixelForAgnosticDecode FromRgb48(Rgb48 source) => default; + + public static TestPixelForAgnosticDecode FromRgba64(Rgba64 source) => default; public bool Equals(TestPixelForAgnosticDecode other) => false; } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs index a596a773..1af5fee7 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs @@ -8,10 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Tests; public abstract partial class TestImageProvider : IXunitSerializable { - public virtual TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) - { - throw new NotSupportedException("GetExpectedBasicTestPatternPixelAt(x,y) only works with BasicTestPattern"); - } + public virtual TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) => throw new NotSupportedException("GetExpectedBasicTestPatternPixelAt(x,y) only works with BasicTestPattern"); private class BasicTestPatternProvider : BlankProvider { @@ -46,16 +43,16 @@ public override Image GetImage() { Span row = accessor.GetRowSpan(y); - row.Slice(0, midX).Fill(TopLeftColor); - row.Slice(midX, this.Width - midX).Fill(TopRightColor); + row[..midX].Fill(TopLeftColor); + row[midX..this.Width].Fill(TopRightColor); } for (int y = midY; y < this.Height; y++) { Span row = accessor.GetRowSpan(y); - row.Slice(0, midX).Fill(BottomLeftColor); - row.Slice(midX, this.Width - midX).Fill(BottomRightColor); + row[..midX].Fill(BottomLeftColor); + row[midX..this.Width].Fill(BottomRightColor); } }); @@ -71,17 +68,10 @@ public override TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) { return x < midX ? TopLeftColor : TopRightColor; } - else - { - return x < midX ? BottomLeftColor : BottomRightColor; - } - } - private static TPixel GetBottomRightColor() - { - TPixel bottomRightColor = default; - bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f)); - return bottomRightColor; + return x < midX ? BottomLeftColor : BottomRightColor; } + + private static TPixel GetBottomRightColor() => TPixel.FromVector4(new Vector4(1f, 0f, 1f, 0.5f)); } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/SolidProvider.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/SolidProvider.cs index a3aa0ee6..0ff20b6a 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/SolidProvider.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/SolidProvider.cs @@ -51,7 +51,7 @@ public override string SourceFileOrDescription public override Image GetImage() { Image image = base.GetImage(); - Color color = new Rgba32(this.r, this.g, this.b, this.a); + Color color = Color.FromPixel(new Rgba32(this.r, this.g, this.b, this.a)); image.GetRootFramePixelBuffer().FastMemoryGroup.Fill(color.ToPixel()); return image; diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs index 0f17058c..9de3eda4 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs @@ -49,7 +49,7 @@ public override Image GetImage() { if (!TestImages.ContainsKey(this.SourceFileOrDescription)) { - var image = new Image(this.Width, this.Height); + Image image = new Image(this.Width, this.Height); DrawTestPattern(image); TestImages.Add(this.SourceFileOrDescription, image); } @@ -80,7 +80,7 @@ private static void VerticalBars(Buffer2D pixels) // topLeft int left = pixels.Width / 2; int right = pixels.Width; - int top = 0; + const int top = 0; int bottom = pixels.Height / 2; int stride = pixels.Width / 12; if (stride < 1) @@ -96,7 +96,7 @@ private static void VerticalBars(Buffer2D pixels) if (x % stride == 0) { p++; - p = p % PinkBluePixels.Length; + p %= PinkBluePixels.Length; } pixels[x, y] = PinkBluePixels[p]; @@ -110,9 +110,9 @@ private static void VerticalBars(Buffer2D pixels) private static void BlackWhiteChecker(Buffer2D pixels) { // topLeft - int left = 0; + const int left = 0; int right = pixels.Width / 2; - int top = 0; + const int top = 0; int bottom = pixels.Height / 2; int stride = pixels.Width / 6; @@ -122,7 +122,7 @@ private static void BlackWhiteChecker(Buffer2D pixels) if (y % stride is 0) { p++; - p = p % BlackWhitePixels.Length; + p %= BlackWhitePixels.Length; } int pstart = p; @@ -131,7 +131,7 @@ private static void BlackWhiteChecker(Buffer2D pixels) if (x % stride is 0) { p++; - p = p % BlackWhitePixels.Length; + p %= BlackWhitePixels.Length; } pixels[x, y] = BlackWhitePixels[p]; @@ -147,38 +147,36 @@ private static void BlackWhiteChecker(Buffer2D pixels) private static void TransparentGradients(Buffer2D pixels) { // topLeft - int left = 0; + const int left = 0; int right = pixels.Width / 2; int top = pixels.Height / 2; int bottom = pixels.Height; int height = (int)Math.Ceiling(pixels.Height / 6f); - var red = Color.Red.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern - var green = Color.Green.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern - var blue = Color.Blue.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern - - var c = default(TPixel); + System.Numerics.Vector4 red = Color.Red.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern + System.Numerics.Vector4 green = Color.Green.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern + System.Numerics.Vector4 blue = Color.Blue.ToPixel().ToVector4(); // use real color so we can see how it translates in the test pattern for (int x = left; x < right; x++) { - blue.W = red.W = green.W = (float)x / (float)right; + blue.W = red.W = green.W = x / (float)right; - c.FromVector4(red); + TPixel c = TPixel.FromVector4(red); int topBand = top; for (int y = topBand; y < top + height; y++) { pixels[x, y] = c; } - topBand = topBand + height; - c.FromVector4(green); + topBand += height; + c = TPixel.FromVector4(green); for (int y = topBand; y < topBand + height; y++) { pixels[x, y] = c; } - topBand = topBand + height; - c.FromVector4(blue); + topBand += height; + c = TPixel.FromVector4(blue); for (int y = topBand; y < bottom; y++) { pixels[x, y] = c; @@ -199,19 +197,15 @@ private static void Rainbow(Buffer2D pixels) int pixelCount = left * top; uint stepsPerPixel = (uint)(uint.MaxValue / pixelCount); - TPixel c = default; - var t = new Rgba32(0); + Rgba32 t = new(0); for (int x = left; x < right; x++) { for (int y = top; y < bottom; y++) { t.PackedValue += stepsPerPixel; - var v = t.ToVector4(); - - // v.W = (x - left) / (float)left; - c.FromVector4(v); - pixels[x, y] = c; + System.Numerics.Vector4 v = t.ToVector4(); + pixels[x, y] = TPixel.FromVector4(v); } } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImagingTestCaseUtility.cs index 66ac5949..f3c727ea 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -288,8 +288,7 @@ public static void ModifyPixel(ImageFrame img, int x, int y, byt where TPixel : unmanaged, IPixel { TPixel pixel = img[x, y]; - Rgba64 rgbaPixel = default; - rgbaPixel.FromScaledVector4(pixel.ToScaledVector4()); + Rgba64 rgbaPixel = Rgba64.FromScaledVector4(pixel.ToScaledVector4()); ushort change = (ushort)Math.Round((perChannelChange / 255F) * 65535F); if (rgbaPixel.R + perChannelChange <= 255) @@ -328,7 +327,6 @@ public static void ModifyPixel(ImageFrame img, int x, int y, byt rgbaPixel.A -= perChannelChange; } - pixel.FromRgba64(rgbaPixel); - img[x, y] = pixel; + img[x, y] = TPixel.FromRgba64(rgbaPixel); } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.cs index 91978246..89227491 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.cs @@ -80,7 +80,7 @@ private static string GetSolutionDirectoryFullPathImpl() return directory.FullName; } - private static string GetFullPath(string relativePath) => + public static string GetFullPath(string relativePath) => IOPath.Combine(SolutionDirectoryFullPath, relativePath) .Replace('\\', IOPath.DirectorySeparatorChar); diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestImageExtensions.cs index 874a5f65..16c03c23 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestImageExtensions.cs @@ -701,7 +701,7 @@ internal static Image ToGrayscaleImage(this Buffer2D buffer, floa { float value = bufferSpan[i] * scale; var v = new Vector4(value, value, value, 1f); - pixels[i].FromVector4(v); + pixels[i] = Rgba32.FromVector4(v); } return image; diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPixel.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPixel.cs index dea98213..531beed2 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPixel.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPixel.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; @@ -31,12 +32,7 @@ public TestPixel(float red, float green, float blue, float alpha) public static implicit operator TPixel(TestPixel d) => d?.AsPixel() ?? default; - public TPixel AsPixel() - { - TPixel pix = default; - pix.FromVector4(new System.Numerics.Vector4(this.Red, this.Green, this.Blue, this.Alpha)); - return pix; - } + public TPixel AsPixel() => TPixel.FromVector4(new Vector4(this.Red, this.Green, this.Blue, this.Alpha)); internal Span AsSpan() => new Span(new[] { this.AsPixel() }); diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestUtils.cs index a7f1309c..d8268521 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestUtils.cs @@ -55,9 +55,6 @@ public static bool IsEquivalentTo(this Image a, Image b, return false; } - var rgb1 = default(Rgb24); - var rgb2 = default(Rgb24); - Buffer2D pixA = a.GetRootFramePixelBuffer(); Buffer2D pixB = b.GetRootFramePixelBuffer(); for (int y = 0; y < a.Height; y++) @@ -76,12 +73,10 @@ public static bool IsEquivalentTo(this Image a, Image b, } else { - Rgba32 rgba = default; - ca.ToRgba32(ref rgba); - rgb1 = rgba.Rgb; - cb.ToRgba32(ref rgba); - rgb2 = rgba.Rgb; - + Rgba32 rgba = ca.ToRgba32(); + Rgb24 rgb1 = rgba.Rgb; + rgba = cb.ToRgba32(); + Rgb24 rgb2 = rgba.Rgb; if (!rgb1.Equals(rgb2)) { return false; @@ -111,7 +106,7 @@ public static IEnumerable> ExpandAllTypes(this Pi return PixelTypes2ClrTypes; } - var result = new Dictionary(); + Dictionary result = new Dictionary(); foreach (PixelTypes pt in AllConcretePixelTypes) { if (pixelTypes.HasAll(pt)) @@ -134,7 +129,7 @@ internal static bool HasAll(this PixelTypes pixelTypes, PixelTypes flagsToCheck) internal static Color GetColorByName(string colorName) { - var f = (FieldInfo)typeof(Color).GetMember(colorName)[0]; + FieldInfo f = (FieldInfo)typeof(Color).GetMember(colorName)[0]; return (Color)f.GetValue(null); } @@ -247,9 +242,9 @@ public static void RunValidatingProcessorTestOnWrappedMemoryImage( { Assert.True(image0.DangerousTryGetSinglePixelMemory(out Memory imageMem)); Span imageSpan = imageMem.Span; - var mmg = TestMemoryManager.CreateAsCopyOf(imageSpan); + TestMemoryManager mmg = TestMemoryManager.CreateAsCopyOf(imageSpan); - using (var image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height)) + using (Image image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height)) { image1.Mutate(process); image1.DebugSave( @@ -298,7 +293,7 @@ internal static void RunRectangleConstrainedValidatingProcessorTest( using (Image image = provider.GetImage()) { - var bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); + Rectangle bounds = new Rectangle(image.Width / 4, image.Width / 4, image.Width / 2, image.Height / 2); image.Mutate(x => process(x, bounds)); image.DebugSave(provider, testOutputDetails); image.CompareToReferenceOutput(comparer, provider, testOutputDetails); diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 404cb5f7..b94834fa 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Collections.Concurrent; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -304,12 +303,12 @@ public void Use_WithSolidFilledImagesAttribute(TestImageProvider Assert.Equal(20, img.Height); Buffer2D pixels = img.GetRootFramePixelBuffer(); - Rgba32 rgba = default; + for (int y = 0; y < pixels.Height; y++) { for (int x = 0; x < pixels.Width; x++) { - pixels[x, y].ToRgba32(ref rgba); + Rgba32 rgba = pixels[x, y].ToRgba32(); Assert.Equal(255, rgba.R); Assert.Equal(100, rgba.G); diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs index 688436e9..10a322cf 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs @@ -19,20 +19,17 @@ public TestUtilityExtensionsTests(ITestOutputHelper output) public static Image CreateTestImage() where TPixel : unmanaged, IPixel { - var image = new Image(10, 10); + Image image = new(10, 10); Buffer2D pixels = image.GetRootFramePixelBuffer(); for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { - var v = new Vector4(i, j, 0, 1); + Vector4 v = new(i, j, 0, 1); v /= 10; - var color = default(TPixel); - color.FromVector4(v); - - pixels[i, j] = color; + pixels[i, j] = TPixel.FromScaledVector4(v); } } From 4fdddc4ed3e689792c4358bbc5da077a6a9749ce Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:09:42 +0200 Subject: [PATCH 10/33] Added Blaze to some benchmarks --- ImageSharp.Drawing.sln | 6 + .../Drawing/DrawPolygon.cs | 141 +++++++++++++++++- .../Drawing/FillPolygon.cs | 80 +++++++++- .../ImageSharp.Drawing.Benchmarks.csproj | 1 + .../ImageSharp.Drawing.Benchmarks/Program.cs | 23 ++- 5 files changed, 246 insertions(+), 5 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index ffc24ef8..6bcfeec1 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -337,6 +337,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpBlaze", "..\SharpBlaze\SharpBlaze\SharpBlaze.csproj", "{FCEDD229-22BC-4B82-87DE-786BBFC52EDE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -359,6 +361,10 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU + {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index d0c85bf8..731e8bf4 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -4,14 +4,18 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Numerics; +using System.Runtime.InteropServices; using BenchmarkDotNet.Attributes; using GeoJSON.Net.Feature; using Newtonsoft.Json; +using SharpBlaze; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SkiaSharp; +using BlazeMatrix = SharpBlaze.Matrix; using SDPointF = System.Drawing.PointF; namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; @@ -21,13 +25,20 @@ public abstract class DrawPolygon private PointF[][] points; private Image image; + private bool isImageSharp; private SDPointF[][] sdPoints; private Bitmap sdBitmap; private Graphics sdGraphics; + private bool isSystem; private SKPath skPath; private SKSurface skSurface; + private bool isSkia; + + private Executor executor; + private DestinationImage vecDst; + private bool isBlaze; protected abstract int Width { get; } @@ -49,9 +60,11 @@ public void Setup() this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray(); this.skPath = new SKPath(); + foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2)) { this.skPath.MoveTo(ptArr[0].X, ptArr[1].Y); + for (int i = 1; i < ptArr.Length; i++) { this.skPath.LineTo(ptArr[i].X, ptArr[i].Y); @@ -60,6 +73,11 @@ public void Setup() this.skPath.LineTo(ptArr[0].X, ptArr[1].Y); } + this.executor = new SerialExecutor(); + this.vecDst = new DestinationImage(); + this.vecDst.UpdateSize(new IntSize(this.Width, this.Height)); + this.vecDst.ClearImage(); + this.image = new Image(this.Width, this.Height); this.sdBitmap = new Bitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); @@ -69,8 +87,44 @@ public void Setup() } [GlobalCleanup] - public void Cleanup() + public unsafe void Cleanup() { + string dir = "Images/DrawPolygon"; + Directory.CreateDirectory(dir); + + if (this.isImageSharp) + { + this.image.SaveAsPng(System.IO.Path.Combine(dir, "ImageSharp.png")); + this.isImageSharp = false; + } + + if (this.isBlaze) + { + using var blazeImage = Image.WrapMemory( + this.vecDst.GetImageData(), + this.vecDst.GetBytesPerRow() * this.vecDst.GetImageHeight(), + this.vecDst.GetImageWidth(), + this.vecDst.GetImageHeight()); + + blazeImage.SaveAsPng(System.IO.Path.Combine(dir, "Blaze.png")); + this.isBlaze = false; + } + + if (this.isSystem) + { + this.sdBitmap.Save(System.IO.Path.Combine(dir, "SystemDrawing.png")); + this.isSystem = false; + } + + if (this.isSkia) + { + using var skSnapshot = this.skSurface.Snapshot(); + using var skEncoded = skSnapshot.Encode(); + using var skFile = new FileStream(System.IO.Path.Combine(dir, "SkiaSharp.png"), FileMode.Create); + skEncoded.SaveTo(skFile); + this.isSkia = false; + } + this.image.Dispose(); this.sdGraphics.Dispose(); this.sdBitmap.Dispose(); @@ -87,11 +141,14 @@ public void SystemDrawing() { this.sdGraphics.DrawPolygon(pen, loop); } + + this.isSystem = true; } [Benchmark] public void ImageSharp() - => this.image.Mutate( + { + this.image.Mutate( c => { foreach (PointF[] loop in this.points) @@ -99,6 +156,8 @@ public void ImageSharp() c.DrawPolygon(Color.White, this.Thickness, loop); } }); + this.isImageSharp = true; + } [Benchmark(Baseline = true)] public void SkiaSharp() @@ -112,6 +171,84 @@ public void SkiaSharp() }; this.skSurface.Canvas.DrawPath(this.skPath, paint); + this.isSkia = true; + } + + [Benchmark] + public void Blaze() + { + VectorImageBuilder builder = new(); + + foreach (PointF[] loop in this.points) + { + var loopPolygon = new Polygon(loop); + var brush = new Processing.SolidBrush(Color.White); + var pen = new SolidPen(brush, this.Thickness); + List> outline = GenerateOutlineList(loopPolygon, pen.StrokeWidth, pen.JointStyle, pen.EndCapStyle); + + foreach (List line in outline) + { + Span ptArr = CollectionsMarshal.AsSpan(line); + + builder.MoveTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); + for (int i = 1; i < ptArr.Length; i++) + { + builder.LineTo(new FloatPoint(ptArr[i].X, ptArr[i].Y)); + } + + builder.LineTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); + + builder.Close(); + } + } + + VectorImage image = builder.ToVectorImage(Color.White.ToPixel().PackedValue); + + this.vecDst.DrawImage(image, BlazeMatrix.Identity, this.executor); + + this.isBlaze = true; + } + + private static List> GenerateOutlineList(IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) + { + if (width <= 0) + { + return []; + } + + List> stroked = []; + + PolygonStroker stroker = new() { Width = width, LineJoin = GetLineJoin(jointStyle), LineCap = GetLineCap(endCapStyle) }; + foreach (ISimplePath simplePath in path.Flatten()) + { + bool isClosed = simplePath.IsClosed || endCapStyle is EndCapStyle.Polygon or EndCapStyle.Joined; + if (simplePath is Path concretePath) + { + if (concretePath.LineSegments.Count == 1 && concretePath.LineSegments[0] is LinearLineSegment lineSegment) + { + stroked.Add(stroker.ProcessPath(lineSegment.Flatten().Span, isClosed)); + continue; + } + } + + stroked.Add(stroker.ProcessPath(simplePath.Points.Span, isClosed)); + } + + return stroked; + + static LineJoin GetLineJoin(JointStyle value) => value switch + { + JointStyle.Square => LineJoin.BevelJoin, + JointStyle.Round => LineJoin.RoundJoin, + _ => LineJoin.MiterJoin, + }; + + static LineCap GetLineCap(EndCapStyle value) => value switch + { + EndCapStyle.Round => LineCap.Round, + EndCapStyle.Square => LineCap.Square, + _ => LineCap.Butt, + }; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs index 85d7f550..d635cb87 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs @@ -7,11 +7,13 @@ using BenchmarkDotNet.Attributes; using GeoJSON.Net.Feature; using Newtonsoft.Json; +using SharpBlaze; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SkiaSharp; +using BlazeMatrix = SharpBlaze.Matrix; using SDBitmap = System.Drawing.Bitmap; using SDPointF = System.Drawing.PointF; @@ -25,10 +27,20 @@ public abstract class FillPolygon private List skPaths; private Image image; + private bool isImageSharp; + private SDBitmap sdBitmap; private Graphics sdGraphics; + private bool isSystem; + private SKBitmap skBitmap; private SKCanvas skCanvas; + private bool isSkia; + + private VectorImage vecImage; + private Executor executor; + private DestinationImage vecDst; + private bool isBlaze; protected abstract int Width { get; } @@ -52,19 +64,34 @@ public void Setup() this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray(); this.skPaths = new List(); + + VectorImageBuilder vecImageBuilder = new(); + foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2)) { var skPath = new SKPath(); skPath.MoveTo(ptArr[0].X, ptArr[1].Y); + vecImageBuilder.MoveTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); + for (int i = 1; i < ptArr.Length; i++) { skPath.LineTo(ptArr[i].X, ptArr[i].Y); + vecImageBuilder.LineTo(new FloatPoint(ptArr[i].X, ptArr[i].Y)); } skPath.LineTo(ptArr[0].X, ptArr[1].Y); this.skPaths.Add(skPath); + + vecImageBuilder.LineTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); + vecImageBuilder.Close(); } + this.vecImage = vecImageBuilder.ToVectorImage(Color.White.ToPixel().PackedValue); + this.executor = new SerialExecutor(); + this.vecDst = new DestinationImage(); + this.vecDst.UpdateSize(new IntSize(this.Width, this.Height)); + this.vecDst.ClearImage(); + this.image = new Image(this.Width, this.Height); this.sdBitmap = new SDBitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); @@ -75,8 +102,42 @@ public void Setup() } [GlobalCleanup] - public void Cleanup() + public unsafe void Cleanup() { + string dir = "Images/FillPolygon"; + Directory.CreateDirectory(dir); + + if (this.isImageSharp) + { + this.image.SaveAsPng(System.IO.Path.Combine(dir, "ImageSharp.png")); + this.isImageSharp = false; + } + + if (this.isBlaze) + { + using var blazeImage = Image.WrapMemory( + this.vecDst.GetImageData(), + this.vecDst.GetBytesPerRow() * this.vecDst.GetImageHeight(), + this.vecDst.GetImageWidth(), + this.vecDst.GetImageHeight()); + + blazeImage.SaveAsPng(System.IO.Path.Combine(dir, "Blaze.png")); + this.isBlaze = false; + } + + if (this.isSystem) + { + this.sdBitmap.Save(System.IO.Path.Combine(dir, "SystemDrawing.png")); + this.isSystem = false; + } + + if (this.isSkia) + { + using var skFile = new FileStream(System.IO.Path.Combine(dir, "SkiaSharp.png"), FileMode.Create); + this.skBitmap.Encode(skFile, SKEncodedImageFormat.Png, 100); + this.isSkia = false; + } + this.image.Dispose(); this.sdGraphics.Dispose(); this.sdBitmap.Dispose(); @@ -97,11 +158,14 @@ public void SystemDrawing() { this.sdGraphics.FillPolygon(brush, loop); } + + this.isSystem = true; } [Benchmark] public void ImageSharp() - => this.image.Mutate(c => + { + this.image.Mutate(c => { foreach (Polygon polygon in this.polygons) { @@ -109,6 +173,9 @@ public void ImageSharp() } }); + this.isImageSharp = true; + } + [Benchmark(Baseline = true)] public void SkiaSharp() { @@ -123,6 +190,15 @@ public void SkiaSharp() }; this.skCanvas.DrawPath(path, paint); } + + this.isSkia = true; + } + + [Benchmark] + public void Blaze() + { + this.vecDst.DrawImage(this.vecImage, BlazeMatrix.Identity, this.executor); + this.isBlaze = true; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 25dd0315..dbead597 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -37,6 +37,7 @@ + diff --git a/tests/ImageSharp.Drawing.Benchmarks/Program.cs b/tests/ImageSharp.Drawing.Benchmarks/Program.cs index f1ac9ca7..97bb7feb 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Program.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Program.cs @@ -2,14 +2,35 @@ // Licensed under the Six Labors Split License. using System.Reflection; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; namespace SixLabors.ImageSharp.Drawing.Benchmarks; +public class InProcessConfig : ManualConfig +{ + public InProcessConfig() + { + AddLogger(ConsoleLogger.Default); + + AddColumnProvider(DefaultColumnProviders.Instance); + + AddExporter(DefaultExporters.Html, DefaultExporters.Csv); + + this.AddJob(Job.MediumRun + .WithToolchain(InProcessEmitToolchain.Instance)); + } +} + public class Program { public static void Main(string[] args) { - new BenchmarkSwitcher(typeof(Program).GetTypeInfo().Assembly).Run(args); + new BenchmarkSwitcher(typeof(Program).GetTypeInfo().Assembly).Run(args, new InProcessConfig()); } } From 99a628341d1a5fe5151d664bfb40343beeb18406 Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Mon, 8 Apr 2024 10:29:47 +0200 Subject: [PATCH 11/33] Optimized ArrayBuilder --- .../Shapes/PolygonClipper/ArrayBuilder{T}.cs | 81 ++++++++++++------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs index db54c89c..916592fd 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; @@ -13,7 +14,6 @@ internal struct ArrayBuilder where T : struct { private const int DefaultCapacity = 4; - private const int MaxCoreClrArrayLength = 0x7FeFFFFF; // Starts out null, initialized on first Add. private T[]? data; @@ -41,17 +41,14 @@ public int Length set { - if (value != this.size) + if (value > 0) { - if (value > 0) - { - this.EnsureCapacity(value); - this.size = value; - } - else - { - this.size = 0; - } + this.EnsureCapacity(value); + this.size = value; + } + else + { + this.size = 0; } } } @@ -81,10 +78,27 @@ public readonly ref T this[int index] public void Add(T item) { int position = this.size; + T[]? array = this.data; - // Expand the array. - this.Length++; - this.data![position] = item; + if (array != null && (uint)position < (uint)array.Length) + { + this.size = position + 1; + array[position] = item; + } + else + { + this.AddWithResize(item); + } + } + + // Non-inline from Add to improve its code quality as uncommon path + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddWithResize(T item) + { + int size = this.size; + this.Grow(size + 1); + this.size = size + 1; + this.data[size] = item; } /// @@ -110,26 +124,33 @@ private void EnsureCapacity(int min) int length = this.data?.Length ?? 0; if (length < min) { - // Same expansion algorithm as List. - uint newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; - if (newCapacity > MaxCoreClrArrayLength) - { - newCapacity = MaxCoreClrArrayLength; - } + this.Grow(min); + } + } - if (newCapacity < min) - { - newCapacity = (uint)min; - } + [MemberNotNull(nameof(this.data))] + private void Grow(int capacity) + { + // Same expansion algorithm as List. + int length = this.data?.Length ?? 0; + int newCapacity = length == 0 ? DefaultCapacity : length * 2; + if ((uint)newCapacity > Array.MaxLength) + { + newCapacity = Array.MaxLength; + } - T[] array = new T[newCapacity]; + if (newCapacity < capacity) + { + newCapacity = capacity; + } - if (this.size > 0) - { - Array.Copy(this.data!, array, this.size); - } + T[] array = new T[newCapacity]; - this.data = array; + if (this.size > 0) + { + Array.Copy(this.data!, array, this.size); } + + this.data = array; } } From 4569fe7996ddbe83bc546103973c4d4a78449de9 Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:23:07 +0200 Subject: [PATCH 12/33] Cleaned up PolygonStroker.Accumulate --- .../Shapes/PolygonClipper/PolygonStroker.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs index 8046d6a5..02a00a53 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -67,26 +67,22 @@ public PathF ProcessPath(ReadOnlySpan pathPoints, bool isClosed) this.AddVertex(0, 0, PathCommand.EndPoly | (PathCommand)PathFlags.Close); } - double x = 0; - double y = 0; + PointF currentPoint = new(0, 0); int startIndex = 0; PointF? lastPoint = null; PathCommand command; PathF results = new(pathPoints.Length * 3); - while (!(command = this.Accumulate(ref x, ref y)).Stop()) + while (!(command = this.Accumulate(ref currentPoint)).Stop()) { - PointF currentPoint; if (command.EndPoly() && results.Count > 0) { PointF initial = results[startIndex]; - currentPoint = new(initial.X, initial.Y); - results.Add(currentPoint); + results.Add(initial); startIndex = results.Count; } else { - currentPoint = new((float)x, (float)y); if (currentPoint != lastPoint) { results.Add(currentPoint); @@ -132,7 +128,7 @@ private void AddVertex(double x, double y, PathCommand cmd) } } - private PathCommand Accumulate(ref double x, ref double y) + private PathCommand Accumulate(ref PointF point) { PathCommand cmd = PathCommand.LineTo; while (!cmd.Stop()) @@ -255,8 +251,7 @@ ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVe else { PointF c = this.outVertices[this.outVertex++]; - x = c.X; - y = c.Y; + point = c; return cmd; } @@ -300,7 +295,6 @@ private void AddVertex(double x, double y, double distance = 0) this.srcVertices.Add(new VertexDistance(x, y, distance)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CloseVertexPath(bool closed) { while (this.srcVertices.Length > 1) From 0285e5d8a27d8956f36a8eb7bc27478fc22022e6 Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:32:49 +0200 Subject: [PATCH 13/33] Reduce allocs in InternalPath --- src/ImageSharp.Drawing/Shapes/InternalPath.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/InternalPath.cs b/src/ImageSharp.Drawing/Shapes/InternalPath.cs index d9becedd..65071872 100644 --- a/src/ImageSharp.Drawing/Shapes/InternalPath.cs +++ b/src/ImageSharp.Drawing/Shapes/InternalPath.cs @@ -4,6 +4,7 @@ using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing; @@ -61,7 +62,7 @@ internal InternalPath(ILineSegment segment, bool isClosedPath) /// The points. /// if set to true [is closed path]. internal InternalPath(ReadOnlyMemory points, bool isClosedPath) - : this(Simplify(points, isClosedPath, true), isClosedPath) + : this(Simplify(points.Span, isClosedPath, true), isClosedPath) { } @@ -247,16 +248,14 @@ private static PointData[] Simplify(IReadOnlyList segments, bool i foreach (ILineSegment seg in segments) { ReadOnlyMemory points = seg.Flatten(); - simplified.AddRange(points.ToArray()); + simplified.AddRange(points.Span); } - return Simplify(simplified.ToArray(), isClosed, removeCloseAndCollinear); + return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear); } - private static PointData[] Simplify(ReadOnlyMemory vectors, bool isClosed, bool removeCloseAndCollinear) + private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, bool removeCloseAndCollinear) { - ReadOnlySpan points = vectors.Span; - int polyCorners = points.Length; if (polyCorners == 0) { From 4bfa88b70cb359b47f08f61704e6ac30a72af97a Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:56:44 +0200 Subject: [PATCH 14/33] Add methods on PolygonStroker for manually composing (line) paths --- .../Shapes/PolygonClipper/PolygonStroker.cs | 35 +++++++++++----- .../Drawing/DrawPolygon.cs | 42 ++++++++++++++----- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs index 02a00a53..4061d300 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -53,26 +53,42 @@ public double Width } } - public PathF ProcessPath(ReadOnlySpan pathPoints, bool isClosed) + public PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed) { this.Reset(); - for (int i = 0; i < pathPoints.Length; i++) + this.AddLinePath(linePoints); + + if (isClosed) { - PointF point = pathPoints[i]; - this.AddVertex(point.X, point.Y, PathCommand.LineTo); + this.ClosePath(); } - if (isClosed) + PathF results = new(linePoints.Length * 3); + this.FinishPath(results); + return results; + } + + public void AddLinePath(ReadOnlySpan linePoints) + { + for (int i = 0; i < linePoints.Length; i++) { - this.AddVertex(0, 0, PathCommand.EndPoly | (PathCommand)PathFlags.Close); + PointF point = linePoints[i]; + this.AddVertex(point.X, point.Y, PathCommand.LineTo); } + } + + public void ClosePath() + { + this.AddVertex(0, 0, PathCommand.EndPoly | (PathCommand)PathFlags.Close); + } + public void FinishPath(List results) + { PointF currentPoint = new(0, 0); int startIndex = 0; PointF? lastPoint = null; PathCommand command; - PathF results = new(pathPoints.Length * 3); while (!(command = this.Accumulate(ref currentPoint)).Stop()) { if (command.EndPoly() && results.Count > 0) @@ -90,12 +106,9 @@ public PathF ProcessPath(ReadOnlySpan pathPoints, bool isClosed) } } } - - return results; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Reset() + public void Reset() { this.srcVertices.Clear(); this.outVertices.Clear(); diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 731e8bf4..2f0d1226 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -211,30 +211,52 @@ public void Blaze() private static List> GenerateOutlineList(IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) { + List> strokedLines = []; + if (width <= 0) { - return []; + return strokedLines; } - List> stroked = []; - - PolygonStroker stroker = new() { Width = width, LineJoin = GetLineJoin(jointStyle), LineCap = GetLineCap(endCapStyle) }; + PolygonStroker stroker = new() + { + Width = width, + LineJoin = GetLineJoin(jointStyle), + LineCap = GetLineCap(endCapStyle) + }; foreach (ISimplePath simplePath in path.Flatten()) { - bool isClosed = simplePath.IsClosed || endCapStyle is EndCapStyle.Polygon or EndCapStyle.Joined; + stroker.Reset(); + + int pointCount = 0; if (simplePath is Path concretePath) { - if (concretePath.LineSegments.Count == 1 && concretePath.LineSegments[0] is LinearLineSegment lineSegment) + foreach (ILineSegment line in concretePath.LineSegments) { - stroked.Add(stroker.ProcessPath(lineSegment.Flatten().Span, isClosed)); - continue; + ReadOnlySpan points = line.Flatten().Span; + stroker.AddLinePath(points); + pointCount += points.Length; } } + else + { + ReadOnlySpan points = simplePath.Points.Span; + stroker.AddLinePath(points); + pointCount = points.Length; + } + + bool isClosed = simplePath.IsClosed || endCapStyle is EndCapStyle.Polygon or EndCapStyle.Joined; + if (isClosed) + { + stroker.ClosePath(); + } - stroked.Add(stroker.ProcessPath(simplePath.Points.Span, isClosed)); + List lineBuilder = new(pointCount * 4); + stroker.FinishPath(lineBuilder); + strokedLines.Add(lineBuilder); } - return stroked; + return strokedLines; static LineJoin GetLineJoin(JointStyle value) => value switch { From 0dbfc3801e4c725e5bddc8ce2cd34d6af0b67128 Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Fri, 3 May 2024 13:44:58 +0200 Subject: [PATCH 15/33] Lazy-init bezier drawing points --- .../Shapes/ArcLineSegment.cs | 7 +----- .../Shapes/CubicBezierLineSegment.cs | 25 +++++++++++++------ .../Shapes/Helpers/ArrayExtensions.cs | 2 +- .../Shapes/LinearLineSegment.cs | 4 +-- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs b/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs index 2d4eed66..1b6a8be8 100644 --- a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs +++ b/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs @@ -44,8 +44,6 @@ public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool { this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep); } - - this.EndPoint = this.linePoints[^1]; } /// @@ -80,18 +78,15 @@ public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAn { this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep); } - - this.EndPoint = this.linePoints[^1]; } private ArcLineSegment(PointF[] linePoints) { this.linePoints = linePoints; - this.EndPoint = this.linePoints[^1]; } /// - public PointF EndPoint { get; } + public PointF EndPoint => this.linePoints[^1]; /// public ReadOnlyMemory Flatten() => this.linePoints; diff --git a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs index 613a3e85..8b387f73 100644 --- a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs +++ b/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs @@ -18,7 +18,8 @@ public sealed class CubicBezierLineSegment : ILineSegment /// /// The line points. /// - private readonly PointF[] linePoints; + private PointF[]? linePoints; + private readonly PointF[] controlPoints; /// @@ -36,10 +37,6 @@ public CubicBezierLineSegment(PointF[] points) { throw new ArgumentOutOfRangeException(nameof(points), "points must be a multiple of 3 plus 1 long."); } - - this.linePoints = GetDrawingPoints(this.controlPoints); - - this.EndPoint = this.controlPoints[this.controlPoints.Length - 1]; } /// @@ -55,19 +52,31 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control { } + /// + public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end) + : this(new[] { start, controlPoint1, controlPoint2, end }) + { + } + /// /// Gets the control points. /// public IReadOnlyList ControlPoints => this.controlPoints; /// - public PointF EndPoint { get; } + public PointF EndPoint => this.controlPoints[^1]; /// - public ReadOnlyMemory Flatten() => this.linePoints; + public ReadOnlyMemory Flatten() => this.linePoints ??= GetDrawingPoints(this.controlPoints); + + /// + /// Gets the control points of this curve. + /// + /// The control points of this curve. + public ReadOnlyMemory GetControlPoints() => this.controlPoints; /// - /// Transforms the current LineSegment using specified matrix. + /// Transforms this line segment using the specified matrix. /// /// The matrix. /// A line segment with the matrix applied to it. diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs index 7ef7162b..a8ba23fc 100644 --- a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs @@ -17,7 +17,7 @@ internal static class ArrayExtensions /// the Merged arrays public static T[] Merge(this T[] source1, T[] source2) { - if (source2 is null) + if (source2 is null || source2.Length == 0) { return source1; } diff --git a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs b/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs index 44f4c642..a79b6bcd 100644 --- a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs +++ b/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs @@ -46,8 +46,6 @@ public LinearLineSegment(PointF[] points) this.points = points ?? throw new ArgumentNullException(nameof(points)); Guard.MustBeGreaterThanOrEqualTo(this.points.Length, 2, nameof(points)); - - this.EndPoint = this.points[this.points.Length - 1]; } /// @@ -56,7 +54,7 @@ public LinearLineSegment(PointF[] points) /// /// The end point. /// - public PointF EndPoint { get; } + public PointF EndPoint => this.points[^1]; /// /// Converts the into a simple linear path.. From 5703f5869c48cb7392ff2ca251b5096f41fe554a Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Fri, 3 May 2024 13:52:29 +0200 Subject: [PATCH 16/33] Access list of paths directly in RichTextGlyphRenderer --- .../Processing/Processors/Text/RichTextGlyphRenderer.cs | 2 +- src/ImageSharp.Drawing/Shapes/InternalPath.cs | 2 +- src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index c4942a52..1aca9e71 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -292,7 +292,7 @@ protected override void EndGlyph() } // Path has already been added to the collection via the base class. - IPath path = this.Paths.Last(); + IPath path = this.PathList[^1]; Point renderLocation = ClampToPixel(path.Bounds.Location); if (this.noCache || this.rasterizationRequired) { diff --git a/src/ImageSharp.Drawing/Shapes/InternalPath.cs b/src/ImageSharp.Drawing/Shapes/InternalPath.cs index 65071872..fa8a4453 100644 --- a/src/ImageSharp.Drawing/Shapes/InternalPath.cs +++ b/src/ImageSharp.Drawing/Shapes/InternalPath.cs @@ -332,7 +332,7 @@ private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, if (isClosed && removeCloseAndCollinear) { // walk back removing collinear points - while (results.Count > 2 && results.Last().Orientation == PointOrientation.Collinear) + while (results.Count > 2 && results[^1].Orientation == PointOrientation.Collinear) { results.RemoveAt(results.Count - 1); } diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs index ba6762d4..e91e3c61 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs @@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Drawing.Text; /// internal class BaseGlyphBuilder : IGlyphRenderer { - private readonly List paths = new(); private Vector2 currentPoint; private GlyphRendererParameters parameters; @@ -27,10 +26,12 @@ internal class BaseGlyphBuilder : IGlyphRenderer /// The default transform. public BaseGlyphBuilder(Matrix3x2 transform) => this.Builder = new PathBuilder(transform); + protected List PathList { get; } = new(); + /// /// Gets the paths that have been rendered by the current instance. /// - public IPathCollection Paths => new PathCollection(this.paths); + public IPathCollection Paths => new PathCollection(this.PathList.ToArray()); /// /// Gets the path builder for the current instance. @@ -74,7 +75,7 @@ void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdContr /// void IGlyphRenderer.EndGlyph() { - this.paths.Add(this.Builder.Build()); + this.PathList.Add(this.Builder.Build()); this.EndGlyph(); } From 0e0d9bf6ebaa16a9bbdbf7e4af17a7d33413f0af Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Fri, 3 May 2024 13:59:49 +0200 Subject: [PATCH 17/33] Reduce ILineSegment[] copies for Path classes --- src/ImageSharp.Drawing/Shapes/Path.cs | 2 +- src/ImageSharp.Drawing/Shapes/PathBuilder.cs | 2 +- src/ImageSharp.Drawing/Shapes/Polygon.cs | 17 +++++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index b7ab77d4..d8d062a5 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -101,7 +101,7 @@ public virtual IPath Transform(Matrix3x2 matrix) ILineSegment[] segments = new ILineSegment[this.lineSegments.Length]; - for (int i = 0; i < this.LineSegments.Count; i++) + for (int i = 0; i < segments.Length; i++) { segments[i] = this.lineSegments[i].Transform(matrix); } diff --git a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs b/src/ImageSharp.Drawing/Shapes/PathBuilder.cs index 520366c1..ad00e0e6 100644 --- a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/PathBuilder.cs @@ -465,7 +465,7 @@ private class Figure public IPath Build() => this.IsClosed - ? new Polygon(this.segments.ToArray()) + ? new Polygon(this.segments.ToArray(), true) : new Path(this.segments.ToArray()); } } diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Shapes/Polygon.cs index 0d78d8fc..fcb9383f 100644 --- a/src/ImageSharp.Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp.Drawing/Shapes/Polygon.cs @@ -24,7 +24,7 @@ public Polygon(PointF[] points) /// /// The segments. public Polygon(params ILineSegment[] segments) - : base((IEnumerable)segments) + : base(segments.ToArray()) { } @@ -55,6 +55,11 @@ internal Polygon(Path path) { } + internal Polygon(ILineSegment[] segments, bool owned) + : base(owned ? segments : segments.ToArray()) + { + } + /// public override bool IsClosed => true; @@ -66,13 +71,13 @@ public override IPath Transform(Matrix3x2 matrix) return this; } - var segments = new ILineSegment[this.LineSegments.Count]; - int i = 0; - foreach (ILineSegment s in this.LineSegments) + ILineSegment[] segments = new ILineSegment[this.LineSegments.Count]; + + for (int i = 0; i < segments.Length; i++) { - segments[i++] = s.Transform(matrix); + segments[i] = this.LineSegments[i].Transform(matrix); } - return new Polygon(segments); + return new Polygon(segments, true); } } From 975c45358f073c5efa79b624e72e145b4e2c1b6b Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Fri, 3 May 2024 14:01:31 +0200 Subject: [PATCH 18/33] Derive EllipsePolygon from Polygon --- .../Shapes/EllipsePolygon.cs | 56 +++++++------------ src/ImageSharp.Drawing/Shapes/Path.cs | 2 +- 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs b/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs index 9d6588b8..2cd0fbb0 100644 --- a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs @@ -8,18 +8,15 @@ namespace SixLabors.ImageSharp.Drawing; /// /// An elliptical shape made up of a single path made up of one of more s. /// -public sealed class EllipsePolygon : IPath, ISimplePath, IPathInternals, IInternalPathOwner +public sealed class EllipsePolygon : Polygon, IPathInternals { - private readonly InternalPath innerPath; - private readonly CubicBezierLineSegment segment; - /// /// Initializes a new instance of the class. /// /// The location the center of the ellipse will be placed. /// The width/height of the final ellipse. public EllipsePolygon(PointF location, SizeF size) - : this(CreateSegment(location, size)) + : base(CreateSegment(location, size)) { } @@ -45,6 +42,11 @@ public EllipsePolygon(float x, float y, float width, float height) { } + private EllipsePolygon(ILineSegment[] segments) + : base(segments, true) + { + } + /// /// Initializes a new instance of the class. /// @@ -56,46 +58,28 @@ public EllipsePolygon(float x, float y, float radius) { } - private EllipsePolygon(CubicBezierLineSegment segment) - { - this.segment = segment; - this.innerPath = new InternalPath(segment, true); - } - - /// - public bool IsClosed => true; - - /// - public ReadOnlyMemory Points => this.innerPath.Points(); - - /// - public RectangleF Bounds => this.innerPath.Bounds; - /// - public PathTypes PathType => PathTypes.Closed; + public override IPath Transform(Matrix3x2 matrix) + { + if (matrix.IsIdentity) + { + return this; + } - /// - public IPath Transform(Matrix3x2 matrix) => matrix.IsIdentity - ? this - : new EllipsePolygon(this.segment.Transform(matrix)); + ILineSegment[] segments = new ILineSegment[this.LineSegments.Count]; - /// - public IPath AsClosedPath() => this; + for (int i = 0; i < segments.Length; i++) + { + segments[i] = this.LineSegments[i].Transform(matrix); + } - /// - public IEnumerable Flatten() - { - yield return this; + return new EllipsePolygon(segments); } /// // TODO switch this out to a calculated algorithm SegmentInfo IPathInternals.PointAlongPath(float distance) - => this.innerPath.PointAlongPath(distance); - - /// - IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() - => new[] { this.innerPath }; + => this.InnerPath.PointAlongPath(distance); private static CubicBezierLineSegment CreateSegment(Vector2 location, SizeF size) { diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index d8d062a5..96b20c65 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -88,7 +88,7 @@ public Path(params ILineSegment[] segments) /// internal bool RemoveCloseAndCollinearPoints { get; set; } = true; - private InternalPath InnerPath => + private protected InternalPath InnerPath => this.innerPath ??= new InternalPath(this.lineSegments, this.IsClosed, this.RemoveCloseAndCollinearPoints); /// From 54487138c888e5056f471df8480df2761d67848c Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Fri, 3 May 2024 14:11:56 +0200 Subject: [PATCH 19/33] Lazy-init InternalPaths and Bounds in ComplexPolygon --- .../Shapes/ComplexPolygon.cs | 110 +++++++++--------- .../Shapes/PathCollection.cs | 37 +++--- 2 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs index 568165e0..04005ab1 100644 --- a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Numerics; namespace SixLabors.ImageSharp.Drawing; @@ -14,8 +15,9 @@ namespace SixLabors.ImageSharp.Drawing; public sealed class ComplexPolygon : IPath, IPathInternals, IInternalPathOwner { private readonly IPath[] paths; - private readonly List internalPaths; - private readonly float length; + private List? internalPaths; + private float length; + private RectangleF? bounds; /// /// Initializes a new instance of the class. @@ -45,53 +47,10 @@ public ComplexPolygon(params IPath[] paths) Guard.NotNull(paths, nameof(paths)); this.paths = paths; - this.internalPaths = new List(this.paths.Length); - - if (paths.Length > 0) - { - float minX = float.MaxValue; - float maxX = float.MinValue; - float minY = float.MaxValue; - float maxY = float.MinValue; - float length = 0; - - foreach (IPath p in this.paths) - { - if (p.Bounds.Left < minX) - { - minX = p.Bounds.Left; - } - - if (p.Bounds.Right > maxX) - { - maxX = p.Bounds.Right; - } - - if (p.Bounds.Top < minY) - { - minY = p.Bounds.Top; - } - - if (p.Bounds.Bottom > maxY) - { - maxY = p.Bounds.Bottom; - } - - foreach (ISimplePath s in p.Flatten()) - { - InternalPath ip = new(s.Points, s.IsClosed); - length += ip.Length; - this.internalPaths.Add(ip); - } - } - this.length = length; - this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); - } - else + if (paths.Length == 0) { - this.length = 0; - this.Bounds = RectangleF.Empty; + this.bounds = RectangleF.Empty; } this.PathType = PathTypes.Mixed; @@ -106,7 +65,7 @@ public ComplexPolygon(params IPath[] paths) public IEnumerable Paths => this.paths; /// - public RectangleF Bounds { get; } + public RectangleF Bounds => this.bounds ??= this.CalcBounds(); /// public IPath Transform(Matrix3x2 matrix) @@ -118,10 +77,10 @@ public IPath Transform(Matrix3x2 matrix) } IPath[] shapes = new IPath[this.paths.Length]; - int i = 0; - foreach (IPath s in this.Paths) + + for (int i = 0; i < shapes.Length; i++) { - shapes[i++] = s.Transform(matrix); + shapes[i] = this.paths[i].Transform(matrix); } return new ComplexPolygon(shapes); @@ -159,6 +118,11 @@ public IPath AsClosedPath() /// SegmentInfo IPathInternals.PointAlongPath(float distance) { + if (this.internalPaths == null) + { + this.InitInternalPaths(); + } + distance %= this.length; foreach (InternalPath p in this.internalPaths) { @@ -177,7 +141,49 @@ SegmentInfo IPathInternals.PointAlongPath(float distance) /// IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() - => this.internalPaths; + { + this.InitInternalPaths(); + return this.internalPaths; + } + + /// + /// Initializes and . + /// + [MemberNotNull(nameof(internalPaths))] + private void InitInternalPaths() + { + this.internalPaths = new List(this.paths.Length); + + foreach (IPath p in this.paths) + { + foreach (ISimplePath s in p.Flatten()) + { + InternalPath ip = new(s.Points, s.IsClosed); + this.length += ip.Length; + this.internalPaths.Add(ip); + } + } + } + + private RectangleF CalcBounds() + { + float minX = float.MaxValue; + float maxX = float.MinValue; + float minY = float.MaxValue; + float maxY = float.MinValue; + + foreach (IPath p in this.paths) + { + RectangleF pBounds = p.Bounds; + + minX = MathF.Min(minX, pBounds.Left); + maxX = MathF.Max(maxX, pBounds.Right); + minY = MathF.Min(minY, pBounds.Top); + maxY = MathF.Max(maxY, pBounds.Bottom); + } + + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } private static InvalidOperationException ThrowOutOfRange() => new("Should not be possible to reach this line"); } diff --git a/src/ImageSharp.Drawing/Shapes/PathCollection.cs b/src/ImageSharp.Drawing/Shapes/PathCollection.cs index 1b9e704a..9ae4bc73 100644 --- a/src/ImageSharp.Drawing/Shapes/PathCollection.cs +++ b/src/ImageSharp.Drawing/Shapes/PathCollection.cs @@ -13,6 +13,7 @@ namespace SixLabors.ImageSharp.Drawing; public class PathCollection : IPathCollection { private readonly IPath[] paths; + private RectangleF? bounds; /// /// Initializes a new instance of the class. @@ -33,28 +34,30 @@ public PathCollection(params IPath[] paths) if (this.paths.Length == 0) { - this.Bounds = new RectangleF(0, 0, 0, 0); + this.bounds = new RectangleF(0, 0, 0, 0); } - else - { - float minX, minY, maxX, maxY; - minX = minY = float.MaxValue; - maxX = maxY = float.MinValue; + } - foreach (IPath path in this.paths) - { - minX = Math.Min(path.Bounds.Left, minX); - minY = Math.Min(path.Bounds.Top, minY); - maxX = Math.Max(path.Bounds.Right, maxX); - maxY = Math.Max(path.Bounds.Bottom, maxY); - } + /// + public RectangleF Bounds => this.bounds ??= this.CalcBounds(); + + private RectangleF CalcBounds() + { + float minX, minY, maxX, maxY; + minX = minY = float.MaxValue; + maxX = maxY = float.MinValue; - this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); + foreach (IPath path in this.paths) + { + RectangleF bounds = path.Bounds; + minX = Math.Min(bounds.Left, minX); + minY = Math.Min(bounds.Top, minY); + maxX = Math.Max(bounds.Right, maxX); + maxY = Math.Max(bounds.Bottom, maxY); } - } - /// - public RectangleF Bounds { get; } + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } /// public IEnumerator GetEnumerator() => ((IEnumerable)this.paths).GetEnumerator(); From 9b91ee415a96cd1c1ea6c6a0fc828b4ed377b411 Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Fri, 3 May 2024 14:16:52 +0200 Subject: [PATCH 20/33] Cleanup DrawPolygon --- .../Drawing/DrawPolygon.cs | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 2f0d1226..2a6ee5d1 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -5,6 +5,7 @@ using System.Drawing.Drawing2D; using System.Numerics; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using BenchmarkDotNet.Attributes; using GeoJSON.Net.Feature; using Newtonsoft.Json; @@ -22,23 +23,25 @@ namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; public abstract class DrawPolygon { + private string artifactDir; + private PointF[][] points; private Image image; - private bool isImageSharp; + private bool savedImageSharp; private SDPointF[][] sdPoints; private Bitmap sdBitmap; private Graphics sdGraphics; - private bool isSystem; + private bool savedSd; private SKPath skPath; private SKSurface skSurface; - private bool isSkia; + private bool savedSkia; private Executor executor; private DestinationImage vecDst; - private bool isBlaze; + private bool savedBlaze; protected abstract int Width { get; } @@ -46,12 +49,19 @@ public abstract class DrawPolygon protected abstract float Thickness { get; } + protected abstract string BenchName { get; } + protected virtual PointF[][] GetPoints(FeatureCollection features) => features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))).ToArray(); + private string GetArtifactPath(string name) => System.IO.Path.Combine(this.artifactDir, name); + [GlobalSetup] public void Setup() { + this.artifactDir = TestEnvironment.GetFullPath($"artifacts\\{BenchName}"); + Directory.CreateDirectory(this.artifactDir); + string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); @@ -87,44 +97,8 @@ public void Setup() } [GlobalCleanup] - public unsafe void Cleanup() + public void Cleanup() { - string dir = "Images/DrawPolygon"; - Directory.CreateDirectory(dir); - - if (this.isImageSharp) - { - this.image.SaveAsPng(System.IO.Path.Combine(dir, "ImageSharp.png")); - this.isImageSharp = false; - } - - if (this.isBlaze) - { - using var blazeImage = Image.WrapMemory( - this.vecDst.GetImageData(), - this.vecDst.GetBytesPerRow() * this.vecDst.GetImageHeight(), - this.vecDst.GetImageWidth(), - this.vecDst.GetImageHeight()); - - blazeImage.SaveAsPng(System.IO.Path.Combine(dir, "Blaze.png")); - this.isBlaze = false; - } - - if (this.isSystem) - { - this.sdBitmap.Save(System.IO.Path.Combine(dir, "SystemDrawing.png")); - this.isSystem = false; - } - - if (this.isSkia) - { - using var skSnapshot = this.skSurface.Snapshot(); - using var skEncoded = skSnapshot.Encode(); - using var skFile = new FileStream(System.IO.Path.Combine(dir, "SkiaSharp.png"), FileMode.Create); - skEncoded.SaveTo(skFile); - this.isSkia = false; - } - this.image.Dispose(); this.sdGraphics.Dispose(); this.sdBitmap.Dispose(); @@ -132,6 +106,7 @@ public unsafe void Cleanup() this.skPath.Dispose(); } + [SupportedOSPlatform("windows")] [Benchmark] public void SystemDrawing() { @@ -142,7 +117,11 @@ public void SystemDrawing() this.sdGraphics.DrawPolygon(pen, loop); } - this.isSystem = true; + if (!this.savedSd) + { + this.sdBitmap.Save(this.GetArtifactPath("SystemDrawing.png")); + this.savedSd = true; + } } [Benchmark] @@ -156,7 +135,12 @@ public void ImageSharp() c.DrawPolygon(Color.White, this.Thickness, loop); } }); - this.isImageSharp = true; + + if (!this.savedImageSharp) + { + this.image.SaveAsPng(this.GetArtifactPath("ImageSharp.png")); + this.savedImageSharp = true; + } } [Benchmark(Baseline = true)] @@ -171,11 +155,19 @@ public void SkiaSharp() }; this.skSurface.Canvas.DrawPath(this.skPath, paint); - this.isSkia = true; + + if (!this.savedSkia) + { + using var skSnapshot = this.skSurface.Snapshot(); + using var skEncoded = skSnapshot.Encode(); + using var skFile = new FileStream(this.GetArtifactPath("SkiaSharp.png"), FileMode.Create); + skEncoded.SaveTo(skFile); + this.savedSkia = true; + } } [Benchmark] - public void Blaze() + public unsafe void Blaze() { VectorImageBuilder builder = new(); @@ -206,10 +198,20 @@ public void Blaze() this.vecDst.DrawImage(image, BlazeMatrix.Identity, this.executor); - this.isBlaze = true; + if (!this.savedBlaze) + { + using var blazeImage = Image.WrapMemory( + this.vecDst.GetImageData(), + this.vecDst.GetBytesPerRow() * this.vecDst.GetImageHeight(), + this.vecDst.GetImageWidth(), + this.vecDst.GetImageHeight()); + + blazeImage.SaveAsPng(this.GetArtifactPath("Blaze.png")); + this.savedBlaze = true; + } } - private static List> GenerateOutlineList(IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) + public static List> GenerateOutlineList(IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) { List> strokedLines = []; @@ -233,9 +235,19 @@ private static List> GenerateOutlineList(IPath path, float width, J { foreach (ILineSegment line in concretePath.LineSegments) { - ReadOnlySpan points = line.Flatten().Span; - stroker.AddLinePath(points); - pointCount += points.Length; + if (line is CubicBezierLineSegment bezier) + { + // TODO: add bezier control points + ReadOnlySpan points = line.Flatten().Span; + stroker.AddLinePath(points); + pointCount += points.Length; + } + else + { + ReadOnlySpan points = line.Flatten().Span; + stroker.AddLinePath(points); + pointCount += points.Length; + } } } else @@ -281,6 +293,8 @@ public class DrawPolygonAll : DrawPolygon protected override int Height => 4800; protected override float Thickness => 2f; + + protected override string BenchName => nameof(DrawPolygonAll); } public class DrawPolygonMediumThin : DrawPolygon @@ -291,6 +305,8 @@ public class DrawPolygonMediumThin : DrawPolygon protected override float Thickness => 1f; + protected override string BenchName => nameof(DrawPolygonMediumThin); + protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); @@ -304,4 +320,6 @@ protected override PointF[][] GetPoints(FeatureCollection features) public class DrawPolygonMediumThick : DrawPolygonMediumThin { protected override float Thickness => 10f; + + protected override string BenchName => nameof(DrawPolygonMediumThick); } From d3ec07ca7e40d102482215b7a88363e8d629dfa7 Mon Sep 17 00:00:00 2001 From: TechPizza <23627133+TechPizzaDev@users.noreply.github.com> Date: Thu, 23 May 2024 14:08:34 +0200 Subject: [PATCH 21/33] Improve copying in ClipperOffset --- .../Shapes/PolygonClipper/ClipperOffset.cs | 11 ++--------- .../Shapes/PolygonClipper/PolygonOffsetter.cs | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs index 1ebcc51c..fd015840 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs @@ -32,11 +32,7 @@ public ComplexPolygon Execute(float width) for (int i = 0; i < solution.Count; i++) { PathF pt = solution[i]; - PointF[] points = new PointF[pt.Count]; - for (int j = 0; j < pt.Count; j++) - { - points[j] = pt[j]; - } + PointF[] points = pt.ToArray(); polygons[i] = new Polygon(points); } @@ -53,10 +49,7 @@ public ComplexPolygon Execute(float width) public void AddPath(ReadOnlySpan pathPoints, JointStyle jointStyle, EndCapStyle endCapStyle) { PathF points = new(pathPoints.Length); - for (int i = 0; i < pathPoints.Length; i++) - { - points.Add(pathPoints[i]); - } + points.AddRange(pathPoints); this.polygonClipperOffset.AddPath(points, jointStyle, endCapStyle); } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs index 084a0c43..7728baba 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs @@ -643,7 +643,7 @@ private class Group { public Group(PathsF paths, JointStyle joinType, EndCapStyle endType = EndCapStyle.Polygon) { - this.InPaths = new PathsF(paths); + this.InPaths = paths; this.JoinType = joinType; this.EndType = endType; this.OutPath = new PathF(); From f8a963b908b798954d518d19ec3edb177b7ae91f Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 20 Oct 2025 14:20:35 +1000 Subject: [PATCH 22/33] Update ImageSharp.Drawing.csproj --- src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index a0526a94..25dc14dc 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -52,8 +52,6 @@ - - From c5c823cc7e8e01a8275684feadcccc280ec929d8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 20 Oct 2025 14:55:59 +1000 Subject: [PATCH 23/33] Fix merge conflicts and missing test output --- .../Common/Extensions/GraphicsOptionsExtensions.cs | 2 +- src/ImageSharp.Drawing/Processing/Pen.cs | 4 ++-- src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs | 6 +----- src/ImageSharp.Drawing/Shapes/Polygon.cs | 2 +- .../Drawing/FillPatternBrushTests.cs | 12 ++++-------- ...argeGeoJson_Mississippi_LinesScaled_Scale(10).png | 3 +++ ...LargeGeoJson_Mississippi_LinesScaled_Scale(3).png | 3 +++ ...LargeGeoJson_Mississippi_LinesScaled_Scale(5).png | 3 +++ 8 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png create mode 100644 tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png diff --git a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs index d913fb94..299ac333 100644 --- a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs +++ b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs @@ -38,7 +38,7 @@ public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Co return false; } - if (color.ToScaledVector4().W != Opaque) + if (color.ToScaledVector4().W != opaque) { return false; } diff --git a/src/ImageSharp.Drawing/Processing/Pen.cs b/src/ImageSharp.Drawing/Processing/Pen.cs index b33b9070..9602c5c9 100644 --- a/src/ImageSharp.Drawing/Processing/Pen.cs +++ b/src/ImageSharp.Drawing/Processing/Pen.cs @@ -52,11 +52,11 @@ protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern) { Guard.NotNull(strokeFill, nameof(strokeFill)); - // Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth)); + Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth)); Guard.NotNull(strokePattern, nameof(strokePattern)); this.StrokeFill = strokeFill; - this.StrokeWidth = strokeWidth > 0 ? strokeWidth : 1; + this.StrokeWidth = strokeWidth; this.pattern = strokePattern; } diff --git a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs b/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs index 474ae150..6b9cac56 100644 --- a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs @@ -79,11 +79,7 @@ public override IPath Transform(Matrix3x2 matrix) /// // TODO switch this out to a calculated algorithm SegmentInfo IPathInternals.PointAlongPath(float distance) - => this.innerPath.PointAlongPath(distance); - - /// - IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() - => [this.innerPath]; + => this.InnerPath.PointAlongPath(distance); private static CubicBezierLineSegment CreateSegment(Vector2 location, SizeF size) { diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Shapes/Polygon.cs index fcb9383f..a4f60e24 100644 --- a/src/ImageSharp.Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp.Drawing/Shapes/Polygon.cs @@ -56,7 +56,7 @@ internal Polygon(Path path) } internal Polygon(ILineSegment[] segments, bool owned) - : base(owned ? segments : segments.ToArray()) + : base(owned ? segments : [.. segments]) { } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs index c7b9faff..3fbba995 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs @@ -30,11 +30,7 @@ private void Test(string name, Color background, Brush brush, Color[,] expectedP int offsetY = r.Next(image.Height / yStride) * yStride; for (int x = 0; x < xStride; x++) { - int actualX = x + offsetX; - int actualY = y + offsetY; - Rgba32 expected = expectedPatternFast[y, x].ToPixel(); // inverted pattern - Rgba32 actual = sourcePixels[actualX, actualY]; - if (expected != actual) + for (int y = 0; y < yStride; y++) { int actualX = x + offsetX; int actualY = y + offsetY; @@ -46,10 +42,10 @@ private void Test(string name, Color background, Brush brush, Color[,] expectedP } } } - } - image.Mutate(x => x.Resize(80, 80, KnownResamplers.NearestNeighbor)); - image.Save($"{path}/{name}x4.png"); + image.Mutate(x => x.Resize(80, 80, KnownResamplers.NearestNeighbor)); + image.Save($"{path}/{name}x4.png"); + } } [Fact] diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png new file mode 100644 index 00000000..4a385c10 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a659c9a2a4538dd9adcf3cfbd2894bf20d79a409ae20efd4bbbe315952ce02d +size 77492 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png new file mode 100644 index 00000000..70958358 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ecc4a0a67422b9be03e9cbcf5936eccd3796790f21ad7c30c4a8f0a39ac9781 +size 17224 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png new file mode 100644 index 00000000..8d64a9aa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86c60cdce213c815e6744346374e7cb853ef220d112fe1b934eaaca16527b1dc +size 33193 From 018962beadd5676f46200ce81b26fbeb2f384ebb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 20 Oct 2025 15:08:57 +1000 Subject: [PATCH 24/33] Get solution building. --- ImageSharp.Drawing.sln | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 6e08f1d6..2520e864 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -339,32 +339,50 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpBlaze", "..\SharpBlaze\SharpBlaze\SharpBlaze.csproj", "{FCEDD229-22BC-4B82-87DE-786BBFC52EDE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpBlaze.Numerics", "..\SharpBlaze\SharpBlaze.Numerics\SharpBlaze.Numerics.csproj", "{5490DFAF-0891-535F-08B4-2BF03C2BB778}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + DebugFast|Any CPU = DebugFast|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.Build.0 = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.ActiveCfg = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.Build.0 = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU + {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.Build.0 = Release|Any CPU + {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5490DFAF-0891-535F-08B4-2BF03C2BB778}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU + {5490DFAF-0891-535F-08B4-2BF03C2BB778}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU + {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9e8c99127bcf9d1612abb4c0d570b72a5030da6c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:00:39 +1000 Subject: [PATCH 25/33] Fix benchmark csproj file. --- .../ImageSharp.Drawing.Benchmarks.csproj | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index f1065652..77c6fbfe 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -13,11 +13,6 @@ CA1822 - - - - - net8.0 @@ -33,9 +28,7 @@ - - - + From 72bd892fca3443ea3cd3e284af9edc83c4047ac3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:04:49 +1000 Subject: [PATCH 26/33] Strip out blaze so we can port back improvements. --- ImageSharp.Drawing.sln | 20 +- .../Drawing/DrawPolygon.cs | 179 +----------------- .../Drawing/FillPolygon.cs | 80 +------- .../ImageSharp.Drawing.Benchmarks.csproj | 1 - 4 files changed, 5 insertions(+), 275 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 2520e864..cffaa97c 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11123.170 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}" ProjectSection(SolutionItems) = preProject @@ -337,10 +337,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpBlaze", "..\SharpBlaze\SharpBlaze\SharpBlaze.csproj", "{FCEDD229-22BC-4B82-87DE-786BBFC52EDE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpBlaze.Numerics", "..\SharpBlaze\SharpBlaze.Numerics\SharpBlaze.Numerics.csproj", "{5490DFAF-0891-535F-08B4-2BF03C2BB778}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -371,18 +367,6 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.Build.0 = Release|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index ce7041e7..380ce246 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -4,19 +4,14 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Numerics; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; using BenchmarkDotNet.Attributes; using GeoJSON.Net.Feature; using Newtonsoft.Json; -using SharpBlaze; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SkiaSharp; -using BlazeMatrix = SharpBlaze.Matrix; using Pen = System.Drawing.Pen; using SDPointF = System.Drawing.PointF; @@ -24,25 +19,16 @@ namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; public abstract class DrawPolygon { - private string artifactDir; - private PointF[][] points; private Image image; - private bool savedImageSharp; private SDPointF[][] sdPoints; private Bitmap sdBitmap; private Graphics sdGraphics; - private bool savedSd; private SKPath skPath; private SKSurface skSurface; - private bool savedSkia; - - private Executor executor; - private DestinationImage vecDst; - private bool savedBlaze; protected abstract int Width { get; } @@ -50,19 +36,12 @@ public abstract class DrawPolygon protected abstract float Thickness { get; } - protected abstract string BenchName { get; } - protected virtual PointF[][] GetPoints(FeatureCollection features) => features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))).ToArray(); - private string GetArtifactPath(string name) => System.IO.Path.Combine(this.artifactDir, name); - [GlobalSetup] public void Setup() { - this.artifactDir = TestEnvironment.GetFullPath($"artifacts\\{BenchName}"); - Directory.CreateDirectory(this.artifactDir); - string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); @@ -71,11 +50,9 @@ public void Setup() this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray(); this.skPath = new SKPath(); - foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2)) { this.skPath.MoveTo(ptArr[0].X, ptArr[1].Y); - for (int i = 1; i < ptArr.Length; i++) { this.skPath.LineTo(ptArr[i].X, ptArr[i].Y); @@ -84,11 +61,6 @@ public void Setup() this.skPath.LineTo(ptArr[0].X, ptArr[1].Y); } - this.executor = new SerialExecutor(); - this.vecDst = new DestinationImage(); - this.vecDst.UpdateSize(new IntSize(this.Width, this.Height)); - this.vecDst.ClearImage(); - this.image = new Image(this.Width, this.Height); this.sdBitmap = new Bitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); @@ -107,7 +79,6 @@ public void Cleanup() this.skPath.Dispose(); } - [SupportedOSPlatform("windows")] [Benchmark] public void SystemDrawing() { @@ -117,18 +88,11 @@ public void SystemDrawing() { this.sdGraphics.DrawPolygon(pen, loop); } - - if (!this.savedSd) - { - this.sdBitmap.Save(this.GetArtifactPath("SystemDrawing.png")); - this.savedSd = true; - } } [Benchmark] public void ImageSharp() - { - this.image.Mutate( + => this.image.Mutate( c => { foreach (PointF[] loop in this.points) @@ -137,13 +101,6 @@ public void ImageSharp() } }); - if (!this.savedImageSharp) - { - this.image.SaveAsPng(this.GetArtifactPath("ImageSharp.png")); - this.savedImageSharp = true; - } - } - [Benchmark(Baseline = true)] public void SkiaSharp() { @@ -156,134 +113,6 @@ public void SkiaSharp() }; this.skSurface.Canvas.DrawPath(this.skPath, paint); - - if (!this.savedSkia) - { - using var skSnapshot = this.skSurface.Snapshot(); - using var skEncoded = skSnapshot.Encode(); - using var skFile = new FileStream(this.GetArtifactPath("SkiaSharp.png"), FileMode.Create); - skEncoded.SaveTo(skFile); - this.savedSkia = true; - } - } - - [Benchmark] - public unsafe void Blaze() - { - VectorImageBuilder builder = new(); - - foreach (PointF[] loop in this.points) - { - var loopPolygon = new Polygon(loop); - var brush = new Processing.SolidBrush(Color.White); - var pen = new SolidPen(brush, this.Thickness); - List> outline = GenerateOutlineList(loopPolygon, pen.StrokeWidth, pen.JointStyle, pen.EndCapStyle); - - foreach (List line in outline) - { - Span ptArr = CollectionsMarshal.AsSpan(line); - - builder.MoveTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); - for (int i = 1; i < ptArr.Length; i++) - { - builder.LineTo(new FloatPoint(ptArr[i].X, ptArr[i].Y)); - } - - builder.LineTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); - - builder.Close(); - } - } - - VectorImage image = builder.ToVectorImage(Color.White.ToPixel().PackedValue); - - this.vecDst.DrawImage(image, BlazeMatrix.Identity, this.executor); - - if (!this.savedBlaze) - { - using var blazeImage = Image.WrapMemory( - this.vecDst.GetImageData(), - this.vecDst.GetBytesPerRow() * this.vecDst.GetImageHeight(), - this.vecDst.GetImageWidth(), - this.vecDst.GetImageHeight()); - - blazeImage.SaveAsPng(this.GetArtifactPath("Blaze.png")); - this.savedBlaze = true; - } - } - - public static List> GenerateOutlineList(IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) - { - List> strokedLines = []; - - if (width <= 0) - { - return strokedLines; - } - - PolygonStroker stroker = new() - { - Width = width, - LineJoin = GetLineJoin(jointStyle), - LineCap = GetLineCap(endCapStyle) - }; - foreach (ISimplePath simplePath in path.Flatten()) - { - stroker.Reset(); - - int pointCount = 0; - if (simplePath is Path concretePath) - { - foreach (ILineSegment line in concretePath.LineSegments) - { - if (line is CubicBezierLineSegment bezier) - { - // TODO: add bezier control points - ReadOnlySpan points = line.Flatten().Span; - stroker.AddLinePath(points); - pointCount += points.Length; - } - else - { - ReadOnlySpan points = line.Flatten().Span; - stroker.AddLinePath(points); - pointCount += points.Length; - } - } - } - else - { - ReadOnlySpan points = simplePath.Points.Span; - stroker.AddLinePath(points); - pointCount = points.Length; - } - - bool isClosed = simplePath.IsClosed || endCapStyle is EndCapStyle.Polygon or EndCapStyle.Joined; - if (isClosed) - { - stroker.ClosePath(); - } - - List lineBuilder = new(pointCount * 4); - stroker.FinishPath(lineBuilder); - strokedLines.Add(lineBuilder); - } - - return strokedLines; - - static LineJoin GetLineJoin(JointStyle value) => value switch - { - JointStyle.Square => LineJoin.BevelJoin, - JointStyle.Round => LineJoin.RoundJoin, - _ => LineJoin.MiterJoin, - }; - - static LineCap GetLineCap(EndCapStyle value) => value switch - { - EndCapStyle.Round => LineCap.Round, - EndCapStyle.Square => LineCap.Square, - _ => LineCap.Butt, - }; } } @@ -294,8 +123,6 @@ public class DrawPolygonAll : DrawPolygon protected override int Height => 4800; protected override float Thickness => 2f; - - protected override string BenchName => nameof(DrawPolygonAll); } public class DrawPolygonMediumThin : DrawPolygon @@ -306,8 +133,6 @@ public class DrawPolygonMediumThin : DrawPolygon protected override float Thickness => 1f; - protected override string BenchName => nameof(DrawPolygonMediumThin); - protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); @@ -321,6 +146,4 @@ protected override PointF[][] GetPoints(FeatureCollection features) public class DrawPolygonMediumThick : DrawPolygonMediumThin { protected override float Thickness => 10f; - - protected override string BenchName => nameof(DrawPolygonMediumThick); } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs index 2373d3ac..60ba9b19 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs @@ -7,13 +7,11 @@ using BenchmarkDotNet.Attributes; using GeoJSON.Net.Feature; using Newtonsoft.Json; -using SharpBlaze; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SkiaSharp; -using BlazeMatrix = SharpBlaze.Matrix; using SDBitmap = System.Drawing.Bitmap; using SDPointF = System.Drawing.PointF; using SolidBrush = System.Drawing.SolidBrush; @@ -28,20 +26,10 @@ public abstract class FillPolygon private List skPaths; private Image image; - private bool isImageSharp; - private SDBitmap sdBitmap; private Graphics sdGraphics; - private bool isSystem; - private SKBitmap skBitmap; private SKCanvas skCanvas; - private bool isSkia; - - private VectorImage vecImage; - private Executor executor; - private DestinationImage vecDst; - private bool isBlaze; protected abstract int Width { get; } @@ -65,34 +53,19 @@ public void Setup() this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray(); this.skPaths = []; - - VectorImageBuilder vecImageBuilder = new(); - foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2)) { SKPath skPath = new(); skPath.MoveTo(ptArr[0].X, ptArr[1].Y); - vecImageBuilder.MoveTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); - for (int i = 1; i < ptArr.Length; i++) { skPath.LineTo(ptArr[i].X, ptArr[i].Y); - vecImageBuilder.LineTo(new FloatPoint(ptArr[i].X, ptArr[i].Y)); } skPath.LineTo(ptArr[0].X, ptArr[1].Y); this.skPaths.Add(skPath); - - vecImageBuilder.LineTo(new FloatPoint(ptArr[0].X, ptArr[1].Y)); - vecImageBuilder.Close(); } - this.vecImage = vecImageBuilder.ToVectorImage(Color.White.ToPixel().PackedValue); - this.executor = new SerialExecutor(); - this.vecDst = new DestinationImage(); - this.vecDst.UpdateSize(new IntSize(this.Width, this.Height)); - this.vecDst.ClearImage(); - this.image = new Image(this.Width, this.Height); this.sdBitmap = new SDBitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); @@ -103,42 +76,8 @@ public void Setup() } [GlobalCleanup] - public unsafe void Cleanup() + public void Cleanup() { - string dir = "Images/FillPolygon"; - Directory.CreateDirectory(dir); - - if (this.isImageSharp) - { - this.image.SaveAsPng(System.IO.Path.Combine(dir, "ImageSharp.png")); - this.isImageSharp = false; - } - - if (this.isBlaze) - { - using var blazeImage = Image.WrapMemory( - this.vecDst.GetImageData(), - this.vecDst.GetBytesPerRow() * this.vecDst.GetImageHeight(), - this.vecDst.GetImageWidth(), - this.vecDst.GetImageHeight()); - - blazeImage.SaveAsPng(System.IO.Path.Combine(dir, "Blaze.png")); - this.isBlaze = false; - } - - if (this.isSystem) - { - this.sdBitmap.Save(System.IO.Path.Combine(dir, "SystemDrawing.png")); - this.isSystem = false; - } - - if (this.isSkia) - { - using var skFile = new FileStream(System.IO.Path.Combine(dir, "SkiaSharp.png"), FileMode.Create); - this.skBitmap.Encode(skFile, SKEncodedImageFormat.Png, 100); - this.isSkia = false; - } - this.image.Dispose(); this.sdGraphics.Dispose(); this.sdBitmap.Dispose(); @@ -159,14 +98,11 @@ public void SystemDrawing() { this.sdGraphics.FillPolygon(brush, loop); } - - this.isSystem = true; } [Benchmark] public void ImageSharp() - { - this.image.Mutate(c => + => this.image.Mutate(c => { foreach (Polygon polygon in this.polygons) { @@ -174,9 +110,6 @@ public void ImageSharp() } }); - this.isImageSharp = true; - } - [Benchmark(Baseline = true)] public void SkiaSharp() { @@ -191,15 +124,6 @@ public void SkiaSharp() }; this.skCanvas.DrawPath(path, paint); } - - this.isSkia = true; - } - - [Benchmark] - public void Blaze() - { - this.vecDst.DrawImage(this.vecImage, BlazeMatrix.Identity, this.executor); - this.isBlaze = true; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 77c6fbfe..0179f706 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -35,7 +35,6 @@ - From c40f450af217588060c84707170d6717924825a4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:11:24 +1000 Subject: [PATCH 27/33] Remove DebugFast --- ImageSharp.Drawing.sln | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index cffaa97c..dbb4000e 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -340,31 +340,23 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - DebugFast|Any CPU = DebugFast|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2E33181E-6E28-4662-A801-E2E7DC206029}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU - {2E33181E-6E28-4662-A801-E2E7DC206029}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.Build.0 = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.Build.0 = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.ActiveCfg = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.Build.0 = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5493F024-0A3F-420C-AC2D-05B77A36025B}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU - {5493F024-0A3F-420C-AC2D-05B77A36025B}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection From 1e064140077759ac1ead78130412232b5dfce208 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:18:42 +1000 Subject: [PATCH 28/33] Remove DebugFast --- ImageSharp.Drawing.sln | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 2520e864..7e2f22c7 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11123.170 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}" ProjectSection(SolutionItems) = preProject @@ -344,43 +344,31 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - DebugFast|Any CPU = DebugFast|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2E33181E-6E28-4662-A801-E2E7DC206029}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU - {2E33181E-6E28-4662-A801-E2E7DC206029}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.Build.0 = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.Build.0 = Debug|Any CPU - {59804113-1DD4-4F80-8D06-35FF71652508}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.ActiveCfg = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.Build.0 = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5493F024-0A3F-420C-AC2D-05B77A36025B}.DebugFast|Any CPU.ActiveCfg = Debug|Any CPU - {5493F024-0A3F-420C-AC2D-05B77A36025B}.DebugFast|Any CPU.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU - {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.Build.0 = Release|Any CPU {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.DebugFast|Any CPU.ActiveCfg = DebugFast|Any CPU - {5490DFAF-0891-535F-08B4-2BF03C2BB778}.DebugFast|Any CPU.Build.0 = DebugFast|Any CPU {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.ActiveCfg = Release|Any CPU {5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection From 539e09dc13b8a0b46326966163f882dcc1822920 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:20:17 +1000 Subject: [PATCH 29/33] Remove bad target framework declaration --- tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index 7c52c6d0..a08486cb 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -13,7 +13,6 @@ - net8.0 net8.0;net9.0 From 4cf8fa3ef605db3ba4cc00b66435fb7baac9e6ba Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:24:44 +1000 Subject: [PATCH 30/33] Remove more bad target framework declarations --- .../DrawShapesWithImageSharp.csproj | 1 - src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 8 -------- 2 files changed, 9 deletions(-) diff --git a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj index 4179a278..a8711843 100644 --- a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj +++ b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj @@ -8,7 +8,6 @@ - net8.0 net9.0;net8.0 diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 25dc14dc..da689136 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -25,24 +25,16 @@ enable Nullable - - - - 3.0 - - net8.0 - true net8.0;net9.0 net8.0 - true From ea766fd75d58df5c9698df38f9bfb7a157473319 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:53:57 +1000 Subject: [PATCH 31/33] Try matching build process with ImageSharp --- .github/workflows/build-and-test.yml | 15 +++++++-------- .../ImageSharp.Drawing.Benchmarks.csproj | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 0f198e11..da06bad9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -4,12 +4,15 @@ on: push: branches: - main + - release/* tags: - "v*" pull_request: branches: - main + - release/* types: [ labeled, opened, synchronize, reopened ] + jobs: # Prime a single LFS cache and expose the exact key for the matrix WarmLFS: @@ -112,14 +115,14 @@ jobs: options: os: buildjet-4vcpu-ubuntu-2204-arm - runs-on: ${{matrix.options.os}} + runs-on: ${{ matrix.options.os }} steps: - name: Install libgdi+, which is required for tests running on ubuntu if: ${{ contains(matrix.options.os, 'ubuntu') }} run: | - sudo apt-get update - sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + sudo apt-get update + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config shell: bash @@ -141,6 +144,7 @@ jobs: key: ${{ needs.WarmLFS.outputs.lfs_key }} - name: Git Pull LFS + shell: bash run: git lfs pull - name: NuGet Install @@ -211,14 +215,10 @@ jobs: with: flags: unittests - Publish: needs: [Build] - runs-on: ubuntu-latest - if: (github.event_name == 'push') - steps: - name: Git Config shell: bash @@ -259,4 +259,3 @@ jobs: run: | dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate - diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 0179f706..642b189d 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -13,7 +13,7 @@ CA1822 - + From fdbb390e4e8b2213a515881f645430ae31dbd357 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 21:59:52 +1000 Subject: [PATCH 32/33] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index da06bad9..7cb5e23f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -122,7 +122,7 @@ jobs: if: ${{ contains(matrix.options.os, 'ubuntu') }} run: | sudo apt-get update - sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev libfontconfig1 - name: Git Config shell: bash From 9c877af3420f7c33ca32ec9cbfa635daa187a9c3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 30 Oct 2025 22:12:19 +1000 Subject: [PATCH 33/33] Try using the no-deps package --- .github/workflows/build-and-test.yml | 2 +- tests/Directory.Build.targets | 1 + .../ImageSharp.Drawing.Benchmarks.csproj | 1 + tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7cb5e23f..da06bad9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -122,7 +122,7 @@ jobs: if: ${{ contains(matrix.options.os, 'ubuntu') }} run: | sudo apt-get update - sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev libfontconfig1 + sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev - name: Git Config shell: bash diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index e77ce0d4..05f5f7a6 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -31,6 +31,7 @@ + diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 642b189d..be0430b2 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -32,6 +32,7 @@ + diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index a08486cb..92880a30 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -32,6 +32,7 @@ +