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/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 575d4a5e..3f753f6c 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 @@ -359,6 +359,14 @@ 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 + {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}.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/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs index 1f68e439..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 (color.ToScaledVector4().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 8c0426e4..da689136 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -35,7 +35,6 @@ net8.0 - true diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 60145fe5..92bf3db8 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -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..9602c5c9 100644 --- a/src/ImageSharp.Drawing/Processing/Pen.cs +++ b/src/ImageSharp.Drawing/Processing/Pen.cs @@ -51,6 +51,7 @@ 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.NotNull(strokePattern, nameof(strokePattern)); diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index ee81f2b4..5da06dec 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/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index bbecff60..db239525 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/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index 6ad6a7dd..c9369761 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -100,9 +100,10 @@ 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 :) TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue)); TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue)); diff --git a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs b/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs index 6e0b8d54..994753da 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/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs index 1918e271..6cfb4319 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/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs index da898e2f..e9a44bd3 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/EllipsePolygon.cs b/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs index fbdec0c6..6b9cac56 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() - => [this.innerPath]; + => this.InnerPath.PointAlongPath(distance); private static CubicBezierLineSegment CreateSegment(Vector2 location, SizeF size) { 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/Helpers/ArrayExtensions.cs b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs index 1039818a..1e2e9c10 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/InternalPath.cs b/src/ImageSharp.Drawing/Shapes/InternalPath.cs index c3a26575..cc6a53ea 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) { } @@ -200,7 +201,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 +218,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,28 +243,26 @@ private static PointOrientation CalulateOrientation(Vector2 qp, Vector2 rq) /// private static PointData[] Simplify(IReadOnlyList segments, bool isClosed, bool removeCloseAndCollinear) { - List simplified = []; + List simplified = new(segments.Count); 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) { return []; } - List results = []; + List results = new(polyCorners); Vector2 lastPoint = points[0]; if (!isClosed) @@ -292,7 +291,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 +303,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]; } @@ -336,13 +332,13 @@ private static PointData[] Simplify(ReadOnlyMemory vectors, bool isClose 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); } } - return results.ToArray(); + return [.. results]; } private struct PointData diff --git a/src/ImageSharp.Drawing/Shapes/JointStyle.cs b/src/ImageSharp.Drawing/Shapes/JointStyle.cs index cf683a45..c1464824 100644 --- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs +++ b/src/ImageSharp.Drawing/Shapes/JointStyle.cs @@ -19,7 +19,24 @@ 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 } + +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/LinearLineSegment.cs b/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs index b6b72ff2..f1170baf 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.. diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index d8560abc..29213304 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; @@ -175,7 +176,7 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan(buffer.ToArray()), jointStyle, endCapStyle); + offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle); } online = !online; @@ -205,12 +206,12 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan(buffer.ToArray()), jointStyle, endCapStyle); + offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle); } buffer.Clear(); diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index 434ac1b0..61d1cccf 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); /// @@ -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 f09a02b0..8a434681 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/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(); diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Shapes/Polygon.cs index 19435d72..a4f60e24 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]) + { + } + /// public override bool IsClosed => true; @@ -67,12 +72,12 @@ public override IPath Transform(Matrix3x2 matrix) } ILineSegment[] segments = new ILineSegment[this.LineSegments.Count]; - int i = 0; - foreach (ILineSegment s in this.LineSegments) + + 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); } } 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..916592fd --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs @@ -0,0 +1,156 @@ +// 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; + +/// +/// 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; + + // 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() + { + if (capacity > 0) + { + this.data = new T[capacity]; + } + } + + /// + /// Gets or sets the number of items in the array. + /// + public int Length + { + readonly get => this.size; + + set + { + 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; + T[]? array = this.data; + + 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; + } + + /// + /// 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) + { + this.Grow(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; + } + + if (newCapacity < capacity) + { + newCapacity = capacity; + } + + 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/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs index 47f090a1..f035a06c 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/ClipperOffset.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs index 8f1367c1..4c94f641 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/ClipperUtils.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs index fc0b7064..39114d8b 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 6f4e3724..042382cd 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 4670ddfc..10c63a6e 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++) { @@ -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 = []; @@ -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 new file mode 100644 index 00000000..4061d300 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -0,0 +1,735 @@ +// 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(1); + private ArrayBuilder srcVertices = new(16); + 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 MiterLimit { get; set; } = 4; + + 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 PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed) + { + this.Reset(); + this.AddLinePath(linePoints); + + if (isClosed) + { + this.ClosePath(); + } + + PathF results = new(linePoints.Length * 3); + this.FinishPath(results); + return results; + } + + public void AddLinePath(ReadOnlySpan linePoints) + { + for (int i = 0; i < linePoints.Length; i++) + { + 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; + + while (!(command = this.Accumulate(ref currentPoint)).Stop()) + { + if (command.EndPoly() && results.Count > 0) + { + PointF initial = results[startIndex]; + results.Add(initial); + startIndex = results.Count; + } + else + { + if (currentPoint != lastPoint) + { + results.Add(currentPoint); + lastPoint = currentPoint; + } + } + } + } + + public void Reset() + { + this.srcVertices.Clear(); + this.outVertices.Clear(); + this.srcVertex = 0; + this.outVertex = 0; + this.closed = 0; + this.status = Status.Initial; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private 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(); + } + } + + private PathCommand Accumulate(ref PointF point) + { + PathCommand cmd = PathCommand.LineTo; + while (!cmd.Stop()) + { + switch (this.status) + { + case Status.Initial: + this.CloseVertexPath(this.closed != 0); + + if (this.srcVertices.Length < 3) + { + this.closed = 0; + } + + 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++]; + point = c; + + 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 distance = 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, distance)); + } + + 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..89383756 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs @@ -0,0 +1,95 @@ +// 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(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 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 +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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)); + } + + [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)); + 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; + } + + [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)); +} 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/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)] diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs index ab1333ee..e6ac53ff 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs @@ -27,10 +27,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 +76,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(); } 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/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.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 0179f706..be0430b2 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -13,7 +13,7 @@ CA1822 - + @@ -32,6 +32,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()); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs index f2a7c2e4..55059018 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs @@ -40,7 +40,7 @@ public void DrawBeziers(TestImageProvider provider, string color 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 95252213..86779df3 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs @@ -12,8 +12,8 @@ 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 @@ -54,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/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index 6b21ae62..be892373 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) { @@ -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)] diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs index 5553faa6..3fbba995 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs @@ -59,7 +59,7 @@ public void ImageShouldBeFloodFilledWithPercent10() { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "Percent10", Color.Blue, Brushes.Percent10(Color.HotPink, Color.LimeGreen), @@ -77,7 +77,7 @@ public void ImageShouldBeFloodFilledWithPercent10Transparent() { Color.Blue, Color.Blue, Color.Blue, Color.Blue } }; - this.Test( + Test( "Percent10_Transparent", Color.Blue, Brushes.Percent10(Color.HotPink), @@ -95,7 +95,7 @@ public void ImageShouldBeFloodFilledWithPercent20() { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen } }; - this.Test( + Test( "Percent20", Color.Blue, Brushes.Percent20(Color.HotPink, Color.LimeGreen), @@ -113,7 +113,7 @@ public void ImageShouldBeFloodFilledWithPercent20_transparent() { Color.Blue, Color.Blue, Color.HotPink, Color.Blue } }; - this.Test( + Test( "Percent20_Transparent", Color.Blue, Brushes.Percent20(Color.HotPink), @@ -131,7 +131,7 @@ public void ImageShouldBeFloodFilledWithHorizontal() { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "Horizontal", Color.Blue, Brushes.Horizontal(Color.HotPink, Color.LimeGreen), @@ -149,7 +149,7 @@ public void ImageShouldBeFloodFilledWithHorizontal_transparent() { Color.Blue, Color.Blue, Color.Blue, Color.Blue } }; - this.Test( + Test( "Horizontal_Transparent", Color.Blue, Brushes.Horizontal(Color.HotPink), @@ -167,7 +167,7 @@ public void ImageShouldBeFloodFilledWithMin() { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } }; - this.Test( + Test( "Min", Color.Blue, Brushes.Min(Color.HotPink, Color.LimeGreen), @@ -185,7 +185,7 @@ public void ImageShouldBeFloodFilledWithMin_transparent() { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, }; - this.Test( + Test( "Min_Transparent", Color.Blue, Brushes.Min(Color.HotPink), @@ -203,7 +203,7 @@ public void ImageShouldBeFloodFilledWithVertical() { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "Vertical", Color.Blue, Brushes.Vertical(Color.HotPink, Color.LimeGreen), @@ -221,7 +221,7 @@ public void ImageShouldBeFloodFilledWithVertical_transparent() { Color.Blue, Color.HotPink, Color.Blue, Color.Blue } }; - this.Test( + Test( "Vertical_Transparent", Color.Blue, Brushes.Vertical(Color.HotPink), @@ -239,7 +239,7 @@ public void ImageShouldBeFloodFilledWithForwardDiagonal() { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } }; - this.Test( + Test( "ForwardDiagonal", Color.Blue, Brushes.ForwardDiagonal(Color.HotPink, Color.LimeGreen), @@ -257,7 +257,7 @@ public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() { Color.HotPink, Color.Blue, Color.Blue, Color.Blue } }; - this.Test( + Test( "ForwardDiagonal_Transparent", Color.Blue, Brushes.ForwardDiagonal(Color.HotPink), @@ -275,7 +275,7 @@ public void ImageShouldBeFloodFilledWithBackwardDiagonal() { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink } }; - this.Test( + Test( "BackwardDiagonal", Color.Blue, Brushes.BackwardDiagonal(Color.HotPink, Color.LimeGreen), @@ -293,7 +293,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/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 @@ + diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs index 1b8b4377..d1881074 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs @@ -28,7 +28,7 @@ public void DrawingLineAtTopShouldDisplay(float stroke) 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] @@ -48,7 +48,7 @@ public void DrawingLineAtBottomShouldDisplay(float stroke) 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] @@ -68,7 +68,7 @@ public void DrawingLineAtLeftShouldDisplay(float stroke) 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] @@ -88,6 +88,6 @@ public void DrawingLineAtRightShouldDisplay(float stroke) 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/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs index e2ed4a94..b5518008 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,10 +68,8 @@ public override TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) { return x < midX ? TopLeftColor : TopRightColor; } - else - { - return x < midX ? BottomLeftColor : BottomRightColor; - } + + return x < midX ? BottomLeftColor : BottomRightColor; } private static TPixel GetBottomRightColor() diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs index 44334853..31f2a0e2 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/ImageProviders/TestPatternProvider.cs @@ -81,7 +81,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) @@ -97,7 +97,7 @@ private static void VerticalBars(Buffer2D pixels) if (x % stride == 0) { p++; - p = p % PinkBluePixels.Length; + p %= PinkBluePixels.Length; } pixels[x, y] = PinkBluePixels[p]; @@ -111,9 +111,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; @@ -123,7 +123,7 @@ private static void BlackWhiteChecker(Buffer2D pixels) if (y % stride is 0) { p++; - p = p % BlackWhitePixels.Length; + p %= BlackWhitePixels.Length; } int pstart = p; @@ -132,7 +132,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]; @@ -148,7 +148,7 @@ 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; @@ -160,7 +160,7 @@ private static void TransparentGradients(Buffer2D pixels) 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; TPixel c = TPixel.FromVector4(red); int topBand = top; @@ -169,14 +169,14 @@ private static void TransparentGradients(Buffer2D pixels) pixels[x, y] = c; } - topBand = topBand + height; + topBand += height; c = TPixel.FromVector4(green); for (int y = topBand; y < topBand + height; y++) { pixels[x, y] = c; } - topBand = topBand + height; + topBand += height; c = TPixel.FromVector4(blue); for (int y = topBand; y < bottom; y++) { diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.cs index d8a85db9..fdf5c29d 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/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 92a49c93..e27d5ec7 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -301,12 +301,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++) { - rgba = pixels[x, y].ToRgba32(); + Rgba32 rgba = pixels[x, y].ToRgba32(); Assert.Equal(255, rgba.R); Assert.Equal(100, rgba.G); 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