From 2e7641796e8a04d3356549d0ac78356be4f624da Mon Sep 17 00:00:00 2001 From: Stefan Nikolei Date: Tue, 30 Sep 2025 20:14:37 +0200 Subject: [PATCH 01/35] Quickly integrate SixLabors.PolygonClipper --- .../ImageSharp.Drawing.csproj | 1 + .../Processing/ShapeOptions.cs | 6 +- .../Shapes/ClipPathExtensions.cs | 2 +- .../Shapes/ClippingOperation.cs | 38 - .../Shapes/PolygonClipper/Clipper.cs | 68 +- .../Shapes/PolygonClipper/PolygonClipper.cs | 3432 ----------------- .../Shapes/PolygonClipper/PolygonOffsetter.cs | 56 +- .../Drawing/FillPolygonTests.cs | 3 +- .../ShapeOptionsDefaultsExtensionsTests.cs | 21 +- .../Shapes/PolygonClipper/ClipperTests.cs | 3 +- 10 files changed, 83 insertions(+), 3547 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/ClippingOperation.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 8c0426e4..9d82a5ba 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -47,6 +47,7 @@ + \ No newline at end of file diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs index 11c188d8..6f079e43 100644 --- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.PolygonClipper; + namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -24,9 +26,9 @@ private ShapeOptions(ShapeOptions source) /// /// Gets or sets the clipping operation. /// - /// Defaults to . + /// Defaults to . /// - public ClippingOperation ClippingOperation { get; set; } = ClippingOperation.Difference; + public BooleanOperation ClippingOperation { get; set; } = BooleanOperation.Difference; /// /// Gets or sets the rule for calculating intersection points. diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs index 690d2291..398ea09e 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs @@ -63,7 +63,7 @@ public static IPath Clip( clipper.AddPath(subjectPath, ClippingType.Subject); clipper.AddPaths(clipPaths, ClippingType.Clip); - IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation, options.IntersectionRule); + IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation); return new ComplexPolygon(result); } diff --git a/src/ImageSharp.Drawing/Shapes/ClippingOperation.cs b/src/ImageSharp.Drawing/Shapes/ClippingOperation.cs deleted file mode 100644 index 4adbfc06..00000000 --- a/src/ImageSharp.Drawing/Shapes/ClippingOperation.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Provides options for boolean clipping operations. -/// -/// -/// All clipping operations except for Difference are commutative. -/// -public enum ClippingOperation -{ - /// - /// No clipping is performed. - /// - None, - - /// - /// Clips regions covered by both subject and clip polygons. - /// - Intersection, - - /// - /// Clips regions covered by subject or clip polygons, or both polygons. - /// - Union, - - /// - /// Clips regions covered by subject, but not clip polygons. - /// - Difference, - - /// - /// Clips regions covered by subject or clip polygons, but not both. - /// - Xor -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs index 47f090a1..2f4c89cf 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.PolygonClipper; + namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// @@ -8,13 +10,8 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// internal class Clipper { - private readonly PolygonClipper polygonClipper; - - /// - /// Initializes a new instance of the class. - /// - public Clipper() - => this.polygonClipper = new PolygonClipper(); + private SixLabors.PolygonClipper.Polygon? subject; + private SixLabors.PolygonClipper.Polygon? clip; /// /// Generates the clipped shapes from the previously provided paths. @@ -22,38 +19,28 @@ public Clipper() /// The clipping operation. /// The intersection rule. /// The . - public IPath[] GenerateClippedShapes(ClippingOperation operation, IntersectionRule rule) + public IPath[] GenerateClippedShapes(BooleanOperation operation) { - PathsF closedPaths = []; - PathsF openPaths = []; + ArgumentNullException.ThrowIfNull(this.subject); + ArgumentNullException.ThrowIfNull(this.clip); - FillRule fillRule = rule == IntersectionRule.EvenOdd ? FillRule.EvenOdd : FillRule.NonZero; - this.polygonClipper.Execute(operation, fillRule, closedPaths, openPaths); + SixLabors.PolygonClipper.PolygonClipper polygonClipper = new(this.subject, this.clip, operation); - IPath[] shapes = new IPath[closedPaths.Count + openPaths.Count]; + SixLabors.PolygonClipper.Polygon result = polygonClipper.Run(); - int index = 0; - for (int i = 0; i < closedPaths.Count; i++) - { - PathF path = closedPaths[i]; - PointF[] points = new PointF[path.Count]; - for (int j = 0; j < path.Count; j++) - { - points[j] = path[j]; - } + IPath[] shapes = new IPath[result.Count]; - shapes[index++] = new Polygon(points); - } - - for (int i = 0; i < openPaths.Count; i++) + int index = 0; + for (int i = 0; i < result.Count; i++) { - PathF path = openPaths[i]; - PointF[] points = new PointF[path.Count]; + Contour contour = result[i]; + PointF[] points = new PointF[contour.Count]; - for (int j = 0; j < path.Count; j++) + for (int j = 0; j < contour.Count; j++) { - points[j] = path[j]; + Vertex vertex = contour[j]; + points[j] = new PointF((float)vertex.X, (float)vertex.Y); } shapes[index++] = new Polygon(points); @@ -100,12 +87,25 @@ public void AddPath(IPath path, ClippingType clippingType) internal void AddPath(ISimplePath path, ClippingType clippingType) { ReadOnlySpan vectors = path.Points.Span; - PathF points = new(vectors.Length); - for (int i = 0; i < vectors.Length; i++) + SixLabors.PolygonClipper.Polygon polygon = []; + Contour contour = new(); + polygon.Add(contour); + + foreach (PointF point in vectors) { - points.Add(vectors[i]); + contour.AddVertex(new Vertex(point.X, point.Y)); } - this.polygonClipper.AddPath(points, clippingType, !path.IsClosed); + switch (clippingType) + { + case ClippingType.Clip: + this.clip = polygon; + break; + case ClippingType.Subject: + this.subject = polygon; + break; + default: + throw new ArgumentOutOfRangeException(nameof(clippingType), clippingType, null); + } } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs deleted file mode 100644 index 6f4e3724..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs +++ /dev/null @@ -1,3432 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -#nullable disable - -using System.Collections; -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -/// -/// Contains functions that cover most polygon boolean and offsetting needs. -/// Ported from and originally licensed -/// under -/// -internal sealed class PolygonClipper -{ - private ClippingOperation clipType; - private FillRule fillRule; - private Active actives; - private Active flaggedHorizontal; - private readonly List minimaList; - private readonly List intersectList; - private readonly List vertexList; - private readonly List outrecList; - private readonly List scanlineList; - private readonly List horzSegList; - private readonly List horzJoinList; - private int currentLocMin; - private float currentBotY; - private bool isSortedMinimaList; - private bool hasOpenPaths; - - public PolygonClipper() - { - this.minimaList = []; - this.intersectList = []; - this.vertexList = []; - this.outrecList = []; - this.scanlineList = []; - this.horzSegList = []; - this.horzJoinList = []; - this.PreserveCollinear = true; - } - - public bool PreserveCollinear { get; set; } - - public bool ReverseSolution { get; set; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddSubject(PathsF paths) => this.AddPaths(paths, ClippingType.Subject); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddPath(PathF path, ClippingType polytype, bool isOpen = false) - { - PathsF tmp = new(1) { path }; - this.AddPaths(tmp, polytype, isOpen); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false) - { - if (isOpen) - { - this.hasOpenPaths = true; - } - - this.isSortedMinimaList = false; - this.AddPathsToVertexList(paths, polytype, isOpen); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(ClippingOperation clipType, FillRule fillRule, PathsF solutionClosed) - => this.Execute(clipType, fillRule, solutionClosed, []); - - public void Execute(ClippingOperation clipType, FillRule fillRule, PathsF solutionClosed, PathsF solutionOpen) - { - solutionClosed.Clear(); - solutionOpen.Clear(); - - try - { - this.ExecuteInternal(clipType, fillRule); - this.BuildPaths(solutionClosed, solutionOpen); - } - catch (Exception ex) - { - throw new ClipperException("An error occurred while attempting to clip the polygon. See the inner exception for details.", ex); - } - finally - { - this.ClearSolutionOnly(); - } - } - - private void ExecuteInternal(ClippingOperation ct, FillRule fillRule) - { - if (ct == ClippingOperation.None) - { - return; - } - - this.fillRule = fillRule; - this.clipType = ct; - this.Reset(); - if (!this.PopScanline(out float y)) - { - return; - } - - while (true) - { - this.InsertLocalMinimaIntoAEL(y); - Active ae; - while (this.PopHorz(out ae)) - { - this.DoHorizontal(ae); - } - - if (this.horzSegList.Count > 0) - { - this.ConvertHorzSegsToJoins(); - this.horzSegList.Clear(); - } - - this.currentBotY = y; // bottom of scanbeam - if (!this.PopScanline(out y)) - { - break; // y new top of scanbeam - } - - this.DoIntersections(y); - this.DoTopOfScanbeam(y); - while (this.PopHorz(out ae)) - { - this.DoHorizontal(ae!); - } - } - - this.ProcessHorzJoins(); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoIntersections(float topY) - { - if (this.BuildIntersectList(topY)) - { - this.ProcessIntersectList(); - this.DisposeIntersectNodes(); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DisposeIntersectNodes() - => this.intersectList.Clear(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddNewIntersectNode(Active ae1, Active ae2, float topY) - { - if (!ClipperUtils.GetIntersectPt(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip)) - { - ip = new Vector2(ae1.CurX, topY); - } - - if (ip.Y > this.currentBotY || ip.Y < topY) - { - float absDx1 = MathF.Abs(ae1.Dx); - float absDx2 = MathF.Abs(ae2.Dx); - - // TODO: Check threshold here once we remove upscaling. - if (absDx1 > 100 && absDx2 > 100) - { - if (absDx1 > absDx2) - { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); - } - else - { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); - } - } - else if (absDx1 > 100) - { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); - } - else if (absDx2 > 100) - { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); - } - else - { - if (ip.Y < topY) - { - ip.Y = topY; - } - else - { - ip.Y = this.currentBotY; - } - - if (absDx1 < absDx2) - { - ip.X = TopX(ae1, ip.Y); - } - else - { - ip.X = TopX(ae2, ip.Y); - } - } - } - - IntersectNode node = new(ip, ae1, ae2); - this.intersectList.Add(node); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt opN) - { - if (opP.Point.X == opN.Point.X) - { - return false; - } - - if (opP.Point.X < opN.Point.X) - { - hs.LeftOp = opP; - hs.RightOp = opN; - hs.LeftToRight = true; - } - else - { - hs.LeftOp = opN; - hs.RightOp = opP; - hs.LeftToRight = false; - } - - return true; - } - - private static bool UpdateHorzSegment(HorzSegment hs) - { - OutPt op = hs.LeftOp; - OutRec outrec = GetRealOutRec(op.OutRec); - bool outrecHasEdges = outrec.FrontEdge != null; - float curr_y = op.Point.Y; - OutPt opP = op, opN = op; - if (outrecHasEdges) - { - OutPt opA = outrec.Pts!, opZ = opA.Next; - while (opP != opZ && opP.Prev.Point.Y == curr_y) - { - opP = opP.Prev; - } - - while (opN != opA && opN.Next.Point.Y == curr_y) - { - opN = opN.Next; - } - } - else - { - while (opP.Prev != opN && opP.Prev.Point.Y == curr_y) - { - opP = opP.Prev; - } - - while (opN.Next != opP && opN.Next.Point.Y == curr_y) - { - opN = opN.Next; - } - } - - bool result = SetHorzSegHeadingForward(hs, opP, opN) && hs.LeftOp.HorizSegment == null; - - if (result) - { - hs.LeftOp.HorizSegment = hs; - } - else - { - hs.RightOp = null; // (for sorting) - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt DuplicateOp(OutPt op, bool insert_after) - { - OutPt result = new(op.Point, op.OutRec); - if (insert_after) - { - result.Next = op.Next; - result.Next.Prev = result; - result.Prev = op; - op.Next = result; - } - else - { - result.Prev = op.Prev; - result.Prev.Next = result; - result.Next = op; - op.Prev = result; - } - - return result; - } - - private void ConvertHorzSegsToJoins() - { - int k = 0; - foreach (HorzSegment hs in this.horzSegList) - { - if (UpdateHorzSegment(hs)) - { - k++; - } - } - - if (k < 2) - { - return; - } - - this.horzSegList.Sort(default(HorzSegSorter)); - - for (int i = 0; i < k - 1; i++) - { - HorzSegment hs1 = this.horzSegList[i]; - - // for each HorzSegment, find others that overlap - for (int j = i + 1; j < k; j++) - { - HorzSegment hs2 = this.horzSegList[j]; - if ((hs2.LeftOp.Point.X >= hs1.RightOp.Point.X) || - (hs2.LeftToRight == hs1.LeftToRight) || - (hs2.RightOp.Point.X <= hs1.LeftOp.Point.X)) - { - continue; - } - - float curr_y = hs1.LeftOp.Point.Y; - if (hs1.LeftToRight) - { - while (hs1.LeftOp.Next.Point.Y == curr_y && - hs1.LeftOp.Next.Point.X <= hs2.LeftOp.Point.X) - { - hs1.LeftOp = hs1.LeftOp.Next; - } - - while (hs2.LeftOp.Prev.Point.Y == curr_y && - hs2.LeftOp.Prev.Point.X <= hs1.LeftOp.Point.X) - { - hs2.LeftOp = hs2.LeftOp.Prev; - } - - HorzJoin join = new(DuplicateOp(hs1.LeftOp, true), DuplicateOp(hs2.LeftOp, false)); - this.horzJoinList.Add(join); - } - else - { - while (hs1.LeftOp.Prev.Point.Y == curr_y && - hs1.LeftOp.Prev.Point.X <= hs2.LeftOp.Point.X) - { - hs1.LeftOp = hs1.LeftOp.Prev; - } - - while (hs2.LeftOp.Next.Point.Y == curr_y && - hs2.LeftOp.Next.Point.X <= hs1.LeftOp.Point.X) - { - hs2.LeftOp = hs2.LeftOp.Next; - } - - HorzJoin join = new(DuplicateOp(hs2.LeftOp, true), DuplicateOp(hs1.LeftOp, false)); - this.horzJoinList.Add(join); - } - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ClearSolutionOnly() - { - while (this.actives != null) - { - this.DeleteFromAEL(this.actives); - } - - this.scanlineList.Clear(); - this.DisposeIntersectNodes(); - this.outrecList.Clear(); - this.horzSegList.Clear(); - this.horzJoinList.Clear(); - } - - private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) - { - solutionClosed.Clear(); - solutionOpen.Clear(); - solutionClosed.Capacity = this.outrecList.Count; - solutionOpen.Capacity = this.outrecList.Count; - - int i = 0; - - // _outrecList.Count is not static here because - // CleanCollinear can indirectly add additional OutRec - while (i < this.outrecList.Count) - { - OutRec outrec = this.outrecList[i++]; - if (outrec.Pts == null) - { - continue; - } - - PathF path = []; - if (outrec.IsOpen) - { - if (BuildPath(outrec.Pts, this.ReverseSolution, true, path)) - { - solutionOpen.Add(path); - } - } - else - { - this.CleanCollinear(outrec); - - // closed paths should always return a Positive orientation - // except when ReverseSolution == true - if (BuildPath(outrec.Pts, this.ReverseSolution, false, path)) - { - solutionClosed.Add(path); - } - } - } - - return true; - } - - private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path) - { - if (op == null || op.Next == op || (!isOpen && op.Next == op.Prev)) - { - return false; - } - - path.Clear(); - - Vector2 lastPt; - OutPt op2; - if (reverse) - { - lastPt = op.Point; - op2 = op.Prev; - } - else - { - op = op.Next; - lastPt = op.Point; - op2 = op.Next; - } - - path.Add(lastPt); - - while (op2 != op) - { - if (op2.Point != lastPt) - { - lastPt = op2.Point; - path.Add(lastPt); - } - - if (reverse) - { - op2 = op2.Prev; - } - else - { - op2 = op2.Next; - } - } - - return path.Count != 3 || !IsVerySmallTriangle(op2); - } - - private void DoHorizontal(Active horz) - /******************************************************************************* - * Notes: Horizontal edges (HEs) at scanline intersections (i.e. at the top or * - * bottom of a scanbeam) are processed as if layered.The order in which HEs * - * are processed doesn't matter. HEs intersect with the bottom vertices of * - * other HEs[#] and with non-horizontal edges [*]. Once these intersections * - * are completed, intermediate HEs are 'promoted' to the next edge in their * - * bounds, and they in turn may be intersected[%] by other HEs. * - * * - * eg: 3 horizontals at a scanline: / | / / * - * | / | (HE3)o ========%========== o * - * o ======= o(HE2) / | / / * - * o ============#=========*======*========#=========o (HE1) * - * / | / | / * - *******************************************************************************/ - { - Vector2 pt; - bool horzIsOpen = IsOpen(horz); - float y = horz.Bot.Y; - - Vertex vertex_max = horzIsOpen ? GetCurrYMaximaVertex_Open(horz) : GetCurrYMaximaVertex(horz); - - // remove 180 deg.spikes and also simplify - // consecutive horizontals when PreserveCollinear = true - if (vertex_max != null && - !horzIsOpen && vertex_max != horz.VertexTop) - { - TrimHorz(horz, this.PreserveCollinear); - } - - bool isLeftToRight = ResetHorzDirection(horz, vertex_max, out float leftX, out float rightX); - - if (IsHotEdge(horz)) - { - OutPt op = AddOutPt(horz, new Vector2(horz.CurX, y)); - this.AddToHorzSegList(op); - } - - OutRec currOutrec = horz.Outrec; - - while (true) - { - // loops through consec. horizontal edges (if open) - Active ae = isLeftToRight ? horz.NextInAEL : horz.PrevInAEL; - - while (ae != null) - { - if (ae.VertexTop == vertex_max) - { - // do this first!! - if (IsHotEdge(horz) && IsJoined(ae!)) - { - this.Split(ae, ae.Top); - } - - if (IsHotEdge(horz)) - { - while (horz.VertexTop != vertex_max) - { - AddOutPt(horz, horz.Top); - this.UpdateEdgeIntoAEL(horz); - } - - if (isLeftToRight) - { - this.AddLocalMaxPoly(horz, ae, horz.Top); - } - else - { - this.AddLocalMaxPoly(ae, horz, horz.Top); - } - } - - this.DeleteFromAEL(ae); - this.DeleteFromAEL(horz); - return; - } - - // if horzEdge is a maxima, keep going until we reach - // its maxima pair, otherwise check for break conditions - if (vertex_max != horz.VertexTop || IsOpenEnd(horz)) - { - // otherwise stop when 'ae' is beyond the end of the horizontal line - if ((isLeftToRight && ae.CurX > rightX) || (!isLeftToRight && ae.CurX < leftX)) - { - break; - } - - if (ae.CurX == horz.Top.X && !IsHorizontal(ae)) - { - pt = NextVertex(horz).Point; - - // to maximize the possibility of putting open edges into - // solutions, we'll only break if it's past HorzEdge's end - if (IsOpen(ae) && !IsSamePolyType(ae, horz) && !IsHotEdge(ae)) - { - if ((isLeftToRight && (TopX(ae, pt.Y) > pt.X)) || - (!isLeftToRight && (TopX(ae, pt.Y) < pt.X))) - { - break; - } - } - - // otherwise for edges at horzEdge's end, only stop when horzEdge's - // outslope is greater than e's slope when heading right or when - // horzEdge's outslope is less than e's slope when heading left. - else if ((isLeftToRight && (TopX(ae, pt.Y) >= pt.X)) || (!isLeftToRight && (TopX(ae, pt.Y) <= pt.X))) - { - break; - } - } - } - - pt = new Vector2(ae.CurX, y); - - if (isLeftToRight) - { - this.IntersectEdges(horz, ae, pt); - this.SwapPositionsInAEL(horz, ae); - horz.CurX = ae.CurX; - ae = horz.NextInAEL; - } - else - { - this.IntersectEdges(ae, horz, pt); - this.SwapPositionsInAEL(ae, horz); - horz.CurX = ae.CurX; - ae = horz.PrevInAEL; - } - - if (IsHotEdge(horz) && (horz.Outrec != currOutrec)) - { - currOutrec = horz.Outrec; - this.AddToHorzSegList(GetLastOp(horz)); - } - - // we've reached the end of this horizontal - } - - // check if we've finished looping - // through consecutive horizontals - // ie open at top - if (horzIsOpen && IsOpenEnd(horz)) - { - if (IsHotEdge(horz)) - { - AddOutPt(horz, horz.Top); - if (IsFront(horz)) - { - horz.Outrec.FrontEdge = null; - } - else - { - horz.Outrec.BackEdge = null; - } - - horz.Outrec = null; - } - - this.DeleteFromAEL(horz); - return; - } - else if (NextVertex(horz).Point.Y != horz.Top.Y) - { - break; - } - - // still more horizontals in bound to process ... - if (IsHotEdge(horz)) - { - AddOutPt(horz, horz.Top); - } - - this.UpdateEdgeIntoAEL(horz); - - if (this.PreserveCollinear && !horzIsOpen && HorzIsSpike(horz)) - { - TrimHorz(horz, true); - } - - isLeftToRight = ResetHorzDirection(horz, vertex_max, out leftX, out rightX); - - // end for loop and end of (possible consecutive) horizontals - } - - if (IsHotEdge(horz)) - { - this.AddToHorzSegList(AddOutPt(horz, horz.Top)); - } - - this.UpdateEdgeIntoAEL(horz); // this is the end of an intermediate horiz. - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoTopOfScanbeam(float y) - { - this.flaggedHorizontal = null; // sel_ is reused to flag horizontals (see PushHorz below) - Active ae = this.actives; - while (ae != null) - { - // NB 'ae' will never be horizontal here - if (ae.Top.Y == y) - { - ae.CurX = ae.Top.X; - if (IsMaxima(ae)) - { - ae = this.DoMaxima(ae); // TOP OF BOUND (MAXIMA) - continue; - } - - // INTERMEDIATE VERTEX ... - if (IsHotEdge(ae)) - { - AddOutPt(ae, ae.Top); - } - - this.UpdateEdgeIntoAEL(ae); - if (IsHorizontal(ae)) - { - this.PushHorz(ae); // horizontals are processed later - } - } - else - { - // i.e. not the top of the edge - ae.CurX = TopX(ae, y); - } - - ae = ae.NextInAEL; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Active DoMaxima(Active ae) - { - Active prevE; - Active nextE, maxPair; - prevE = ae.PrevInAEL; - nextE = ae.NextInAEL; - - if (IsOpenEnd(ae)) - { - if (IsHotEdge(ae)) - { - AddOutPt(ae, ae.Top); - } - - if (!IsHorizontal(ae)) - { - if (IsHotEdge(ae)) - { - if (IsFront(ae)) - { - ae.Outrec.FrontEdge = null; - } - else - { - ae.Outrec.BackEdge = null; - } - - ae.Outrec = null; - } - - this.DeleteFromAEL(ae); - } - - return nextE; - } - - maxPair = GetMaximaPair(ae); - if (maxPair == null) - { - return nextE; // eMaxPair is horizontal - } - - if (IsJoined(ae)) - { - this.Split(ae, ae.Top); - } - - if (IsJoined(maxPair)) - { - this.Split(maxPair, maxPair.Top); - } - - // only non-horizontal maxima here. - // process any edges between maxima pair ... - while (nextE != maxPair) - { - this.IntersectEdges(ae, nextE!, ae.Top); - this.SwapPositionsInAEL(ae, nextE!); - nextE = ae.NextInAEL; - } - - if (IsOpen(ae)) - { - if (IsHotEdge(ae)) - { - this.AddLocalMaxPoly(ae, maxPair, ae.Top); - } - - this.DeleteFromAEL(maxPair); - this.DeleteFromAEL(ae); - return prevE != null ? prevE.NextInAEL : this.actives; - } - - // here ae.nextInAel == ENext == EMaxPair ... - if (IsHotEdge(ae)) - { - this.AddLocalMaxPoly(ae, maxPair, ae.Top); - } - - this.DeleteFromAEL(ae); - this.DeleteFromAEL(maxPair); - return prevE != null ? prevE.NextInAEL : this.actives; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void TrimHorz(Active horzEdge, bool preserveCollinear) - { - bool wasTrimmed = false; - Vector2 pt = NextVertex(horzEdge).Point; - - while (pt.Y == horzEdge.Top.Y) - { - // always trim 180 deg. spikes (in closed paths) - // but otherwise break if preserveCollinear = true - if (preserveCollinear && (pt.X < horzEdge.Top.X) != (horzEdge.Bot.X < horzEdge.Top.X)) - { - break; - } - - horzEdge.VertexTop = NextVertex(horzEdge); - horzEdge.Top = pt; - wasTrimmed = true; - if (IsMaxima(horzEdge)) - { - break; - } - - pt = NextVertex(horzEdge).Point; - } - - if (wasTrimmed) - { - SetDx(horzEdge); // +/-infinity - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddToHorzSegList(OutPt op) - { - if (op.OutRec.IsOpen) - { - return; - } - - this.horzSegList.Add(new HorzSegment(op)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt GetLastOp(Active hotEdge) - { - OutRec outrec = hotEdge.Outrec; - return (hotEdge == outrec.FrontEdge) ? outrec.Pts : outrec.Pts.Next; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex GetCurrYMaximaVertex_Open(Active ae) - { - Vertex result = ae.VertexTop; - if (ae.WindDx > 0) - { - while (result.Next.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None)) - { - result = result.Next; - } - } - else - { - while (result.Prev.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None)) - { - result = result.Prev; - } - } - - if (!IsMaxima(result)) - { - result = null; // not a maxima - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex GetCurrYMaximaVertex(Active ae) - { - Vertex result = ae.VertexTop; - if (ae.WindDx > 0) - { - while (result.Next.Point.Y == result.Point.Y) - { - result = result.Next; - } - } - else - { - while (result.Prev.Point.Y == result.Point.Y) - { - result = result.Prev; - } - } - - if (!IsMaxima(result)) - { - result = null; // not a maxima - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsVerySmallTriangle(OutPt op) - => op.Next.Next == op.Prev - && (PtsReallyClose(op.Prev.Point, op.Next.Point) - || PtsReallyClose(op.Point, op.Next.Point) - || PtsReallyClose(op.Point, op.Prev.Point)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsValidClosedPath(OutPt op) - => op != null && op.Next != op && (op.Next != op.Prev || !IsVerySmallTriangle(op)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt DisposeOutPt(OutPt op) - { - OutPt result = op.Next == op ? null : op.Next; - op.Prev.Next = op.Next; - op.Next.Prev = op.Prev; - - return result; - } - - private void ProcessHorzJoins() - { - foreach (HorzJoin j in this.horzJoinList) - { - OutRec or1 = GetRealOutRec(j.Op1.OutRec); - OutRec or2 = GetRealOutRec(j.Op2.OutRec); - - OutPt op1b = j.Op1.Next; - OutPt op2b = j.Op2.Prev; - j.Op1.Next = j.Op2; - j.Op2.Prev = j.Op1; - op1b.Prev = op2b; - op2b.Next = op1b; - - // 'join' is really a split - if (or1 == or2) - { - or2 = new OutRec - { - Pts = op1b - }; - - FixOutRecPts(or2); - - if (or1.Pts.OutRec == or2) - { - or1.Pts = j.Op1; - or1.Pts.OutRec = or1; - } - - or2.Owner = or1; - - this.outrecList.Add(or2); - } - else - { - or2.Pts = null; - or2.Owner = or1; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool PtsReallyClose(Vector2 pt1, Vector2 pt2) - - // TODO: Check scale once we can remove upscaling. - => (Math.Abs(pt1.X - pt2.X) < 2F) && (Math.Abs(pt1.Y - pt2.Y) < 2F); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CleanCollinear(OutRec outrec) - { - outrec = GetRealOutRec(outrec); - - if (outrec?.IsOpen != false) - { - return; - } - - if (!IsValidClosedPath(outrec.Pts)) - { - outrec.Pts = null; - return; - } - - OutPt startOp = outrec.Pts; - OutPt op2 = startOp; - do - { - // NB if preserveCollinear == true, then only remove 180 deg. spikes - if ((ClipperUtils.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0) - && ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (ClipperUtils.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0))) - { - if (op2 == outrec.Pts) - { - outrec.Pts = op2.Prev; - } - - op2 = DisposeOutPt(op2); - if (!IsValidClosedPath(op2)) - { - outrec.Pts = null; - return; - } - - startOp = op2; - continue; - } - - op2 = op2.Next; - } - while (op2 != startOp); - - this.FixSelfIntersects(outrec); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoSplitOp(OutRec outrec, OutPt splitOp) - { - // splitOp.prev <=> splitOp && - // splitOp.next <=> splitOp.next.next are intersecting - OutPt prevOp = splitOp.Prev; - OutPt nextNextOp = splitOp.Next.Next; - outrec.Pts = prevOp; - - ClipperUtils.GetIntersectPoint( - prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip); - - float area1 = Area(prevOp); - float absArea1 = Math.Abs(area1); - - if (absArea1 < 2) - { - outrec.Pts = null; - return; - } - - float area2 = AreaTriangle(ip, splitOp.Point, splitOp.Next.Point); - float absArea2 = Math.Abs(area2); - - // de-link splitOp and splitOp.next from the path - // while inserting the intersection point - if (ip == prevOp.Point || ip == nextNextOp.Point) - { - nextNextOp.Prev = prevOp; - prevOp.Next = nextNextOp; - } - else - { - OutPt newOp2 = new(ip, outrec) - { - Prev = prevOp, - Next = nextNextOp - }; - - nextNextOp.Prev = newOp2; - prevOp.Next = newOp2; - } - - // nb: area1 is the path's area *before* splitting, whereas area2 is - // the area of the triangle containing splitOp & splitOp.next. - // So the only way for these areas to have the same sign is if - // the split triangle is larger than the path containing prevOp or - // if there's more than one self=intersection. - if (absArea2 > 1 && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0)))) - { - OutRec newOutRec = this.NewOutRec(); - newOutRec.Owner = outrec.Owner; - splitOp.OutRec = newOutRec; - splitOp.Next.OutRec = newOutRec; - - OutPt newOp = new(ip, newOutRec) { Prev = splitOp.Next, Next = splitOp }; - newOutRec.Pts = newOp; - splitOp.Prev = newOp; - splitOp.Next.Next = newOp; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void FixSelfIntersects(OutRec outrec) - { - OutPt op2 = outrec.Pts; - - // triangles can't self-intersect - while (op2.Prev != op2.Next.Next) - { - if (ClipperUtils.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point)) - { - this.DoSplitOp(outrec, op2); - if (outrec.Pts == null) - { - return; - } - - op2 = outrec.Pts; - continue; - } - else - { - op2 = op2.Next; - } - - if (op2 == outrec.Pts) - { - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Reset() - { - if (!this.isSortedMinimaList) - { - this.minimaList.Sort(default(LocMinSorter)); - this.isSortedMinimaList = true; - } - - this.scanlineList.Capacity = this.minimaList.Count; - for (int i = this.minimaList.Count - 1; i >= 0; i--) - { - this.scanlineList.Add(this.minimaList[i].Vertex.Point.Y); - } - - this.currentBotY = 0; - this.currentLocMin = 0; - this.actives = null; - this.flaggedHorizontal = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InsertScanline(float y) - { - int index = this.scanlineList.BinarySearch(y); - if (index >= 0) - { - return; - } - - index = ~index; - this.scanlineList.Insert(index, y); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool PopScanline(out float y) - { - int cnt = this.scanlineList.Count - 1; - if (cnt < 0) - { - y = 0; - return false; - } - - y = this.scanlineList[cnt]; - this.scanlineList.RemoveAt(cnt--); - while (cnt >= 0 && y == this.scanlineList[cnt]) - { - this.scanlineList.RemoveAt(cnt--); - } - - return true; - } - - private void InsertLocalMinimaIntoAEL(float botY) - { - LocalMinima localMinima; - Active leftBound, rightBound; - - // Add any local minima (if any) at BotY - // NB horizontal local minima edges should contain locMin.vertex.prev - while (this.HasLocMinAtY(botY)) - { - localMinima = this.PopLocalMinima(); - if ((localMinima.Vertex.Flags & VertexFlags.OpenStart) != VertexFlags.None) - { - leftBound = null; - } - else - { - leftBound = new Active - { - Bot = localMinima.Vertex.Point, - CurX = localMinima.Vertex.Point.X, - WindDx = -1, - VertexTop = localMinima.Vertex.Prev, - Top = localMinima.Vertex.Prev.Point, - Outrec = null, - LocalMin = localMinima - }; - SetDx(leftBound); - } - - if ((localMinima.Vertex.Flags & VertexFlags.OpenEnd) != VertexFlags.None) - { - rightBound = null; - } - else - { - rightBound = new Active - { - Bot = localMinima.Vertex.Point, - CurX = localMinima.Vertex.Point.X, - WindDx = 1, - VertexTop = localMinima.Vertex.Next, // i.e. ascending - Top = localMinima.Vertex.Next.Point, - Outrec = null, - LocalMin = localMinima - }; - SetDx(rightBound); - } - - // Currently LeftB is just the descending bound and RightB is the ascending. - // Now if the LeftB isn't on the left of RightB then we need swap them. - if (leftBound != null && rightBound != null) - { - if (IsHorizontal(leftBound)) - { - if (IsHeadingRightHorz(leftBound)) - { - SwapActives(ref leftBound, ref rightBound); - } - } - else if (IsHorizontal(rightBound)) - { - if (IsHeadingLeftHorz(rightBound)) - { - SwapActives(ref leftBound, ref rightBound); - } - } - else if (leftBound.Dx < rightBound.Dx) - { - SwapActives(ref leftBound, ref rightBound); - } - - // so when leftBound has windDx == 1, the polygon will be oriented - // counter-clockwise in Cartesian coords (clockwise with inverted Y). - } - else if (leftBound == null) - { - leftBound = rightBound; - rightBound = null; - } - - bool contributing; - leftBound.IsLeftBound = true; - this.InsertLeftEdge(leftBound); - - if (IsOpen(leftBound)) - { - this.SetWindCountForOpenPathEdge(leftBound); - contributing = this.IsContributingOpen(leftBound); - } - else - { - this.SetWindCountForClosedPathEdge(leftBound); - contributing = this.IsContributingClosed(leftBound); - } - - if (rightBound != null) - { - rightBound.WindCount = leftBound.WindCount; - rightBound.WindCount2 = leftBound.WindCount2; - InsertRightEdge(leftBound, rightBound); /////// - - if (contributing) - { - this.AddLocalMinPoly(leftBound, rightBound, leftBound.Bot, true); - if (!IsHorizontal(leftBound)) - { - this.CheckJoinLeft(leftBound, leftBound.Bot); - } - } - - while (rightBound.NextInAEL != null && IsValidAelOrder(rightBound.NextInAEL, rightBound)) - { - this.IntersectEdges(rightBound, rightBound.NextInAEL, rightBound.Bot); - this.SwapPositionsInAEL(rightBound, rightBound.NextInAEL); - } - - if (IsHorizontal(rightBound)) - { - this.PushHorz(rightBound); - } - else - { - this.CheckJoinRight(rightBound, rightBound.Bot); - this.InsertScanline(rightBound.Top.Y); - } - } - else if (contributing) - { - this.StartOpenPath(leftBound, leftBound.Bot); - } - - if (IsHorizontal(leftBound)) - { - this.PushHorz(leftBound); - } - else - { - this.InsertScanline(leftBound.Top.Y); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active ExtractFromSEL(Active ae) - { - Active res = ae.NextInSEL; - if (res != null) - { - res.PrevInSEL = ae.PrevInSEL; - } - - ae.PrevInSEL.NextInSEL = res; - return res; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Insert1Before2InSEL(Active ae1, Active ae2) - { - ae1.PrevInSEL = ae2.PrevInSEL; - if (ae1.PrevInSEL != null) - { - ae1.PrevInSEL.NextInSEL = ae1; - } - - ae1.NextInSEL = ae2; - ae2.PrevInSEL = ae1; - } - - private bool BuildIntersectList(float topY) - { - if (this.actives == null || this.actives.NextInAEL == null) - { - return false; - } - - // Calculate edge positions at the top of the current scanbeam, and from this - // we will determine the intersections required to reach these new positions. - this.AdjustCurrXAndCopyToSEL(topY); - - // Find all edge intersections in the current scanbeam using a stable merge - // sort that ensures only adjacent edges are intersecting. Intersect info is - // stored in FIntersectList ready to be processed in ProcessIntersectList. - // Re merge sorts see https://stackoverflow.com/a/46319131/359538 - Active left = this.flaggedHorizontal; - Active right; - Active lEnd; - Active rEnd; - Active currBase; - Active prevBase; - Active tmp; - - while (left.Jump != null) - { - prevBase = null; - while (left?.Jump != null) - { - currBase = left; - right = left.Jump; - lEnd = right; - rEnd = right.Jump; - left.Jump = rEnd; - while (left != lEnd && right != rEnd) - { - if (right.CurX < left.CurX) - { - tmp = right.PrevInSEL; - while (true) - { - this.AddNewIntersectNode(tmp, right, topY); - if (tmp == left) - { - break; - } - - tmp = tmp.PrevInSEL; - } - - tmp = right; - right = ExtractFromSEL(tmp); - lEnd = right; - Insert1Before2InSEL(tmp, left); - if (left == currBase) - { - currBase = tmp; - currBase.Jump = rEnd; - if (prevBase == null) - { - this.flaggedHorizontal = currBase; - } - else - { - prevBase.Jump = currBase; - } - } - } - else - { - left = left.NextInSEL; - } - } - - prevBase = currBase; - left = rEnd; - } - - left = this.flaggedHorizontal; - } - - return this.intersectList.Count > 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessIntersectList() - { - // We now have a list of intersections required so that edges will be - // correctly positioned at the top of the scanbeam. However, it's important - // that edge intersections are processed from the bottom up, but it's also - // crucial that intersections only occur between adjacent edges. - - // First we do a quicksort so intersections proceed in a bottom up order ... - this.intersectList.Sort(default(IntersectListSort)); - - // Now as we process these intersections, we must sometimes adjust the order - // to ensure that intersecting edges are always adjacent ... - for (int i = 0; i < this.intersectList.Count; ++i) - { - if (!EdgesAdjacentInAEL(this.intersectList[i])) - { - int j = i + 1; - while (!EdgesAdjacentInAEL(this.intersectList[j])) - { - j++; - } - - // swap - (this.intersectList[j], this.intersectList[i]) = - (this.intersectList[i], this.intersectList[j]); - } - - IntersectNode node = this.intersectList[i]; - this.IntersectEdges(node.Edge1, node.Edge2, node.Point); - this.SwapPositionsInAEL(node.Edge1, node.Edge2); - - node.Edge1.CurX = node.Point.X; - node.Edge2.CurX = node.Point.X; - this.CheckJoinLeft(node.Edge2, node.Point, true); - this.CheckJoinRight(node.Edge1, node.Point, true); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SwapPositionsInAEL(Active ae1, Active ae2) - { - // preconditon: ae1 must be immediately to the left of ae2 - Active next = ae2.NextInAEL; - if (next != null) - { - next.PrevInAEL = ae1; - } - - Active prev = ae1.PrevInAEL; - if (prev != null) - { - prev.NextInAEL = ae2; - } - - ae2.PrevInAEL = prev; - ae2.NextInAEL = ae1; - ae1.PrevInAEL = ae2; - ae1.NextInAEL = next; - if (ae2.PrevInAEL == null) - { - this.actives = ae2; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ResetHorzDirection(Active horz, Vertex vertexMax, out float leftX, out float rightX) - { - if (horz.Bot.X == horz.Top.X) - { - // the horizontal edge is going nowhere ... - leftX = horz.CurX; - rightX = horz.CurX; - Active ae = horz.NextInAEL; - while (ae != null && ae.VertexTop != vertexMax) - { - ae = ae.NextInAEL; - } - - return ae != null; - } - - if (horz.CurX < horz.Top.X) - { - leftX = horz.CurX; - rightX = horz.Top.X; - return true; - } - - leftX = horz.Top.X; - rightX = horz.CurX; - return false; // right to left - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HorzIsSpike(Active horz) - { - Vector2 nextPt = NextVertex(horz).Point; - return (horz.Bot.X < horz.Top.X) != (horz.Top.X < nextPt.X); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active FindEdgeWithMatchingLocMin(Active e) - { - Active result = e.NextInAEL; - while (result != null) - { - if (result.LocalMin == e.LocalMin) - { - return result; - } - - if (!IsHorizontal(result) && e.Bot != result.Bot) - { - result = null; - } - else - { - result = result.NextInAEL; - } - } - - result = e.PrevInAEL; - while (result != null) - { - if (result.LocalMin == e.LocalMin) - { - return result; - } - - if (!IsHorizontal(result) && e.Bot != result.Bot) - { - return null; - } - - result = result.PrevInAEL; - } - - return result; - } - - private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt) - { - OutPt resultOp = null; - - // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... - if (this.hasOpenPaths && (IsOpen(ae1) || IsOpen(ae2))) - { - if (IsOpen(ae1) && IsOpen(ae2)) - { - return null; - } - - // the following line avoids duplicating quite a bit of code - if (IsOpen(ae2)) - { - SwapActives(ref ae1, ref ae2); - } - - if (IsJoined(ae2)) - { - this.Split(ae2, pt); // needed for safety - } - - if (this.clipType == ClippingOperation.Union) - { - if (!IsHotEdge(ae2)) - { - return null; - } - } - else if (ae2.LocalMin.Polytype == ClippingType.Subject) - { - return null; - } - - switch (this.fillRule) - { - case FillRule.Positive: - if (ae2.WindCount != 1) - { - return null; - } - - break; - case FillRule.Negative: - if (ae2.WindCount != -1) - { - return null; - } - - break; - default: - if (Math.Abs(ae2.WindCount) != 1) - { - return null; - } - - break; - } - - // toggle contribution ... - if (IsHotEdge(ae1)) - { - resultOp = AddOutPt(ae1, pt); - if (IsFront(ae1)) - { - ae1.Outrec.FrontEdge = null; - } - else - { - ae1.Outrec.BackEdge = null; - } - - ae1.Outrec = null; - } - - // horizontal edges can pass under open paths at a LocMins - else if (pt == ae1.LocalMin.Vertex.Point && !IsOpenEnd(ae1.LocalMin.Vertex)) - { - // find the other side of the LocMin and - // if it's 'hot' join up with it ... - Active ae3 = FindEdgeWithMatchingLocMin(ae1); - if (ae3 != null && IsHotEdge(ae3)) - { - ae1.Outrec = ae3.Outrec; - if (ae1.WindDx > 0) - { - SetSides(ae3.Outrec!, ae1, ae3); - } - else - { - SetSides(ae3.Outrec!, ae3, ae1); - } - - return ae3.Outrec.Pts; - } - - resultOp = this.StartOpenPath(ae1, pt); - } - else - { - resultOp = this.StartOpenPath(ae1, pt); - } - - return resultOp; - } - - // MANAGING CLOSED PATHS FROM HERE ON - if (IsJoined(ae1)) - { - this.Split(ae1, pt); - } - - if (IsJoined(ae2)) - { - this.Split(ae2, pt); - } - - // UPDATE WINDING COUNTS... - int oldE1WindCount, oldE2WindCount; - if (ae1.LocalMin.Polytype == ae2.LocalMin.Polytype) - { - if (this.fillRule == FillRule.EvenOdd) - { - oldE1WindCount = ae1.WindCount; - ae1.WindCount = ae2.WindCount; - ae2.WindCount = oldE1WindCount; - } - else - { - if (ae1.WindCount + ae2.WindDx == 0) - { - ae1.WindCount = -ae1.WindCount; - } - else - { - ae1.WindCount += ae2.WindDx; - } - - if (ae2.WindCount - ae1.WindDx == 0) - { - ae2.WindCount = -ae2.WindCount; - } - else - { - ae2.WindCount -= ae1.WindDx; - } - } - } - else - { - if (this.fillRule != FillRule.EvenOdd) - { - ae1.WindCount2 += ae2.WindDx; - } - else - { - ae1.WindCount2 = ae1.WindCount2 == 0 ? 1 : 0; - } - - if (this.fillRule != FillRule.EvenOdd) - { - ae2.WindCount2 -= ae1.WindDx; - } - else - { - ae2.WindCount2 = ae2.WindCount2 == 0 ? 1 : 0; - } - } - - switch (this.fillRule) - { - case FillRule.Positive: - oldE1WindCount = ae1.WindCount; - oldE2WindCount = ae2.WindCount; - break; - case FillRule.Negative: - oldE1WindCount = -ae1.WindCount; - oldE2WindCount = -ae2.WindCount; - break; - default: - oldE1WindCount = Math.Abs(ae1.WindCount); - oldE2WindCount = Math.Abs(ae2.WindCount); - break; - } - - bool e1WindCountIs0or1 = oldE1WindCount is 0 or 1; - bool e2WindCountIs0or1 = oldE2WindCount is 0 or 1; - - if ((!IsHotEdge(ae1) && !e1WindCountIs0or1) || (!IsHotEdge(ae2) && !e2WindCountIs0or1)) - { - return null; - } - - // NOW PROCESS THE INTERSECTION ... - - // if both edges are 'hot' ... - if (IsHotEdge(ae1) && IsHotEdge(ae2)) - { - if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1) || - (ae1.LocalMin.Polytype != ae2.LocalMin.Polytype && this.clipType != ClippingOperation.Xor)) - { - resultOp = this.AddLocalMaxPoly(ae1, ae2, pt); - } - else if (IsFront(ae1) || (ae1.Outrec == ae2.Outrec)) - { - // this 'else if' condition isn't strictly needed but - // it's sensible to split polygons that ony touch at - // a common vertex (not at common edges). - resultOp = this.AddLocalMaxPoly(ae1, ae2, pt); - this.AddLocalMinPoly(ae1, ae2, pt); - } - else - { - // can't treat as maxima & minima - resultOp = AddOutPt(ae1, pt); - AddOutPt(ae2, pt); - SwapOutrecs(ae1, ae2); - } - } - - // if one or other edge is 'hot' ... - else if (IsHotEdge(ae1)) - { - resultOp = AddOutPt(ae1, pt); - SwapOutrecs(ae1, ae2); - } - else if (IsHotEdge(ae2)) - { - resultOp = AddOutPt(ae2, pt); - SwapOutrecs(ae1, ae2); - } - - // neither edge is 'hot' - else - { - float e1Wc2, e2Wc2; - switch (this.fillRule) - { - case FillRule.Positive: - e1Wc2 = ae1.WindCount2; - e2Wc2 = ae2.WindCount2; - break; - case FillRule.Negative: - e1Wc2 = -ae1.WindCount2; - e2Wc2 = -ae2.WindCount2; - break; - default: - e1Wc2 = Math.Abs(ae1.WindCount2); - e2Wc2 = Math.Abs(ae2.WindCount2); - break; - } - - if (!IsSamePolyType(ae1, ae2)) - { - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - } - else if (oldE1WindCount == 1 && oldE2WindCount == 1) - { - resultOp = null; - switch (this.clipType) - { - case ClippingOperation.Union: - if (e1Wc2 > 0 && e2Wc2 > 0) - { - return null; - } - - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - break; - - case ClippingOperation.Difference: - if (((GetPolyType(ae1) == ClippingType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) - || ((GetPolyType(ae1) == ClippingType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) - { - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - } - - break; - - case ClippingOperation.Xor: - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - break; - - default: // ClipType.Intersection: - if (e1Wc2 <= 0 || e2Wc2 <= 0) - { - return null; - } - - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - break; - } - } - } - - return resultOp; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DeleteFromAEL(Active ae) - { - Active prev = ae.PrevInAEL; - Active next = ae.NextInAEL; - if (prev == null && next == null && (ae != this.actives)) - { - return; // already deleted - } - - if (prev != null) - { - prev.NextInAEL = next; - } - else - { - this.actives = next; - } - - if (next != null) - { - next.PrevInAEL = prev; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AdjustCurrXAndCopyToSEL(float topY) - { - Active ae = this.actives; - this.flaggedHorizontal = ae; - while (ae != null) - { - ae.PrevInSEL = ae.PrevInAEL; - ae.NextInSEL = ae.NextInAEL; - ae.Jump = ae.NextInSEL; - if (ae.JoinWith == JoinWith.Left) - { - ae.CurX = ae.PrevInAEL.CurX; // this also avoids complications - } - else - { - ae.CurX = TopX(ae, topY); - } - - // NB don't update ae.curr.Y yet (see AddNewIntersectNode) - ae = ae.NextInAEL; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool HasLocMinAtY(float y) - => this.currentLocMin < this.minimaList.Count && this.minimaList[this.currentLocMin].Vertex.Point.Y == y; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private LocalMinima PopLocalMinima() - => this.minimaList[this.currentLocMin++]; - - private void AddPathsToVertexList(PathsF paths, ClippingType polytype, bool isOpen) - { - int totalVertCnt = 0; - for (int i = 0; i < paths.Count; i++) - { - PathF path = paths[i]; - totalVertCnt += path.Count; - } - - this.vertexList.Capacity = this.vertexList.Count + totalVertCnt; - - foreach (PathF path in paths) - { - Vertex v0 = null, prev_v = null, curr_v; - foreach (Vector2 pt in path) - { - if (v0 == null) - { - v0 = new Vertex(pt, VertexFlags.None, null); - this.vertexList.Add(v0); - prev_v = v0; - } - else if (prev_v.Point != pt) - { - // ie skips duplicates - curr_v = new Vertex(pt, VertexFlags.None, prev_v); - this.vertexList.Add(curr_v); - prev_v.Next = curr_v; - prev_v = curr_v; - } - } - - if (prev_v == null || prev_v.Prev == null) - { - continue; - } - - if (!isOpen && prev_v.Point == v0.Point) - { - prev_v = prev_v.Prev; - } - - prev_v.Next = v0; - v0.Prev = prev_v; - if (!isOpen && prev_v.Next == prev_v) - { - continue; - } - - // OK, we have a valid path - bool going_up, going_up0; - if (isOpen) - { - curr_v = v0.Next; - while (curr_v != v0 && curr_v.Point.Y == v0.Point.Y) - { - curr_v = curr_v.Next; - } - - going_up = curr_v.Point.Y <= v0.Point.Y; - if (going_up) - { - v0.Flags = VertexFlags.OpenStart; - this.AddLocMin(v0, polytype, true); - } - else - { - v0.Flags = VertexFlags.OpenStart | VertexFlags.LocalMax; - } - } - else - { - // closed path - prev_v = v0.Prev; - while (prev_v != v0 && prev_v.Point.Y == v0.Point.Y) - { - prev_v = prev_v.Prev; - } - - if (prev_v == v0) - { - continue; // only open paths can be completely flat - } - - going_up = prev_v.Point.Y > v0.Point.Y; - } - - going_up0 = going_up; - prev_v = v0; - curr_v = v0.Next; - while (curr_v != v0) - { - if (curr_v.Point.Y > prev_v.Point.Y && going_up) - { - prev_v.Flags |= VertexFlags.LocalMax; - going_up = false; - } - else if (curr_v.Point.Y < prev_v.Point.Y && !going_up) - { - going_up = true; - this.AddLocMin(prev_v, polytype, isOpen); - } - - prev_v = curr_v; - curr_v = curr_v.Next; - } - - if (isOpen) - { - prev_v.Flags |= VertexFlags.OpenEnd; - if (going_up) - { - prev_v.Flags |= VertexFlags.LocalMax; - } - else - { - this.AddLocMin(prev_v, polytype, isOpen); - } - } - else if (going_up != going_up0) - { - if (going_up0) - { - this.AddLocMin(prev_v, polytype, false); - } - else - { - prev_v.Flags |= VertexFlags.LocalMax; - } - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddLocMin(Vertex vert, ClippingType polytype, bool isOpen) - { - // make sure the vertex is added only once. - if ((vert.Flags & VertexFlags.LocalMin) != VertexFlags.None) - { - return; - } - - vert.Flags |= VertexFlags.LocalMin; - - LocalMinima lm = new(vert, polytype, isOpen); - this.minimaList.Add(lm); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PushHorz(Active ae) - { - ae.NextInSEL = this.flaggedHorizontal; - this.flaggedHorizontal = ae; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool PopHorz(out Active ae) - { - ae = this.flaggedHorizontal; - if (this.flaggedHorizontal == null) - { - return false; - } - - this.flaggedHorizontal = this.flaggedHorizontal.NextInSEL; - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutPt AddLocalMinPoly(Active ae1, Active ae2, Vector2 pt, bool isNew = false) - { - OutRec outrec = this.NewOutRec(); - ae1.Outrec = outrec; - ae2.Outrec = outrec; - - if (IsOpen(ae1)) - { - outrec.Owner = null; - outrec.IsOpen = true; - if (ae1.WindDx > 0) - { - SetSides(outrec, ae1, ae2); - } - else - { - SetSides(outrec, ae2, ae1); - } - } - else - { - outrec.IsOpen = false; - Active prevHotEdge = GetPrevHotEdge(ae1); - - // e.windDx is the winding direction of the **input** paths - // and unrelated to the winding direction of output polygons. - // Output orientation is determined by e.outrec.frontE which is - // the ascending edge (see AddLocalMinPoly). - if (prevHotEdge != null) - { - outrec.Owner = prevHotEdge.Outrec; - if (OutrecIsAscending(prevHotEdge) == isNew) - { - SetSides(outrec, ae2, ae1); - } - else - { - SetSides(outrec, ae1, ae2); - } - } - else - { - outrec.Owner = null; - if (isNew) - { - SetSides(outrec, ae1, ae2); - } - else - { - SetSides(outrec, ae2, ae1); - } - } - } - - OutPt op = new(pt, outrec); - outrec.Pts = op; - return op; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetDx(Active ae) - => ae.Dx = GetDx(ae.Bot, ae.Top); - - /******************************************************************************* - * Dx: 0(90deg) * - * | * - * +inf (180deg) <--- o --. -inf (0deg) * - *******************************************************************************/ - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float GetDx(Vector2 pt1, Vector2 pt2) - { - float dy = pt2.Y - pt1.Y; - if (dy != 0) - { - return (pt2.X - pt1.X) / dy; - } - - if (pt2.X > pt1.X) - { - return float.NegativeInfinity; - } - - return float.PositiveInfinity; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float TopX(Active ae, float currentY) - { - Vector2 top = ae.Top; - Vector2 bottom = ae.Bot; - - if ((currentY == top.Y) || (top.X == bottom.X)) - { - return top.X; - } - - if (currentY == bottom.Y) - { - return bottom.X; - } - - return bottom.X + (ae.Dx * (currentY - bottom.Y)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHorizontal(Active ae) - => ae.Top.Y == ae.Bot.Y; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHeadingRightHorz(Active ae) - => float.IsNegativeInfinity(ae.Dx); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHeadingLeftHorz(Active ae) - => float.IsPositiveInfinity(ae.Dx); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SwapActives(ref Active ae1, ref Active ae2) - => (ae2, ae1) = (ae1, ae2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ClippingType GetPolyType(Active ae) - => ae.LocalMin.Polytype; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSamePolyType(Active ae1, Active ae2) - => ae1.LocalMin.Polytype == ae2.LocalMin.Polytype; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContributingClosed(Active ae) - { - switch (this.fillRule) - { - case FillRule.Positive: - if (ae.WindCount != 1) - { - return false; - } - - break; - case FillRule.Negative: - if (ae.WindCount != -1) - { - return false; - } - - break; - case FillRule.NonZero: - if (Math.Abs(ae.WindCount) != 1) - { - return false; - } - - break; - } - - switch (this.clipType) - { - case ClippingOperation.Intersection: - return this.fillRule switch - { - FillRule.Positive => ae.WindCount2 > 0, - FillRule.Negative => ae.WindCount2 < 0, - _ => ae.WindCount2 != 0, - }; - - case ClippingOperation.Union: - return this.fillRule switch - { - FillRule.Positive => ae.WindCount2 <= 0, - FillRule.Negative => ae.WindCount2 >= 0, - _ => ae.WindCount2 == 0, - }; - - case ClippingOperation.Difference: - bool result = this.fillRule switch - { - FillRule.Positive => ae.WindCount2 <= 0, - FillRule.Negative => ae.WindCount2 >= 0, - _ => ae.WindCount2 == 0, - }; - return (GetPolyType(ae) == ClippingType.Subject) ? result : !result; - - case ClippingOperation.Xor: - return true; // XOr is always contributing unless open - - default: - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContributingOpen(Active ae) - { - bool isInClip, isInSubj; - switch (this.fillRule) - { - case FillRule.Positive: - isInSubj = ae.WindCount > 0; - isInClip = ae.WindCount2 > 0; - break; - case FillRule.Negative: - isInSubj = ae.WindCount < 0; - isInClip = ae.WindCount2 < 0; - break; - default: - isInSubj = ae.WindCount != 0; - isInClip = ae.WindCount2 != 0; - break; - } - - bool result = this.clipType switch - { - ClippingOperation.Intersection => isInClip, - ClippingOperation.Union => !isInSubj && !isInClip, - _ => !isInClip - }; - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetWindCountForClosedPathEdge(Active ae) - { - // Wind counts refer to polygon regions not edges, so here an edge's WindCnt - // indicates the higher of the wind counts for the two regions touching the - // edge. (nb: Adjacent regions can only ever have their wind counts differ by - // one. Also, open paths have no meaningful wind directions or counts.) - Active ae2 = ae.PrevInAEL; - - // find the nearest closed path edge of the same PolyType in AEL (heading left) - ClippingType pt = GetPolyType(ae); - while (ae2 != null && (GetPolyType(ae2) != pt || IsOpen(ae2))) - { - ae2 = ae2.PrevInAEL; - } - - if (ae2 == null) - { - ae.WindCount = ae.WindDx; - ae2 = this.actives; - } - else if (this.fillRule == FillRule.EvenOdd) - { - ae.WindCount = ae.WindDx; - ae.WindCount2 = ae2.WindCount2; - ae2 = ae2.NextInAEL; - } - else - { - // NonZero, positive, or negative filling here ... - // when e2's WindCnt is in the SAME direction as its WindDx, - // then polygon will fill on the right of 'e2' (and 'e' will be inside) - // nb: neither e2.WindCnt nor e2.WindDx should ever be 0. - if (ae2.WindCount * ae2.WindDx < 0) - { - // opposite directions so 'ae' is outside 'ae2' ... - if (Math.Abs(ae2.WindCount) > 1) - { - // outside prev poly but still inside another. - if (ae2.WindDx * ae.WindDx < 0) - { - // reversing direction so use the same WC - ae.WindCount = ae2.WindCount; - } - else - { - // otherwise keep 'reducing' the WC by 1 (i.e. towards 0) ... - ae.WindCount = ae2.WindCount + ae.WindDx; - } - } - else - { - // now outside all polys of same polytype so set own WC ... - ae.WindCount = IsOpen(ae) ? 1 : ae.WindDx; - } - } - else - { - // 'ae' must be inside 'ae2' - if (ae2.WindDx * ae.WindDx < 0) - { - // reversing direction so use the same WC - ae.WindCount = ae2.WindCount; - } - else - { - // otherwise keep 'increasing' the WC by 1 (i.e. away from 0) ... - ae.WindCount = ae2.WindCount + ae.WindDx; - } - } - - ae.WindCount2 = ae2.WindCount2; - ae2 = ae2.NextInAEL; // i.e. get ready to calc WindCnt2 - } - - // update windCount2 ... - if (this.fillRule == FillRule.EvenOdd) - { - while (ae2 != ae) - { - if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) - { - ae.WindCount2 = ae.WindCount2 == 0 ? 1 : 0; - } - - ae2 = ae2.NextInAEL; - } - } - else - { - while (ae2 != ae) - { - if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) - { - ae.WindCount2 += ae2.WindDx; - } - - ae2 = ae2.NextInAEL; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetWindCountForOpenPathEdge(Active ae) - { - Active ae2 = this.actives; - if (this.fillRule == FillRule.EvenOdd) - { - int cnt1 = 0, cnt2 = 0; - while (ae2 != ae) - { - if (GetPolyType(ae2!) == ClippingType.Clip) - { - cnt2++; - } - else if (!IsOpen(ae2!)) - { - cnt1++; - } - - ae2 = ae2.NextInAEL; - } - - ae.WindCount = IsOdd(cnt1) ? 1 : 0; - ae.WindCount2 = IsOdd(cnt2) ? 1 : 0; - } - else - { - while (ae2 != ae) - { - if (GetPolyType(ae2!) == ClippingType.Clip) - { - ae.WindCount2 += ae2.WindDx; - } - else if (!IsOpen(ae2!)) - { - ae.WindCount += ae2.WindDx; - } - - ae2 = ae2.NextInAEL; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsValidAelOrder(Active resident, Active newcomer) - { - if (newcomer.CurX != resident.CurX) - { - return newcomer.CurX > resident.CurX; - } - - // get the turning direction a1.top, a2.bot, a2.top - float d = ClipperUtils.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top); - if (d != 0) - { - return d < 0; - } - - // edges must be collinear to get here - - // for starting open paths, place them according to - // the direction they're about to turn - if (!IsMaxima(resident) && (resident.Top.Y > newcomer.Top.Y)) - { - return ClipperUtils.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0; - } - - if (!IsMaxima(newcomer) && (newcomer.Top.Y > resident.Top.Y)) - { - return ClipperUtils.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0; - } - - float y = newcomer.Bot.Y; - bool newcomerIsLeft = newcomer.IsLeftBound; - - if (resident.Bot.Y != y || resident.LocalMin.Vertex.Point.Y != y) - { - return newcomer.IsLeftBound; - } - - // resident must also have just been inserted - if (resident.IsLeftBound != newcomerIsLeft) - { - return newcomerIsLeft; - } - - if (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0) - { - return true; - } - - // compare turning direction of the alternate bound - return (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InsertLeftEdge(Active ae) - { - Active ae2; - - if (this.actives == null) - { - ae.PrevInAEL = null; - ae.NextInAEL = null; - this.actives = ae; - } - else if (!IsValidAelOrder(this.actives, ae)) - { - ae.PrevInAEL = null; - ae.NextInAEL = this.actives; - this.actives.PrevInAEL = ae; - this.actives = ae; - } - else - { - ae2 = this.actives; - while (ae2.NextInAEL != null && IsValidAelOrder(ae2.NextInAEL, ae)) - { - ae2 = ae2.NextInAEL; - } - - // don't separate joined edges - if (ae2.JoinWith == JoinWith.Right) - { - ae2 = ae2.NextInAEL; - } - - ae.NextInAEL = ae2.NextInAEL; - if (ae2.NextInAEL != null) - { - ae2.NextInAEL.PrevInAEL = ae; - } - - ae.PrevInAEL = ae2; - ae2.NextInAEL = ae; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void InsertRightEdge(Active ae, Active ae2) - { - ae2.NextInAEL = ae.NextInAEL; - if (ae.NextInAEL != null) - { - ae.NextInAEL.PrevInAEL = ae2; - } - - ae2.PrevInAEL = ae; - ae.NextInAEL = ae2; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex NextVertex(Active ae) - { - if (ae.WindDx > 0) - { - return ae.VertexTop.Next; - } - - return ae.VertexTop.Prev; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex PrevPrevVertex(Active ae) - { - if (ae.WindDx > 0) - { - return ae.VertexTop.Prev.Prev; - } - - return ae.VertexTop.Next.Next; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsMaxima(Vertex vertex) - => (vertex.Flags & VertexFlags.LocalMax) != VertexFlags.None; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsMaxima(Active ae) - => IsMaxima(ae.VertexTop); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active GetMaximaPair(Active ae) - { - Active ae2; - ae2 = ae.NextInAEL; - while (ae2 != null) - { - if (ae2.VertexTop == ae.VertexTop) - { - return ae2; // Found! - } - - ae2 = ae2.NextInAEL; - } - - return null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOdd(int val) - => (val & 1) != 0; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHotEdge(Active ae) - => ae.Outrec != null; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOpen(Active ae) - => ae.LocalMin.IsOpen; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOpenEnd(Active ae) - => ae.LocalMin.IsOpen && IsOpenEnd(ae.VertexTop); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOpenEnd(Vertex v) - => (v.Flags & (VertexFlags.OpenStart | VertexFlags.OpenEnd)) != VertexFlags.None; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active GetPrevHotEdge(Active ae) - { - Active prev = ae.PrevInAEL; - while (prev != null && (IsOpen(prev) || !IsHotEdge(prev))) - { - prev = prev.PrevInAEL; - } - - return prev; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void JoinOutrecPaths(Active ae1, Active ae2) - { - // join ae2 outrec path onto ae1 outrec path and then delete ae2 outrec path - // pointers. (NB Only very rarely do the joining ends share the same coords.) - OutPt p1Start = ae1.Outrec.Pts; - OutPt p2Start = ae2.Outrec.Pts; - OutPt p1End = p1Start.Next; - OutPt p2End = p2Start.Next; - if (IsFront(ae1)) - { - p2End.Prev = p1Start; - p1Start.Next = p2End; - p2Start.Next = p1End; - p1End.Prev = p2Start; - ae1.Outrec.Pts = p2Start; - - // nb: if IsOpen(e1) then e1 & e2 must be a 'maximaPair' - ae1.Outrec.FrontEdge = ae2.Outrec.FrontEdge; - if (ae1.Outrec.FrontEdge != null) - { - ae1.Outrec.FrontEdge.Outrec = ae1.Outrec; - } - } - else - { - p1End.Prev = p2Start; - p2Start.Next = p1End; - p1Start.Next = p2End; - p2End.Prev = p1Start; - - ae1.Outrec.BackEdge = ae2.Outrec.BackEdge; - if (ae1.Outrec.BackEdge != null) - { - ae1.Outrec.BackEdge.Outrec = ae1.Outrec; - } - } - - // after joining, the ae2.OutRec must contains no vertices ... - ae2.Outrec.FrontEdge = null; - ae2.Outrec.BackEdge = null; - ae2.Outrec.Pts = null; - SetOwner(ae2.Outrec, ae1.Outrec); - - if (IsOpenEnd(ae1)) - { - ae2.Outrec.Pts = ae1.Outrec.Pts; - ae1.Outrec.Pts = null; - } - - // and ae1 and ae2 are maxima and are about to be dropped from the Actives list. - ae1.Outrec = null; - ae2.Outrec = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt AddOutPt(Active ae, Vector2 pt) - { - // Outrec.OutPts: a circular doubly-linked-list of POutPt where ... - // opFront[.Prev]* ~~~> opBack & opBack == opFront.Next - OutRec outrec = ae.Outrec; - bool toFront = IsFront(ae); - OutPt opFront = outrec.Pts; - OutPt opBack = opFront.Next; - - if (toFront && (pt == opFront.Point)) - { - return opFront; - } - else if (!toFront && (pt == opBack.Point)) - { - return opBack; - } - - OutPt newOp = new(pt, outrec); - opBack.Prev = newOp; - newOp.Prev = opFront; - newOp.Next = opBack; - opFront.Next = newOp; - if (toFront) - { - outrec.Pts = newOp; - } - - return newOp; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutRec NewOutRec() - { - OutRec result = new() - { - Idx = this.outrecList.Count - }; - this.outrecList.Add(result); - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutPt StartOpenPath(Active ae, Vector2 pt) - { - OutRec outrec = this.NewOutRec(); - outrec.IsOpen = true; - if (ae.WindDx > 0) - { - outrec.FrontEdge = ae; - outrec.BackEdge = null; - } - else - { - outrec.FrontEdge = null; - outrec.BackEdge = ae; - } - - ae.Outrec = outrec; - OutPt op = new(pt, outrec); - outrec.Pts = op; - return op; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateEdgeIntoAEL(Active ae) - { - ae.Bot = ae.Top; - ae.VertexTop = NextVertex(ae); - ae.Top = ae.VertexTop.Point; - ae.CurX = ae.Bot.X; - SetDx(ae); - - if (IsJoined(ae)) - { - this.Split(ae, ae.Bot); - } - - if (IsHorizontal(ae)) - { - return; - } - - this.InsertScanline(ae.Top.Y); - - this.CheckJoinLeft(ae, ae.Bot); - this.CheckJoinRight(ae, ae.Bot, true); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetSides(OutRec outrec, Active startEdge, Active endEdge) - { - outrec.FrontEdge = startEdge; - outrec.BackEdge = endEdge; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SwapOutrecs(Active ae1, Active ae2) - { - OutRec or1 = ae1.Outrec; // at least one edge has - OutRec or2 = ae2.Outrec; // an assigned outrec - if (or1 == or2) - { - (or1.BackEdge, or1.FrontEdge) = (or1.FrontEdge, or1.BackEdge); - return; - } - - if (or1 != null) - { - if (ae1 == or1.FrontEdge) - { - or1.FrontEdge = ae2; - } - else - { - or1.BackEdge = ae2; - } - } - - if (or2 != null) - { - if (ae2 == or2.FrontEdge) - { - or2.FrontEdge = ae1; - } - else - { - or2.BackEdge = ae1; - } - } - - ae1.Outrec = or2; - ae2.Outrec = or1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetOwner(OutRec outrec, OutRec newOwner) - { - // precondition1: new_owner is never null - while (newOwner.Owner != null && newOwner.Owner.Pts == null) - { - newOwner.Owner = newOwner.Owner.Owner; - } - - // make sure that outrec isn't an owner of newOwner - OutRec tmp = newOwner; - while (tmp != null && tmp != outrec) - { - tmp = tmp.Owner; - } - - if (tmp != null) - { - newOwner.Owner = outrec.Owner; - } - - outrec.Owner = newOwner; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float Area(OutPt op) - { - // https://en.wikipedia.org/wiki/Shoelace_formula - float area = 0; - OutPt op2 = op; - do - { - area += (op2.Prev.Point.Y + op2.Point.Y) * (op2.Prev.Point.X - op2.Point.X); - op2 = op2.Next; - } - while (op2 != op); - return area * .5F; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float AreaTriangle(Vector2 pt1, Vector2 pt2, Vector2 pt3) - => ((pt3.Y + pt1.Y) * (pt3.X - pt1.X)) - + ((pt1.Y + pt2.Y) * (pt1.X - pt2.X)) - + ((pt2.Y + pt3.Y) * (pt2.X - pt3.X)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutRec GetRealOutRec(OutRec outRec) - { - while ((outRec != null) && (outRec.Pts == null)) - { - outRec = outRec.Owner; - } - - return outRec; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void UncoupleOutRec(Active ae) - { - OutRec outrec = ae.Outrec; - if (outrec == null) - { - return; - } - - outrec.FrontEdge.Outrec = null; - outrec.BackEdge.Outrec = null; - outrec.FrontEdge = null; - outrec.BackEdge = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool OutrecIsAscending(Active hotEdge) - => hotEdge == hotEdge.Outrec.FrontEdge; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SwapFrontBackSides(OutRec outrec) - { - // while this proc. is needed for open paths - // it's almost never needed for closed paths - (outrec.BackEdge, outrec.FrontEdge) = (outrec.FrontEdge, outrec.BackEdge); - outrec.Pts = outrec.Pts.Next; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool EdgesAdjacentInAEL(IntersectNode inode) - => (inode.Edge1.NextInAEL == inode.Edge2) || (inode.Edge1.PrevInAEL == inode.Edge2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false) - { - Active prev = e.PrevInAEL; - if (prev == null - || IsOpen(e) - || IsOpen(prev) - || !IsHotEdge(e) - || !IsHotEdge(prev)) - { - return; - } - - // Avoid trivial joins - if ((pt.Y < e.Top.Y + 2 || pt.Y < prev.Top.Y + 2) - && ((e.Bot.Y > pt.Y) || (prev.Bot.Y > pt.Y))) - { - return; - } - - if (checkCurrX) - { - if (ClipperUtils.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25) - { - return; - } - } - else if (e.CurX != prev.CurX) - { - return; - } - - if (ClipperUtils.CrossProduct(e.Top, pt, prev.Top) != 0) - { - return; - } - - if (e.Outrec.Idx == prev.Outrec.Idx) - { - this.AddLocalMaxPoly(prev, e, pt); - } - else if (e.Outrec.Idx < prev.Outrec.Idx) - { - JoinOutrecPaths(e, prev); - } - else - { - JoinOutrecPaths(prev, e); - } - - prev.JoinWith = JoinWith.Right; - e.JoinWith = JoinWith.Left; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false) - { - Active next = e.NextInAEL; - if (IsOpen(e) - || !IsHotEdge(e) - || IsJoined(e) - || next == null - || IsOpen(next) - || !IsHotEdge(next)) - { - return; - } - - // Avoid trivial joins - if ((pt.Y < e.Top.Y + 2 || pt.Y < next.Top.Y + 2) - && ((e.Bot.Y > pt.Y) || (next.Bot.Y > pt.Y))) - { - return; - } - - if (checkCurrX) - { - if (ClipperUtils.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25) - { - return; - } - } - else if (e.CurX != next.CurX) - { - return; - } - - if (ClipperUtils.CrossProduct(e.Top, pt, next.Top) != 0) - { - return; - } - - if (e.Outrec.Idx == next.Outrec.Idx) - { - this.AddLocalMaxPoly(e, next, pt); - } - else if (e.Outrec.Idx < next.Outrec.Idx) - { - JoinOutrecPaths(e, next); - } - else - { - JoinOutrecPaths(next, e); - } - - e.JoinWith = JoinWith.Right; - next.JoinWith = JoinWith.Left; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void FixOutRecPts(OutRec outrec) - { - OutPt op = outrec.Pts; - do - { - op.OutRec = outrec; - op = op.Next; - } - while (op != outrec.Pts); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutPt AddLocalMaxPoly(Active ae1, Active ae2, Vector2 pt) - { - if (IsJoined(ae1)) - { - this.Split(ae1, pt); - } - - if (IsJoined(ae2)) - { - this.Split(ae2, pt); - } - - if (IsFront(ae1) == IsFront(ae2)) - { - if (IsOpenEnd(ae1)) - { - SwapFrontBackSides(ae1.Outrec!); - } - else if (IsOpenEnd(ae2)) - { - SwapFrontBackSides(ae2.Outrec!); - } - else - { - return null; - } - } - - OutPt result = AddOutPt(ae1, pt); - if (ae1.Outrec == ae2.Outrec) - { - OutRec outrec = ae1.Outrec; - outrec.Pts = result; - UncoupleOutRec(ae1); - } - - // and to preserve the winding orientation of outrec ... - else if (IsOpen(ae1)) - { - if (ae1.WindDx < 0) - { - JoinOutrecPaths(ae1, ae2); - } - else - { - JoinOutrecPaths(ae2, ae1); - } - } - else if (ae1.Outrec.Idx < ae2.Outrec.Idx) - { - JoinOutrecPaths(ae1, ae2); - } - else - { - JoinOutrecPaths(ae2, ae1); - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsJoined(Active e) - => e.JoinWith != JoinWith.None; - - private void Split(Active e, Vector2 currPt) - { - if (e.JoinWith == JoinWith.Right) - { - e.JoinWith = JoinWith.None; - e.NextInAEL.JoinWith = JoinWith.None; - this.AddLocalMinPoly(e, e.NextInAEL, currPt, true); - } - else - { - e.JoinWith = JoinWith.None; - e.PrevInAEL.JoinWith = JoinWith.None; - this.AddLocalMinPoly(e.PrevInAEL, e, currPt, true); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsFront(Active ae) - => ae == ae.Outrec.FrontEdge; - - private struct LocMinSorter : IComparer - { - public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2) - => locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y); - } - - private readonly struct LocalMinima - { - public readonly Vertex Vertex; - public readonly ClippingType Polytype; - public readonly bool IsOpen; - - public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false) - { - this.Vertex = vertex; - this.Polytype = polytype; - this.IsOpen = isOpen; - } - - public static bool operator ==(LocalMinima lm1, LocalMinima lm2) - - // TODO: Check this. Why ref equals. - => ReferenceEquals(lm1.Vertex, lm2.Vertex); - - public static bool operator !=(LocalMinima lm1, LocalMinima lm2) - => !(lm1 == lm2); - - public override bool Equals(object obj) - => obj is LocalMinima minima && this == minima; - - public override int GetHashCode() - => this.Vertex.GetHashCode(); - } - - // IntersectNode: a structure representing 2 intersecting edges. - // Intersections must be sorted so they are processed from the largest - // Y coordinates to the smallest while keeping edges adjacent. - private readonly struct IntersectNode - { - public readonly Vector2 Point; - public readonly Active Edge1; - public readonly Active Edge2; - - public IntersectNode(Vector2 pt, Active edge1, Active edge2) - { - this.Point = pt; - this.Edge1 = edge1; - this.Edge2 = edge2; - } - } - - private struct HorzSegSorter : IComparer - { - public readonly int Compare(HorzSegment hs1, HorzSegment hs2) - { - if (hs1 == null || hs2 == null) - { - return 0; - } - - if (hs1.RightOp == null) - { - return hs2.RightOp == null ? 0 : 1; - } - else if (hs2.RightOp == null) - { - return -1; - } - else - { - return hs1.LeftOp.Point.X.CompareTo(hs2.LeftOp.Point.X); - } - } - } - - private struct IntersectListSort : IComparer - { - public readonly int Compare(IntersectNode a, IntersectNode b) - { - if (a.Point.Y == b.Point.Y) - { - if (a.Point.X == b.Point.X) - { - return 0; - } - - return (a.Point.X < b.Point.X) ? -1 : 1; - } - - return (a.Point.Y > b.Point.Y) ? -1 : 1; - } - } - - private class HorzSegment - { - public HorzSegment(OutPt op) - { - this.LeftOp = op; - this.RightOp = null; - this.LeftToRight = true; - } - - public OutPt LeftOp { get; set; } - - public OutPt RightOp { get; set; } - - public bool LeftToRight { get; set; } - } - - private class HorzJoin - { - public HorzJoin(OutPt ltor, OutPt rtol) - { - this.Op1 = ltor; - this.Op2 = rtol; - } - - public OutPt Op1 { get; } - - public OutPt Op2 { get; } - } - - // OutPt: vertex data structure for clipping solutions - private class OutPt - { - public OutPt(Vector2 pt, OutRec outrec) - { - this.Point = pt; - this.OutRec = outrec; - this.Next = this; - this.Prev = this; - this.HorizSegment = null; - } - - public Vector2 Point { get; } - - public OutPt Next { get; set; } - - public OutPt Prev { get; set; } - - public OutRec OutRec { get; set; } - - public HorzSegment HorizSegment { get; set; } - } - - // OutRec: path data structure for clipping solutions - private class OutRec - { - public int Idx { get; set; } - - public OutRec Owner { get; set; } - - public Active FrontEdge { get; set; } - - public Active BackEdge { get; set; } - - public OutPt Pts { get; set; } - - public PolyPathF PolyPath { get; set; } - - public BoundsF Bounds { get; set; } - - public PathF Path { get; set; } = []; - - public bool IsOpen { get; set; } - - public List Splits { get; set; } - } - - private class Vertex - { - public Vertex(Vector2 pt, VertexFlags flags, Vertex prev) - { - this.Point = pt; - this.Flags = flags; - this.Next = null; - this.Prev = prev; - } - - public Vector2 Point { get; } - - public Vertex Next { get; set; } - - public Vertex Prev { get; set; } - - public VertexFlags Flags { get; set; } - } - - private class Active - { - public Vector2 Bot { get; set; } - - public Vector2 Top { get; set; } - - public float CurX { get; set; } // current (updated at every new scanline) - - public float Dx { get; set; } - - public int WindDx { get; set; } // 1 or -1 depending on winding direction - - public int WindCount { get; set; } - - public int WindCount2 { get; set; } // winding count of the opposite polytype - - public OutRec Outrec { get; set; } - - // AEL: 'active edge list' (Vatti's AET - active edge table) - // a linked list of all edges (from left to right) that are present - // (or 'active') within the current scanbeam (a horizontal 'beam' that - // sweeps from bottom to top over the paths in the clipping operation). - public Active PrevInAEL { get; set; } - - public Active NextInAEL { get; set; } - - // SEL: 'sorted edge list' (Vatti's ST - sorted table) - // linked list used when sorting edges into their new positions at the - // top of scanbeams, but also (re)used to process horizontals. - public Active PrevInSEL { get; set; } - - public Active NextInSEL { get; set; } - - public Active Jump { get; set; } - - public Vertex VertexTop { get; set; } - - public LocalMinima LocalMin { get; set; } // the bottom of an edge 'bound' (also Vatti) - - public bool IsLeftBound { get; set; } - - public JoinWith JoinWith { get; set; } - } -} - -internal class PolyPathF : IEnumerable -{ - private readonly PolyPathF parent; - private readonly List items = []; - - public PolyPathF(PolyPathF parent = null) - => this.parent = parent; - - public PathF Polygon { get; private set; } // polytree root's polygon == null - - public int Level => this.GetLevel(); - - public bool IsHole => this.GetIsHole(); - - public int Count => this.items.Count; - - public PolyPathF this[int index] => this.items[index]; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PolyPathF AddChild(PathF p) - { - PolyPathF child = new(this) - { - Polygon = p - }; - - this.items.Add(child); - return child; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float Area() - { - float result = this.Polygon == null ? 0 : ClipperUtils.Area(this.Polygon); - for (int i = 0; i < this.items.Count; i++) - { - PolyPathF child = this.items[i]; - result += child.Area(); - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Clear() => this.items.Clear(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool GetIsHole() - { - int lvl = this.Level; - return lvl != 0 && (lvl & 1) == 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetLevel() - { - int result = 0; - PolyPathF pp = this.parent; - while (pp != null) - { - ++result; - pp = pp.parent; - } - - return result; - } - - public IEnumerator GetEnumerator() => this.items.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => this.items.GetEnumerator(); -} - -internal class PolyTreeF : PolyPathF -{ -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs index 4670ddfc..17daf144 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs @@ -80,34 +80,34 @@ public void Execute(float delta, PathsF solution) return; } - // Clean up self-intersections. - PolygonClipper clipper = new() - { - PreserveCollinear = this.PreserveCollinear, - - // The solution should retain the orientation of the input - ReverseSolution = this.ReverseSolution != this.groupList[0].PathsReversed - }; - - clipper.AddSubject(this.solution); - if (this.groupList[0].PathsReversed) - { - clipper.Execute(ClippingOperation.Union, FillRule.Negative, solution); - } - else - { - clipper.Execute(ClippingOperation.Union, FillRule.Positive, solution); - } - - // PolygonClipper will throw for unhandled exceptions but if a result is empty - // we should just return the original path. - if (solution.Count == 0) - { - foreach (PathF path in this.solution) - { - solution.Add(path); - } - } + // // Clean up self-intersections. + // PolygonClipper clipper = new() + // { + // PreserveCollinear = this.PreserveCollinear, + // + // // The solution should retain the orientation of the input + // ReverseSolution = this.ReverseSolution != this.groupList[0].PathsReversed + // }; + // + // clipper.AddSubject(this.solution); + // if (this.groupList[0].PathsReversed) + // { + // clipper.Execute(ClippingOperation.Union, FillRule.Negative, solution); + // } + // else + // { + // clipper.Execute(ClippingOperation.Union, FillRule.Positive, solution); + // } + // + // // PolygonClipper will throw for unhandled exceptions but if a result is empty + // // we should just return the original path. + // if (solution.Count == 0) + // { + // foreach (PathF path in this.solution) + // { + // solution.Add(path); + // } + // } } private void ExecuteInternal(float delta) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index bc4963cd..f1948185 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; @@ -189,7 +190,7 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi Star star = new(64, 64, 5, 24, 64); // See http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/ClipType.htm for reference. - foreach (ClippingOperation operation in (ClippingOperation[])Enum.GetValues(typeof(ClippingOperation))) + foreach (BooleanOperation operation in (BooleanOperation[])Enum.GetValues(typeof(BooleanOperation))) { ShapeOptions options = new() { ClippingOperation = operation }; IPath shape = star.Clip(options, circle); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs index 28a20662..1853828e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -27,7 +28,7 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance() { ShapeOptions option = new() { - ClippingOperation = ClippingOperation.Intersection, + ClippingOperation = BooleanOperation.Intersection, IntersectionRule = IntersectionRule.NonZero }; Configuration config = new(); @@ -36,18 +37,18 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance() context.SetShapeOptions(o => { - Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values + Assert.Equal(BooleanOperation.Intersection, o.ClippingOperation); // has original values Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule); - o.ClippingOperation = ClippingOperation.Xor; + o.ClippingOperation = BooleanOperation.Xor; o.IntersectionRule = IntersectionRule.EvenOdd; }); ShapeOptions returnedOption = context.GetShapeOptions(); - Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation); + Assert.Equal(BooleanOperation.Xor, returnedOption.ClippingOperation); Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule); - Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated + Assert.Equal(BooleanOperation.Intersection, option.ClippingOperation); // hasn't been mutated Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule); } @@ -67,7 +68,7 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance() { ShapeOptions option = new() { - ClippingOperation = ClippingOperation.Intersection, + ClippingOperation = BooleanOperation.Intersection, IntersectionRule = IntersectionRule.NonZero }; Configuration config = new(); @@ -75,16 +76,16 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance() config.SetShapeOptions(o => { - Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values + Assert.Equal(BooleanOperation.Intersection, o.ClippingOperation); // has original values Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule); - o.ClippingOperation = ClippingOperation.Xor; + o.ClippingOperation = BooleanOperation.Xor; o.IntersectionRule = IntersectionRule.EvenOdd; }); ShapeOptions returnedOption = config.GetShapeOptions(); - Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation); + Assert.Equal(BooleanOperation.Xor, returnedOption.ClippingOperation); Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule); - Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated + Assert.Equal(BooleanOperation.Intersection, option.ClippingOperation); // hasn't been mutated Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule); } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index 5d85c26a..eab1aee3 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs @@ -4,6 +4,7 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; +using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Tests.PolygonClipper; @@ -38,7 +39,7 @@ private IEnumerable Clip(IPath shape, params IPath[] hole) } } - return clipper.GenerateClippedShapes(ClippingOperation.Difference, IntersectionRule.EvenOdd); + return clipper.GenerateClippedShapes(BooleanOperation.Difference); } [Fact] From 6c6ff30a5f7382f7d1057ebc672d23b7e75a18f3 Mon Sep 17 00:00:00 2001 From: Stefan Nikolei Date: Tue, 7 Oct 2025 14:20:28 +0200 Subject: [PATCH 02/35] First draft of PolygonOffsette First draft of implementing PolygonOffsetter Also added back the IntersectionRule to GenerateClippedShapes. It is not implemented yet --- .../Shapes/ClipPathExtensions.cs | 2 +- src/ImageSharp.Drawing/Shapes/ISimplePath.cs | 23 ++++++++++ .../Shapes/PolygonClipper/Clipper.cs | 13 +----- .../Shapes/PolygonClipper/PolygonOffsetter.cs | 45 ++++++++++++++++--- .../Shapes/PolygonClipper/ClipperTests.cs | 2 +- 5 files changed, 65 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs index 398ea09e..690d2291 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs @@ -63,7 +63,7 @@ public static IPath Clip( clipper.AddPath(subjectPath, ClippingType.Subject); clipper.AddPaths(clipPaths, ClippingType.Clip); - IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation); + IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation, options.IntersectionRule); return new ComplexPolygon(result); } diff --git a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs index 70727c95..29b85fd2 100644 --- a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs +++ b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs @@ -1,6 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; +using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +using SixLabors.PolygonClipper; + namespace SixLabors.ImageSharp.Drawing; /// @@ -17,4 +22,22 @@ public interface ISimplePath /// Gets the points that make this up as a simple linear path. /// ReadOnlyMemory Points { get; } + + /// + /// Converts to + /// + /// The converted polygon. + internal SixLabors.PolygonClipper.Polygon ToPolygon() + { + SixLabors.PolygonClipper.Polygon polygon = []; + Contour contour = new(); + polygon.Add(contour); + + foreach (PointF point in this.Points.Span) + { + contour.AddVertex(new Vertex(point.X, point.Y)); + } + + return polygon; + } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs index 2f4c89cf..653ea92a 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs @@ -19,7 +19,7 @@ internal class Clipper /// The clipping operation. /// The intersection rule. /// The . - public IPath[] GenerateClippedShapes(BooleanOperation operation) + public IPath[] GenerateClippedShapes(BooleanOperation operation, IntersectionRule rule) { ArgumentNullException.ThrowIfNull(this.subject); ArgumentNullException.ThrowIfNull(this.clip); @@ -28,7 +28,6 @@ public IPath[] GenerateClippedShapes(BooleanOperation operation) SixLabors.PolygonClipper.Polygon result = polygonClipper.Run(); - IPath[] shapes = new IPath[result.Count]; int index = 0; @@ -86,15 +85,7 @@ public void AddPath(IPath path, ClippingType clippingType) /// Type of the poly. internal void AddPath(ISimplePath path, ClippingType clippingType) { - ReadOnlySpan vectors = path.Points.Span; - SixLabors.PolygonClipper.Polygon polygon = []; - Contour contour = new(); - polygon.Add(contour); - - foreach (PointF point in vectors) - { - contour.AddVertex(new Vertex(point.X, point.Y)); - } + SixLabors.PolygonClipper.Polygon polygon = path.ToPolygon(); switch (clippingType) { diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs index 17daf144..31bfc71a 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; @@ -101,13 +102,25 @@ public void Execute(float delta, PathsF solution) // // // PolygonClipper will throw for unhandled exceptions but if a result is empty // // we should just return the original path. - // if (solution.Count == 0) - // { - // foreach (PathF path in this.solution) - // { - // solution.Add(path); - // } - // } + SixLabors.PolygonClipper.Polygon result = SixLabors.PolygonClipper.PolygonClipper.Union(this.solution.ToPolygon(), solution.ToPolygon()); + + if (result.Count == 0) + { + foreach (PathF path in this.solution) + { + solution.Add(path); + } + } + + foreach (Contour contour in result) + { + PathF path = new(contour.Count); + solution.Add(path); + foreach (Vertex vertex in contour) + { + path.Add(new Vector2((float)vertex.X, (float)vertex.Y)); + } + } } private void ExecuteInternal(float delta) @@ -680,6 +693,24 @@ public PathsF(int capacity) : base(capacity) { } + + internal SixLabors.PolygonClipper.Polygon ToPolygon() + { + SixLabors.PolygonClipper.Polygon polygon = []; + + foreach (PathF pathF in this) + { + Contour contour = new(); + polygon.Add(contour); + + foreach (Vector2 point in pathF) + { + contour.AddVertex(new Vertex(point.X, point.Y)); + } + } + + return polygon; + } } internal class PathF : List diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index eab1aee3..6c1a69c6 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs @@ -39,7 +39,7 @@ private IEnumerable Clip(IPath shape, params IPath[] hole) } } - return clipper.GenerateClippedShapes(BooleanOperation.Difference); + return clipper.GenerateClippedShapes(BooleanOperation.Difference, IntersectionRule.EvenOdd); } [Fact] From 8f9696fff2c0891e06b34e23de41c75bc50ee1fb Mon Sep 17 00:00:00 2001 From: Stefan Nikolei Date: Tue, 7 Oct 2025 15:22:40 +0200 Subject: [PATCH 03/35] Fix ClipTest.Issue250 * Initialized clip and subject with empty polygon add a contour per path --- src/ImageSharp.Drawing/Shapes/ISimplePath.cs | 6 ++-- .../Shapes/PolygonClipper/Clipper.cs | 10 +++---- .../Shapes/PolygonClipper/FillRule.cs | 23 --------------- .../Shapes/PolygonClipper/JoinWith.cs | 29 ------------------- .../Shapes/PolygonClipper/VertexFlags.cs | 14 --------- 5 files changed, 7 insertions(+), 75 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/JoinWith.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs diff --git a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs index 29b85fd2..f5e0a84e 100644 --- a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs +++ b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs @@ -27,17 +27,15 @@ public interface ISimplePath /// Converts to /// /// The converted polygon. - internal SixLabors.PolygonClipper.Polygon ToPolygon() + internal SixLabors.PolygonClipper.Contour ToContour() { - SixLabors.PolygonClipper.Polygon polygon = []; Contour contour = new(); - polygon.Add(contour); foreach (PointF point in this.Points.Span) { contour.AddVertex(new Vertex(point.X, point.Y)); } - return polygon; + return contour; } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs index 653ea92a..bd968de5 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs @@ -10,8 +10,8 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// internal class Clipper { - private SixLabors.PolygonClipper.Polygon? subject; - private SixLabors.PolygonClipper.Polygon? clip; + private SixLabors.PolygonClipper.Polygon subject = []; + private SixLabors.PolygonClipper.Polygon clip = []; /// /// Generates the clipped shapes from the previously provided paths. @@ -85,15 +85,15 @@ public void AddPath(IPath path, ClippingType clippingType) /// Type of the poly. internal void AddPath(ISimplePath path, ClippingType clippingType) { - SixLabors.PolygonClipper.Polygon polygon = path.ToPolygon(); + Contour contour = path.ToContour(); switch (clippingType) { case ClippingType.Clip: - this.clip = polygon; + this.clip.Add(contour); break; case ClippingType.Subject: - this.subject = polygon; + this.subject.Add(contour); break; default: throw new ArgumentOutOfRangeException(nameof(clippingType), clippingType, null); diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs deleted file mode 100644 index a4f42b29..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/FillRule.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -/// -/// By far the most widely used filling rules for polygons are EvenOdd -/// and NonZero, sometimes called Alternate and Winding respectively. -/// -/// -/// -/// TODO: This overlaps with the enum. -/// We should see if we can enhance the to support all these rules. -/// -internal enum FillRule -{ - EvenOdd, - NonZero, - Positive, - Negative -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/JoinWith.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/JoinWith.cs deleted file mode 100644 index acfbef55..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/JoinWith.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -internal enum JoinWith -{ - None, - Left, - Right -} - -internal enum HorzPosition -{ - Bottom, - Middle, - Top -} - -// Vertex: a pre-clipping data structure. It is used to separate polygons -// into ascending and descending 'bounds' (or sides) that start at local -// minima and ascend to a local maxima, before descending again. -[Flags] -internal enum PointInPolygonResult -{ - IsOn = 0, - IsInside = 1, - IsOutside = 2 -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs deleted file mode 100644 index 2a990ecf..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexFlags.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -[Flags] -internal enum VertexFlags -{ - None = 0, - OpenStart = 1, - OpenEnd = 2, - LocalMax = 4, - LocalMin = 8 -} From 087f4809b7fca02a1724897e00f851b5ef56fa4d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Oct 2025 14:22:03 +1000 Subject: [PATCH 04/35] Wire up factory --- .../Shapes/ClipPathExtensions.cs | 4 +- .../Shapes/PolygonClipper/Clipper.cs | 69 ++-- .../PolygonClipper/PolygonClipperFactory.cs | 358 ++++++++++++++++++ .../Drawing/FillPolygonTests.cs | 22 +- .../Shapes/PolygonClipper/ClipperTests.cs | 9 +- 5 files changed, 410 insertions(+), 52 deletions(-) create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs index 690d2291..15812355 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs @@ -58,12 +58,12 @@ public static IPath Clip( ShapeOptions options, IEnumerable clipPaths) { - Clipper clipper = new(); + Clipper clipper = new(options.IntersectionRule); clipper.AddPath(subjectPath, ClippingType.Subject); clipper.AddPaths(clipPaths, ClippingType.Clip); - IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation, options.IntersectionRule); + IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation); return new ComplexPolygon(result); } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs index 653ea92a..2334a6f6 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs @@ -2,31 +2,39 @@ // Licensed under the Six Labors Split License. using SixLabors.PolygonClipper; +using ClipperPolygon = SixLabors.PolygonClipper.Polygon; +using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// -/// Library to clip polygons. +/// Performs polygon clipping operations. /// -internal class Clipper +internal sealed class Clipper { - private SixLabors.PolygonClipper.Polygon? subject; - private SixLabors.PolygonClipper.Polygon? clip; + private ClipperPolygon? subject; + private ClipperPolygon? clip; + private readonly IntersectionRule rule; + + /// + /// Initializes a new instance of the class. + /// + /// The intersection rule. + public Clipper(IntersectionRule rule) => this.rule = rule; /// /// Generates the clipped shapes from the previously provided paths. /// /// The clipping operation. - /// The intersection rule. /// The . - public IPath[] GenerateClippedShapes(BooleanOperation operation, IntersectionRule rule) + public IPath[] GenerateClippedShapes(BooleanOperation operation) { ArgumentNullException.ThrowIfNull(this.subject); ArgumentNullException.ThrowIfNull(this.clip); - SixLabors.PolygonClipper.PolygonClipper polygonClipper = new(this.subject, this.clip, operation); + PolygonClipperAction polygonClipper = new(this.subject, this.clip, operation); - SixLabors.PolygonClipper.Polygon result = polygonClipper.Run(); + ClipperPolygon result = polygonClipper.Run(); IPath[] shapes = new IPath[result.Count]; @@ -49,7 +57,7 @@ public IPath[] GenerateClippedShapes(BooleanOperation operation, IntersectionRul } /// - /// Adds the shapes. + /// Adds the collection of paths. /// /// The paths. /// The clipping type. @@ -57,9 +65,21 @@ public void AddPaths(IEnumerable paths, ClippingType clippingType) { Guard.NotNull(paths, nameof(paths)); - foreach (IPath p in paths) + // Accumulate all paths of the complex shape into a single polygon. + ClipperPolygon polygon = []; + + foreach (IPath path in paths) { - this.AddPath(p, clippingType); + polygon = PolygonClipperFactory.FromSimplePaths(path.Flatten(), this.rule, polygon); + } + + if (clippingType == ClippingType.Clip) + { + this.clip = polygon; + } + else + { + this.subject = polygon; } } @@ -72,31 +92,14 @@ public void AddPath(IPath path, ClippingType clippingType) { Guard.NotNull(path, nameof(path)); - foreach (ISimplePath p in path.Flatten()) + ClipperPolygon polygon = PolygonClipperFactory.FromSimplePaths(path.Flatten(), this.rule); + if (clippingType == ClippingType.Clip) { - this.AddPath(p, clippingType); + this.clip = polygon; } - } - - /// - /// Adds the path. - /// - /// The path. - /// Type of the poly. - internal void AddPath(ISimplePath path, ClippingType clippingType) - { - SixLabors.PolygonClipper.Polygon polygon = path.ToPolygon(); - - switch (clippingType) + else { - case ClippingType.Clip: - this.clip = polygon; - break; - case ClippingType.Subject: - this.subject = polygon; - break; - default: - throw new ArgumentOutOfRangeException(nameof(clippingType), clippingType, null); + this.subject = polygon; } } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs new file mode 100644 index 00000000..4b50d7d1 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs @@ -0,0 +1,358 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.PolygonClipper; +using ClipperPolygon = SixLabors.PolygonClipper.Polygon; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; + +/// +/// Builders for from ImageSharp paths. +/// PolygonClipper requires explicit orientation and nesting of contours ImageSharp polygons do not contain that information +/// so we must derive that from the input. +/// +internal static class PolygonClipperFactory +{ + /// + /// Builds a from closed rings. + /// + /// + /// Pipeline: + /// 1) Filter to closed paths with ≥3 unique points, copy to rings. + /// 2) Compute signed area via the shoelace formula to get orientation and magnitude. + /// 3) For each ring, pick its lexicographic bottom-left vertex. + /// 4) Parent assignment: for ring i, shoot a conceptual vertical ray downward from its bottom-left point + /// and test containment against all other rings using the selected . + /// The parent is the smallest-area ring that contains the point. + /// 5) Depth is the number of ancestors by repeated parent lookup. + /// 6) Materialize s, enforce even depth CCW and odd depth CW, + /// set and , add to and wire holes. + /// Notes: + /// - Step 4 mirrors the parent-detection approach formalized in Martínez–Rueda 2013. + /// - Containment uses Even-Odd or Non-Zero consistently, so glyph-like inputs can use Non-Zero. + /// - Boundary handling: points exactly on edges are not special-cased here, which is typical for nesting. + /// + /// Closed simple paths. + /// Containment rule for nesting, or . + /// Optional existing polygon to populate. + /// The constructed . + public static ClipperPolygon FromSimplePaths(IEnumerable paths, IntersectionRule rule, ClipperPolygon? polygon = null) + { + // Gather rings as Vertex lists (explicitly closed), plus per-ring metadata. + List> rings = []; + List areas = []; + List bottomLeft = []; + + foreach (ISimplePath p in paths) + { + if (!p.IsClosed) + { + // TODO: could append first point to close, but that fabricates geometry. + continue; + } + + ReadOnlySpan s = p.Points.Span; + int n = s.Length; + + // Need at least 3 points to form area. + if (n < 3) + { + continue; + } + + // Copy all points as-is. + List ring = new(n); + for (int i = 0; i < n; i++) + { + ring.Add(new Vertex(s[i].X, s[i].Y)); + } + + // Ensure explicit closure: start == end. + if (ring.Count > 0) + { + Vertex first = ring[0]; + Vertex last = ring[^1]; + if (first.X != last.X || first.Y != last.Y) + { + ring.Add(first); + } + } + + // After closure, still require at least 3 unique vertices. + if (ring.Count < 4) // 3 unique + repeated first == last + { + continue; + } + + rings.Add(ring); + + // SignedArea must handle a closed ring (last == first). + areas.Add(SignedArea(ring)); + + // Choose lexicographic bottom-left vertex index for nesting test. + bottomLeft.Add(IndexOfBottomLeft(ring)); + } + + int m = rings.Count; + if (m == 0) + { + return []; + } + + // Parent assignment: pick the smallest-area ring that contains the bottom-left vertex. + int[] parent = new int[m]; + Array.Fill(parent, -1); + + for (int i = 0; i < m; i++) + { + Vertex q = rings[i][bottomLeft[i]]; + int best = -1; + double bestArea = double.MaxValue; + + for (int j = 0; j < m; j++) + { + if (i == j) + { + continue; + } + + if (IsPointInPolygon(q, rings[j], rule)) + { + double a = Math.Abs(areas[j]); + if (a < bestArea) + { + bestArea = a; + best = j; + } + } + } + + parent[i] = best; + } + + // Depth = number of ancestors by following Parent links. + int[] depth = new int[m]; + for (int i = 0; i < m; i++) + { + int d = 0; + for (int pIdx = parent[i]; pIdx >= 0; pIdx = parent[pIdx]) + { + d++; + } + + depth[i] = d; + } + + // Emit contours, enforce orientation by depth, and wire into polygon. + polygon ??= []; + for (int i = 0; i < m; i++) + { + Contour c = new(); + + // Stream vertices into the contour. Ring is already explicitly closed. + foreach (Vertex v in rings[i]) + { + c.AddVertex(v); + } + + // Orientation convention: even depth = outer => CCW, odd depth = hole => CW. + if ((depth[i] & 1) == 0) + { + c.SetCounterClockwise(); + } + else + { + c.SetClockwise(); + } + + // Topology annotations. + c.ParentIndex = parent[i] >= 0 ? parent[i] : null; + c.Depth = depth[i]; + + polygon.Add(c); + } + + // Record hole indices for parents now that indices are stable. + for (int i = 0; i < m; i++) + { + int pIdx = parent[i]; + if (pIdx >= 0) + { + polygon[pIdx].AddHoleIndex(i); + } + } + + return polygon; + } + + /// + /// Computes the signed area of a closed ring using the shoelace formula. + /// + /// Ring of vertices. + /// + /// Formula: + /// + /// A = 0.5 * Σ cross(v[j], v[i]) with j = (i - 1) mod n + /// + /// where cross(a,b) = a.X * b.Y - a.Y * b.X. + /// Interpretation: + /// - A > 0 means counter-clockwise orientation. + /// - A < 0 means clockwise orientation. + /// + private static double SignedArea(List r) + { + double area = 0d; + + for (int i = 0, j = r.Count - 1; i < r.Count; j = i, i++) + { + area += Vertex.Cross(r[j], r[i]); + } + + return 0.5d * area; + } + + /// + /// Returns the index of the lexicographically bottom-left vertex. + /// + /// Ring of vertices. + /// + /// Lexicographic order (X then Y) yields a unique seed for nesting tests and matches + /// common parent-detection proofs that cast a ray from the lowest-leftmost point. + /// + private static int IndexOfBottomLeft(List r) + { + int k = 0; + + for (int i = 1; i < r.Count; i++) + { + Vertex a = r[i]; + Vertex b = r[k]; + + if (a.X < b.X || (a.X == b.X && a.Y < b.Y)) + { + k = i; + } + } + + return k; + } + + /// + /// Dispatches to the selected point-in-polygon implementation. + /// + /// Query point. + /// Closed ring. + /// Fill rule. + private static bool IsPointInPolygon(in Vertex p, List ring, IntersectionRule rule) + { + if (rule == IntersectionRule.EvenOdd) + { + return PointInPolygonEvenOdd(p, ring); + } + + return PointInPolygonNonZero(p, ring); + } + + /// + /// Even-odd point-in-polygon via ray casting. + /// + /// Query point. + /// Closed ring. + /// + /// Let a horizontal ray start at and extend to +∞ in X. + /// For each edge (a→b), count an intersection if the edge straddles the ray’s Y + /// and the ray’s X is strictly less than the edge’s X at that Y: + /// + /// intersects = ((b.Y > p.Y) != (a.Y > p.Y)) amp;& p.X < x_at_pY(a,b) + /// + /// Parity of the count determines interior. + /// Horizontal edges contribute zero because the straddle test excludes equal Y. + /// Using a half-open interval on Y prevents double-counting shared vertices. + /// + private static bool PointInPolygonEvenOdd(in Vertex p, List ring) + { + bool inside = false; + int n = ring.Count; + int j = n - 1; + + for (int i = 0; i < n; j = i, i++) + { + Vertex a = ring[j]; + Vertex b = ring[i]; + + bool straddles = (b.Y > p.Y) != (a.Y > p.Y); + + if (straddles) + { + double ySpan = a.Y - b.Y; + double xAtPY = (((a.X - b.X) * (p.Y - b.Y)) / (ySpan == 0d ? double.Epsilon : ySpan)) + b.X; + + if (p.X < xAtPY) + { + inside = !inside; + } + } + } + + return inside; + } + + /// + /// Non-zero winding point-in-polygon. + /// + /// Query point. + /// Closed ring. + /// + /// Scan all edges (a→b). + /// - If the edge crosses the scanline upward (a.Y ≤ p.Y && b.Y > p.Y) and + /// lies strictly to the left of the edge, increment the winding. + /// - If it crosses downward (a.Y > p.Y && b.Y ≤ p.Y) and + /// lies strictly to the right, decrement the winding. + /// The point is inside iff the winding number is non-zero. + /// Left/right is decided by the sign of the cross product of vectors a→b and a→p. + /// + private static bool PointInPolygonNonZero(in Vertex p, List ring) + { + int winding = 0; + int n = ring.Count; + + for (int i = 0, j = n - 1; i < n; j = i, i++) + { + Vertex a = ring[j]; + Vertex b = ring[i]; + + if (a.Y <= p.Y) + { + if (b.Y > p.Y && IsLeft(a, b, p)) + { + winding++; + } + } + else if (b.Y <= p.Y && !IsLeft(a, b, p)) + { + winding--; + } + } + + return winding != 0; + } + + /// + /// Returns true if is strictly left of the directed edge a→b. + /// + /// Edge start. + /// Edge end. + /// Query point. + /// + /// Tests the sign of the 2D cross product: + /// + /// cross = (b - a) × (p - a) = (b.X - a.X)*(p.Y - a.Y) - (b.Y - a.Y)*(p.X - a.X) + /// + /// Left if cross > 0, right if cross < 0, collinear if cross == 0. + /// + private static bool IsLeft(Vertex a, Vertex b, Vertex p) + { + double cross = ((b.X - a.X) * (p.Y - a.Y)) - ((b.Y - a.Y) * (p.X - a.X)); + return cross > 0d; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index f1948185..f4f45bc9 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -28,9 +28,9 @@ public void FillPolygon_Solid_Basic(TestImageProvider provider, c => c.SetGraphicsOptions(options) .FillPolygon(Color.White, polygon1) .FillPolygon(Color.White, polygon2), + testOutputDetails: $"aa{antialias}", appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false, - testOutputDetails: $"aa{antialias}"); + appendSourceFileOrDescription: false); } [Theory] @@ -178,8 +178,8 @@ public void FillPolygon_StarCircle(TestImageProvider provider) provider.RunValidatingProcessorTest( c => c.Fill(Color.White, shape), comparer: ImageComparer.TolerantPercentage(0.01f), - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } [Theory] @@ -197,10 +197,10 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi provider.RunValidatingProcessorTest( c => c.Fill(Color.DeepPink, circle).Fill(Color.LightGray, star).Fill(Color.ForestGreen, shape), - comparer: ImageComparer.TolerantPercentage(0.01F), testOutputDetails: operation.ToString(), - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); + comparer: ImageComparer.TolerantPercentage(0.01F), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } } @@ -301,8 +301,8 @@ public void Fill_RegularPolygon(TestImageProvider provider, int provider.RunValidatingProcessorTest( c => c.Fill(color, polygon), testOutput, - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } public static readonly TheoryData Fill_EllipsePolygon_Data = @@ -337,8 +337,8 @@ public void Fill_EllipsePolygon(TestImageProvider provider, bool c.Fill(color, polygon); }, testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } [Theory] diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index 6c1a69c6..2c5961d7 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs @@ -28,18 +28,15 @@ public class ClipperTests private IEnumerable Clip(IPath shape, params IPath[] hole) { - Clipper clipper = new(); + Clipper clipper = new(IntersectionRule.EvenOdd); clipper.AddPath(shape, ClippingType.Subject); if (hole != null) { - foreach (IPath s in hole) - { - clipper.AddPath(s, ClippingType.Clip); - } + clipper.AddPaths(hole, ClippingType.Clip); } - return clipper.GenerateClippedShapes(BooleanOperation.Difference, IntersectionRule.EvenOdd); + return clipper.GenerateClippedShapes(BooleanOperation.Difference); } [Fact] From ef89dd068b6cb20f8712dc70f49128a7754834bd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Oct 2025 21:49:42 +1000 Subject: [PATCH 05/35] Use Vertex.Cross --- .../Shapes/PolygonClipper/PolygonClipperFactory.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs index 4b50d7d1..8a26ae42 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using SixLabors.PolygonClipper; using ClipperPolygon = SixLabors.PolygonClipper.Polygon; @@ -100,6 +101,7 @@ public static ClipperPolygon FromSimplePaths(IEnumerable paths, Int } // Parent assignment: pick the smallest-area ring that contains the bottom-left vertex. + // TODO: We can use pooling here if we care about large numbers of rings. int[] parent = new int[m]; Array.Fill(parent, -1); @@ -131,6 +133,7 @@ public static ClipperPolygon FromSimplePaths(IEnumerable paths, Int } // Depth = number of ancestors by following Parent links. + // TODO: We can pool this if we care about large numbers of rings. int[] depth = new int[m]; for (int i = 0; i < m; i++) { @@ -350,9 +353,6 @@ private static bool PointInPolygonNonZero(in Vertex p, List ring) /// /// Left if cross > 0, right if cross < 0, collinear if cross == 0. /// - private static bool IsLeft(Vertex a, Vertex b, Vertex p) - { - double cross = ((b.X - a.X) * (p.Y - a.Y)) - ((b.Y - a.Y) * (p.X - a.X)); - return cross > 0d; - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsLeft(Vertex a, Vertex b, Vertex p) => Vertex.Cross(b - a, p - a) > 0d; } From 5b2179b5447a79fcd88a8e2a690b7a4dded39b07 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 31 Oct 2025 23:18:37 +1000 Subject: [PATCH 06/35] Replace PolygonOffsetter with PolygonStroker from AGG --- ImageSharp.Drawing.sln | 10 +- .../Shapes/ClipPathExtensions.cs | 6 +- src/ImageSharp.Drawing/Shapes/EndCapStyle.cs | 17 + src/ImageSharp.Drawing/Shapes/IPath.cs | 10 +- src/ImageSharp.Drawing/Shapes/JointStyle.cs | 53 ++ .../Shapes/OutlinePathExtensions.cs | 154 ++-- src/ImageSharp.Drawing/Shapes/Polygon.cs | 14 + .../Shapes/PolygonClipper/BoundsF.cs | 90 --- .../{Clipper.cs => ClippedShapeGenerator.cs} | 50 +- .../Shapes/PolygonClipper/ClipperException.cs | 37 - .../Shapes/PolygonClipper/ClipperOffset.cs | 84 -- .../Shapes/PolygonClipper/ClipperUtils.cs | 236 ------ .../PolygonClipper/PolygonClipperFactory.cs | 26 + .../Shapes/PolygonClipper/PolygonOffsetter.cs | 731 ------------------ .../Shapes/PolygonClipper/PolygonStroker.cs | 70 +- .../PolygonClipper/StrokedShapeGenerator.cs | 207 +++++ .../Shapes/PolygonClipper/VertexDistance.cs | 2 + .../Shapes/PolygonClipper/ClipperTests.cs | 2 +- .../RectangularPolygonValueComparer.cs | 4 +- 19 files changed, 493 insertions(+), 1310 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs rename src/ImageSharp.Drawing/Shapes/PolygonClipper/{Clipper.cs => ClippedShapeGenerator.cs} (55%) delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonClipper/StrokedShapeGenerator.cs diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 3f753f6c..74e8e154 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 18 -VisualStudioVersion = 18.0.11123.170 d18.0 +VisualStudioVersion = 18.0.11123.170 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}" ProjectSection(SolutionItems) = preProject @@ -359,14 +359,6 @@ 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/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs index 15812355..37506211 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs @@ -17,7 +17,6 @@ public static class ClipPathExtensions /// The subject path. /// The clipping paths. /// The clipped . - /// Thrown when an error occurred while attempting to clip the polygon. public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths) => subjectPath.Clip((IEnumerable)clipPaths); @@ -28,7 +27,6 @@ public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths) /// The shape options. /// The clipping paths. /// The clipped . - /// Thrown when an error occurred while attempting to clip the polygon. public static IPath Clip( this IPath subjectPath, ShapeOptions options, @@ -41,7 +39,6 @@ public static IPath Clip( /// The subject path. /// The clipping paths. /// The clipped . - /// Thrown when an error occurred while attempting to clip the polygon. public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) => subjectPath.Clip(new ShapeOptions(), clipPaths); @@ -52,13 +49,12 @@ public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) /// The shape options. /// The clipping paths. /// The clipped . - /// Thrown when an error occurred while attempting to clip the polygon. public static IPath Clip( this IPath subjectPath, ShapeOptions options, IEnumerable clipPaths) { - Clipper clipper = new(options.IntersectionRule); + ClippedShapeGenerator clipper = new(options.IntersectionRule); clipper.AddPath(subjectPath, ClippingType.Subject); clipper.AddPaths(clipPaths, ClippingType.Clip); diff --git a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs index 50607e20..f5d8d0f5 100644 --- a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs +++ b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs @@ -34,9 +34,26 @@ public enum EndCapStyle Joined = 4 } +/// +/// Specifies the shape to be used at the ends of open lines or paths when stroking. +/// internal enum LineCap { + /// + /// The stroke ends exactly at the endpoint. + /// No extension is added beyond the path's end coordinates. + /// Butt, + + /// + /// The stroke extends beyond the endpoint by half the line width, + /// producing a square edge. + /// Square, + + /// + /// The stroke ends with a semicircular cap, + /// extending beyond the endpoint by half the line width. + /// Round } diff --git a/src/ImageSharp.Drawing/Shapes/IPath.cs b/src/ImageSharp.Drawing/Shapes/IPath.cs index 755f53d7..4e8be584 100644 --- a/src/ImageSharp.Drawing/Shapes/IPath.cs +++ b/src/ImageSharp.Drawing/Shapes/IPath.cs @@ -13,29 +13,29 @@ public interface IPath /// /// Gets a value indicating whether this instance is closed, open or a composite path with a mixture of open and closed figures. /// - PathTypes PathType { get; } + public PathTypes PathType { get; } /// /// Gets the bounds enclosing the path. /// - RectangleF Bounds { get; } + public RectangleF Bounds { get; } /// /// Converts the into a simple linear path. /// /// Returns the current as simple linear path. - IEnumerable Flatten(); + public IEnumerable Flatten(); /// /// Transforms the path using the specified matrix. /// /// The matrix. /// A new path with the matrix applied to it. - IPath Transform(Matrix3x2 matrix); + public IPath Transform(Matrix3x2 matrix); /// /// Returns this path with all figures closed. /// /// A new close . - IPath AsClosedPath(); + public IPath AsClosedPath(); } diff --git a/src/ImageSharp.Drawing/Shapes/JointStyle.cs b/src/ImageSharp.Drawing/Shapes/JointStyle.cs index c1464824..d3d4d58e 100644 --- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs +++ b/src/ImageSharp.Drawing/Shapes/JointStyle.cs @@ -24,19 +24,72 @@ public enum JointStyle Miter = 2 } +/// +/// Specifies how the connection between two consecutive line segments (a join) +/// is rendered when stroking paths or polygons. +/// internal enum LineJoin { + /// + /// Joins lines by extending their outer edges until they meet at a sharp corner. + /// The miter length is limited by the miter limit; if exceeded, the join may fall back to a bevel. + /// MiterJoin = 0, + + /// + /// Joins lines by extending their outer edges to form a miter, + /// but if the miter length exceeds the miter limit, the join is truncated + /// at the limit distance rather than falling back to a bevel. + /// MiterJoinRevert = 1, + + /// + /// Joins lines by connecting them with a circular arc centered at the join point, + /// producing a smooth, rounded corner. + /// RoundJoin = 2, + + /// + /// Joins lines by connecting the outer corners directly with a straight line, + /// forming a flat edge at the join point. + /// BevelJoin = 3, + + /// + /// Joins lines by forming a miter, but if the miter limit is exceeded, + /// the join falls back to a round join instead of a bevel. + /// MiterJoinRound = 4 } +/// +/// Specifies how inner corners of a stroked path or polygon are rendered +/// when the path turns sharply inward. These settings control how the interior +/// edge of the stroke is joined at such corners. +/// internal enum InnerJoin { + /// + /// Joins inner corners by connecting the edges with a straight line, + /// producing a flat, beveled appearance. + /// InnerBevel, + + /// + /// Joins inner corners by extending the inner edges until they meet at a sharp point. + /// This can create long, narrow joins for acute angles. + /// InnerMiter, + + /// + /// Joins inner corners with a notched appearance, + /// forming a small cut or indentation at the join. + /// InnerJag, + + /// + /// Joins inner corners using a circular arc between the edges, + /// creating a smooth, rounded interior transition. + /// InnerRound } diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index 29213304..fa88e5c4 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; namespace SixLabors.ImageSharp.Drawing; @@ -16,38 +15,12 @@ public static class OutlinePathExtensions private const JointStyle DefaultJointStyle = JointStyle.Square; private const EndCapStyle DefaultEndCapStyle = EndCapStyle.Butt; - /// - /// Calculates the scaling matrixes tha tmust be applied to the inout and output paths of for successful clipping. - /// - /// the requested width - /// The matrix to apply to the input path - /// The matrix to apply to the output path - /// The final width to use internally to outlining - private static float CalculateScalingMatrix(float width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix) - { - // when the thickness is below a 0.5 threshold we need to scale - // the source path (up) and result path (down) by a factor to ensure - // the offest is greater than 0.5 to ensure offsetting isn't skipped. - scaleUpMartrix = Matrix3x2.Identity; - scaleDownMartrix = Matrix3x2.Identity; - if (width < 0.5) - { - float scale = 1 / width; - scaleUpMartrix = Matrix3x2.CreateScale(scale); - scaleDownMartrix = Matrix3x2.CreateScale(width); - width = 1; - } - - return width; - } - /// /// Generates an outline of the path. /// /// The path to outline /// The outline width. /// A new representing the outline. - /// Thrown when an offset cannot be calculated. public static IPath GenerateOutline(this IPath path, float width) => GenerateOutline(path, width, DefaultJointStyle, DefaultEndCapStyle); @@ -59,7 +32,6 @@ public static IPath GenerateOutline(this IPath path, float width) /// The style to apply to the joints. /// The style to apply to the end caps. /// A new representing the outline. - /// Thrown when an offset cannot be calculated. public static IPath GenerateOutline(this IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) { if (width <= 0) @@ -67,14 +39,8 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi return Path.Empty; } - width = CalculateScalingMatrix(width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix); - - ClipperOffset offset = new(MiterOffsetDelta); - - // transform is noop for Matrix3x2.Identity - offset.AddPath(path.Transform(scaleUpMartrix), jointStyle, endCapStyle); - - return offset.Execute(width).Transform(scaleDownMartrix); + StrokedShapeGenerator generator = new(MiterOffsetDelta); + return new ComplexPolygon(generator.GenerateStrokedShapes(path, width)); } /// @@ -84,7 +50,6 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi /// The outline width. /// The pattern made of multiples of the width. /// A new representing the outline. - /// Thrown when an offset cannot be calculated. public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern) => path.GenerateOutline(width, pattern, false); @@ -96,7 +61,6 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe pattern made of multiples of the width. /// Whether the first item in the pattern is on or off. /// A new representing the outline. - /// Thrown when an offset cannot be calculated. public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) => GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle); @@ -109,7 +73,6 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe style to apply to the joints. /// The style to apply to the end caps. /// A new representing the outline. - /// Thrown when an offset cannot be calculated. public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, JointStyle jointStyle, EndCapStyle endCapStyle) => GenerateOutline(path, width, pattern, false, jointStyle, endCapStyle); @@ -123,7 +86,6 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe style to apply to the joints. /// The style to apply to the end caps. /// A new representing the outline. - /// Thrown when an offset cannot be calculated. public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle) { if (width <= 0) @@ -136,88 +98,110 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan paths = path.Flatten(); - IEnumerable paths = path.Transform(scaleUpMartrix).Flatten(); + List outlines = []; + List buffer = new(64); // arbitrary initial capacity hint. - ClipperOffset offset = new(MiterOffsetDelta); - List buffer = []; foreach (ISimplePath p in paths) { bool online = !startOff; - float targetLength = pattern[0] * width; int patternPos = 0; - ReadOnlySpan points = p.Points.Span; + float targetLength = pattern[patternPos] * width; - // Create a new list of points representing the new outline - int pCount = points.Length; - if (!p.IsClosed) + ReadOnlySpan pts = p.Points.Span; + if (pts.Length < 2) { - pCount--; + continue; } + // number of edges to traverse (no wrap for open paths) + int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1; + int i = 0; - Vector2 currentPoint = points[0]; + Vector2 current = pts[0]; - while (i < pCount) + while (i < edgeCount) { - int next = (i + 1) % points.Length; - Vector2 targetPoint = points[next]; - float distToNext = Vector2.Distance(currentPoint, targetPoint); - if (distToNext > targetLength) + int nextIndex = p.IsClosed ? (i + 1) % pts.Length : i + 1; + Vector2 next = pts[nextIndex]; + float segLen = Vector2.Distance(current, next); + + if (segLen <= eps) { - // find a point between the 2 - float t = targetLength / distToNext; + current = next; + i++; + continue; + } + + if (segLen + eps < targetLength) + { + buffer.Add(current); + current = next; + i++; + targetLength -= segLen; + continue; + } - Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t); - buffer.Add(currentPoint); - buffer.Add(point); + if (MathF.Abs(segLen - targetLength) <= eps) + { + buffer.Add(current); + buffer.Add(next); - // we now inset a line joining - if (online) + if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) { - offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle); + outlines.Add([.. buffer]); } - online = !online; - buffer.Clear(); + online = !online; - currentPoint = point; - - // next length + current = next; + i++; patternPos = (patternPos + 1) % pattern.Length; targetLength = pattern[patternPos] * width; + continue; } - else if (distToNext <= targetLength) + + // split inside this segment + float t = targetLength / segLen; // 0 < t < 1 here + Vector2 split = current + (t * (next - current)); + + buffer.Add(current); + buffer.Add(split); + + if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) { - buffer.Add(currentPoint); - currentPoint = targetPoint; - i++; - targetLength -= distToNext; + outlines.Add([.. buffer]); } + + buffer.Clear(); + online = !online; + + current = split; // continue along the same geometric segment + + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * width; } + // flush tail of the last dash span, if any if (buffer.Count > 0) { - if (p.IsClosed) - { - buffer.Add(points[0]); - } - else - { - buffer.Add(points[^1]); - } + buffer.Add(current); // terminate at the true end position - if (online) + if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) { - offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle); + outlines.Add([.. buffer]); } buffer.Clear(); } } - return offset.Execute(width).Transform(scaleDownMartrix); + // Each outline span is stroked as an open polyline; the union cleans overlaps. + StrokedShapeGenerator generator = new(MiterOffsetDelta); + return new ComplexPolygon(generator.GenerateStrokedShapes(outlines, width)); } } diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Shapes/Polygon.cs index a4f60e24..e928d32e 100644 --- a/src/ImageSharp.Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp.Drawing/Shapes/Polygon.cs @@ -55,6 +55,20 @@ internal Polygon(Path path) { } + /// + /// Initializes a new instance of the class using the specified line segments. + /// + /// + /// If owned is set to , modifications to the segments array after construction may affect + /// the Polygon instance. If owned is , the segments are copied to ensure the Polygon is not affected by + /// external changes. + /// + /// An array of line segments that define the edges of the polygon. The order of segments determines the shape of + /// the polygon. + /// + /// to indicate that the Polygon instance takes ownership of the segments array; + /// to create a copy of the array. + /// internal Polygon(ILineSegment[] segments, bool owned) : base(owned ? segments : [.. segments]) { diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs deleted file mode 100644 index 9d48889a..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/BoundsF.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -internal struct BoundsF -{ - public float Left; - public float Top; - public float Right; - public float Bottom; - - public BoundsF(float l, float t, float r, float b) - { - this.Left = l; - this.Top = t; - this.Right = r; - this.Bottom = b; - } - - public BoundsF(BoundsF bounds) - { - this.Left = bounds.Left; - this.Top = bounds.Top; - this.Right = bounds.Right; - this.Bottom = bounds.Bottom; - } - - public BoundsF(bool isValid) - { - if (isValid) - { - this.Left = 0; - this.Top = 0; - this.Right = 0; - this.Bottom = 0; - } - else - { - this.Left = float.MaxValue; - this.Top = float.MaxValue; - this.Right = -float.MaxValue; - this.Bottom = -float.MaxValue; - } - } - - public float Width - { - readonly get => this.Right - this.Left; - set => this.Right = this.Left + value; - } - - public float Height - { - readonly get => this.Bottom - this.Top; - set => this.Bottom = this.Top + value; - } - - public readonly bool IsEmpty() - => this.Bottom <= this.Top || this.Right <= this.Left; - - public readonly Vector2 MidPoint() - => new Vector2(this.Left + this.Right, this.Top + this.Bottom) * .5F; - - public readonly bool Contains(Vector2 pt) - => pt.X > this.Left - && pt.X < this.Right - && pt.Y > this.Top && pt.Y < this.Bottom; - - public readonly bool Contains(BoundsF bounds) - => bounds.Left >= this.Left - && bounds.Right <= this.Right - && bounds.Top >= this.Top - && bounds.Bottom <= this.Bottom; - - public readonly bool Intersects(BoundsF bounds) - => (Math.Max(this.Left, bounds.Left) < Math.Min(this.Right, bounds.Right)) - && (Math.Max(this.Top, bounds.Top) < Math.Min(this.Bottom, bounds.Bottom)); - - public readonly PathF AsPath() - => new(4) - { - new Vector2(this.Left, this.Top), - new Vector2(this.Right, this.Top), - new Vector2(this.Right, this.Bottom), - new Vector2(this.Left, this.Bottom) - }; -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippedShapeGenerator.cs similarity index 55% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs rename to src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippedShapeGenerator.cs index 2334a6f6..5e723c90 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippedShapeGenerator.cs @@ -8,25 +8,36 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// -/// Performs polygon clipping operations. +/// Generates clipped shapes from one or more input paths using polygon boolean operations. /// -internal sealed class Clipper +/// +/// This class provides a high-level wrapper around the low-level . +/// It accumulates subject and clip polygons, applies the specified , +/// and converts the resulting polygon contours back into instances suitable +/// for rendering or further processing. +/// +internal sealed class ClippedShapeGenerator { private ClipperPolygon? subject; private ClipperPolygon? clip; private readonly IntersectionRule rule; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The intersection rule. - public Clipper(IntersectionRule rule) => this.rule = rule; + public ClippedShapeGenerator(IntersectionRule rule) => this.rule = rule; /// - /// Generates the clipped shapes from the previously provided paths. + /// Generates the final clipped shapes from the previously provided subject and clip paths. /// - /// The clipping operation. - /// The . + /// + /// The boolean operation to perform, such as , + /// , or . + /// + /// + /// An array of instances representing the result of the boolean operation. + /// public IPath[] GenerateClippedShapes(BooleanOperation operation) { ArgumentNullException.ThrowIfNull(this.subject); @@ -57,21 +68,20 @@ public IPath[] GenerateClippedShapes(BooleanOperation operation) } /// - /// Adds the collection of paths. + /// Adds a collection of paths to the current clipping operation. /// - /// The paths. - /// The clipping type. + /// + /// The paths to add. Each path may represent a simple or complex polygon. + /// + /// + /// Determines whether the paths are assigned to the subject or clip polygon. + /// public void AddPaths(IEnumerable paths, ClippingType clippingType) { Guard.NotNull(paths, nameof(paths)); // Accumulate all paths of the complex shape into a single polygon. - ClipperPolygon polygon = []; - - foreach (IPath path in paths) - { - polygon = PolygonClipperFactory.FromSimplePaths(path.Flatten(), this.rule, polygon); - } + ClipperPolygon polygon = PolygonClipperFactory.FromPaths(paths, this.rule); if (clippingType == ClippingType.Clip) { @@ -84,10 +94,12 @@ public void AddPaths(IEnumerable paths, ClippingType clippingType) } /// - /// Adds the path. + /// Adds a single path to the current clipping operation. /// - /// The path. - /// The clipping type. + /// The path to add. + /// + /// Determines whether the path is assigned to the subject or clip polygon. + /// public void AddPath(IPath path, ClippingType clippingType) { Guard.NotNull(path, nameof(path)); diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs deleted file mode 100644 index 39ddcfa0..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -/// -/// The exception that is thrown when an error occurs clipping a polygon. -/// -public class ClipperException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public ClipperException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public ClipperException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class with a specified error message and a - /// reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a - /// reference if no inner exception is specified. - public ClipperException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs deleted file mode 100644 index 4c94f641..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperOffset.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -/// -/// Wrapper for clipper offset -/// -internal class ClipperOffset -{ - private readonly PolygonOffsetter polygonClipperOffset; - - /// - /// Initializes a new instance of the class. - /// - /// meter limit - /// arc tolerance - public ClipperOffset(float meterLimit = 2F, float arcTolerance = .25F) - => this.polygonClipperOffset = new PolygonOffsetter(meterLimit, arcTolerance); - - /// - /// Calculates an offset polygon based on the given path and width. - /// - /// Width - /// path offset - public ComplexPolygon Execute(float width) - { - PathsF solution = []; - this.polygonClipperOffset.Execute(width, solution); - - Polygon[] polygons = new Polygon[solution.Count]; - for (int i = 0; i < solution.Count; i++) - { - PathF pt = solution[i]; - PointF[] points = pt.ToArray(); - - polygons[i] = new Polygon(points); - } - - return new ComplexPolygon(polygons); - } - - /// - /// Adds the path points - /// - /// The path points - /// Joint Style - /// Endcap Style - public void AddPath(ReadOnlySpan pathPoints, JointStyle jointStyle, EndCapStyle endCapStyle) - { - PathF points = new(pathPoints.Length); - points.AddRange(pathPoints); - - this.polygonClipperOffset.AddPath(points, jointStyle, endCapStyle); - } - - /// - /// Adds the path. - /// - /// The path. - /// Joint Style - /// Endcap Style - public void AddPath(IPath path, JointStyle jointStyle, EndCapStyle endCapStyle) - { - Guard.NotNull(path, nameof(path)); - - foreach (ISimplePath p in path.Flatten()) - { - this.AddPath(p, jointStyle, endCapStyle); - } - } - - /// - /// Adds the path. - /// - /// The path. - /// Joint Style - /// Endcap Style - private void AddPath(ISimplePath path, JointStyle jointStyle, EndCapStyle endCapStyle) - { - ReadOnlySpan vectors = path.Points.Span; - this.AddPath(vectors, jointStyle, path.IsClosed ? EndCapStyle.Joined : endCapStyle); - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs deleted file mode 100644 index 39114d8b..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperUtils.cs +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -internal static class ClipperUtils -{ - public const float DefaultArcTolerance = .25F; - public const float FloatingPointTolerance = 1e-05F; - public const float DefaultMinimumEdgeLength = .1F; - - // TODO: rename to Pow2? - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Sqr(float value) => value * value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Area(PathF path) - { - // https://en.wikipedia.org/wiki/Shoelace_formula - float a = 0F; - if (path.Count < 3) - { - return a; - } - - Vector2 prevPt = path[path.Count - 1]; - for (int i = 0; i < path.Count; i++) - { - Vector2 pt = path[i]; - a += (prevPt.Y + pt.Y) * (prevPt.X - pt.X); - prevPt = pt; - } - - return a * .5F; - } - - public static PathF StripDuplicates(PathF path, bool isClosedPath) - { - int cnt = path.Count; - PathF result = new(cnt); - if (cnt == 0) - { - return result; - } - - PointF lastPt = path[0]; - result.Add(lastPt); - for (int i = 1; i < cnt; i++) - { - if (lastPt != path[i]) - { - lastPt = path[i]; - result.Add(lastPt); - } - } - - if (isClosedPath && lastPt == result[0]) - { - result.RemoveAt(result.Count - 1); - } - - return result; - } - - public static PathF Ellipse(Vector2 center, float radiusX, float radiusY = 0, int steps = 0) - { - if (radiusX <= 0) - { - return []; - } - - if (radiusY <= 0) - { - radiusY = radiusX; - } - - if (steps <= 2) - { - steps = (int)MathF.Ceiling(MathF.PI * MathF.Sqrt((radiusX + radiusY) * .5F)); - } - - float si = MathF.Sin(2 * MathF.PI / steps); - float co = MathF.Cos(2 * MathF.PI / steps); - float dx = co, dy = si; - PathF result = new(steps) { new Vector2(center.X + radiusX, center.Y) }; - Vector2 radiusXY = new(radiusX, radiusY); - for (int i = 1; i < steps; ++i) - { - result.Add(center + (radiusXY * new Vector2(dx, dy))); - float x = (dx * co) - (dy * si); - dy = (dy * co) + (dx * si); - dx = x; - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DotProduct(Vector2 vec1, Vector2 vec2) - => Vector2.Dot(vec1, vec2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float CrossProduct(Vector2 vec1, Vector2 vec2) - => (vec1.Y * vec2.X) - (vec2.Y * vec1.X); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float CrossProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) - => ((pt2.X - pt1.X) * (pt3.Y - pt2.Y)) - ((pt2.Y - pt1.Y) * (pt3.X - pt2.X)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) - => Vector2.Dot(pt2 - pt1, pt3 - pt2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAlmostZero(float value) - => MathF.Abs(value) <= FloatingPointTolerance; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 line2) - { - Vector2 ab = pt - line1; - Vector2 cd = line2 - line1; - if (cd == Vector2.Zero) - { - return 0; - } - - return Sqr(CrossProduct(cd, ab)) / DotProduct(cd, cd); - } - - public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Vector2 seg2b, bool inclusive = false) - { - if (inclusive) - { - float res1 = CrossProduct(seg1a, seg2a, seg2b); - float res2 = CrossProduct(seg1b, seg2a, seg2b); - if (res1 * res2 > 0) - { - return false; - } - - float res3 = CrossProduct(seg2a, seg1a, seg1b); - float res4 = CrossProduct(seg2b, seg1a, seg1b); - if (res3 * res4 > 0) - { - return false; - } - - // ensure NOT collinear - return res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0; - } - - return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0) - && (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool GetIntersectPt(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) - { - Vector2 dxy1 = ln1b - ln1a; - Vector2 dxy2 = ln2b - ln2a; - float cp = CrossProduct(dxy1, dxy2); - if (cp == 0F) - { - ip = default; - return false; - } - - float qx = CrossProduct(ln1a, dxy1); - float qy = CrossProduct(ln2a, dxy2); - - ip = ((dxy1 * qy) - (dxy2 * qx)) / cp; - return ip != new Vector2(float.MaxValue); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) - { - Vector2 dxy1 = ln1b - ln1a; - Vector2 dxy2 = ln2b - ln2a; - float det = CrossProduct(dxy1, dxy2); - if (det == 0F) - { - ip = default; - return false; - } - - float t = (((ln1a.X - ln2a.X) * dxy2.Y) - ((ln1a.Y - ln2a.Y) * dxy2.X)) / det; - if (t <= 0F) - { - ip = ln1a; - } - else if (t >= 1F) - { - ip = ln1b; - } - else - { - ip = ln1a + (t * dxy1); - } - - return true; - } - - public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 seg2) - { - if (seg1 == seg2) - { - return seg1; - } - - Vector2 dxy = seg2 - seg1; - Vector2 oxy = (offPt - seg1) * dxy; - float q = (oxy.X + oxy.Y) / DotProduct(dxy, dxy); - - if (q < 0) - { - q = 0; - } - else if (q > 1) - { - q = 1; - } - - return seg1 + (dxy * q); - } - - public static PathF ReversePath(PathF path) - { - path.Reverse(); - return path; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs index 8a26ae42..0f0e6063 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs @@ -14,6 +14,32 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; /// internal static class PolygonClipperFactory { + /// + /// Creates a new polygon by combining multiple paths using the specified intersection rule. + /// + /// Use this method to construct complex polygons from multiple input paths, such as when + /// importing shapes from vector graphics or combining user-drawn segments. The resulting polygon's structure + /// depends on the order and geometry of the input paths as well as the chosen intersection rule. + /// + /// + /// A collection of paths that define the shapes to be combined into a single polygon. Each path is expected to + /// represent a simple or complex shape. + /// + /// Containment rule for nesting, or . + /// A representing the union of all input paths, combined according to the specified intersection rule. + public static ClipperPolygon FromPaths(IEnumerable paths, IntersectionRule rule) + { + // Accumulate all paths of the complex shape into a single polygon. + ClipperPolygon polygon = []; + + foreach (IPath path in paths) + { + polygon = FromSimplePaths(path.Flatten(), rule, polygon); + } + + return polygon; + } + /// /// Builds a from closed rings. /// diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs deleted file mode 100644 index 9feb302c..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs +++ /dev/null @@ -1,731 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.PolygonClipper; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -/// -/// Contains functions to offset paths (inflate/shrink). -/// Ported from and originally licensed -/// under -/// -internal sealed class PolygonOffsetter -{ - private const float Tolerance = 1.0E-6F; - private readonly List groupList = []; - private readonly PathF normals = []; - private readonly PathsF solution = []; - private float groupDelta; // *0.5 for open paths; *-1.0 for negative areas - private float delta; - private float absGroupDelta; - private float mitLimSqr; - private float stepsPerRad; - private float stepSin; - private float stepCos; - private JointStyle joinType; - private EndCapStyle endType; - - public PolygonOffsetter( - float miterLimit = 2F, - float arcTolerance = 0F, - bool preserveCollinear = false, - bool reverseSolution = false) - { - this.MiterLimit = miterLimit; - this.ArcTolerance = arcTolerance; - this.MergeGroups = true; - this.PreserveCollinear = preserveCollinear; - this.ReverseSolution = reverseSolution; - } - - public float ArcTolerance { get; } - - public bool MergeGroups { get; } - - public float MiterLimit { get; } - - public bool PreserveCollinear { get; } - - public bool ReverseSolution { get; } - - public void AddPath(PathF path, JointStyle joinType, EndCapStyle endType) - { - if (path.Count == 0) - { - return; - } - - PathsF pp = new(1) { path }; - this.AddPaths(pp, joinType, endType); - } - - public void AddPaths(PathsF paths, JointStyle joinType, EndCapStyle endType) - { - if (paths.Count == 0) - { - return; - } - - this.groupList.Add(new Group(paths, joinType, endType)); - } - - public void Execute(float delta, PathsF solution) - { - solution.Clear(); - this.ExecuteInternal(delta); - if (this.groupList.Count == 0) - { - return; - } - - // // Clean up self-intersections. - // PolygonClipper clipper = new() - // { - // PreserveCollinear = this.PreserveCollinear, - // - // // The solution should retain the orientation of the input - // ReverseSolution = this.ReverseSolution != this.groupList[0].PathsReversed - // }; - // - // clipper.AddSubject(this.solution); - // if (this.groupList[0].PathsReversed) - // { - // clipper.Execute(ClippingOperation.Union, FillRule.Negative, solution); - // } - // else - // { - // clipper.Execute(ClippingOperation.Union, FillRule.Positive, solution); - // } - // - // // PolygonClipper will throw for unhandled exceptions but if a result is empty - // // we should just return the original path. - SixLabors.PolygonClipper.Polygon result = SixLabors.PolygonClipper.PolygonClipper.Union(this.solution.ToPolygon(), solution.ToPolygon()); - - if (result.Count == 0) - { - foreach (PathF path in this.solution) - { - solution.Add(path); - } - } - - foreach (Contour contour in result) - { - PathF path = new(contour.Count); - solution.Add(path); - foreach (Vertex vertex in contour) - { - path.Add(new Vector2((float)vertex.X, (float)vertex.Y)); - } - } - } - - private void ExecuteInternal(float delta) - { - this.solution.Clear(); - if (this.groupList.Count == 0) - { - return; - } - - if (MathF.Abs(delta) < .5F) - { - foreach (Group group in this.groupList) - { - foreach (PathF path in group.InPaths) - { - this.solution.Add(path); - } - } - } - else - { - this.delta = delta; - this.mitLimSqr = this.MiterLimit <= 1 ? 2F : 2F / ClipperUtils.Sqr(this.MiterLimit); - foreach (Group group in this.groupList) - { - this.DoGroupOffset(group); - } - } - } - - private void DoGroupOffset(Group group) - { - if (group.EndType == EndCapStyle.Polygon) - { - // The lowermost polygon must be an outer polygon. So we can use that as the - // designated orientation for outer polygons (needed for tidy-up clipping). - GetBoundsAndLowestPolyIdx(group.InPaths, out int lowestIdx, out _); - if (lowestIdx < 0) - { - return; - } - - float area = ClipperUtils.Area(group.InPaths[lowestIdx]); - group.PathsReversed = area < 0; - if (group.PathsReversed) - { - this.groupDelta = -this.delta; - } - else - { - this.groupDelta = this.delta; - } - } - else - { - group.PathsReversed = false; - this.groupDelta = MathF.Abs(this.delta) * .5F; - } - - this.absGroupDelta = MathF.Abs(this.groupDelta); - this.joinType = group.JoinType; - this.endType = group.EndType; - - // Calculate a sensible number of steps (for 360 deg for the given offset). - if (group.JoinType == JointStyle.Round || group.EndType == EndCapStyle.Round) - { - // arcTol - when fArcTolerance is undefined (0), the amount of - // curve imprecision that's allowed is based on the size of the - // offset (delta). Obviously very large offsets will almost always - // require much less precision. See also offset_triginometry2.svg - float arcTol = this.ArcTolerance > 0.01F - ? this.ArcTolerance - : (float)Math.Log10(2 + this.absGroupDelta) * ClipperUtils.DefaultArcTolerance; - float stepsPer360 = MathF.PI / (float)Math.Acos(1 - (arcTol / this.absGroupDelta)); - this.stepSin = MathF.Sin(2 * MathF.PI / stepsPer360); - this.stepCos = MathF.Cos(2 * MathF.PI / stepsPer360); - - if (this.groupDelta < 0) - { - this.stepSin = -this.stepSin; - } - - this.stepsPerRad = stepsPer360 / (2 * MathF.PI); - } - - bool isJoined = group.EndType is EndCapStyle.Joined or EndCapStyle.Polygon; - - foreach (PathF p in group.InPaths) - { - PathF path = ClipperUtils.StripDuplicates(p, isJoined); - int cnt = path.Count; - if ((cnt == 0) || ((cnt < 3) && (this.endType == EndCapStyle.Polygon))) - { - continue; - } - - if (cnt == 1) - { - group.OutPath = []; - - // Single vertex so build a circle or square. - if (group.EndType == EndCapStyle.Round) - { - float r = this.absGroupDelta; - group.OutPath = ClipperUtils.Ellipse(path[0], r, r); - } - else - { - float d = this.groupDelta; - Vector2 xy = path[0]; - BoundsF r = new(xy.X - d, xy.Y - d, xy.X + d, xy.Y + d); - group.OutPath = r.AsPath(); - } - - group.OutPaths.Add(group.OutPath); - } - else - { - if (cnt == 2 && group.EndType == EndCapStyle.Joined) - { - if (group.JoinType == JointStyle.Round) - { - this.endType = EndCapStyle.Round; - } - else - { - this.endType = EndCapStyle.Square; - } - } - - this.BuildNormals(path); - - if (this.endType == EndCapStyle.Polygon) - { - this.OffsetPolygon(group, path); - } - else if (this.endType == EndCapStyle.Joined) - { - this.OffsetOpenJoined(group, path); - } - else - { - this.OffsetOpenPath(group, path); - } - } - } - - this.solution.AddRange(group.OutPaths); - group.OutPaths.Clear(); - } - - private static void GetBoundsAndLowestPolyIdx(PathsF paths, out int index, out BoundsF bounds) - { - // TODO: default? - bounds = new BoundsF(false); // ie invalid rect - float pX = float.MinValue; - index = -1; - for (int i = 0; i < paths.Count; i++) - { - foreach (Vector2 pt in paths[i]) - { - if (pt.Y >= bounds.Bottom) - { - if (pt.Y > bounds.Bottom || pt.X < pX) - { - index = i; - pX = pt.X; - bounds.Bottom = pt.Y; - } - } - else if (pt.Y < bounds.Top) - { - bounds.Top = pt.Y; - } - - if (pt.X > bounds.Right) - { - bounds.Right = pt.X; - } - else if (pt.X < bounds.Left) - { - bounds.Left = pt.X; - } - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void BuildNormals(PathF path) - { - int cnt = path.Count; - this.normals.Clear(); - this.normals.EnsureCapacity(cnt); - - for (int i = 0; i < cnt - 1; i++) - { - this.normals.Add(GetUnitNormal(path[i], path[i + 1])); - } - - this.normals.Add(GetUnitNormal(path[cnt - 1], path[0])); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OffsetOpenJoined(Group group, PathF path) - { - this.OffsetPolygon(group, path); - - // TODO: Just reverse inline? - path = ClipperUtils.ReversePath(path); - this.BuildNormals(path); - this.OffsetPolygon(group, path); - } - - private void OffsetOpenPath(Group group, PathF path) - { - group.OutPath = new PathF(path.Count); - int highI = path.Count - 1; - - // Further reduced extraneous vertices in solutions (#499) - if (MathF.Abs(this.groupDelta) < Tolerance) - { - group.OutPath.Add(path[0]); - } - else - { - // do the line start cap - switch (this.endType) - { - case EndCapStyle.Butt: - group.OutPath.Add(path[0] - (this.normals[0] * this.groupDelta)); - group.OutPath.Add(this.GetPerpendic(path[0], this.normals[0])); - break; - case EndCapStyle.Round: - this.DoRound(group, path, 0, 0, MathF.PI); - break; - default: - this.DoSquare(group, path, 0, 0); - break; - } - } - - // offset the left side going forward - for (int i = 1, k = 0; i < highI; i++) - { - this.OffsetPoint(group, path, i, ref k); - } - - // reverse normals ... - for (int i = highI; i > 0; i--) - { - this.normals[i] = Vector2.Negate(this.normals[i - 1]); - } - - this.normals[0] = this.normals[highI]; - - // do the line end cap - switch (this.endType) - { - case EndCapStyle.Butt: - group.OutPath.Add(new Vector2( - path[highI].X - (this.normals[highI].X * this.groupDelta), - path[highI].Y - (this.normals[highI].Y * this.groupDelta))); - group.OutPath.Add(this.GetPerpendic(path[highI], this.normals[highI])); - break; - case EndCapStyle.Round: - this.DoRound(group, path, highI, highI, MathF.PI); - break; - default: - this.DoSquare(group, path, highI, highI); - break; - } - - // offset the left side going back - for (int i = highI, k = 0; i > 0; i--) - { - this.OffsetPoint(group, path, i, ref k); - } - - group.OutPaths.Add(group.OutPath); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector2 GetUnitNormal(Vector2 pt1, Vector2 pt2) - { - Vector2 dxy = pt2 - pt1; - if (dxy == Vector2.Zero) - { - return default; - } - - dxy *= 1F / MathF.Sqrt(ClipperUtils.DotProduct(dxy, dxy)); - return new Vector2(dxy.Y, -dxy.X); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void OffsetPolygon(Group group, PathF path) - { - // Dereference the current outpath. - group.OutPath = new PathF(path.Count); - int cnt = path.Count, prev = cnt - 1; - for (int i = 0; i < cnt; i++) - { - this.OffsetPoint(group, path, i, ref prev); - } - - group.OutPaths.Add(group.OutPath); - } - - private void OffsetPoint(Group group, PathF path, int j, ref int k) - { - // Further reduced extraneous vertices in solutions (#499) - if (MathF.Abs(this.groupDelta) < Tolerance) - { - group.OutPath.Add(path[j]); - return; - } - - // Let A = change in angle where edges join - // A == 0: ie no change in angle (flat join) - // A == PI: edges 'spike' - // sin(A) < 0: right turning - // cos(A) < 0: change in angle is more than 90 degree - float sinA = ClipperUtils.CrossProduct(this.normals[j], this.normals[k]); - float cosA = ClipperUtils.DotProduct(this.normals[j], this.normals[k]); - if (sinA > 1F) - { - sinA = 1F; - } - else if (sinA < -1F) - { - sinA = -1F; - } - - // almost straight - less than 1 degree (#424) - if (cosA > 0.99F) - { - this.DoMiter(group, path, j, k, cosA); - } - else if (cosA > -0.99F && (sinA * this.groupDelta < 0F)) - { - // is concave - group.OutPath.Add(this.GetPerpendic(path[j], this.normals[k])); - - // this extra point is the only (simple) way to ensure that - // path reversals are fully cleaned with the trailing clipper - group.OutPath.Add(path[j]); // (#405) - group.OutPath.Add(this.GetPerpendic(path[j], this.normals[j])); - } - else if (this.joinType == JointStyle.Miter) - { - // miter unless the angle is so acute the miter would exceeds ML - if (cosA > this.mitLimSqr - 1) - { - this.DoMiter(group, path, j, k, cosA); - } - else - { - this.DoSquare(group, path, j, k); - } - } - else if (this.joinType == JointStyle.Square) - { - // angle less than 8 degrees or a squared join - this.DoSquare(group, path, j, k); - } - else - { - this.DoRound(group, path, j, k, MathF.Atan2(sinA, cosA)); - } - - k = j; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Vector2 GetPerpendic(Vector2 pt, Vector2 norm) - => pt + (norm * this.groupDelta); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoSquare(Group group, PathF path, int j, int k) - { - Vector2 vec; - if (j == k) - { - vec = new Vector2(this.normals[0].Y, -this.normals[0].X); - } - else - { - vec = GetAvgUnitVector( - new Vector2(-this.normals[k].Y, this.normals[k].X), - new Vector2(this.normals[j].Y, -this.normals[j].X)); - } - - // now offset the original vertex delta units along unit vector - Vector2 ptQ = path[j]; - ptQ = TranslatePoint(ptQ, this.absGroupDelta * vec.X, this.absGroupDelta * vec.Y); - - // get perpendicular vertices - Vector2 pt1 = TranslatePoint(ptQ, this.groupDelta * vec.Y, this.groupDelta * -vec.X); - Vector2 pt2 = TranslatePoint(ptQ, this.groupDelta * -vec.Y, this.groupDelta * vec.X); - - // get 2 vertices along one edge offset - Vector2 pt3 = this.GetPerpendic(path[k], this.normals[k]); - - if (j == k) - { - Vector2 pt4 = pt3 + (vec * this.groupDelta); - Vector2 pt = IntersectPoint(pt1, pt2, pt3, pt4); - - // get the second intersect point through reflecion - group.OutPath.Add(ReflectPoint(pt, ptQ)); - group.OutPath.Add(pt); - } - else - { - Vector2 pt4 = this.GetPerpendic(path[j], this.normals[k]); - Vector2 pt = IntersectPoint(pt1, pt2, pt3, pt4); - - group.OutPath.Add(pt); - - // Get the second intersect point through reflecion - group.OutPath.Add(ReflectPoint(pt, ptQ)); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoMiter(Group group, PathF path, int j, int k, float cosA) - { - float q = this.groupDelta / (cosA + 1); - Vector2 pv = path[j]; - Vector2 nk = this.normals[k]; - Vector2 nj = this.normals[j]; - group.OutPath.Add(pv + ((nk + nj) * q)); - } - - private void DoRound(Group group, PathF path, int j, int k, float angle) - { - Vector2 pt = path[j]; - Vector2 offsetVec = this.normals[k] * new Vector2(this.groupDelta); - if (j == k) - { - offsetVec = Vector2.Negate(offsetVec); - } - - group.OutPath.Add(pt + offsetVec); - - // avoid 180deg concave - if (angle > -MathF.PI + .01F) - { - int steps = Math.Max(2, (int)Math.Ceiling(this.stepsPerRad * MathF.Abs(angle))); - - // ie 1 less than steps - for (int i = 1; i < steps; i++) - { - offsetVec = new Vector2((offsetVec.X * this.stepCos) - (this.stepSin * offsetVec.Y), (offsetVec.X * this.stepSin) + (offsetVec.Y * this.stepCos)); - - group.OutPath.Add(pt + offsetVec); - } - } - - group.OutPath.Add(this.GetPerpendic(pt, this.normals[j])); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector2 TranslatePoint(Vector2 pt, float dx, float dy) - => pt + new Vector2(dx, dy); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector2 ReflectPoint(Vector2 pt, Vector2 pivot) - => pivot + (pivot - pt); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector2 IntersectPoint(Vector2 pt1a, Vector2 pt1b, Vector2 pt2a, Vector2 pt2b) - { - // vertical - if (ClipperUtils.IsAlmostZero(pt1a.X - pt1b.X)) - { - if (ClipperUtils.IsAlmostZero(pt2a.X - pt2b.X)) - { - return default; - } - - float m2 = (pt2b.Y - pt2a.Y) / (pt2b.X - pt2a.X); - float b2 = pt2a.Y - (m2 * pt2a.X); - return new Vector2(pt1a.X, (m2 * pt1a.X) + b2); - } - - // vertical - if (ClipperUtils.IsAlmostZero(pt2a.X - pt2b.X)) - { - float m1 = (pt1b.Y - pt1a.Y) / (pt1b.X - pt1a.X); - float b1 = pt1a.Y - (m1 * pt1a.X); - return new Vector2(pt2a.X, (m1 * pt2a.X) + b1); - } - else - { - float m1 = (pt1b.Y - pt1a.Y) / (pt1b.X - pt1a.X); - float b1 = pt1a.Y - (m1 * pt1a.X); - float m2 = (pt2b.Y - pt2a.Y) / (pt2b.X - pt2a.X); - float b2 = pt2a.Y - (m2 * pt2a.X); - if (ClipperUtils.IsAlmostZero(m1 - m2)) - { - return default; - } - - float x = (b2 - b1) / (m1 - m2); - return new Vector2(x, (m1 * x) + b1); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector2 GetAvgUnitVector(Vector2 vec1, Vector2 vec2) - => NormalizeVector(vec1 + vec2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float Hypotenuse(Vector2 vector) - => MathF.Sqrt(Vector2.Dot(vector, vector)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector2 NormalizeVector(Vector2 vector) - { - float h = Hypotenuse(vector); - if (ClipperUtils.IsAlmostZero(h)) - { - return default; - } - - float inverseHypot = 1 / h; - return vector * inverseHypot; - } - - private class Group - { - public Group(PathsF paths, JointStyle joinType, EndCapStyle endType = EndCapStyle.Polygon) - { - this.InPaths = paths; - this.JoinType = joinType; - this.EndType = endType; - this.OutPath = []; - this.OutPaths = []; - this.PathsReversed = false; - } - - public PathF OutPath { get; set; } - - public PathsF OutPaths { get; } - - public JointStyle JoinType { get; } - - public EndCapStyle EndType { get; set; } - - public bool PathsReversed { get; set; } - - public PathsF InPaths { get; } - } -} - -internal class PathsF : List -{ - public PathsF() - { - } - - public PathsF(IEnumerable items) - : base(items) - { - } - - public PathsF(int capacity) - : base(capacity) - { - } - - internal SixLabors.PolygonClipper.Polygon ToPolygon() - { - SixLabors.PolygonClipper.Polygon polygon = []; - - foreach (PathF pathF in this) - { - Contour contour = new(); - polygon.Add(contour); - - foreach (Vector2 point in pathF) - { - contour.AddVertex(new Vertex(point.X, point.Y)); - } - } - - return polygon; - } -} - -internal class PathF : List -{ - public PathF() - { - } - - public PathF(IEnumerable items) - : base(items) - { - } - - public PathF(int capacity) - : base(capacity) - { - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs index 4061d300..7b962812 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs @@ -3,10 +3,31 @@ using System.Runtime.CompilerServices; +#pragma warning disable SA1201 // Elements should appear in the correct order namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; -#pragma warning disable SA1201 // Elements should appear in the correct order +/// +/// Generates polygonal stroke outlines for vector paths using analytic joins and caps. +/// +/// +/// +/// This class performs geometric stroking of input paths, producing an explicit polygonal +/// outline suitable for filling or clipping. It replicates the behavior of analytic stroking +/// as implemented in vector renderers (e.g., AGG, Skia), without relying on rasterization. +/// +/// +/// The stroker supports multiple join and cap styles, adjustable miter limits, and an +/// approximation scale for arc and round joins. It operates entirely in double precision +/// for numerical stability, emitting coordinates for downstream use +/// in polygon merging or clipping operations. +/// +/// +/// Used by higher-level utility to produce consistent, +/// merged outlines for stroked paths and dashed spans. +/// +/// internal sealed class PolygonStroker + { private ArrayBuilder outVertices = new(1); private ArrayBuilder srcVertices = new(16); @@ -53,8 +74,13 @@ public double Width } } - public PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed) + public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) { + if (linePoints.Length < 2) + { + return []; + } + this.Reset(); this.AddLinePath(linePoints); @@ -63,9 +89,9 @@ public PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed) this.ClosePath(); } - PathF results = new(linePoints.Length * 3); + List results = new(linePoints.Length * 3); this.FinishPath(results); - return results; + return [.. results]; } public void AddLinePath(ReadOnlySpan linePoints) @@ -79,7 +105,9 @@ public void AddLinePath(ReadOnlySpan linePoints) public void ClosePath() { - this.AddVertex(0, 0, PathCommand.EndPoly | (PathCommand)PathFlags.Close); + // Mark the current src path as closed; no geometry is pushed here. + this.closed = (int)PathFlags.Close; + this.status = Status.Initial; } public void FinishPath(List results) @@ -327,6 +355,8 @@ private void CloseVertexPath(bool closed) this.srcVertices.RemoveLast(); } + // Remove the tail pair (vd2 and its predecessor vd1) and re-add the tail 't'. + // Re-adding forces a fresh Measure() against the new predecessor, collapsing zero-length edges. if (this.srcVertices.Length != 0) { this.srcVertices.RemoveLast(); @@ -340,6 +370,7 @@ private void CloseVertexPath(bool closed) return; } + // TODO: Why check again? Doesn't the while loop above already ensure this? while (this.srcVertices.Length > 1) { ref VertexDistance vd1 = ref this.srcVertices[^1]; @@ -489,6 +520,15 @@ private void CalcCap(ref VertexDistance v0, ref VertexDistance v1, double len) { this.outVertices.Clear(); + if (len < Constants.Misc.VertexDistanceEpsilon) + { + // Degenerate cap: emit a symmetric butt cap of zero span. + // This avoids div-by-zero in direction computation. + this.AddPoint(v0.X, v0.Y); + this.AddPoint(v1.X, v1.Y); + return; + } + double dx1 = (v1.Y - v0.Y) / len; double dy1 = (v1.X - v0.X) / len; double dx2 = 0; @@ -544,6 +584,26 @@ private void CalcCap(ref VertexDistance v0, ref VertexDistance v1, double len) private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDistance v2, double len1, double len2) { + const double eps = Constants.Misc.VertexDistanceEpsilon; + if (len1 < eps || len2 < eps) + { + // Degenerate join: reuse the non-degenerate edge length for both offsets + // to emit a simple bevel and avoid unstable direction math. + this.outVertices.Clear(); + + double l1 = len1 >= eps ? len1 : len2; + double l2 = len2 >= eps ? len2 : len1; + + double offX1 = this.strokeWidth * (v1.Y - v0.Y) / l1; + double offY1 = this.strokeWidth * (v1.X - v0.X) / l1; + double offX2 = this.strokeWidth * (v2.Y - v1.Y) / l2; + double offY2 = this.strokeWidth * (v2.X - v1.X) / l2; + + this.AddPoint(v1.X + offX1, v1.Y - offY1); + this.AddPoint(v1.X + offX2, v1.Y - offY2); + return; + } + 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; diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/StrokedShapeGenerator.cs new file mode 100644 index 00000000..4fddefe1 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/StrokedShapeGenerator.cs @@ -0,0 +1,207 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.PolygonClipper; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; + +/// +/// Generates stroked and merged shapes using polygon stroking and boolean clipping. +/// +internal sealed class StrokedShapeGenerator +{ + private readonly PolygonStroker polygonStroker; + + /// + /// Initializes a new instance of the class. + /// + /// meter limit + /// arc tolerance + public StrokedShapeGenerator(float meterLimit = 2F, float arcTolerance = .25F) + { + // TODO: We need to consume the joint type properties here. + // to do so we need to replace the existing ones with our new enums and update + // the overloads and pens. + this.polygonStroker = new PolygonStroker(); + } + + /// + /// Strokes a collection of dashed polyline spans and returns a merged outline. + /// + /// + /// The input spans. Each array is treated as an open polyline + /// and is stroked using the current stroker settings. + /// Spans that are null or contain fewer than 2 points are ignored. + /// + /// The stroke width in the caller’s coordinate space. + /// + /// An array of closed paths representing the stroked outline after boolean merge. + /// Returns an empty array when no valid spans are provided. Returns a single path + /// when only one valid stroked ring is produced. + /// + /// + /// This method streams each dashed span through the internal stroker as an open polyline, + /// producing closed stroke rings. To clean self overlaps, the rings are split between + /// subject and clip sets and a is performed. + /// The split ensures at least two operands so the union resolves overlaps. + /// The union uses to preserve winding density. + /// + public IPath[] GenerateStrokedShapes(List spans, float width) + { + // PolygonClipper is not designed to clean up self-intersecting geometry within a single polygon. + // It operates strictly on two polygon operands (subject and clip) and only resolves overlaps + // between them. To force cleanup of dashed stroke overlaps, we alternate assigning each + // stroked segment to subject or clip, ensuring at least two operands exist so the union + // operation performs a true merge rather than a no-op on a single polygon. + + // 1) Stroke each dashed span as open. + this.polygonStroker.Width = width; + + List rings = new(spans.Count); + foreach (PointF[] span in spans) + { + if (span == null || span.Length < 2) + { + continue; + } + + PointF[] stroked = this.polygonStroker.ProcessPath(span, isClosed: false); + if (stroked.Length < 3) + { + continue; + } + + rings.Add(new Polygon(new LinearLineSegment(stroked))); + } + + int count = rings.Count; + if (count == 0) + { + return []; + } + + if (count == 1) + { + // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty. + return [rings[0]]; + } + + // 2) Partition so the first and last are on different polygons + List subjectRings = new(count); + List clipRings = new(count); + + // First => subject + subjectRings.Add(rings[0]); + + // Middle by alternation using a single bool flag + bool assignToSubject = false; // start with clip for i=1 + for (int i = 1; i < count - 1; i++) + { + if (assignToSubject) + { + subjectRings.Add(rings[i]); + } + else + { + clipRings.Add(rings[i]); + } + + assignToSubject = !assignToSubject; + } + + // Last => opposite of first (i.e., clip) + clipRings.Add(rings[count - 1]); + + // 3) Union subject vs clip + ClippedShapeGenerator clipper = new(IntersectionRule.NonZero); + clipper.AddPaths(subjectRings, ClippingType.Subject); + clipper.AddPaths(clipRings, ClippingType.Clip); + return clipper.GenerateClippedShapes(BooleanOperation.Union); + } + + /// + /// Strokes a path and returns a merged outline from its flattened segments. + /// + /// The source path. It is flattened using the current flattening settings. + /// The stroke width in the caller’s coordinate space. + /// + /// An array of closed paths representing the stroked outline after boolean merge. + /// Returns an empty array when no valid rings are produced. Returns a single path + /// when only one valid stroked ring exists. + /// + /// + /// Each flattened simple path is streamed through the internal stroker as open or closed + /// according to . The resulting stroke rings are split + /// between subject and clip sets and combined using . + /// This split is required because the Martinez based clipper resolves overlaps only between + /// two operands. Using preserves fill across overlaps + /// and prevents unintended holes in the merged outline. + /// + public IPath[] GenerateStrokedShapes(IPath path, float width) + { + // 1) Stroke the input path into closed rings + List rings = []; + this.polygonStroker.Width = width; + + foreach (ISimplePath p in path.Flatten()) + { + PointF[] stroked = this.polygonStroker.ProcessPath(p.Points.Span, p.IsClosed); + if (stroked.Length < 3) + { + continue; // skip degenerate outputs + } + + rings.Add(new Polygon(new LinearLineSegment(stroked))); + } + + int count = rings.Count; + if (count == 0) + { + return []; + } + + if (count == 1) + { + // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty. + return [rings[0]]; + } + + // 2) Partition so the first and last are on different polygons + // PolygonClipper is not designed to clean up self-intersecting geometry within a single polygon. + // It operates strictly on two polygon operands (subject and clip) and only resolves overlaps + // between them. To force cleanup of overlaps, we alternate assigning each stroked ring to + // subject or clip, ensuring at least two operands exist so the union performs a true merge. + List subjectRings = new(count); + List clipRings = new(count); + + // First => subject + subjectRings.Add(rings[0]); + + // Middle by alternation using a single bool flag + bool assignToSubject = false; // start with clip for i=1 + for (int i = 1; i < count - 1; i++) + { + if (assignToSubject) + { + subjectRings.Add(rings[i]); + } + else + { + clipRings.Add(rings[i]); + } + + assignToSubject = !assignToSubject; + } + + // Last => opposite of first (i.e., clip) + clipRings.Add(rings[count - 1]); + + // 3) Union subject vs clip + ClippedShapeGenerator clipper = new(IntersectionRule.NonZero); + clipper.AddPaths(subjectRings, ClippingType.Subject); + clipper.AddPaths(clipRings, ClippingType.Clip); + + // 4) Return the cleaned, merged outline + return clipper.GenerateClippedShapes(BooleanOperation.Union); + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs index 89383756..b625d5cd 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs @@ -5,6 +5,8 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +// TODO: We can improve the performance of some of the operations here by using unsafe casting to Vector128 +// Like we do in PolygonClipper. internal struct VertexDistance { private const double Dd = 1.0 / Constants.Misc.VertexDistanceEpsilon; diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index 2c5961d7..6524c7c8 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs @@ -28,7 +28,7 @@ public class ClipperTests private IEnumerable Clip(IPath shape, params IPath[] hole) { - Clipper clipper = new(IntersectionRule.EvenOdd); + ClippedShapeGenerator clipper = new(IntersectionRule.EvenOdd); clipper.AddPath(shape, ClippingType.Subject); if (hole != null) diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs index 65583e1d..91f64607 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/RectangularPolygonValueComparer.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; /// @@ -10,7 +8,7 @@ namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; /// internal static class RectangularPolygonValueComparer { - public const float DefaultTolerance = ClipperUtils.FloatingPointTolerance; + public const float DefaultTolerance = 1e-05F; public static bool Equals(RectangularPolygon x, RectangularPolygon y, float epsilon = DefaultTolerance) => Math.Abs(x.Left - y.Left) < epsilon From bf57cfe83168e53714f99856d82d229defd7692e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 31 Oct 2025 23:29:58 +1000 Subject: [PATCH 07/35] Update ISimplePath.cs --- src/ImageSharp.Drawing/Shapes/ISimplePath.cs | 25 ++------------------ 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs index f5e0a84e..cabea969 100644 --- a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs +++ b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs @@ -1,11 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices.ComTypes; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; -using SixLabors.PolygonClipper; - namespace SixLabors.ImageSharp.Drawing; /// @@ -16,26 +11,10 @@ public interface ISimplePath /// /// Gets a value indicating whether this instance is a closed path. /// - bool IsClosed { get; } + public bool IsClosed { get; } /// /// Gets the points that make this up as a simple linear path. /// - ReadOnlyMemory Points { get; } - - /// - /// Converts to - /// - /// The converted polygon. - internal SixLabors.PolygonClipper.Contour ToContour() - { - Contour contour = new(); - - foreach (PointF point in this.Points.Span) - { - contour.AddVertex(new Vertex(point.X, point.Y)); - } - - return contour; - } + public ReadOnlyMemory Points { get; } } From f9763530782d7592bd68f0cbebc3f4d6c09b38a1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 31 Oct 2025 23:41:09 +1000 Subject: [PATCH 08/35] Update namespace --- src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs | 2 +- .../Shapes/{PolygonClipper => Helpers}/ArrayBuilder{T}.cs | 2 +- src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs | 2 +- .../ClippedShapeGenerator.cs | 2 +- .../{PolygonClipper => PolygonGeometry}/ClippingType.cs | 2 +- .../PolygonClipperFactory.cs | 2 +- .../{PolygonClipper => PolygonGeometry}/PolygonStroker.cs | 5 +++-- .../StrokedShapeGenerator.cs | 2 +- .../{PolygonClipper => PolygonGeometry}/VertexDistance.cs | 2 +- .../Shapes/PolygonClipper/ClipperTests.cs | 2 +- 10 files changed, 12 insertions(+), 11 deletions(-) rename src/ImageSharp.Drawing/Shapes/{PolygonClipper => Helpers}/ArrayBuilder{T}.cs (98%) rename src/ImageSharp.Drawing/Shapes/{PolygonClipper => PolygonGeometry}/ClippedShapeGenerator.cs (98%) rename src/ImageSharp.Drawing/Shapes/{PolygonClipper => PolygonGeometry}/ClippingType.cs (86%) rename src/ImageSharp.Drawing/Shapes/{PolygonClipper => PolygonGeometry}/PolygonClipperFactory.cs (99%) rename src/ImageSharp.Drawing/Shapes/{PolygonClipper => PolygonGeometry}/PolygonStroker.cs (99%) rename src/ImageSharp.Drawing/Shapes/{PolygonClipper => PolygonGeometry}/StrokedShapeGenerator.cs (99%) rename src/ImageSharp.Drawing/Shapes/{PolygonClipper => PolygonGeometry}/VertexDistance.cs (97%) diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs index 37506211..498126da 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; namespace SixLabors.ImageSharp.Drawing; diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs rename to src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs index 916592fd..c8e7cc26 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ArrayBuilder{T}.cs +++ b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs @@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Shapes.Helpers; /// /// A helper type for avoiding allocations while building arrays. diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index fa88e5c4..7533df08 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; namespace SixLabors.ImageSharp.Drawing; diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippedShapeGenerator.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs index 5e723c90..9a6ac206 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs @@ -5,7 +5,7 @@ using ClipperPolygon = SixLabors.PolygonClipper.Polygon; using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Generates clipped shapes from one or more input paths using polygon boolean operations. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs similarity index 86% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs index 00aa96a4..f2e252f2 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Defines the polygon clipping type. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs index 0f0e6063..f904629e 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipperFactory.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs @@ -5,7 +5,7 @@ using SixLabors.PolygonClipper; using ClipperPolygon = SixLabors.PolygonClipper.Polygon; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Builders for from ImageSharp paths. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs index 7b962812..9d5ac054 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs @@ -2,9 +2,10 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Drawing.Shapes.Helpers; #pragma warning disable SA1201 // Elements should appear in the correct order -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Generates polygonal stroke outlines for vector paths using analytic joins and caps. @@ -47,7 +48,7 @@ internal sealed class PolygonStroker public double ApproximationScale { get; set; } = 1.0; - public LineJoin LineJoin { get; set; } = LineJoin.MiterJoin; + public LineJoin LineJoin { get; set; } = LineJoin.BevelJoin; public LineCap LineCap { get; set; } = LineCap.Butt; diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/StrokedShapeGenerator.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs index 4fddefe1..a2277acb 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs @@ -3,7 +3,7 @@ using SixLabors.PolygonClipper; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Generates stroked and merged shapes using polygon stroking and boolean clipping. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs index b625d5cd..c27a5658 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; // TODO: We can improve the performance of some of the operations here by using unsafe casting to Vector128 // Like we do in PolygonClipper. diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index 6524c7c8..6a48e823 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; using SixLabors.PolygonClipper; From ac98cc6fce77a9ae2e1ebf5e65699ea0f55bdfdb Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Feb 2026 15:23:43 +1000 Subject: [PATCH 09/35] Update ImageSharp.Drawing.sln to Visual Studio 18 Removed Visual Studio Version 17 entry from solution file. --- ImageSharp.Drawing.sln | 2 -- 1 file changed, 2 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index b3b0c3cc..74e8e154 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -2,8 +2,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11123.170 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36623.8 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}" ProjectSection(SolutionItems) = preProject From 33afe06a8e075e56465232ebb5aee2edb93a6bff Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Feb 2026 15:24:42 +1000 Subject: [PATCH 10/35] Update package references in ImageSharp.Drawing.csproj --- src/ImageSharp.Drawing/ImageSharp.Drawing.csproj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index e172d5f4..d0a1b489 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -44,11 +44,9 @@ - - - + - \ No newline at end of file + From 20f646075c86aa5fcac6bd0b64e881f6de4d7d70 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Feb 2026 16:49:30 +1000 Subject: [PATCH 11/35] Only clip when we need to --- .../PolygonGeometry/StrokedShapeGenerator.cs | 104 +++++++++++++++++- .../Shapes/PolygonGeometry/VertexDistance.cs | 3 +- .../Shapes/Text/BaseGlyphBuilder.cs | 3 +- src/ImageSharp.Drawing/Utilities/Intersect.cs | 42 ++++++- 4 files changed, 142 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs index a2277acb..917ccb96 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Utilities; using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; @@ -57,6 +59,7 @@ public IPath[] GenerateStrokedShapes(List spans, float width) // 1) Stroke each dashed span as open. this.polygonStroker.Width = width; + List ringPoints = new(spans.Count); List rings = new(spans.Count); foreach (PointF[] span in spans) { @@ -71,6 +74,7 @@ public IPath[] GenerateStrokedShapes(List spans, float width) continue; } + ringPoints.Add(stroked); rings.Add(new Polygon(new LinearLineSegment(stroked))); } @@ -80,10 +84,9 @@ public IPath[] GenerateStrokedShapes(List spans, float width) return []; } - if (count == 1) + if (!HasIntersections(ringPoints)) { - // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty. - return [rings[0]]; + return count == 1 ? [rings[0]] : [.. rings]; } // 2) Partition so the first and last are on different polygons @@ -140,6 +143,7 @@ public IPath[] GenerateStrokedShapes(List spans, float width) public IPath[] GenerateStrokedShapes(IPath path, float width) { // 1) Stroke the input path into closed rings + List ringPoints = []; List rings = []; this.polygonStroker.Width = width; @@ -151,6 +155,7 @@ public IPath[] GenerateStrokedShapes(IPath path, float width) continue; // skip degenerate outputs } + ringPoints.Add(stroked); rings.Add(new Polygon(new LinearLineSegment(stroked))); } @@ -160,10 +165,9 @@ public IPath[] GenerateStrokedShapes(IPath path, float width) return []; } - if (count == 1) + if (!HasIntersections(ringPoints)) { - // Only one stroked ring. Return as-is; two-operand union requires both sides non-empty. - return [rings[0]]; + return count == 1 ? [rings[0]] : [.. rings]; } // 2) Partition so the first and last are on different polygons @@ -204,4 +208,92 @@ public IPath[] GenerateStrokedShapes(IPath path, float width) // 4) Return the cleaned, merged outline return clipper.GenerateClippedShapes(BooleanOperation.Union); } + + /// + /// Determines whether any of the provided rings contain self-intersections or intersect with other rings. + /// + /// + /// This method performs a conservative scan to detect intersections among the provided rings. It + /// checks for both self-intersections within each ring and intersections between different rings. Rings are treated + /// as polylines; if a ring is closed (its first and last points are equal), the closing segment is included in the + /// intersection checks. This method is intended for fast intersection detection and may be used to determine + /// whether further geometric processing, such as clipping, is necessary. + /// + /// + /// A list of rings, where each ring is represented as an array of points defining its vertices. Each ring is + /// expected to be a sequence of points forming a polyline or polygon. + /// + /// if any ring self-intersects or any two rings intersect; otherwise, . + private static bool HasIntersections(List rings) + { + // Detect whether any stroked ring self-intersects or intersects another ring. + // This is a fast, conservative scan used to decide whether we can skip clipping. + Vector2 intersection = default; + + for (int r = 0; r < rings.Count; r++) + { + PointF[] ring = rings[r]; + int segmentCount = ring.Length - 1; + if (segmentCount < 2) + { + continue; + } + + // 1) Self-intersection scan for the current ring. + // Adjacent segments share a vertex and are skipped to avoid trivial hits. + bool isClosed = ring[0] == ring[^1]; + for (int i = 0; i < segmentCount; i++) + { + Vector2 a0 = new(ring[i].X, ring[i].Y); + Vector2 a1 = new(ring[i + 1].X, ring[i + 1].Y); + + for (int j = i + 1; j < segmentCount; j++) + { + // Skip neighbors and the closing edge pair in a closed ring. + if (j == i + 1 || (isClosed && i == 0 && j == segmentCount - 1)) + { + continue; + } + + Vector2 b0 = new(ring[j].X, ring[j].Y); + Vector2 b1 = new(ring[j + 1].X, ring[j + 1].Y); + if (Intersect.LineSegmentToLineSegmentIgnoreCollinear(a0, a1, b0, b1, ref intersection)) + { + return true; + } + } + } + + // 2) Cross-ring intersection scan against later rings only. + // This avoids double work while checking all ring pairs. + for (int s = r + 1; s < rings.Count; s++) + { + PointF[] other = rings[s]; + int otherSegmentCount = other.Length - 1; + if (otherSegmentCount < 1) + { + continue; + } + + for (int i = 0; i < segmentCount; i++) + { + Vector2 a0 = new(ring[i].X, ring[i].Y); + Vector2 a1 = new(ring[i + 1].X, ring[i + 1].Y); + + for (int j = 0; j < otherSegmentCount; j++) + { + Vector2 b0 = new(other[j].X, other[j].Y); + Vector2 b1 = new(other[j + 1].X, other[j + 1].Y); + if (Intersect.LineSegmentToLineSegmentIgnoreCollinear(a0, a1, b0, b1, ref intersection)) + { + return true; + } + } + } + } + } + + // No intersections detected. + return false; + } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs index c27a5658..8dd0e724 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs @@ -93,5 +93,6 @@ public static bool CalcIntersection(double ax, double ay, double bx, double by, } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double CrossProduct(double x1, double y1, double x2, double y2, double x, double y) => ((x - x2) * (y2 - y1)) - ((y - y2) * (x2 - x1)); + public static 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/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs index 8cc45fbe..8734440e 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs @@ -6,6 +6,7 @@ using SixLabors.Fonts; using SixLabors.Fonts.Rendering; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Text; @@ -220,7 +221,7 @@ void IGlyphRenderer.EndLayer() ShapeOptions options = new() { - ClippingOperation = ClippingOperation.Intersection, + ClippingOperation = BooleanOperation.Intersection, IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule) }; diff --git a/src/ImageSharp.Drawing/Utilities/Intersect.cs b/src/ImageSharp.Drawing/Utilities/Intersect.cs index 624f0953..e20ed9eb 100644 --- a/src/ImageSharp.Drawing/Utilities/Intersect.cs +++ b/src/ImageSharp.Drawing/Utilities/Intersect.cs @@ -1,33 +1,71 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; namespace SixLabors.ImageSharp.Drawing.Utilities; +/// +/// Lightweight 2D segment intersection helpers for polygon and path processing. +/// +/// +/// This is intentionally small and allocation-free. It favors speed and numerical tolerance +/// over exhaustive classification (e.g., collinear overlap detection), which keeps it fast +/// enough for per-segment scanning in stroking or clipping preparation passes. +/// internal static class Intersect { + // Epsilon used for floating-point tolerance. We treat values within ±Eps as zero. + // This helps avoid instability when segments are nearly parallel or endpoints are + // very close to the intersection boundary. private const float Eps = 1e-3f; private const float MinusEps = -Eps; private const float OnePlusEps = 1 + Eps; + /// + /// Tests two line segments for intersection, ignoring collinear overlap. + /// + /// Start of segment A. + /// End of segment A. + /// Start of segment B. + /// End of segment B. + /// + /// Receives the intersection point when the segments intersect within tolerance. + /// When no intersection is detected, the value is left unchanged. + /// + /// + /// if the segments intersect within their extents (including endpoints), + /// if they are disjoint or collinear. + /// + /// + /// The method is based on solving two parametric line equations and uses a small epsilon + /// window around [0, 1] to account for floating-point error. Collinear cases are rejected + /// early (crossD ≈ 0) to keep the method fast; callers that need collinear overlap detection + /// must implement that separately. + /// public static bool LineSegmentToLineSegmentIgnoreCollinear(Vector2 a0, Vector2 a1, Vector2 b0, Vector2 b1, ref Vector2 intersectionPoint) { + // Direction vectors of the segments. float dax = a1.X - a0.X; float day = a1.Y - a0.Y; float dbx = b1.X - b0.X; float dby = b1.Y - b0.Y; + // Cross product of directions. When near zero, the lines are parallel or collinear. float crossD = (-dbx * day) + (dax * dby); - if (crossD > MinusEps && crossD < Eps) + // Reject parallel/collinear lines. Collinear overlap is intentionally ignored. + if (crossD is > MinusEps and < Eps) { return false; } + // Solve for parameters s and t where: + // a0 + t*(a1-a0) = b0 + s*(b1-b0) float s = ((-day * (a0.X - b0.X)) + (dax * (a0.Y - b0.Y))) / crossD; float t = ((dbx * (a0.Y - b0.Y)) - (dby * (a0.X - b0.X))) / crossD; + // If both parameters are within [0,1] (with tolerance), the segments intersect. if (s > MinusEps && s < OnePlusEps && t > MinusEps && t < OnePlusEps) { intersectionPoint.X = a0.X + (t * dax); From 1174d908611d075832cdefed7f4003cc25ca1497 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Feb 2026 23:19:34 +1000 Subject: [PATCH 12/35] Do not use polygonclipper --- samples/DrawShapesWithImageSharp/Program.cs | 47 +- .../ImageSharp.Drawing.csproj | 1 - .../Processing/PathGradientBrush.cs | 2 +- .../Processing/PatternPen.cs | 2 +- src/ImageSharp.Drawing/Processing/Pen.cs | 16 +- .../Processing/PenOptions.cs | 12 +- .../Processing/ShapeOptions.cs | 6 +- src/ImageSharp.Drawing/Processing/SolidPen.cs | 2 +- .../Processing/StrokeOptions.cs | 63 + .../Shapes/BooleanOperation.cs | 31 + .../Shapes/ClipPathExtensions.cs | 2 +- src/ImageSharp.Drawing/Shapes/EndCapStyle.cs | 59 - src/ImageSharp.Drawing/Shapes/InnerJoin.cs | 36 + src/ImageSharp.Drawing/Shapes/JointStyle.cs | 95 - src/ImageSharp.Drawing/Shapes/LineCap.cs | 28 + src/ImageSharp.Drawing/Shapes/LineJoin.cs | 42 + .../Shapes/OutlinePathExtensions.cs | 78 +- .../Shapes/PolygonGeometry/ArrayBuilder{T}.cs | 156 + .../Shapes/PolygonGeometry/BoundsF.cs | 90 + .../PolygonGeometry/ClippedShapeGenerator.cs | 85 +- .../Shapes/PolygonGeometry/Clipper.cs | 111 + .../PolygonGeometry/ClipperException.cs | 37 + .../Shapes/PolygonGeometry/ClipperFillRule.cs | 23 + .../Shapes/PolygonGeometry/ClipperUtils.cs | 236 ++ .../Shapes/PolygonGeometry/JoinWith.cs | 29 + .../Shapes/PolygonGeometry/PolygonClipper.cs | 3461 +++++++++++++++++ .../PolygonGeometry/PolygonClipperFactory.cs | 384 -- .../Shapes/PolygonGeometry/PolygonStroker.cs | 109 +- .../PolygonGeometry/StrokedShapeGenerator.cs | 107 +- .../Shapes/PolygonGeometry/VertexFlags.cs | 14 + .../Shapes/Text/BaseGlyphBuilder.cs | 3 +- .../Drawing/DrawLinesTests.cs | 30 +- .../Drawing/FillPolygonTests.cs | 3 +- .../Drawing/Paths/DrawBezier.cs | 8 +- .../Drawing/Paths/DrawLine.cs | 8 +- .../Drawing/Paths/DrawPath.cs | 8 +- .../Drawing/Paths/DrawPathCollection.cs | 10 +- .../Drawing/Paths/DrawPolygon.cs | 26 +- .../Drawing/Paths/DrawRectangle.cs | 4 +- .../Issues/Issue_323.cs | 22 +- .../ShapeOptionsDefaultsExtensionsTests.cs | 21 +- .../Shapes/PolygonClipper/ClipperTests.cs | 1 - 42 files changed, 4684 insertions(+), 824 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/StrokeOptions.cs create mode 100644 src/ImageSharp.Drawing/Shapes/BooleanOperation.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/EndCapStyle.cs create mode 100644 src/ImageSharp.Drawing/Shapes/InnerJoin.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/JointStyle.cs create mode 100644 src/ImageSharp.Drawing/Shapes/LineCap.cs create mode 100644 src/ImageSharp.Drawing/Shapes/LineJoin.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs index e2862d48..04497dc9 100644 --- a/samples/DrawShapesWithImageSharp/Program.cs +++ b/samples/DrawShapesWithImageSharp/Program.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Globalization; using System.Numerics; using SixLabors.Fonts; using SixLabors.ImageSharp; @@ -26,13 +27,13 @@ public static void Main(string[] args) private static void OutputStars() { - OutputStarOutline(5, 150, 250, width: 20, jointStyle: JointStyle.Miter); - OutputStarOutline(5, 150, 250, width: 20, jointStyle: JointStyle.Round); - OutputStarOutline(5, 150, 250, width: 20, jointStyle: JointStyle.Square); + OutputStarOutline(5, 150, 250, width: 20, jointStyle: LineJoin.Miter); + OutputStarOutline(5, 150, 250, width: 20, jointStyle: LineJoin.Round); + OutputStarOutline(5, 150, 250, width: 20, jointStyle: LineJoin.Bevel); - OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: JointStyle.Square, cap: EndCapStyle.Butt); - OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: JointStyle.Round, cap: EndCapStyle.Round); - OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: JointStyle.Square, cap: EndCapStyle.Square); + OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: LineJoin.Bevel, cap: LineCap.Butt); + OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: LineJoin.Round, cap: LineCap.Round); + OutputStarOutlineDashed(5, 150, 250, width: 20, jointStyle: LineJoin.Bevel, cap: LineCap.Square); OutputStar(3, 5); OutputStar(4); @@ -103,15 +104,13 @@ private static void DrawSerializedOPenSansLetterShape_a() const string path = @"36.57813x49.16406 35.41797x43.67969 35.41797x43.67969 35.13672x43.67969 35.13672x43.67969 34.41629x44.54843 33.69641x45.34412 32.97708x46.06674 32.2583x46.71631 31.54007x47.29282 30.82239x47.79626 30.10526x48.22665 29.38867x48.58398 29.38867x48.58398 28.65012x48.88474 27.86707x49.14539 27.03952x49.36594 26.16748x49.54639 25.25095x49.68674 24.28992x49.78699 23.28439x49.84714 22.23438x49.86719 22.23438x49.86719 21.52775x49.85564 20.84048x49.82104 20.17258x49.76337 19.52405x49.68262 18.28506x49.4519 17.12354x49.12891 16.03946x48.71362 15.03284x48.20605 14.10367x47.6062 13.25195x46.91406 13.25195x46.91406 12.48978x46.13678 11.82922x45.28149 11.27029x44.34821 10.81299x43.33691 10.45731x42.24762 10.20325x41.08032 10.05081x39.83502 10.0127x39.18312 10x38.51172 10x38.51172 10.01823x37.79307 10.07292x37.09613 10.16407x36.42088 10.29169x35.76733 10.6563x34.52533 11.16675x33.37012 11.82304x32.3017 12.62518x31.32007 13.57317x30.42523 14.10185x30.01036 14.66699x29.61719 15.2686x29.24571 15.90666x28.89594 16.58119x28.56786 17.29218x28.26147 18.03962x27.97679 18.82353x27.71381 19.6439x27.47252 20.50073x27.25293 22.32378x26.87885 24.29266x26.59155 26.40739x26.39105 28.66797x26.27734 28.66797x26.27734 35.20703x26.06641 35.20703x26.06641 35.20703x23.67578 35.20703x23.67578 35.17654x22.57907 35.08508x21.55652 34.93265x20.60812 34.71924x19.73389 34.44485x18.93381 34.1095x18.20789 33.71317x17.55612 33.25586x16.97852 33.25586x16.97852 32.73154x16.47177 32.13416x16.03259 31.46371x15.66098 30.72021x15.35693 29.90366x15.12045 29.01404x14.95154 28.05136x14.85019 27.01563x14.81641 27.01563x14.81641 25.79175x14.86255 24.52832x15.00098 23.88177x15.1048 23.22534x15.23169 21.88281x15.55469 20.50073x15.96997 19.0791x16.47754 17.61792x17.07739 16.11719x17.76953 16.11719x17.76953 14.32422x13.30469 14.32422x13.30469 15.04465x12.92841 15.7821x12.573 17.30811x11.9248 18.90222x11.36011 20.56445x10.87891 20.56445x10.87891 22.26184x10.49438 23.96143x10.21973 24.81204x10.1236 25.66321x10.05493 26.51492x10.01373 27.36719x10 27.36719x10 29.03409x10.04779 29.82572x10.10753 30.58948x10.19116 31.32536x10.29869 32.03336x10.43011 32.71348x10.58543 33.36572x10.76465 34.58658x11.19476 35.69592x11.72046 36.69376x12.34174 37.58008x13.05859 37.58008x13.05859 38.35873x13.88092 39.03357x14.8186 39.60458x15.87164 40.07178x17.04004 40.26644x17.6675 40.43515x18.32379 40.5779x19.00893 40.6947x19.7229 40.78555x20.46571 40.85043x21.23737 40.88937x22.03786 40.90234x22.86719 40.90234x22.86719 40.90234x49.16406 23.39453x45.05078 24.06655x45.03911 24.72031x45.00409 25.97302x44.86401 27.15268x44.63055 28.25928x44.30371 29.29282x43.88348 30.2533x43.36987 31.14072x42.76288 31.95508x42.0625 31.95508x42.0625 32.6843x41.27808 33.31628x40.41895 33.85104x39.48511 34.28857x38.47656 34.62888x37.39331 34.87195x36.23535 35.01779x35.00269 35.06641x33.69531 35.06641x33.69531 35.06641x30.21484 35.06641x30.21484 29.23047x30.46094 29.23047x30.46094 27.55093x30.54855 25.9928x30.68835 24.55606x30.88034 23.24072x31.12451 22.04678x31.42087 20.97424x31.76941 20.0231x32.17014 19.19336x32.62305 19.19336x32.62305 18.47238x33.13528 17.84753x33.71399 17.31882x34.35916 16.88623x35.0708 16.54977x35.84891 16.30945x36.69348 16.16525x37.60452 16.11719x38.58203 16.11719x38.58203 16.14713x39.34943 16.23694x40.06958 16.38663x40.74249 16.59619x41.36816 17.19495x42.47778 18.0332x43.39844 18.0332x43.39844 19.08679x44.12134 19.68527x44.40533 20.33154x44.6377 21.0256x44.81842 21.76746x44.94751 22.5571x45.02496 23.39453x45.05078"; string[] paths = path.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); - Polygon[] polys = paths.Select(line => + Polygon[] polys = [.. paths.Select(line => { string[] pl = line.Split([' '], StringSplitOptions.RemoveEmptyEntries); - PointF[] points = pl.Select(p => p.Split('x')) - .Select(p => new PointF(float.Parse(p[0]), float.Parse(p[1]))) - .ToArray(); + PointF[] points = [.. pl.Select(p => p.Split('x')).Select(p => new PointF(float.Parse(p[0], CultureInfo.InvariantCulture), float.Parse(p[1], CultureInfo.InvariantCulture)))]; return new Polygon(points); - }).ToArray(); + })]; ComplexPolygon complex = new(polys); complex.SaveImage("letter", "a.png"); @@ -122,16 +121,14 @@ private static void DrawSerializedOPenSansLetterShape_o() const string path = @"45.40234x29.93359 45.3838x31.09519 45.32819x32.22452 45.23549x33.32157 45.10571x34.38635 44.93886x35.41886 44.73492x36.4191 44.49391x37.38706 44.21582x38.32275 43.90065x39.22617 43.5484x40.09732 43.15907x40.9362 42.73267x41.7428 42.26918x42.51713 41.76862x43.25919 41.23097x43.96897 40.65625x44.64648 40.65625x44.64648 40.04884x45.28719 39.41315x45.88657 38.74916x46.4446 38.05688x46.9613 37.33632x47.43667 36.58746x47.8707 35.81032x48.26339 35.00488x48.61475 34.17116x48.92477 33.30914x49.19345 32.41884x49.4208 31.50024x49.60681 30.55336x49.75149 29.57819x49.85483 28.57472x49.91683 27.54297x49.9375 27.54297x49.9375 26.2691x49.8996 25.03149x49.78589 23.83014x49.59637 22.66504x49.33105 21.53619x48.98993 20.4436x48.573 19.38727x48.08026 18.36719x47.51172 18.36719x47.51172 17.3938x46.87231 16.47754x46.16699 15.61841x45.39575 14.81641x44.55859 14.07153x43.65552 13.38379x42.68652 12.75317x41.65161 12.17969x40.55078 12.17969x40.55078 11.66882x39.39282 11.22607x38.18652 10.85144x36.93188 10.54492x35.62891 10.30652x34.27759 10.13623x32.87793 10.03406x31.42993 10x29.93359 10x29.93359 10.0184x28.77213 10.07361x27.64322 10.16562x26.54685 10.29443x25.48303 10.46005x24.45176 10.66248x23.45303 10.9017x22.48685 11.17773x21.55322 11.49057x20.65214 11.84021x19.7836 12.22665x18.94761 12.6499x18.14417 13.10995x17.37327 13.60681x16.63492 14.14047x15.92912 14.71094x15.25586 14.71094x15.25586 15.31409x14.61941 15.9458x14.02402 16.60608x13.46969 17.29492x12.95642 18.01233x12.48421 18.7583x12.05307 19.53284x11.66299 20.33594x11.31396 21.1676x11.006 22.02783x10.73911 22.91663x10.51327 23.83398x10.32849 24.77991x10.18478 25.75439x10.08212 26.75745x10.02053 27.78906x10 27.78906x10 28.78683x10.02101 29.75864x10.08405 30.70449x10.1891 31.62439x10.33618 32.51833x10.52528 33.38632x10.75641 34.22836x11.02956 35.04443x11.34473 35.83456x11.70192 36.59872x12.10114 37.33694x12.54237 38.04919x13.02563 38.7355x13.55092 39.39584x14.11823 40.03024x14.72755 40.63867x15.37891 40.63867x15.37891 41.21552x16.0661 41.75516x16.78296 42.25757x17.52948 42.72278x18.30566 43.15077x19.11151 43.54153x19.94702 43.89509x20.81219 44.21143x21.70703 44.49055x22.63153 44.73245x23.58569 44.93714x24.56952 45.10461x25.58301 45.23487x26.62616 45.32791x27.69897 45.38374x28.80145 45.40234x29.93359 16.04688x29.93359 16.09302x31.72437 16.23145x33.40527 16.33527x34.20453 16.46216x34.97632 16.61212x35.72064 16.78516x36.4375 16.98126x37.12689 17.20044x37.78882 17.44269x38.42328 17.70801x39.03027 18.30786x40.16187 19x41.18359 19x41.18359 19.78168x42.08997 20.65015x42.87549 21.60541x43.54016 22.64746x44.08398 23.77631x44.50696 24.99194x44.80908 26.29437x44.99036 26.97813x45.03568 27.68359x45.05078 27.68359x45.05078 28.38912x45.03575 29.07309x44.99063 30.37634x44.81018 31.59335x44.50943 32.72412x44.08838 33.76865x43.54703 34.72693x42.88538 35.59897x42.10342 36.38477x41.20117 36.38477x41.20117 37.08102x40.18301 37.68445x39.05334 37.95135x38.44669 38.19504x37.81216 38.41552x37.14976 38.61279x36.45947 38.78686x35.74131 38.93771x34.99527 39.06536x34.22135 39.1698x33.41956 39.30905x31.73233 39.35547x29.93359 39.35547x29.93359 39.30905x28.15189 39.1698x26.48059 39.06536x25.68635 38.93771x24.91971 38.78686x24.18067 38.61279x23.46924 38.41552x22.78541 38.19504x22.12918 37.95135x21.50056 37.68445x20.89954 37.08102x19.7803 36.38477x18.77148 36.38477x18.77148 35.59787x17.87747 34.72253x17.10266 33.75876x16.44705 32.70654x15.91064 31.56589x15.49344 30.33679x15.19543 29.68908x15.09113 29.01926x15.01663 28.32732x14.97193 27.61328x14.95703 27.61328x14.95703 26.90796x14.97173 26.22461x15.01581 24.92383x15.19214 23.71094x15.48602 22.58594x15.89746 21.54883x16.42645 20.59961x17.073 19.73828x17.8371 18.96484x18.71875 18.96484x18.71875 18.28094x19.71686 17.68823x20.83032 17.42607x21.43031 17.18671x22.05914 16.97014x22.71681 16.77637x23.40332 16.60539x24.11867 16.45721x24.86285 16.33183x25.63588 16.22925x26.43774 16.09247x28.12799 16.04688x29.93359 "; string[] paths = path.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); - Polygon[] polys = paths.Select(line => + Polygon[] polys = [.. paths.Select(line => { string[] pl = line.Split([' '], StringSplitOptions.RemoveEmptyEntries); - PointF[] points = pl.Select(p => p.Split('x')) - .Select(p => new PointF(float.Parse(p[0]), float.Parse(p[1]))) - .ToArray(); + PointF[] points = [.. pl.Select(p => p.Split('x')).Select(p => new PointF(float.Parse(p[0], CultureInfo.InvariantCulture), float.Parse(p[1], CultureInfo.InvariantCulture)))]; return new Polygon(points); - }).ToArray(); + })]; ComplexPolygon complex = new(polys); complex.SaveImage("letter", "o.png"); @@ -182,23 +179,33 @@ private static void OutputDrawnShapeHourGlass() sb.Build().Translate(0, 10).Scale(10).SaveImage("drawing", $"HourGlass.png"); } - private static void OutputStarOutline(int points, float inner = 10, float outer = 20, float width = 5, JointStyle jointStyle = JointStyle.Miter) + private static void OutputStarOutline(int points, float inner = 10, float outer = 20, float width = 5, LineJoin jointStyle = LineJoin.Miter) { // center the shape outerRadii + 10 px away from edges float offset = outer + 10; Star star = new(offset, offset, points, inner, outer); - IPath outline = star.GenerateOutline(width, jointStyle, EndCapStyle.Butt); + StrokeOptions options = new() + { + LineJoin = jointStyle, + LineCap = LineCap.Butt + }; + IPath outline = star.GenerateOutline(width, options); outline.SaveImage("Stars", $"StarOutline_{points}_{jointStyle}.png"); } - private static void OutputStarOutlineDashed(int points, float inner = 10, float outer = 20, float width = 5, JointStyle jointStyle = JointStyle.Miter, EndCapStyle cap = EndCapStyle.Butt) + private static void OutputStarOutlineDashed(int points, float inner = 10, float outer = 20, float width = 5, LineJoin jointStyle = LineJoin.Miter, LineCap cap = LineCap.Butt) { // center the shape outerRadii + 10 px away from edges float offset = outer + 10; Star star = new(offset, offset, points, inner, outer); - IPath outline = star.GenerateOutline(width, [3, 3], jointStyle, cap); + StrokeOptions options = new() + { + LineCap = cap, + LineJoin = jointStyle + }; + IPath outline = star.GenerateOutline(width, [3, 3], options); outline.SaveImage("Stars", $"StarOutlineDashed_{points}_{jointStyle}_{cap}.png"); } diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index d0a1b489..488180d6 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -46,7 +46,6 @@ - diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index 78799c43..ef315427 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -224,7 +224,7 @@ public PathGradientBrushApplicator( : base(configuration, options, source) { this.edges = edges; - Vector2[] points = edges.Select(s => s.Start).ToArray(); + Vector2[] points = [.. edges.Select(s => s.Start)]; this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count; this.centerColor = centerColor.ToScaledVector4(); diff --git a/src/ImageSharp.Drawing/Processing/PatternPen.cs b/src/ImageSharp.Drawing/Processing/PatternPen.cs index c2872be5..f6da8ee0 100644 --- a/src/ImageSharp.Drawing/Processing/PatternPen.cs +++ b/src/ImageSharp.Drawing/Processing/PatternPen.cs @@ -75,5 +75,5 @@ public override bool Equals(Pen? other) /// public override IPath GeneratePath(IPath path, float strokeWidth) - => path.GenerateOutline(strokeWidth, this.StrokePattern, this.JointStyle, this.EndCapStyle); + => path.GenerateOutline(strokeWidth, this.StrokePattern, this.StrokeOptions); } diff --git a/src/ImageSharp.Drawing/Processing/Pen.cs b/src/ImageSharp.Drawing/Processing/Pen.cs index 9602c5c9..e3fbd309 100644 --- a/src/ImageSharp.Drawing/Processing/Pen.cs +++ b/src/ImageSharp.Drawing/Processing/Pen.cs @@ -58,6 +58,7 @@ protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern) this.StrokeFill = strokeFill; this.StrokeWidth = strokeWidth; this.pattern = strokePattern; + this.StrokeOptions = new StrokeOptions(); } /// @@ -69,8 +70,7 @@ protected Pen(PenOptions options) this.StrokeFill = options.StrokeFill; this.StrokeWidth = options.StrokeWidth; this.pattern = options.StrokePattern; - this.JointStyle = options.JointStyle; - this.EndCapStyle = options.EndCapStyle; + this.StrokeOptions = options.StrokeOptions ?? new StrokeOptions(); } /// @@ -82,11 +82,8 @@ protected Pen(PenOptions options) /// public ReadOnlySpan StrokePattern => this.pattern; - /// - public JointStyle JointStyle { get; } - - /// - public EndCapStyle EndCapStyle { get; } + /// + public StrokeOptions StrokeOptions { get; } /// /// Applies the styling from the pen to a path and generate a new path with the final vector. @@ -108,9 +105,8 @@ public IPath GeneratePath(IPath path) public virtual bool Equals(Pen? other) => other != null && this.StrokeWidth == other.StrokeWidth - && this.JointStyle == other.JointStyle - && this.EndCapStyle == other.EndCapStyle && this.StrokeFill.Equals(other.StrokeFill) + && this.StrokeOptions.Equals(other.StrokeOptions) && this.StrokePattern.SequenceEqual(other.StrokePattern); /// @@ -118,5 +114,5 @@ public virtual bool Equals(Pen? other) /// public override int GetHashCode() - => HashCode.Combine(this.StrokeWidth, this.JointStyle, this.EndCapStyle, this.StrokeFill, this.pattern); + => HashCode.Combine(this.StrokeWidth, this.StrokeFill, this.StrokeOptions, this.pattern); } diff --git a/src/ImageSharp.Drawing/Processing/PenOptions.cs b/src/ImageSharp.Drawing/Processing/PenOptions.cs index d000b9f9..9cd1ab22 100644 --- a/src/ImageSharp.Drawing/Processing/PenOptions.cs +++ b/src/ImageSharp.Drawing/Processing/PenOptions.cs @@ -51,8 +51,7 @@ public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern) this.StrokeFill = strokeFill; this.StrokeWidth = strokeWidth; this.StrokePattern = strokePattern ?? Pens.EmptyPattern; - this.JointStyle = JointStyle.Square; - this.EndCapStyle = EndCapStyle.Butt; + this.StrokeOptions = new StrokeOptions(); } /// @@ -71,12 +70,7 @@ public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern) public float[] StrokePattern { get; } /// - /// Gets or sets the joint style. + /// Gets or sets the stroke geometry options used to stroke paths drawn with this pen. /// - public JointStyle JointStyle { get; set; } - - /// - /// Gets or sets the end cap style. - /// - public EndCapStyle EndCapStyle { get; set; } + public StrokeOptions? StrokeOptions { get; set; } } diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs index 6f079e43..4df70625 100644 --- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.PolygonClipper; - namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -20,7 +18,7 @@ public ShapeOptions() private ShapeOptions(ShapeOptions source) { this.IntersectionRule = source.IntersectionRule; - this.ClippingOperation = source.ClippingOperation; + this.BooleanOperation = source.BooleanOperation; } /// @@ -28,7 +26,7 @@ private ShapeOptions(ShapeOptions source) /// /// Defaults to . /// - public BooleanOperation ClippingOperation { get; set; } = BooleanOperation.Difference; + public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference; /// /// Gets or sets the rule for calculating intersection points. diff --git a/src/ImageSharp.Drawing/Processing/SolidPen.cs b/src/ImageSharp.Drawing/Processing/SolidPen.cs index e2c827e1..b56e465a 100644 --- a/src/ImageSharp.Drawing/Processing/SolidPen.cs +++ b/src/ImageSharp.Drawing/Processing/SolidPen.cs @@ -68,5 +68,5 @@ public override bool Equals(Pen? other) /// public override IPath GeneratePath(IPath path, float strokeWidth) - => path.GenerateOutline(strokeWidth, this.JointStyle, this.EndCapStyle); + => path.GenerateOutline(strokeWidth, this.StrokeOptions); } diff --git a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs new file mode 100644 index 00000000..4e4b34e8 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Provides configuration options for geometric stroke generation. +/// +public sealed class StrokeOptions : IEquatable +{ + /// + /// Gets or sets the miter limit used to clamp outer miter joins. + /// + public double MiterLimit { get; set; } = 4; + + /// + /// Gets or sets the inner miter limit used to clamp joins on acute interior angles. + /// + public double InnerMiterLimit { get; set; } = 1.01; + + /// + /// Gets or sets the arc approximation scale used for round joins and caps. + /// + public double ApproximationScale { get; set; } = 1.0; + + /// + /// Gets or sets the outer line join style used for stroking corners. + /// + public LineJoin LineJoin { get; set; } = LineJoin.Bevel; + + /// + /// Gets or sets the line cap style used for open path ends. + /// + public LineCap LineCap { get; set; } = LineCap.Butt; + + /// + /// Gets or sets the join style used for sharp interior angles. + /// + public InnerJoin InnerJoin { get; set; } = InnerJoin.Miter; + + /// + public override bool Equals(object? obj) => this.Equals(obj as StrokeOptions); + + /// + public bool Equals(StrokeOptions? other) + => other is not null && + this.MiterLimit == other.MiterLimit && + this.InnerMiterLimit == other.InnerMiterLimit && + this.ApproximationScale == other.ApproximationScale && + this.LineJoin == other.LineJoin && + this.LineCap == other.LineCap && + this.InnerJoin == other.InnerJoin; + + /// + public override int GetHashCode() + => HashCode.Combine( + this.MiterLimit, + this.InnerMiterLimit, + this.ApproximationScale, + this.LineJoin, + this.LineCap, + this.InnerJoin); +} diff --git a/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs b/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs new file mode 100644 index 00000000..7ee16019 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Specifies the type of boolean operation to perform on polygons. +/// +public enum BooleanOperation +{ + /// + /// The intersection operation, which results in the area common to both polygons. + /// + Intersection = 0, + + /// + /// The union operation, which results in the combined area of both polygons. + /// + Union = 1, + + /// + /// The difference operation, which subtracts the clipping polygon from the subject polygon. + /// + Difference = 2, + + /// + /// The exclusive OR (XOR) operation, which results in the area covered by exactly one polygon, + /// excluding the overlapping areas. + /// + Xor = 3 +} diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs index 498126da..a1101853 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs @@ -59,7 +59,7 @@ public static IPath Clip( clipper.AddPath(subjectPath, ClippingType.Subject); clipper.AddPaths(clipPaths, ClippingType.Clip); - IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation); + IPath[] result = clipper.GenerateClippedShapes(options.BooleanOperation); return new ComplexPolygon(result); } diff --git a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs deleted file mode 100644 index f5d8d0f5..00000000 --- a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// The style to apply to the end cap when generating an outline. -/// -public enum EndCapStyle -{ - /// - /// The outline stops exactly at the end of the path. - /// - Butt = 0, - - /// - /// The outline extends with a rounded style passed the end of the path. - /// - Round = 1, - - /// - /// The outlines ends squared off passed the end of the path. - /// - Square = 2, - - /// - /// The outline is treated as a polygon. - /// - Polygon = 3, - - /// - /// The outlines ends are joined and the path treated as a polyline - /// - Joined = 4 -} - -/// -/// Specifies the shape to be used at the ends of open lines or paths when stroking. -/// -internal enum LineCap -{ - /// - /// The stroke ends exactly at the endpoint. - /// No extension is added beyond the path's end coordinates. - /// - Butt, - - /// - /// The stroke extends beyond the endpoint by half the line width, - /// producing a square edge. - /// - Square, - - /// - /// The stroke ends with a semicircular cap, - /// extending beyond the endpoint by half the line width. - /// - Round -} diff --git a/src/ImageSharp.Drawing/Shapes/InnerJoin.cs b/src/ImageSharp.Drawing/Shapes/InnerJoin.cs new file mode 100644 index 00000000..c8c1c7b3 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/InnerJoin.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Specifies how inner corners of a stroked path or polygon are rendered +/// when the path turns sharply inward. These settings control how the interior +/// edge of the stroke is joined at such corners. +/// +public enum InnerJoin +{ + /// + /// Joins inner corners by connecting the edges with a straight line, + /// producing a flat, beveled appearance. + /// + Bevel, + + /// + /// Joins inner corners by extending the inner edges until they meet at a sharp point. + /// This can create long, narrow joins for acute angles. + /// + Miter, + + /// + /// Joins inner corners with a notched appearance, + /// forming a small cut or indentation at the join. + /// + Jag, + + /// + /// Joins inner corners using a circular arc between the edges, + /// creating a smooth, rounded interior transition. + /// + Round +} diff --git a/src/ImageSharp.Drawing/Shapes/JointStyle.cs b/src/ImageSharp.Drawing/Shapes/JointStyle.cs deleted file mode 100644 index d3d4d58e..00000000 --- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// The style to apply to the joints when generating an outline. -/// -public enum JointStyle -{ - /// - /// Joints are squared off 1 width distance from the corner. - /// - Square = 0, - - /// - /// Rounded joints. Joints generate with a rounded profile. - /// - Round = 1, - - /// - /// 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 -} - -/// -/// Specifies how the connection between two consecutive line segments (a join) -/// is rendered when stroking paths or polygons. -/// -internal enum LineJoin -{ - /// - /// Joins lines by extending their outer edges until they meet at a sharp corner. - /// The miter length is limited by the miter limit; if exceeded, the join may fall back to a bevel. - /// - MiterJoin = 0, - - /// - /// Joins lines by extending their outer edges to form a miter, - /// but if the miter length exceeds the miter limit, the join is truncated - /// at the limit distance rather than falling back to a bevel. - /// - MiterJoinRevert = 1, - - /// - /// Joins lines by connecting them with a circular arc centered at the join point, - /// producing a smooth, rounded corner. - /// - RoundJoin = 2, - - /// - /// Joins lines by connecting the outer corners directly with a straight line, - /// forming a flat edge at the join point. - /// - BevelJoin = 3, - - /// - /// Joins lines by forming a miter, but if the miter limit is exceeded, - /// the join falls back to a round join instead of a bevel. - /// - MiterJoinRound = 4 -} - -/// -/// Specifies how inner corners of a stroked path or polygon are rendered -/// when the path turns sharply inward. These settings control how the interior -/// edge of the stroke is joined at such corners. -/// -internal enum InnerJoin -{ - /// - /// Joins inner corners by connecting the edges with a straight line, - /// producing a flat, beveled appearance. - /// - InnerBevel, - - /// - /// Joins inner corners by extending the inner edges until they meet at a sharp point. - /// This can create long, narrow joins for acute angles. - /// - InnerMiter, - - /// - /// Joins inner corners with a notched appearance, - /// forming a small cut or indentation at the join. - /// - InnerJag, - - /// - /// Joins inner corners using a circular arc between the edges, - /// creating a smooth, rounded interior transition. - /// - InnerRound -} diff --git a/src/ImageSharp.Drawing/Shapes/LineCap.cs b/src/ImageSharp.Drawing/Shapes/LineCap.cs new file mode 100644 index 00000000..1df99225 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/LineCap.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Specifies the shape to be used at the ends of open lines or paths when stroking. +/// +public enum LineCap +{ + /// + /// The stroke ends exactly at the endpoint. + /// No extension is added beyond the path's end coordinates. + /// + Butt, + + /// + /// The stroke extends beyond the endpoint by half the line width, + /// producing a square edge. + /// + Square, + + /// + /// The stroke ends with a semicircular cap, + /// extending beyond the endpoint by half the line width. + /// + Round +} diff --git a/src/ImageSharp.Drawing/Shapes/LineJoin.cs b/src/ImageSharp.Drawing/Shapes/LineJoin.cs new file mode 100644 index 00000000..4ea8ea81 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/LineJoin.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Specifies how the connection between two consecutive line segments (a join) +/// is rendered when stroking paths or polygons. +/// +public enum LineJoin +{ + /// + /// Joins lines by extending their outer edges until they meet at a sharp corner. + /// The miter length is limited by the miter limit; if exceeded, the join may fall back to a bevel. + /// + Miter = 0, + + /// + /// Joins lines by extending their outer edges to form a miter, + /// but if the miter length exceeds the miter limit, the join is truncated + /// at the limit distance rather than falling back to a bevel. + /// + MiterRevert = 1, + + /// + /// Joins lines by connecting them with a circular arc centered at the join point, + /// producing a smooth, rounded corner. + /// + Round = 2, + + /// + /// Joins lines by connecting the outer corners directly with a straight line, + /// forming a flat edge at the join point. + /// + Bevel = 3, + + /// + /// Joins lines by forming a miter, but if the miter limit is exceeded, + /// the join falls back to a round join instead of a bevel. + /// + MiterRound = 4 +} diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index 7533df08..466a597f 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 SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; namespace SixLabors.ImageSharp.Drawing; @@ -11,10 +12,6 @@ namespace SixLabors.ImageSharp.Drawing; /// public static class OutlinePathExtensions { - private const float MiterOffsetDelta = 20; - private const JointStyle DefaultJointStyle = JointStyle.Square; - private const EndCapStyle DefaultEndCapStyle = EndCapStyle.Butt; - /// /// Generates an outline of the path. /// @@ -22,24 +19,23 @@ public static class OutlinePathExtensions /// The outline width. /// A new representing the outline. public static IPath GenerateOutline(this IPath path, float width) - => GenerateOutline(path, width, DefaultJointStyle, DefaultEndCapStyle); + => GenerateOutline(path, width, new StrokeOptions()); /// /// Generates an outline of the path. /// /// The path to outline /// The outline width. - /// The style to apply to the joints. - /// The style to apply to the end caps. + /// The stroke geometry options. /// A new representing the outline. - public static IPath GenerateOutline(this IPath path, float width, JointStyle jointStyle, EndCapStyle endCapStyle) + public static IPath GenerateOutline(this IPath path, float width, StrokeOptions strokeOptions) { if (width <= 0) { return Path.Empty; } - StrokedShapeGenerator generator = new(MiterOffsetDelta); + StrokedShapeGenerator generator = new(strokeOptions); return new ComplexPolygon(generator.GenerateStrokedShapes(path, width)); } @@ -59,10 +55,10 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe path to outline /// The outline width. /// The pattern made of multiples of the width. - /// Whether the first item in the pattern is on or off. + /// The stroke geometry options. /// A new representing the outline. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) - => GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle); + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, StrokeOptions strokeOptions) + => GenerateOutline(path, width, pattern, false, strokeOptions); /// /// Generates an outline of the path with alternating on and off segments based on the pattern. @@ -70,11 +66,10 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe path to outline /// The outline width. /// The pattern made of multiples of the width. - /// The style to apply to the joints. - /// The style to apply to the end caps. + /// Whether the first item in the pattern is on or off. /// A new representing the outline. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, JointStyle jointStyle, EndCapStyle endCapStyle) - => GenerateOutline(path, width, pattern, false, jointStyle, endCapStyle); + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) + => GenerateOutline(path, width, pattern, startOff, new StrokeOptions()); /// /// Generates an outline of the path with alternating on and off segments based on the pattern. @@ -83,10 +78,14 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanThe outline width. /// The pattern made of multiples of the width. /// Whether the first item in the pattern is on or off. - /// The style to apply to the joints. - /// The style to apply to the end caps. + /// The stroke geometry options. /// A new representing the outline. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle) + public static IPath GenerateOutline( + this IPath path, + float width, + ReadOnlySpan pattern, + bool startOff, + StrokeOptions strokeOptions) { if (width <= 0) { @@ -95,10 +94,24 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan paths = path.Flatten(); @@ -119,6 +132,24 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan eps) + { + // Avoid runaway segmentation by falling back when the dash count explodes. + float estimatedSegments = (totalLength / patternLength) * pattern.Length; + if (estimatedSegments > maxPatternSegments) + { + return path.GenerateOutline(width, strokeOptions); + } + } int i = 0; Vector2 current = pts[0]; @@ -129,6 +160,7 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan +/// 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/PolygonGeometry/BoundsF.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs new file mode 100644 index 00000000..14ac870b --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +internal struct BoundsF +{ + public float Left; + public float Top; + public float Right; + public float Bottom; + + public BoundsF(float l, float t, float r, float b) + { + this.Left = l; + this.Top = t; + this.Right = r; + this.Bottom = b; + } + + public BoundsF(BoundsF bounds) + { + this.Left = bounds.Left; + this.Top = bounds.Top; + this.Right = bounds.Right; + this.Bottom = bounds.Bottom; + } + + public BoundsF(bool isValid) + { + if (isValid) + { + this.Left = 0; + this.Top = 0; + this.Right = 0; + this.Bottom = 0; + } + else + { + this.Left = float.MaxValue; + this.Top = float.MaxValue; + this.Right = -float.MaxValue; + this.Bottom = -float.MaxValue; + } + } + + public float Width + { + readonly get => this.Right - this.Left; + set => this.Right = this.Left + value; + } + + public float Height + { + readonly get => this.Bottom - this.Top; + set => this.Bottom = this.Top + value; + } + + public readonly bool IsEmpty() + => this.Bottom <= this.Top || this.Right <= this.Left; + + public readonly Vector2 MidPoint() + => new Vector2(this.Left + this.Right, this.Top + this.Bottom) * .5F; + + public readonly bool Contains(Vector2 pt) + => pt.X > this.Left + && pt.X < this.Right + && pt.Y > this.Top && pt.Y < this.Bottom; + + public readonly bool Contains(BoundsF bounds) + => bounds.Left >= this.Left + && bounds.Right <= this.Right + && bounds.Top >= this.Top + && bounds.Bottom <= this.Bottom; + + public readonly bool Intersects(BoundsF bounds) + => (Math.Max(this.Left, bounds.Left) < Math.Min(this.Right, bounds.Right)) + && (Math.Max(this.Top, bounds.Top) < Math.Min(this.Bottom, bounds.Bottom)); + + public readonly PathF AsPath() + => + [ + new Vector2(this.Left, this.Top), + new Vector2(this.Right, this.Top), + new Vector2(this.Right, this.Bottom), + new Vector2(this.Left, this.Bottom) + ]; +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs index 9a6ac206..7fff6b6e 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs @@ -1,32 +1,31 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.PolygonClipper; -using ClipperPolygon = SixLabors.PolygonClipper.Polygon; -using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; - namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Generates clipped shapes from one or more input paths using polygon boolean operations. /// /// -/// This class provides a high-level wrapper around the low-level . +/// This class provides a high-level wrapper around the low-level . /// It accumulates subject and clip polygons, applies the specified , /// and converts the resulting polygon contours back into instances suitable /// for rendering or further processing. /// internal sealed class ClippedShapeGenerator { - private ClipperPolygon? subject; - private ClipperPolygon? clip; + private readonly PolygonClipper polygonClipper; private readonly IntersectionRule rule; /// /// Initializes a new instance of the class. /// /// The intersection rule. - public ClippedShapeGenerator(IntersectionRule rule) => this.rule = rule; + public ClippedShapeGenerator(IntersectionRule rule) + { + this.rule = rule; + this.polygonClipper = new PolygonClipper() { PreserveCollinear = true }; + } /// /// Generates the final clipped shapes from the previously provided subject and clip paths. @@ -35,30 +34,48 @@ internal sealed class ClippedShapeGenerator /// The boolean operation to perform, such as , /// , or . /// + /// TEMP. Remove when we update IntersectionRule to add missing entries. /// /// An array of instances representing the result of the boolean operation. /// - public IPath[] GenerateClippedShapes(BooleanOperation operation) + public IPath[] GenerateClippedShapes(BooleanOperation operation, bool? positive = null) { - ArgumentNullException.ThrowIfNull(this.subject); - ArgumentNullException.ThrowIfNull(this.clip); + PathsF closedPaths = []; + PathsF openPaths = []; - PolygonClipperAction polygonClipper = new(this.subject, this.clip, operation); + ClipperFillRule fillRule = this.rule == IntersectionRule.EvenOdd ? ClipperFillRule.EvenOdd : ClipperFillRule.NonZero; + + if (positive.HasValue) + { + fillRule = positive.Value ? ClipperFillRule.Positive : ClipperFillRule.Negative; + } - ClipperPolygon result = polygonClipper.Run(); + this.polygonClipper.Execute(operation, fillRule, closedPaths, openPaths); - IPath[] shapes = new IPath[result.Count]; + IPath[] shapes = new IPath[closedPaths.Count + openPaths.Count]; int index = 0; - for (int i = 0; i < result.Count; i++) + for (int i = 0; i < closedPaths.Count; i++) { - Contour contour = result[i]; - PointF[] points = new PointF[contour.Count]; + PathF path = closedPaths[i]; + PointF[] points = new PointF[path.Count]; - for (int j = 0; j < contour.Count; j++) + for (int j = 0; j < path.Count; j++) { - Vertex vertex = contour[j]; - points[j] = new PointF((float)vertex.X, (float)vertex.Y); + points[j] = path[j]; + } + + shapes[index++] = new Polygon(points); + } + + for (int i = 0; i < openPaths.Count; i++) + { + PathF path = openPaths[i]; + PointF[] points = new PointF[path.Count]; + + for (int j = 0; j < path.Count; j++) + { + points[j] = path[j]; } shapes[index++] = new Polygon(points); @@ -80,16 +97,9 @@ public void AddPaths(IEnumerable paths, ClippingType clippingType) { Guard.NotNull(paths, nameof(paths)); - // Accumulate all paths of the complex shape into a single polygon. - ClipperPolygon polygon = PolygonClipperFactory.FromPaths(paths, this.rule); - - if (clippingType == ClippingType.Clip) - { - this.clip = polygon; - } - else + foreach (IPath p in paths) { - this.subject = polygon; + this.AddPath(p, clippingType); } } @@ -104,14 +114,21 @@ public void AddPath(IPath path, ClippingType clippingType) { Guard.NotNull(path, nameof(path)); - ClipperPolygon polygon = PolygonClipperFactory.FromSimplePaths(path.Flatten(), this.rule); - if (clippingType == ClippingType.Clip) + foreach (ISimplePath p in path.Flatten()) { - this.clip = polygon; + this.AddPath(p, clippingType); } - else + } + + private void AddPath(ISimplePath path, ClippingType clippingType) + { + ReadOnlySpan vectors = path.Points.Span; + PathF points = new(vectors.Length); + for (int i = 0; i < vectors.Length; i++) { - this.subject = polygon; + points.Add(vectors[i]); } + + this.polygonClipper.AddPath(points, clippingType, !path.IsClosed); } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs new file mode 100644 index 00000000..71b26112 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +/// +/// Library to clip polygons. +/// +internal class Clipper +{ + private readonly PolygonClipper polygonClipper; + + /// + /// Initializes a new instance of the class. + /// + public Clipper() + => this.polygonClipper = new PolygonClipper() { PreserveCollinear = true }; + + /// + /// Generates the clipped shapes from the previously provided paths. + /// + /// The clipping operation. + /// The intersection rule. + /// The . + public IPath[] GenerateClippedShapes(BooleanOperation operation, IntersectionRule rule) + { + PathsF closedPaths = []; + PathsF openPaths = []; + + ClipperFillRule fillRule = rule == IntersectionRule.EvenOdd ? ClipperFillRule.EvenOdd : ClipperFillRule.NonZero; + this.polygonClipper.Execute(operation, fillRule, closedPaths, openPaths); + + IPath[] shapes = new IPath[closedPaths.Count + openPaths.Count]; + + int index = 0; + for (int i = 0; i < closedPaths.Count; i++) + { + PathF path = closedPaths[i]; + PointF[] points = new PointF[path.Count]; + + for (int j = 0; j < path.Count; j++) + { + points[j] = path[j]; + } + + shapes[index++] = new Polygon(points); + } + + for (int i = 0; i < openPaths.Count; i++) + { + PathF path = openPaths[i]; + PointF[] points = new PointF[path.Count]; + + for (int j = 0; j < path.Count; j++) + { + points[j] = path[j]; + } + + shapes[index++] = new Polygon(points); + } + + return shapes; + } + + /// + /// Adds the shapes. + /// + /// The paths. + /// The clipping type. + public void AddPaths(IEnumerable paths, ClippingType clippingType) + { + Guard.NotNull(paths, nameof(paths)); + + foreach (IPath p in paths) + { + this.AddPath(p, clippingType); + } + } + + /// + /// Adds the path. + /// + /// The path. + /// The clipping type. + public void AddPath(IPath path, ClippingType clippingType) + { + Guard.NotNull(path, nameof(path)); + + foreach (ISimplePath p in path.Flatten()) + { + this.AddPath(p, clippingType); + } + } + + /// + /// Adds the path. + /// + /// The path. + /// Type of the poly. + internal void AddPath(ISimplePath path, ClippingType clippingType) + { + ReadOnlySpan vectors = path.Points.Span; + PathF points = new(vectors.Length); + for (int i = 0; i < vectors.Length; i++) + { + points.Add(vectors[i]); + } + + this.polygonClipper.AddPath(points, clippingType, !path.IsClosed); + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs new file mode 100644 index 00000000..d22aff79 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +/// +/// The exception that is thrown when an error occurs clipping a polygon. +/// +public class ClipperException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public ClipperException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public ClipperException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and a + /// reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a + /// reference if no inner exception is specified. + public ClipperException(string message, Exception innerException) + : base(message, innerException) + { + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs new file mode 100644 index 00000000..90d1c614 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +/// +/// By far the most widely used filling rules for polygons are EvenOdd +/// and NonZero, sometimes called Alternate and Winding respectively. +/// +/// +/// +/// TODO: This overlaps with the enum. +/// We should see if we can enhance the to support all these rules. +/// +internal enum ClipperFillRule +{ + EvenOdd, + NonZero, + Positive, + Negative +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs new file mode 100644 index 00000000..6ed77da4 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs @@ -0,0 +1,236 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +internal static class ClipperUtils +{ + public const float DefaultArcTolerance = .25F; + public const float FloatingPointTolerance = 1e-05F; + public const float DefaultMinimumEdgeLength = .1F; + + // TODO: rename to Pow2? + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Sqr(float value) => value * value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Area(PathF path) + { + // https://en.wikipedia.org/wiki/Shoelace_formula + float a = 0F; + if (path.Count < 3) + { + return a; + } + + Vector2 prevPt = path[path.Count - 1]; + for (int i = 0; i < path.Count; i++) + { + Vector2 pt = path[i]; + a += (prevPt.Y + pt.Y) * (prevPt.X - pt.X); + prevPt = pt; + } + + return a * .5F; + } + + public static PathF StripDuplicates(PathF path, bool isClosedPath) + { + int cnt = path.Count; + PathF result = new(cnt); + if (cnt == 0) + { + return result; + } + + PointF lastPt = path[0]; + result.Add(lastPt); + for (int i = 1; i < cnt; i++) + { + if (lastPt != path[i]) + { + lastPt = path[i]; + result.Add(lastPt); + } + } + + if (isClosedPath && lastPt == result[0]) + { + result.RemoveAt(result.Count - 1); + } + + return result; + } + + public static PathF Ellipse(Vector2 center, float radiusX, float radiusY = 0, int steps = 0) + { + if (radiusX <= 0) + { + return []; + } + + if (radiusY <= 0) + { + radiusY = radiusX; + } + + if (steps <= 2) + { + steps = (int)MathF.Ceiling(MathF.PI * MathF.Sqrt((radiusX + radiusY) * .5F)); + } + + float si = MathF.Sin(2 * MathF.PI / steps); + float co = MathF.Cos(2 * MathF.PI / steps); + float dx = co, dy = si; + PathF result = new(steps) { new Vector2(center.X + radiusX, center.Y) }; + Vector2 radiusXY = new(radiusX, radiusY); + for (int i = 1; i < steps; ++i) + { + result.Add(center + (radiusXY * new Vector2(dx, dy))); + float x = (dx * co) - (dy * si); + dy = (dy * co) + (dx * si); + dx = x; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DotProduct(Vector2 vec1, Vector2 vec2) + => Vector2.Dot(vec1, vec2); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float CrossProduct(Vector2 vec1, Vector2 vec2) + => (vec1.Y * vec2.X) - (vec2.Y * vec1.X); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float CrossProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) + => ((pt2.X - pt1.X) * (pt3.Y - pt2.Y)) - ((pt2.Y - pt1.Y) * (pt3.X - pt2.X)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) + => Vector2.Dot(pt2 - pt1, pt3 - pt2); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAlmostZero(float value) + => MathF.Abs(value) <= FloatingPointTolerance; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 line2) + { + Vector2 ab = pt - line1; + Vector2 cd = line2 - line1; + if (cd == Vector2.Zero) + { + return 0; + } + + return Sqr(CrossProduct(cd, ab)) / DotProduct(cd, cd); + } + + public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Vector2 seg2b, bool inclusive = false) + { + if (inclusive) + { + float res1 = CrossProduct(seg1a, seg2a, seg2b); + float res2 = CrossProduct(seg1b, seg2a, seg2b); + if (res1 * res2 > 0) + { + return false; + } + + float res3 = CrossProduct(seg2a, seg1a, seg1b); + float res4 = CrossProduct(seg2b, seg1a, seg1b); + if (res3 * res4 > 0) + { + return false; + } + + // ensure NOT collinear + return res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0; + } + + return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0) + && (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool GetIntersectPt(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) + { + Vector2 dxy1 = ln1b - ln1a; + Vector2 dxy2 = ln2b - ln2a; + float cp = CrossProduct(dxy1, dxy2); + if (cp == 0F) + { + ip = default; + return false; + } + + float qx = CrossProduct(ln1a, dxy1); + float qy = CrossProduct(ln2a, dxy2); + + ip = ((dxy1 * qy) - (dxy2 * qx)) / cp; + return ip != new Vector2(float.MaxValue); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) + { + Vector2 dxy1 = ln1b - ln1a; + Vector2 dxy2 = ln2b - ln2a; + float det = CrossProduct(dxy1, dxy2); + if (det == 0F) + { + ip = default; + return false; + } + + float t = (((ln1a.X - ln2a.X) * dxy2.Y) - ((ln1a.Y - ln2a.Y) * dxy2.X)) / det; + if (t <= 0F) + { + ip = ln1a; + } + else if (t >= 1F) + { + ip = ln1b; + } + else + { + ip = ln1a + (t * dxy1); + } + + return true; + } + + public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 seg2) + { + if (seg1 == seg2) + { + return seg1; + } + + Vector2 dxy = seg2 - seg1; + Vector2 oxy = (offPt - seg1) * dxy; + float q = (oxy.X + oxy.Y) / DotProduct(dxy, dxy); + + if (q < 0) + { + q = 0; + } + else if (q > 1) + { + q = 1; + } + + return seg1 + (dxy * q); + } + + public static PathF ReversePath(PathF path) + { + path.Reverse(); + return path; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs new file mode 100644 index 00000000..83ca61ad --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +internal enum JoinWith +{ + None, + Left, + Right +} + +internal enum HorzPosition +{ + Bottom, + Middle, + Top +} + +// Vertex: a pre-clipping data structure. It is used to separate polygons +// into ascending and descending 'bounds' (or sides) that start at local +// minima and ascend to a local maxima, before descending again. +[Flags] +internal enum PointInPolygonResult +{ + IsOn = 0, + IsInside = 1, + IsOutside = 2 +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs new file mode 100644 index 00000000..cd4c4363 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs @@ -0,0 +1,3461 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable disable + +using System.Collections; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +/// +/// Contains functions that cover most polygon boolean and offsetting needs. +/// Ported from and originally licensed +/// under +/// +internal sealed class PolygonClipper +{ + private BooleanOperation clipType; + private ClipperFillRule fillRule; + private Active actives; + private Active flaggedHorizontal; + private readonly List minimaList; + private readonly List intersectList; + private readonly List vertexList; + private readonly List outrecList; + private readonly List scanlineList; + private readonly List horzSegList; + private readonly List horzJoinList; + private int currentLocMin; + private float currentBotY; + private bool isSortedMinimaList; + private bool hasOpenPaths; + + public PolygonClipper() + { + this.minimaList = []; + this.intersectList = []; + this.vertexList = []; + this.outrecList = []; + this.scanlineList = []; + this.horzSegList = []; + this.horzJoinList = []; + this.PreserveCollinear = true; + } + + public bool PreserveCollinear { get; set; } + + public bool ReverseSolution { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddSubject(PathsF paths) => this.AddPaths(paths, ClippingType.Subject); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddPath(PathF path, ClippingType polytype, bool isOpen = false) + { + PathsF tmp = [path]; + this.AddPaths(tmp, polytype, isOpen); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false) + { + if (isOpen) + { + this.hasOpenPaths = true; + } + + this.isSortedMinimaList = false; + this.AddPathsToVertexList(paths, polytype, isOpen); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed) + => this.Execute(clipType, fillRule, solutionClosed, []); + + public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed, PathsF solutionOpen) + { + solutionClosed.Clear(); + solutionOpen.Clear(); + + try + { + this.ExecuteInternal(clipType, fillRule); + this.BuildPaths(solutionClosed, solutionOpen); + } + catch (Exception ex) + { + throw new ClipperException("An error occurred while attempting to clip the polygon. See the inner exception for details.", ex); + } + finally + { + this.ClearSolutionOnly(); + } + } + + private void ExecuteInternal(BooleanOperation ct, ClipperFillRule fillRule) + { + this.fillRule = fillRule; + this.clipType = ct; + this.Reset(); + if (!this.PopScanline(out float y)) + { + return; + } + + while (true) + { + this.InsertLocalMinimaIntoAEL(y); + Active ae; + while (this.PopHorz(out ae)) + { + this.DoHorizontal(ae); + } + + if (this.horzSegList.Count > 0) + { + this.ConvertHorzSegsToJoins(); + this.horzSegList.Clear(); + } + + this.currentBotY = y; // bottom of scanbeam + if (!this.PopScanline(out y)) + { + break; // y new top of scanbeam + } + + this.DoIntersections(y); + this.DoTopOfScanbeam(y); + while (this.PopHorz(out ae)) + { + this.DoHorizontal(ae!); + } + } + + this.ProcessHorzJoins(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoIntersections(float topY) + { + if (this.BuildIntersectList(topY)) + { + this.ProcessIntersectList(); + this.DisposeIntersectNodes(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DisposeIntersectNodes() + => this.intersectList.Clear(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddNewIntersectNode(Active ae1, Active ae2, float topY) + { + if (!ClipperUtils.GetIntersectPt(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip)) + { + ip = new Vector2(ae1.CurX, topY); + } + + if (ip.Y > this.currentBotY || ip.Y < topY) + { + float absDx1 = MathF.Abs(ae1.Dx); + float absDx2 = MathF.Abs(ae2.Dx); + + // TODO: Check threshold here once we remove upscaling. + if (absDx1 > 100 && absDx2 > 100) + { + if (absDx1 > absDx2) + { + ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); + } + else + { + ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); + } + } + else if (absDx1 > 100) + { + ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); + } + else if (absDx2 > 100) + { + ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); + } + else + { + if (ip.Y < topY) + { + ip.Y = topY; + } + else + { + ip.Y = this.currentBotY; + } + + if (absDx1 < absDx2) + { + ip.X = TopX(ae1, ip.Y); + } + else + { + ip.X = TopX(ae2, ip.Y); + } + } + } + + IntersectNode node = new(ip, ae1, ae2); + this.intersectList.Add(node); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt opN) + { + if (opP.Point.X == opN.Point.X) + { + return false; + } + + if (opP.Point.X < opN.Point.X) + { + hs.LeftOp = opP; + hs.RightOp = opN; + hs.LeftToRight = true; + } + else + { + hs.LeftOp = opN; + hs.RightOp = opP; + hs.LeftToRight = false; + } + + return true; + } + + private static bool UpdateHorzSegment(HorzSegment hs) + { + OutPt op = hs.LeftOp; + OutRec outrec = GetRealOutRec(op.OutRec); + bool outrecHasEdges = outrec.FrontEdge != null; + float curr_y = op.Point.Y; + OutPt opP = op, opN = op; + if (outrecHasEdges) + { + OutPt opA = outrec.Pts!, opZ = opA.Next; + while (opP != opZ && opP.Prev.Point.Y == curr_y) + { + opP = opP.Prev; + } + + while (opN != opA && opN.Next.Point.Y == curr_y) + { + opN = opN.Next; + } + } + else + { + while (opP.Prev != opN && opP.Prev.Point.Y == curr_y) + { + opP = opP.Prev; + } + + while (opN.Next != opP && opN.Next.Point.Y == curr_y) + { + opN = opN.Next; + } + } + + bool result = SetHorzSegHeadingForward(hs, opP, opN) && hs.LeftOp.HorizSegment == null; + + if (result) + { + hs.LeftOp.HorizSegment = hs; + } + else + { + hs.RightOp = null; // (for sorting) + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt DuplicateOp(OutPt op, bool insert_after) + { + OutPt result = new(op.Point, op.OutRec); + if (insert_after) + { + result.Next = op.Next; + result.Next.Prev = result; + result.Prev = op; + op.Next = result; + } + else + { + result.Prev = op.Prev; + result.Prev.Next = result; + result.Next = op; + op.Prev = result; + } + + return result; + } + + private void ConvertHorzSegsToJoins() + { + int k = 0; + foreach (HorzSegment hs in this.horzSegList) + { + if (UpdateHorzSegment(hs)) + { + k++; + } + } + + if (k < 2) + { + return; + } + + this.horzSegList.Sort(default(HorzSegSorter)); + + for (int i = 0; i < k - 1; i++) + { + HorzSegment hs1 = this.horzSegList[i]; + + // for each HorzSegment, find others that overlap + for (int j = i + 1; j < k; j++) + { + HorzSegment hs2 = this.horzSegList[j]; + if ((hs2.LeftOp.Point.X >= hs1.RightOp.Point.X) || + (hs2.LeftToRight == hs1.LeftToRight) || + (hs2.RightOp.Point.X <= hs1.LeftOp.Point.X)) + { + continue; + } + + float curr_y = hs1.LeftOp.Point.Y; + if (hs1.LeftToRight) + { + while (hs1.LeftOp.Next.Point.Y == curr_y && + hs1.LeftOp.Next.Point.X <= hs2.LeftOp.Point.X) + { + hs1.LeftOp = hs1.LeftOp.Next; + } + + while (hs2.LeftOp.Prev.Point.Y == curr_y && + hs2.LeftOp.Prev.Point.X <= hs1.LeftOp.Point.X) + { + hs2.LeftOp = hs2.LeftOp.Prev; + } + + HorzJoin join = new(DuplicateOp(hs1.LeftOp, true), DuplicateOp(hs2.LeftOp, false)); + this.horzJoinList.Add(join); + } + else + { + while (hs1.LeftOp.Prev.Point.Y == curr_y && + hs1.LeftOp.Prev.Point.X <= hs2.LeftOp.Point.X) + { + hs1.LeftOp = hs1.LeftOp.Prev; + } + + while (hs2.LeftOp.Next.Point.Y == curr_y && + hs2.LeftOp.Next.Point.X <= hs1.LeftOp.Point.X) + { + hs2.LeftOp = hs2.LeftOp.Next; + } + + HorzJoin join = new(DuplicateOp(hs2.LeftOp, true), DuplicateOp(hs1.LeftOp, false)); + this.horzJoinList.Add(join); + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearSolutionOnly() + { + while (this.actives != null) + { + this.DeleteFromAEL(this.actives); + } + + this.scanlineList.Clear(); + this.DisposeIntersectNodes(); + this.outrecList.Clear(); + this.horzSegList.Clear(); + this.horzJoinList.Clear(); + } + + private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) + { + solutionClosed.Clear(); + solutionOpen.Clear(); + solutionClosed.EnsureCapacity(this.outrecList.Count); + solutionOpen.EnsureCapacity(this.outrecList.Count); + + int i = 0; + + // _outrecList.Count is not static here because + // CleanCollinear can indirectly add additional OutRec + while (i < this.outrecList.Count) + { + OutRec outrec = this.outrecList[i++]; + if (outrec.Pts == null) + { + continue; + } + + PathF path = []; + if (outrec.IsOpen) + { + if (BuildPath(outrec.Pts, this.ReverseSolution, true, path)) + { + solutionOpen.Add(path); + } + } + else + { + this.CleanCollinear(outrec); + + // closed paths should always return a Positive orientation + // except when ReverseSolution == true + if (BuildPath(outrec.Pts, this.ReverseSolution, false, path)) + { + solutionClosed.Add(path); + } + } + } + + return true; + } + + private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path) + { + if (op == null || op.Next == op || (!isOpen && op.Next == op.Prev)) + { + return false; + } + + path.Clear(); + + Vector2 lastPt; + OutPt op2; + if (reverse) + { + lastPt = op.Point; + op2 = op.Prev; + } + else + { + op = op.Next; + lastPt = op.Point; + op2 = op.Next; + } + + path.Add(lastPt); + + while (op2 != op) + { + if (op2.Point != lastPt) + { + lastPt = op2.Point; + path.Add(lastPt); + } + + if (reverse) + { + op2 = op2.Prev; + } + else + { + op2 = op2.Next; + } + } + + return path.Count != 3 || !IsVerySmallTriangle(op2); + } + + private void DoHorizontal(Active horz) + /******************************************************************************* + * Notes: Horizontal edges (HEs) at scanline intersections (i.e. at the top or * + * bottom of a scanbeam) are processed as if layered.The order in which HEs * + * are processed doesn't matter. HEs intersect with the bottom vertices of * + * other HEs[#] and with non-horizontal edges [*]. Once these intersections * + * are completed, intermediate HEs are 'promoted' to the next edge in their * + * bounds, and they in turn may be intersected[%] by other HEs. * + * * + * eg: 3 horizontals at a scanline: / | / / * + * | / | (HE3)o ========%========== o * + * o ======= o(HE2) / | / / * + * o ============#=========*======*========#=========o (HE1) * + * / | / | / * + *******************************************************************************/ + { + Vector2 pt; + bool horzIsOpen = IsOpen(horz); + float y = horz.Bot.Y; + + Vertex vertex_max = horzIsOpen ? GetCurrYMaximaVertex_Open(horz) : GetCurrYMaximaVertex(horz); + + // remove 180 deg.spikes and also simplify + // consecutive horizontals when PreserveCollinear = true + if (vertex_max != null && + !horzIsOpen && vertex_max != horz.VertexTop) + { + TrimHorz(horz, this.PreserveCollinear); + } + + bool isLeftToRight = ResetHorzDirection(horz, vertex_max, out float leftX, out float rightX); + + if (IsHotEdge(horz)) + { + OutPt op = AddOutPt(horz, new Vector2(horz.CurX, y)); + this.AddToHorzSegList(op); + } + + OutRec currOutrec = horz.Outrec; + + while (true) + { + // loops through consec. horizontal edges (if open) + Active ae = isLeftToRight ? horz.NextInAEL : horz.PrevInAEL; + + while (ae != null) + { + if (ae.VertexTop == vertex_max) + { + // do this first!! + if (IsHotEdge(horz) && IsJoined(ae!)) + { + this.Split(ae, ae.Top); + } + + if (IsHotEdge(horz)) + { + while (horz.VertexTop != vertex_max) + { + AddOutPt(horz, horz.Top); + this.UpdateEdgeIntoAEL(horz); + } + + if (isLeftToRight) + { + this.AddLocalMaxPoly(horz, ae, horz.Top); + } + else + { + this.AddLocalMaxPoly(ae, horz, horz.Top); + } + } + + this.DeleteFromAEL(ae); + this.DeleteFromAEL(horz); + return; + } + + // if horzEdge is a maxima, keep going until we reach + // its maxima pair, otherwise check for break conditions + if (vertex_max != horz.VertexTop || IsOpenEnd(horz)) + { + // otherwise stop when 'ae' is beyond the end of the horizontal line + if ((isLeftToRight && ae.CurX > rightX) || (!isLeftToRight && ae.CurX < leftX)) + { + break; + } + + if (ae.CurX == horz.Top.X && !IsHorizontal(ae)) + { + pt = NextVertex(horz).Point; + + // to maximize the possibility of putting open edges into + // solutions, we'll only break if it's past HorzEdge's end + if (IsOpen(ae) && !IsSamePolyType(ae, horz) && !IsHotEdge(ae)) + { + if ((isLeftToRight && (TopX(ae, pt.Y) > pt.X)) || + (!isLeftToRight && (TopX(ae, pt.Y) < pt.X))) + { + break; + } + } + + // otherwise for edges at horzEdge's end, only stop when horzEdge's + // outslope is greater than e's slope when heading right or when + // horzEdge's outslope is less than e's slope when heading left. + else if ((isLeftToRight && (TopX(ae, pt.Y) >= pt.X)) || (!isLeftToRight && (TopX(ae, pt.Y) <= pt.X))) + { + break; + } + } + } + + pt = new Vector2(ae.CurX, y); + + if (isLeftToRight) + { + this.IntersectEdges(horz, ae, pt); + this.SwapPositionsInAEL(horz, ae); + horz.CurX = ae.CurX; + ae = horz.NextInAEL; + } + else + { + this.IntersectEdges(ae, horz, pt); + this.SwapPositionsInAEL(ae, horz); + horz.CurX = ae.CurX; + ae = horz.PrevInAEL; + } + + if (IsHotEdge(horz) && (horz.Outrec != currOutrec)) + { + currOutrec = horz.Outrec; + this.AddToHorzSegList(GetLastOp(horz)); + } + + // we've reached the end of this horizontal + } + + // check if we've finished looping + // through consecutive horizontals + // ie open at top + if (horzIsOpen && IsOpenEnd(horz)) + { + if (IsHotEdge(horz)) + { + AddOutPt(horz, horz.Top); + if (IsFront(horz)) + { + horz.Outrec.FrontEdge = null; + } + else + { + horz.Outrec.BackEdge = null; + } + + horz.Outrec = null; + } + + this.DeleteFromAEL(horz); + return; + } + else if (NextVertex(horz).Point.Y != horz.Top.Y) + { + break; + } + + // still more horizontals in bound to process ... + if (IsHotEdge(horz)) + { + AddOutPt(horz, horz.Top); + } + + this.UpdateEdgeIntoAEL(horz); + + if (this.PreserveCollinear && !horzIsOpen && HorzIsSpike(horz)) + { + TrimHorz(horz, true); + } + + isLeftToRight = ResetHorzDirection(horz, vertex_max, out leftX, out rightX); + + // end for loop and end of (possible consecutive) horizontals + } + + if (IsHotEdge(horz)) + { + this.AddToHorzSegList(AddOutPt(horz, horz.Top)); + } + + this.UpdateEdgeIntoAEL(horz); // this is the end of an intermediate horiz. + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoTopOfScanbeam(float y) + { + this.flaggedHorizontal = null; // sel_ is reused to flag horizontals (see PushHorz below) + Active ae = this.actives; + while (ae != null) + { + // NB 'ae' will never be horizontal here + if (ae.Top.Y == y) + { + ae.CurX = ae.Top.X; + if (IsMaxima(ae)) + { + ae = this.DoMaxima(ae); // TOP OF BOUND (MAXIMA) + continue; + } + + // INTERMEDIATE VERTEX ... + if (IsHotEdge(ae)) + { + AddOutPt(ae, ae.Top); + } + + this.UpdateEdgeIntoAEL(ae); + if (IsHorizontal(ae)) + { + this.PushHorz(ae); // horizontals are processed later + } + } + else + { + // i.e. not the top of the edge + ae.CurX = TopX(ae, y); + } + + ae = ae.NextInAEL; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Active DoMaxima(Active ae) + { + Active prevE; + Active nextE, maxPair; + prevE = ae.PrevInAEL; + nextE = ae.NextInAEL; + + if (IsOpenEnd(ae)) + { + if (IsHotEdge(ae)) + { + AddOutPt(ae, ae.Top); + } + + if (!IsHorizontal(ae)) + { + if (IsHotEdge(ae)) + { + if (IsFront(ae)) + { + ae.Outrec.FrontEdge = null; + } + else + { + ae.Outrec.BackEdge = null; + } + + ae.Outrec = null; + } + + this.DeleteFromAEL(ae); + } + + return nextE; + } + + maxPair = GetMaximaPair(ae); + if (maxPair == null) + { + return nextE; // eMaxPair is horizontal + } + + if (IsJoined(ae)) + { + this.Split(ae, ae.Top); + } + + if (IsJoined(maxPair)) + { + this.Split(maxPair, maxPair.Top); + } + + // only non-horizontal maxima here. + // process any edges between maxima pair ... + while (nextE != maxPair) + { + this.IntersectEdges(ae, nextE!, ae.Top); + this.SwapPositionsInAEL(ae, nextE!); + nextE = ae.NextInAEL; + } + + if (IsOpen(ae)) + { + if (IsHotEdge(ae)) + { + this.AddLocalMaxPoly(ae, maxPair, ae.Top); + } + + this.DeleteFromAEL(maxPair); + this.DeleteFromAEL(ae); + return prevE != null ? prevE.NextInAEL : this.actives; + } + + // here ae.nextInAel == ENext == EMaxPair ... + if (IsHotEdge(ae)) + { + this.AddLocalMaxPoly(ae, maxPair, ae.Top); + } + + this.DeleteFromAEL(ae); + this.DeleteFromAEL(maxPair); + return prevE != null ? prevE.NextInAEL : this.actives; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void TrimHorz(Active horzEdge, bool preserveCollinear) + { + bool wasTrimmed = false; + Vector2 pt = NextVertex(horzEdge).Point; + + while (pt.Y == horzEdge.Top.Y) + { + // always trim 180 deg. spikes (in closed paths) + // but otherwise break if preserveCollinear = true + if (preserveCollinear && (pt.X < horzEdge.Top.X) != (horzEdge.Bot.X < horzEdge.Top.X)) + { + break; + } + + horzEdge.VertexTop = NextVertex(horzEdge); + horzEdge.Top = pt; + wasTrimmed = true; + if (IsMaxima(horzEdge)) + { + break; + } + + pt = NextVertex(horzEdge).Point; + } + + if (wasTrimmed) + { + SetDx(horzEdge); // +/-infinity + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddToHorzSegList(OutPt op) + { + if (op.OutRec.IsOpen) + { + return; + } + + this.horzSegList.Add(new HorzSegment(op)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt GetLastOp(Active hotEdge) + { + OutRec outrec = hotEdge.Outrec; + return (hotEdge == outrec.FrontEdge) ? outrec.Pts : outrec.Pts.Next; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex GetCurrYMaximaVertex_Open(Active ae) + { + Vertex result = ae.VertexTop; + if (ae.WindDx > 0) + { + while (result.Next.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None)) + { + result = result.Next; + } + } + else + { + while (result.Prev.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None)) + { + result = result.Prev; + } + } + + if (!IsMaxima(result)) + { + result = null; // not a maxima + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex GetCurrYMaximaVertex(Active ae) + { + Vertex result = ae.VertexTop; + if (ae.WindDx > 0) + { + while (result.Next.Point.Y == result.Point.Y) + { + result = result.Next; + } + } + else + { + while (result.Prev.Point.Y == result.Point.Y) + { + result = result.Prev; + } + } + + if (!IsMaxima(result)) + { + result = null; // not a maxima + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsVerySmallTriangle(OutPt op) + => op.Next.Next == op.Prev + && (PtsReallyClose(op.Prev.Point, op.Next.Point) + || PtsReallyClose(op.Point, op.Next.Point) + || PtsReallyClose(op.Point, op.Prev.Point)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidClosedPath(OutPt op) + => op != null && op.Next != op && (op.Next != op.Prev || !IsVerySmallTriangle(op)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt DisposeOutPt(OutPt op) + { + OutPt result = op.Next == op ? null : op.Next; + op.Prev.Next = op.Next; + op.Next.Prev = op.Prev; + + return result; + } + + private void ProcessHorzJoins() + { + foreach (HorzJoin j in this.horzJoinList) + { + OutRec or1 = GetRealOutRec(j.Op1.OutRec); + OutRec or2 = GetRealOutRec(j.Op2.OutRec); + + OutPt op1b = j.Op1.Next; + OutPt op2b = j.Op2.Prev; + j.Op1.Next = j.Op2; + j.Op2.Prev = j.Op1; + op1b.Prev = op2b; + op2b.Next = op1b; + + // 'join' is really a split + if (or1 == or2) + { + or2 = new OutRec + { + Pts = op1b + }; + + FixOutRecPts(or2); + + if (or1.Pts.OutRec == or2) + { + or1.Pts = j.Op1; + or1.Pts.OutRec = or1; + } + + or2.Owner = or1; + + this.outrecList.Add(or2); + } + else + { + or2.Pts = null; + or2.Owner = or1; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool PtsReallyClose(Vector2 pt1, Vector2 pt2) + + // TODO: Check scale once we can remove upscaling. + => (Math.Abs(pt1.X - pt2.X) < 2F) && (Math.Abs(pt1.Y - pt2.Y) < 2F); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CleanCollinear(OutRec outrec) + { + outrec = GetRealOutRec(outrec); + + if (outrec?.IsOpen != false) + { + return; + } + + if (!IsValidClosedPath(outrec.Pts)) + { + outrec.Pts = null; + return; + } + + OutPt startOp = outrec.Pts; + OutPt op2 = startOp; + do + { + // NB if preserveCollinear == true, then only remove 180 deg. spikes + if ((ClipperUtils.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0) + && ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (ClipperUtils.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0))) + { + if (op2 == outrec.Pts) + { + outrec.Pts = op2.Prev; + } + + op2 = DisposeOutPt(op2); + if (!IsValidClosedPath(op2)) + { + outrec.Pts = null; + return; + } + + startOp = op2; + continue; + } + + op2 = op2.Next; + } + while (op2 != startOp); + + this.FixSelfIntersects(outrec); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DoSplitOp(OutRec outrec, OutPt splitOp) + { + // splitOp.prev <=> splitOp && + // splitOp.next <=> splitOp.next.next are intersecting + OutPt prevOp = splitOp.Prev; + OutPt nextNextOp = splitOp.Next.Next; + outrec.Pts = prevOp; + + ClipperUtils.GetIntersectPoint( + prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip); + + float area1 = Area(prevOp); + float absArea1 = Math.Abs(area1); + + if (absArea1 < 2) + { + outrec.Pts = null; + return; + } + + float area2 = AreaTriangle(ip, splitOp.Point, splitOp.Next.Point); + float absArea2 = Math.Abs(area2); + + // de-link splitOp and splitOp.next from the path + // while inserting the intersection point + if (ip == prevOp.Point || ip == nextNextOp.Point) + { + nextNextOp.Prev = prevOp; + prevOp.Next = nextNextOp; + } + else + { + OutPt newOp2 = new(ip, outrec) + { + Prev = prevOp, + Next = nextNextOp + }; + + nextNextOp.Prev = newOp2; + prevOp.Next = newOp2; + } + + // nb: area1 is the path's area *before* splitting, whereas area2 is + // the area of the triangle containing splitOp & splitOp.next. + // So the only way for these areas to have the same sign is if + // the split triangle is larger than the path containing prevOp or + // if there's more than one self=intersection. + if (absArea2 > 1 && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0)))) + { + OutRec newOutRec = this.NewOutRec(); + newOutRec.Owner = outrec.Owner; + splitOp.OutRec = newOutRec; + splitOp.Next.OutRec = newOutRec; + + OutPt newOp = new(ip, newOutRec) { Prev = splitOp.Next, Next = splitOp }; + newOutRec.Pts = newOp; + splitOp.Prev = newOp; + splitOp.Next.Next = newOp; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FixSelfIntersects(OutRec outrec) + { + OutPt op2 = outrec.Pts; + + // triangles can't self-intersect + while (op2.Prev != op2.Next.Next) + { + if (ClipperUtils.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point)) + { + this.DoSplitOp(outrec, op2); + if (outrec.Pts == null) + { + return; + } + + op2 = outrec.Pts; + continue; + } + else + { + op2 = op2.Next; + } + + if (op2 == outrec.Pts) + { + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Reset() + { + if (!this.isSortedMinimaList) + { + this.minimaList.Sort(default(LocMinSorter)); + this.isSortedMinimaList = true; + } + + 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); + } + + this.currentBotY = 0; + this.currentLocMin = 0; + this.actives = null; + this.flaggedHorizontal = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InsertScanline(float y) + { + int index = this.scanlineList.BinarySearch(y); + if (index >= 0) + { + return; + } + + index = ~index; + this.scanlineList.Insert(index, y); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool PopScanline(out float y) + { + int cnt = this.scanlineList.Count - 1; + if (cnt < 0) + { + y = 0; + return false; + } + + y = this.scanlineList[cnt]; + this.scanlineList.RemoveAt(cnt--); + while (cnt >= 0 && y == this.scanlineList[cnt]) + { + this.scanlineList.RemoveAt(cnt--); + } + + return true; + } + + private void InsertLocalMinimaIntoAEL(float botY) + { + LocalMinima localMinima; + Active leftBound, rightBound; + + // Add any local minima (if any) at BotY + // NB horizontal local minima edges should contain locMin.vertex.prev + while (this.HasLocMinAtY(botY)) + { + localMinima = this.PopLocalMinima(); + if ((localMinima.Vertex.Flags & VertexFlags.OpenStart) != VertexFlags.None) + { + leftBound = null; + } + else + { + leftBound = new Active + { + Bot = localMinima.Vertex.Point, + CurX = localMinima.Vertex.Point.X, + WindDx = -1, + VertexTop = localMinima.Vertex.Prev, + Top = localMinima.Vertex.Prev.Point, + Outrec = null, + LocalMin = localMinima + }; + SetDx(leftBound); + } + + if ((localMinima.Vertex.Flags & VertexFlags.OpenEnd) != VertexFlags.None) + { + rightBound = null; + } + else + { + rightBound = new Active + { + Bot = localMinima.Vertex.Point, + CurX = localMinima.Vertex.Point.X, + WindDx = 1, + VertexTop = localMinima.Vertex.Next, // i.e. ascending + Top = localMinima.Vertex.Next.Point, + Outrec = null, + LocalMin = localMinima + }; + SetDx(rightBound); + } + + // Currently LeftB is just the descending bound and RightB is the ascending. + // Now if the LeftB isn't on the left of RightB then we need swap them. + if (leftBound != null && rightBound != null) + { + if (IsHorizontal(leftBound)) + { + if (IsHeadingRightHorz(leftBound)) + { + SwapActives(ref leftBound, ref rightBound); + } + } + else if (IsHorizontal(rightBound)) + { + if (IsHeadingLeftHorz(rightBound)) + { + SwapActives(ref leftBound, ref rightBound); + } + } + else if (leftBound.Dx < rightBound.Dx) + { + SwapActives(ref leftBound, ref rightBound); + } + + // so when leftBound has windDx == 1, the polygon will be oriented + // counter-clockwise in Cartesian coords (clockwise with inverted Y). + } + else if (leftBound == null) + { + leftBound = rightBound; + rightBound = null; + } + + bool contributing; + leftBound.IsLeftBound = true; + this.InsertLeftEdge(leftBound); + + if (IsOpen(leftBound)) + { + this.SetWindCountForOpenPathEdge(leftBound); + contributing = this.IsContributingOpen(leftBound); + } + else + { + this.SetWindCountForClosedPathEdge(leftBound); + contributing = this.IsContributingClosed(leftBound); + } + + if (rightBound != null) + { + rightBound.WindCount = leftBound.WindCount; + rightBound.WindCount2 = leftBound.WindCount2; + InsertRightEdge(leftBound, rightBound); /////// + + if (contributing) + { + this.AddLocalMinPoly(leftBound, rightBound, leftBound.Bot, true); + if (!IsHorizontal(leftBound)) + { + this.CheckJoinLeft(leftBound, leftBound.Bot); + } + } + + while (rightBound.NextInAEL != null && IsValidAelOrder(rightBound.NextInAEL, rightBound)) + { + this.IntersectEdges(rightBound, rightBound.NextInAEL, rightBound.Bot); + this.SwapPositionsInAEL(rightBound, rightBound.NextInAEL); + } + + if (IsHorizontal(rightBound)) + { + this.PushHorz(rightBound); + } + else + { + this.CheckJoinRight(rightBound, rightBound.Bot); + this.InsertScanline(rightBound.Top.Y); + } + } + else if (contributing) + { + this.StartOpenPath(leftBound, leftBound.Bot); + } + + if (IsHorizontal(leftBound)) + { + this.PushHorz(leftBound); + } + else + { + this.InsertScanline(leftBound.Top.Y); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active ExtractFromSEL(Active ae) + { + Active res = ae.NextInSEL; + if (res != null) + { + res.PrevInSEL = ae.PrevInSEL; + } + + ae.PrevInSEL.NextInSEL = res; + return res; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Insert1Before2InSEL(Active ae1, Active ae2) + { + ae1.PrevInSEL = ae2.PrevInSEL; + if (ae1.PrevInSEL != null) + { + ae1.PrevInSEL.NextInSEL = ae1; + } + + ae1.NextInSEL = ae2; + ae2.PrevInSEL = ae1; + } + + private bool BuildIntersectList(float topY) + { + if (this.actives == null || this.actives.NextInAEL == null) + { + return false; + } + + // Calculate edge positions at the top of the current scanbeam, and from this + // we will determine the intersections required to reach these new positions. + this.AdjustCurrXAndCopyToSEL(topY); + + // Find all edge intersections in the current scanbeam using a stable merge + // sort that ensures only adjacent edges are intersecting. Intersect info is + // stored in FIntersectList ready to be processed in ProcessIntersectList. + // Re merge sorts see https://stackoverflow.com/a/46319131/359538 + Active left = this.flaggedHorizontal; + Active right; + Active lEnd; + Active rEnd; + Active currBase; + Active prevBase; + Active tmp; + + while (left.Jump != null) + { + prevBase = null; + while (left?.Jump != null) + { + currBase = left; + right = left.Jump; + lEnd = right; + rEnd = right.Jump; + left.Jump = rEnd; + while (left != lEnd && right != rEnd) + { + if (right.CurX < left.CurX) + { + tmp = right.PrevInSEL; + while (true) + { + this.AddNewIntersectNode(tmp, right, topY); + if (tmp == left) + { + break; + } + + tmp = tmp.PrevInSEL; + } + + tmp = right; + right = ExtractFromSEL(tmp); + lEnd = right; + Insert1Before2InSEL(tmp, left); + if (left == currBase) + { + currBase = tmp; + currBase.Jump = rEnd; + if (prevBase == null) + { + this.flaggedHorizontal = currBase; + } + else + { + prevBase.Jump = currBase; + } + } + } + else + { + left = left.NextInSEL; + } + } + + prevBase = currBase; + left = rEnd; + } + + left = this.flaggedHorizontal; + } + + return this.intersectList.Count > 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ProcessIntersectList() + { + // We now have a list of intersections required so that edges will be + // correctly positioned at the top of the scanbeam. However, it's important + // that edge intersections are processed from the bottom up, but it's also + // crucial that intersections only occur between adjacent edges. + + // First we do a quicksort so intersections proceed in a bottom up order ... + this.intersectList.Sort(default(IntersectListSort)); + + // Now as we process these intersections, we must sometimes adjust the order + // to ensure that intersecting edges are always adjacent ... + for (int i = 0; i < this.intersectList.Count; ++i) + { + if (!EdgesAdjacentInAEL(this.intersectList[i])) + { + int j = i + 1; + while (!EdgesAdjacentInAEL(this.intersectList[j])) + { + j++; + } + + // swap + (this.intersectList[j], this.intersectList[i]) = + (this.intersectList[i], this.intersectList[j]); + } + + IntersectNode node = this.intersectList[i]; + this.IntersectEdges(node.Edge1, node.Edge2, node.Point); + this.SwapPositionsInAEL(node.Edge1, node.Edge2); + + node.Edge1.CurX = node.Point.X; + node.Edge2.CurX = node.Point.X; + this.CheckJoinLeft(node.Edge2, node.Point, true); + this.CheckJoinRight(node.Edge1, node.Point, true); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SwapPositionsInAEL(Active ae1, Active ae2) + { + // preconditon: ae1 must be immediately to the left of ae2 + Active next = ae2.NextInAEL; + if (next != null) + { + next.PrevInAEL = ae1; + } + + Active prev = ae1.PrevInAEL; + if (prev != null) + { + prev.NextInAEL = ae2; + } + + ae2.PrevInAEL = prev; + ae2.NextInAEL = ae1; + ae1.PrevInAEL = ae2; + ae1.NextInAEL = next; + if (ae2.PrevInAEL == null) + { + this.actives = ae2; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ResetHorzDirection(Active horz, Vertex vertexMax, out float leftX, out float rightX) + { + if (horz.Bot.X == horz.Top.X) + { + // the horizontal edge is going nowhere ... + leftX = horz.CurX; + rightX = horz.CurX; + Active ae = horz.NextInAEL; + while (ae != null && ae.VertexTop != vertexMax) + { + ae = ae.NextInAEL; + } + + return ae != null; + } + + if (horz.CurX < horz.Top.X) + { + leftX = horz.CurX; + rightX = horz.Top.X; + return true; + } + + leftX = horz.Top.X; + rightX = horz.CurX; + return false; // right to left + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool HorzIsSpike(Active horz) + { + Vector2 nextPt = NextVertex(horz).Point; + return (horz.Bot.X < horz.Top.X) != (horz.Top.X < nextPt.X); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active FindEdgeWithMatchingLocMin(Active e) + { + Active result = e.NextInAEL; + while (result != null) + { + if (result.LocalMin == e.LocalMin) + { + return result; + } + + if (!IsHorizontal(result) && e.Bot != result.Bot) + { + result = null; + } + else + { + result = result.NextInAEL; + } + } + + result = e.PrevInAEL; + while (result != null) + { + if (result.LocalMin == e.LocalMin) + { + return result; + } + + if (!IsHorizontal(result) && e.Bot != result.Bot) + { + return null; + } + + result = result.PrevInAEL; + } + + return result; + } + + private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt) + { + OutPt resultOp = null; + + // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... + if (this.hasOpenPaths && (IsOpen(ae1) || IsOpen(ae2))) + { + if (IsOpen(ae1) && IsOpen(ae2)) + { + return null; + } + + // the following line avoids duplicating quite a bit of code + if (IsOpen(ae2)) + { + SwapActives(ref ae1, ref ae2); + } + + if (IsJoined(ae2)) + { + this.Split(ae2, pt); // needed for safety + } + + if (this.clipType == BooleanOperation.Union) + { + if (!IsHotEdge(ae2)) + { + return null; + } + } + else if (ae2.LocalMin.Polytype == ClippingType.Subject) + { + return null; + } + + switch (this.fillRule) + { + case ClipperFillRule.Positive: + if (ae2.WindCount != 1) + { + return null; + } + + break; + case ClipperFillRule.Negative: + if (ae2.WindCount != -1) + { + return null; + } + + break; + default: + if (Math.Abs(ae2.WindCount) != 1) + { + return null; + } + + break; + } + + // toggle contribution ... + if (IsHotEdge(ae1)) + { + resultOp = AddOutPt(ae1, pt); + if (IsFront(ae1)) + { + ae1.Outrec.FrontEdge = null; + } + else + { + ae1.Outrec.BackEdge = null; + } + + ae1.Outrec = null; + } + + // horizontal edges can pass under open paths at a LocMins + else if (pt == ae1.LocalMin.Vertex.Point && !IsOpenEnd(ae1.LocalMin.Vertex)) + { + // find the other side of the LocMin and + // if it's 'hot' join up with it ... + Active ae3 = FindEdgeWithMatchingLocMin(ae1); + if (ae3 != null && IsHotEdge(ae3)) + { + ae1.Outrec = ae3.Outrec; + if (ae1.WindDx > 0) + { + SetSides(ae3.Outrec!, ae1, ae3); + } + else + { + SetSides(ae3.Outrec!, ae3, ae1); + } + + return ae3.Outrec.Pts; + } + + resultOp = this.StartOpenPath(ae1, pt); + } + else + { + resultOp = this.StartOpenPath(ae1, pt); + } + + return resultOp; + } + + // MANAGING CLOSED PATHS FROM HERE ON + if (IsJoined(ae1)) + { + this.Split(ae1, pt); + } + + if (IsJoined(ae2)) + { + this.Split(ae2, pt); + } + + // UPDATE WINDING COUNTS... + int oldE1WindCount, oldE2WindCount; + if (ae1.LocalMin.Polytype == ae2.LocalMin.Polytype) + { + if (this.fillRule == ClipperFillRule.EvenOdd) + { + oldE1WindCount = ae1.WindCount; + ae1.WindCount = ae2.WindCount; + ae2.WindCount = oldE1WindCount; + } + else + { + if (ae1.WindCount + ae2.WindDx == 0) + { + ae1.WindCount = -ae1.WindCount; + } + else + { + ae1.WindCount += ae2.WindDx; + } + + if (ae2.WindCount - ae1.WindDx == 0) + { + ae2.WindCount = -ae2.WindCount; + } + else + { + ae2.WindCount -= ae1.WindDx; + } + } + } + else + { + if (this.fillRule != ClipperFillRule.EvenOdd) + { + ae1.WindCount2 += ae2.WindDx; + } + else + { + ae1.WindCount2 = ae1.WindCount2 == 0 ? 1 : 0; + } + + if (this.fillRule != ClipperFillRule.EvenOdd) + { + ae2.WindCount2 -= ae1.WindDx; + } + else + { + ae2.WindCount2 = ae2.WindCount2 == 0 ? 1 : 0; + } + } + + switch (this.fillRule) + { + case ClipperFillRule.Positive: + oldE1WindCount = ae1.WindCount; + oldE2WindCount = ae2.WindCount; + break; + case ClipperFillRule.Negative: + oldE1WindCount = -ae1.WindCount; + oldE2WindCount = -ae2.WindCount; + break; + default: + oldE1WindCount = Math.Abs(ae1.WindCount); + oldE2WindCount = Math.Abs(ae2.WindCount); + break; + } + + bool e1WindCountIs0or1 = oldE1WindCount is 0 or 1; + bool e2WindCountIs0or1 = oldE2WindCount is 0 or 1; + + if ((!IsHotEdge(ae1) && !e1WindCountIs0or1) || (!IsHotEdge(ae2) && !e2WindCountIs0or1)) + { + return null; + } + + // NOW PROCESS THE INTERSECTION ... + + // if both edges are 'hot' ... + if (IsHotEdge(ae1) && IsHotEdge(ae2)) + { + if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1) || + (ae1.LocalMin.Polytype != ae2.LocalMin.Polytype && this.clipType != BooleanOperation.Xor)) + { + resultOp = this.AddLocalMaxPoly(ae1, ae2, pt); + } + else if (IsFront(ae1) || (ae1.Outrec == ae2.Outrec)) + { + // this 'else if' condition isn't strictly needed but + // it's sensible to split polygons that ony touch at + // a common vertex (not at common edges). + resultOp = this.AddLocalMaxPoly(ae1, ae2, pt); + this.AddLocalMinPoly(ae1, ae2, pt); + } + else + { + // can't treat as maxima & minima + resultOp = AddOutPt(ae1, pt); + AddOutPt(ae2, pt); + SwapOutrecs(ae1, ae2); + } + } + + // if one or other edge is 'hot' ... + else if (IsHotEdge(ae1)) + { + resultOp = AddOutPt(ae1, pt); + SwapOutrecs(ae1, ae2); + } + else if (IsHotEdge(ae2)) + { + resultOp = AddOutPt(ae2, pt); + SwapOutrecs(ae1, ae2); + } + + // neither edge is 'hot' + else + { + float e1Wc2, e2Wc2; + switch (this.fillRule) + { + case ClipperFillRule.Positive: + e1Wc2 = ae1.WindCount2; + e2Wc2 = ae2.WindCount2; + break; + case ClipperFillRule.Negative: + e1Wc2 = -ae1.WindCount2; + e2Wc2 = -ae2.WindCount2; + break; + default: + e1Wc2 = Math.Abs(ae1.WindCount2); + e2Wc2 = Math.Abs(ae2.WindCount2); + break; + } + + if (!IsSamePolyType(ae1, ae2)) + { + resultOp = this.AddLocalMinPoly(ae1, ae2, pt); + } + else if (oldE1WindCount == 1 && oldE2WindCount == 1) + { + resultOp = null; + switch (this.clipType) + { + case BooleanOperation.Union: + if (e1Wc2 > 0 && e2Wc2 > 0) + { + return null; + } + + resultOp = this.AddLocalMinPoly(ae1, ae2, pt); + break; + + case BooleanOperation.Difference: + if (((GetPolyType(ae1) == ClippingType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) + || ((GetPolyType(ae1) == ClippingType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) + { + resultOp = this.AddLocalMinPoly(ae1, ae2, pt); + } + + break; + + case BooleanOperation.Xor: + resultOp = this.AddLocalMinPoly(ae1, ae2, pt); + break; + + default: // ClipType.Intersection: + if (e1Wc2 <= 0 || e2Wc2 <= 0) + { + return null; + } + + resultOp = this.AddLocalMinPoly(ae1, ae2, pt); + break; + } + } + } + + return resultOp; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DeleteFromAEL(Active ae) + { + Active prev = ae.PrevInAEL; + Active next = ae.NextInAEL; + if (prev == null && next == null && (ae != this.actives)) + { + return; // already deleted + } + + if (prev != null) + { + prev.NextInAEL = next; + } + else + { + this.actives = next; + } + + if (next != null) + { + next.PrevInAEL = prev; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AdjustCurrXAndCopyToSEL(float topY) + { + Active ae = this.actives; + this.flaggedHorizontal = ae; + while (ae != null) + { + ae.PrevInSEL = ae.PrevInAEL; + ae.NextInSEL = ae.NextInAEL; + ae.Jump = ae.NextInSEL; + if (ae.JoinWith == JoinWith.Left) + { + ae.CurX = ae.PrevInAEL.CurX; // this also avoids complications + } + else + { + ae.CurX = TopX(ae, topY); + } + + // NB don't update ae.curr.Y yet (see AddNewIntersectNode) + ae = ae.NextInAEL; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool HasLocMinAtY(float y) + => this.currentLocMin < this.minimaList.Count && this.minimaList[this.currentLocMin].Vertex.Point.Y == y; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private LocalMinima PopLocalMinima() + => this.minimaList[this.currentLocMin++]; + + private void AddPathsToVertexList(PathsF paths, ClippingType polytype, bool isOpen) + { + int totalVertCnt = 0; + for (int i = 0; i < paths.Count; i++) + { + PathF path = paths[i]; + totalVertCnt += path.Count; + } + + this.vertexList.EnsureCapacity(this.vertexList.Count + totalVertCnt); + + foreach (PathF path in paths) + { + Vertex v0 = null, prev_v = null, curr_v; + foreach (Vector2 pt in path) + { + if (v0 == null) + { + v0 = new Vertex(pt, VertexFlags.None, null); + this.vertexList.Add(v0); + prev_v = v0; + } + else if (prev_v.Point != pt) + { + // ie skips duplicates + curr_v = new Vertex(pt, VertexFlags.None, prev_v); + this.vertexList.Add(curr_v); + prev_v.Next = curr_v; + prev_v = curr_v; + } + } + + if (prev_v == null || prev_v.Prev == null) + { + continue; + } + + if (!isOpen && prev_v.Point == v0.Point) + { + prev_v = prev_v.Prev; + } + + prev_v.Next = v0; + v0.Prev = prev_v; + if (!isOpen && prev_v.Next == prev_v) + { + continue; + } + + // OK, we have a valid path + bool going_up, going_up0; + if (isOpen) + { + curr_v = v0.Next; + while (curr_v != v0 && curr_v.Point.Y == v0.Point.Y) + { + curr_v = curr_v.Next; + } + + going_up = curr_v.Point.Y <= v0.Point.Y; + if (going_up) + { + v0.Flags = VertexFlags.OpenStart; + this.AddLocMin(v0, polytype, true); + } + else + { + v0.Flags = VertexFlags.OpenStart | VertexFlags.LocalMax; + } + } + else + { + // closed path + prev_v = v0.Prev; + while (prev_v != v0 && prev_v.Point.Y == v0.Point.Y) + { + prev_v = prev_v.Prev; + } + + if (prev_v == v0) + { + continue; // only open paths can be completely flat + } + + going_up = prev_v.Point.Y > v0.Point.Y; + } + + going_up0 = going_up; + prev_v = v0; + curr_v = v0.Next; + while (curr_v != v0) + { + if (curr_v.Point.Y > prev_v.Point.Y && going_up) + { + prev_v.Flags |= VertexFlags.LocalMax; + going_up = false; + } + else if (curr_v.Point.Y < prev_v.Point.Y && !going_up) + { + going_up = true; + this.AddLocMin(prev_v, polytype, isOpen); + } + + prev_v = curr_v; + curr_v = curr_v.Next; + } + + if (isOpen) + { + prev_v.Flags |= VertexFlags.OpenEnd; + if (going_up) + { + prev_v.Flags |= VertexFlags.LocalMax; + } + else + { + this.AddLocMin(prev_v, polytype, isOpen); + } + } + else if (going_up != going_up0) + { + if (going_up0) + { + this.AddLocMin(prev_v, polytype, false); + } + else + { + prev_v.Flags |= VertexFlags.LocalMax; + } + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddLocMin(Vertex vert, ClippingType polytype, bool isOpen) + { + // make sure the vertex is added only once. + if ((vert.Flags & VertexFlags.LocalMin) != VertexFlags.None) + { + return; + } + + vert.Flags |= VertexFlags.LocalMin; + + LocalMinima lm = new(vert, polytype, isOpen); + this.minimaList.Add(lm); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PushHorz(Active ae) + { + ae.NextInSEL = this.flaggedHorizontal; + this.flaggedHorizontal = ae; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool PopHorz(out Active ae) + { + ae = this.flaggedHorizontal; + if (this.flaggedHorizontal == null) + { + return false; + } + + this.flaggedHorizontal = this.flaggedHorizontal.NextInSEL; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutPt AddLocalMinPoly(Active ae1, Active ae2, Vector2 pt, bool isNew = false) + { + OutRec outrec = this.NewOutRec(); + ae1.Outrec = outrec; + ae2.Outrec = outrec; + + if (IsOpen(ae1)) + { + outrec.Owner = null; + outrec.IsOpen = true; + if (ae1.WindDx > 0) + { + SetSides(outrec, ae1, ae2); + } + else + { + SetSides(outrec, ae2, ae1); + } + } + else + { + outrec.IsOpen = false; + Active prevHotEdge = GetPrevHotEdge(ae1); + + // e.windDx is the winding direction of the **input** paths + // and unrelated to the winding direction of output polygons. + // Output orientation is determined by e.outrec.frontE which is + // the ascending edge (see AddLocalMinPoly). + if (prevHotEdge != null) + { + outrec.Owner = prevHotEdge.Outrec; + if (OutrecIsAscending(prevHotEdge) == isNew) + { + SetSides(outrec, ae2, ae1); + } + else + { + SetSides(outrec, ae1, ae2); + } + } + else + { + outrec.Owner = null; + if (isNew) + { + SetSides(outrec, ae1, ae2); + } + else + { + SetSides(outrec, ae2, ae1); + } + } + } + + OutPt op = new(pt, outrec); + outrec.Pts = op; + return op; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetDx(Active ae) + => ae.Dx = GetDx(ae.Bot, ae.Top); + + /******************************************************************************* + * Dx: 0(90deg) * + * | * + * +inf (180deg) <--- o --. -inf (0deg) * + *******************************************************************************/ + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetDx(Vector2 pt1, Vector2 pt2) + { + float dy = pt2.Y - pt1.Y; + if (dy != 0) + { + return (pt2.X - pt1.X) / dy; + } + + if (pt2.X > pt1.X) + { + return float.NegativeInfinity; + } + + return float.PositiveInfinity; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float TopX(Active ae, float currentY) + { + Vector2 top = ae.Top; + Vector2 bottom = ae.Bot; + + if ((currentY == top.Y) || (top.X == bottom.X)) + { + return top.X; + } + + if (currentY == bottom.Y) + { + return bottom.X; + } + + return bottom.X + (ae.Dx * (currentY - bottom.Y)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHorizontal(Active ae) + => ae.Top.Y == ae.Bot.Y; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHeadingRightHorz(Active ae) + => float.IsNegativeInfinity(ae.Dx); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHeadingLeftHorz(Active ae) + => float.IsPositiveInfinity(ae.Dx); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapActives(ref Active ae1, ref Active ae2) + => (ae2, ae1) = (ae1, ae2); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ClippingType GetPolyType(Active ae) + => ae.LocalMin.Polytype; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSamePolyType(Active ae1, Active ae2) + => ae1.LocalMin.Polytype == ae2.LocalMin.Polytype; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsContributingClosed(Active ae) + { + switch (this.fillRule) + { + case ClipperFillRule.Positive: + if (ae.WindCount != 1) + { + return false; + } + + break; + case ClipperFillRule.Negative: + if (ae.WindCount != -1) + { + return false; + } + + break; + case ClipperFillRule.NonZero: + if (Math.Abs(ae.WindCount) != 1) + { + return false; + } + + break; + } + + switch (this.clipType) + { + case BooleanOperation.Intersection: + return this.fillRule switch + { + ClipperFillRule.Positive => ae.WindCount2 > 0, + ClipperFillRule.Negative => ae.WindCount2 < 0, + _ => ae.WindCount2 != 0, + }; + + case BooleanOperation.Union: + return this.fillRule switch + { + ClipperFillRule.Positive => ae.WindCount2 <= 0, + ClipperFillRule.Negative => ae.WindCount2 >= 0, + _ => ae.WindCount2 == 0, + }; + + case BooleanOperation.Difference: + bool result = this.fillRule switch + { + ClipperFillRule.Positive => ae.WindCount2 <= 0, + ClipperFillRule.Negative => ae.WindCount2 >= 0, + _ => ae.WindCount2 == 0, + }; + return (GetPolyType(ae) == ClippingType.Subject) ? result : !result; + + case BooleanOperation.Xor: + return true; // XOr is always contributing unless open + + default: + return false; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsContributingOpen(Active ae) + { + bool isInClip, isInSubj; + switch (this.fillRule) + { + case ClipperFillRule.Positive: + isInSubj = ae.WindCount > 0; + isInClip = ae.WindCount2 > 0; + break; + case ClipperFillRule.Negative: + isInSubj = ae.WindCount < 0; + isInClip = ae.WindCount2 < 0; + break; + default: + isInSubj = ae.WindCount != 0; + isInClip = ae.WindCount2 != 0; + break; + } + + bool result = this.clipType switch + { + BooleanOperation.Intersection => isInClip, + BooleanOperation.Union => !isInSubj && !isInClip, + _ => !isInClip + }; + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetWindCountForClosedPathEdge(Active ae) + { + // Wind counts refer to polygon regions not edges, so here an edge's WindCnt + // indicates the higher of the wind counts for the two regions touching the + // edge. (nb: Adjacent regions can only ever have their wind counts differ by + // one. Also, open paths have no meaningful wind directions or counts.) + Active ae2 = ae.PrevInAEL; + + // find the nearest closed path edge of the same PolyType in AEL (heading left) + ClippingType pt = GetPolyType(ae); + while (ae2 != null && (GetPolyType(ae2) != pt || IsOpen(ae2))) + { + ae2 = ae2.PrevInAEL; + } + + if (ae2 == null) + { + ae.WindCount = ae.WindDx; + ae2 = this.actives; + } + else if (this.fillRule == ClipperFillRule.EvenOdd) + { + ae.WindCount = ae.WindDx; + ae.WindCount2 = ae2.WindCount2; + ae2 = ae2.NextInAEL; + } + else + { + // NonZero, positive, or negative filling here ... + // when e2's WindCnt is in the SAME direction as its WindDx, + // then polygon will fill on the right of 'e2' (and 'e' will be inside) + // nb: neither e2.WindCnt nor e2.WindDx should ever be 0. + if (ae2.WindCount * ae2.WindDx < 0) + { + // opposite directions so 'ae' is outside 'ae2' ... + if (Math.Abs(ae2.WindCount) > 1) + { + // outside prev poly but still inside another. + if (ae2.WindDx * ae.WindDx < 0) + { + // reversing direction so use the same WC + ae.WindCount = ae2.WindCount; + } + else + { + // otherwise keep 'reducing' the WC by 1 (i.e. towards 0) ... + ae.WindCount = ae2.WindCount + ae.WindDx; + } + } + else + { + // now outside all polys of same polytype so set own WC ... + ae.WindCount = IsOpen(ae) ? 1 : ae.WindDx; + } + } + else + { + // 'ae' must be inside 'ae2' + if (ae2.WindDx * ae.WindDx < 0) + { + // reversing direction so use the same WC + ae.WindCount = ae2.WindCount; + } + else + { + // otherwise keep 'increasing' the WC by 1 (i.e. away from 0) ... + ae.WindCount = ae2.WindCount + ae.WindDx; + } + } + + ae.WindCount2 = ae2.WindCount2; + ae2 = ae2.NextInAEL; // i.e. get ready to calc WindCnt2 + } + + // update windCount2 ... + if (this.fillRule == ClipperFillRule.EvenOdd) + { + while (ae2 != ae) + { + if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) + { + ae.WindCount2 = ae.WindCount2 == 0 ? 1 : 0; + } + + ae2 = ae2.NextInAEL; + } + } + else + { + while (ae2 != ae) + { + if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) + { + ae.WindCount2 += ae2.WindDx; + } + + ae2 = ae2.NextInAEL; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetWindCountForOpenPathEdge(Active ae) + { + Active ae2 = this.actives; + if (this.fillRule == ClipperFillRule.EvenOdd) + { + int cnt1 = 0, cnt2 = 0; + while (ae2 != ae) + { + if (GetPolyType(ae2!) == ClippingType.Clip) + { + cnt2++; + } + else if (!IsOpen(ae2!)) + { + cnt1++; + } + + ae2 = ae2.NextInAEL; + } + + ae.WindCount = IsOdd(cnt1) ? 1 : 0; + ae.WindCount2 = IsOdd(cnt2) ? 1 : 0; + } + else + { + while (ae2 != ae) + { + if (GetPolyType(ae2!) == ClippingType.Clip) + { + ae.WindCount2 += ae2.WindDx; + } + else if (!IsOpen(ae2!)) + { + ae.WindCount += ae2.WindDx; + } + + ae2 = ae2.NextInAEL; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidAelOrder(Active resident, Active newcomer) + { + if (newcomer.CurX != resident.CurX) + { + return newcomer.CurX > resident.CurX; + } + + // get the turning direction a1.top, a2.bot, a2.top + float d = ClipperUtils.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top); + if (d != 0) + { + return d < 0; + } + + // edges must be collinear to get here + + // for starting open paths, place them according to + // the direction they're about to turn + if (!IsMaxima(resident) && (resident.Top.Y > newcomer.Top.Y)) + { + return ClipperUtils.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0; + } + + if (!IsMaxima(newcomer) && (newcomer.Top.Y > resident.Top.Y)) + { + return ClipperUtils.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0; + } + + float y = newcomer.Bot.Y; + bool newcomerIsLeft = newcomer.IsLeftBound; + + if (resident.Bot.Y != y || resident.LocalMin.Vertex.Point.Y != y) + { + return newcomer.IsLeftBound; + } + + // resident must also have just been inserted + if (resident.IsLeftBound != newcomerIsLeft) + { + return newcomerIsLeft; + } + + if (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0) + { + return true; + } + + // compare turning direction of the alternate bound + return (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InsertLeftEdge(Active ae) + { + Active ae2; + + if (this.actives == null) + { + ae.PrevInAEL = null; + ae.NextInAEL = null; + this.actives = ae; + } + else if (!IsValidAelOrder(this.actives, ae)) + { + ae.PrevInAEL = null; + ae.NextInAEL = this.actives; + this.actives.PrevInAEL = ae; + this.actives = ae; + } + else + { + ae2 = this.actives; + while (ae2.NextInAEL != null && IsValidAelOrder(ae2.NextInAEL, ae)) + { + ae2 = ae2.NextInAEL; + } + + // don't separate joined edges + if (ae2.JoinWith == JoinWith.Right) + { + ae2 = ae2.NextInAEL; + } + + ae.NextInAEL = ae2.NextInAEL; + if (ae2.NextInAEL != null) + { + ae2.NextInAEL.PrevInAEL = ae; + } + + ae.PrevInAEL = ae2; + ae2.NextInAEL = ae; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InsertRightEdge(Active ae, Active ae2) + { + ae2.NextInAEL = ae.NextInAEL; + if (ae.NextInAEL != null) + { + ae.NextInAEL.PrevInAEL = ae2; + } + + ae2.PrevInAEL = ae; + ae.NextInAEL = ae2; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex NextVertex(Active ae) + { + if (ae.WindDx > 0) + { + return ae.VertexTop.Next; + } + + return ae.VertexTop.Prev; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vertex PrevPrevVertex(Active ae) + { + if (ae.WindDx > 0) + { + return ae.VertexTop.Prev.Prev; + } + + return ae.VertexTop.Next.Next; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMaxima(Vertex vertex) + => (vertex.Flags & VertexFlags.LocalMax) != VertexFlags.None; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMaxima(Active ae) + => IsMaxima(ae.VertexTop); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active GetMaximaPair(Active ae) + { + Active ae2; + ae2 = ae.NextInAEL; + while (ae2 != null) + { + if (ae2.VertexTop == ae.VertexTop) + { + return ae2; // Found! + } + + ae2 = ae2.NextInAEL; + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOdd(int val) + => (val & 1) != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHotEdge(Active ae) + => ae.Outrec != null; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOpen(Active ae) + => ae.LocalMin.IsOpen; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOpenEnd(Active ae) + => ae.LocalMin.IsOpen && IsOpenEnd(ae.VertexTop); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsOpenEnd(Vertex v) + => (v.Flags & (VertexFlags.OpenStart | VertexFlags.OpenEnd)) != VertexFlags.None; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Active GetPrevHotEdge(Active ae) + { + Active prev = ae.PrevInAEL; + while (prev != null && (IsOpen(prev) || !IsHotEdge(prev))) + { + prev = prev.PrevInAEL; + } + + return prev; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void JoinOutrecPaths(Active ae1, Active ae2) + { + // join ae2 outrec path onto ae1 outrec path and then delete ae2 outrec path + // pointers. (NB Only very rarely do the joining ends share the same coords.) + OutPt p1Start = ae1.Outrec.Pts; + OutPt p2Start = ae2.Outrec.Pts; + OutPt p1End = p1Start.Next; + OutPt p2End = p2Start.Next; + if (IsFront(ae1)) + { + p2End.Prev = p1Start; + p1Start.Next = p2End; + p2Start.Next = p1End; + p1End.Prev = p2Start; + ae1.Outrec.Pts = p2Start; + + // nb: if IsOpen(e1) then e1 & e2 must be a 'maximaPair' + ae1.Outrec.FrontEdge = ae2.Outrec.FrontEdge; + if (ae1.Outrec.FrontEdge != null) + { + ae1.Outrec.FrontEdge.Outrec = ae1.Outrec; + } + } + else + { + p1End.Prev = p2Start; + p2Start.Next = p1End; + p1Start.Next = p2End; + p2End.Prev = p1Start; + + ae1.Outrec.BackEdge = ae2.Outrec.BackEdge; + if (ae1.Outrec.BackEdge != null) + { + ae1.Outrec.BackEdge.Outrec = ae1.Outrec; + } + } + + // after joining, the ae2.OutRec must contains no vertices ... + ae2.Outrec.FrontEdge = null; + ae2.Outrec.BackEdge = null; + ae2.Outrec.Pts = null; + SetOwner(ae2.Outrec, ae1.Outrec); + + if (IsOpenEnd(ae1)) + { + ae2.Outrec.Pts = ae1.Outrec.Pts; + ae1.Outrec.Pts = null; + } + + // and ae1 and ae2 are maxima and are about to be dropped from the Actives list. + ae1.Outrec = null; + ae2.Outrec = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutPt AddOutPt(Active ae, Vector2 pt) + { + // Outrec.OutPts: a circular doubly-linked-list of POutPt where ... + // opFront[.Prev]* ~~~> opBack & opBack == opFront.Next + OutRec outrec = ae.Outrec; + bool toFront = IsFront(ae); + OutPt opFront = outrec.Pts; + OutPt opBack = opFront.Next; + + if (toFront && (pt == opFront.Point)) + { + return opFront; + } + else if (!toFront && (pt == opBack.Point)) + { + return opBack; + } + + OutPt newOp = new(pt, outrec); + opBack.Prev = newOp; + newOp.Prev = opFront; + newOp.Next = opBack; + opFront.Next = newOp; + if (toFront) + { + outrec.Pts = newOp; + } + + return newOp; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutRec NewOutRec() + { + OutRec result = new() + { + Idx = this.outrecList.Count + }; + this.outrecList.Add(result); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutPt StartOpenPath(Active ae, Vector2 pt) + { + OutRec outrec = this.NewOutRec(); + outrec.IsOpen = true; + if (ae.WindDx > 0) + { + outrec.FrontEdge = ae; + outrec.BackEdge = null; + } + else + { + outrec.FrontEdge = null; + outrec.BackEdge = ae; + } + + ae.Outrec = outrec; + OutPt op = new(pt, outrec); + outrec.Pts = op; + return op; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UpdateEdgeIntoAEL(Active ae) + { + ae.Bot = ae.Top; + ae.VertexTop = NextVertex(ae); + ae.Top = ae.VertexTop.Point; + ae.CurX = ae.Bot.X; + SetDx(ae); + + if (IsJoined(ae)) + { + this.Split(ae, ae.Bot); + } + + if (IsHorizontal(ae)) + { + return; + } + + this.InsertScanline(ae.Top.Y); + + this.CheckJoinLeft(ae, ae.Bot); + this.CheckJoinRight(ae, ae.Bot, true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetSides(OutRec outrec, Active startEdge, Active endEdge) + { + outrec.FrontEdge = startEdge; + outrec.BackEdge = endEdge; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapOutrecs(Active ae1, Active ae2) + { + OutRec or1 = ae1.Outrec; // at least one edge has + OutRec or2 = ae2.Outrec; // an assigned outrec + if (or1 == or2) + { + (or1.BackEdge, or1.FrontEdge) = (or1.FrontEdge, or1.BackEdge); + return; + } + + if (or1 != null) + { + if (ae1 == or1.FrontEdge) + { + or1.FrontEdge = ae2; + } + else + { + or1.BackEdge = ae2; + } + } + + if (or2 != null) + { + if (ae2 == or2.FrontEdge) + { + or2.FrontEdge = ae1; + } + else + { + or2.BackEdge = ae1; + } + } + + ae1.Outrec = or2; + ae2.Outrec = or1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SetOwner(OutRec outrec, OutRec newOwner) + { + // precondition1: new_owner is never null + while (newOwner.Owner != null && newOwner.Owner.Pts == null) + { + newOwner.Owner = newOwner.Owner.Owner; + } + + // make sure that outrec isn't an owner of newOwner + OutRec tmp = newOwner; + while (tmp != null && tmp != outrec) + { + tmp = tmp.Owner; + } + + if (tmp != null) + { + newOwner.Owner = outrec.Owner; + } + + outrec.Owner = newOwner; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Area(OutPt op) + { + // https://en.wikipedia.org/wiki/Shoelace_formula + float area = 0; + OutPt op2 = op; + do + { + area += (op2.Prev.Point.Y + op2.Point.Y) * (op2.Prev.Point.X - op2.Point.X); + op2 = op2.Next; + } + while (op2 != op); + return area * .5F; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float AreaTriangle(Vector2 pt1, Vector2 pt2, Vector2 pt3) + => ((pt3.Y + pt1.Y) * (pt3.X - pt1.X)) + + ((pt1.Y + pt2.Y) * (pt1.X - pt2.X)) + + ((pt2.Y + pt3.Y) * (pt2.X - pt3.X)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static OutRec GetRealOutRec(OutRec outRec) + { + while ((outRec != null) && (outRec.Pts == null)) + { + outRec = outRec.Owner; + } + + return outRec; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UncoupleOutRec(Active ae) + { + OutRec outrec = ae.Outrec; + if (outrec == null) + { + return; + } + + outrec.FrontEdge.Outrec = null; + outrec.BackEdge.Outrec = null; + outrec.FrontEdge = null; + outrec.BackEdge = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool OutrecIsAscending(Active hotEdge) + => hotEdge == hotEdge.Outrec.FrontEdge; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void SwapFrontBackSides(OutRec outrec) + { + // while this proc. is needed for open paths + // it's almost never needed for closed paths + (outrec.BackEdge, outrec.FrontEdge) = (outrec.FrontEdge, outrec.BackEdge); + outrec.Pts = outrec.Pts.Next; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool EdgesAdjacentInAEL(IntersectNode inode) + => (inode.Edge1.NextInAEL == inode.Edge2) || (inode.Edge1.PrevInAEL == inode.Edge2); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false) + { + Active prev = e.PrevInAEL; + if (prev == null + || IsOpen(e) + || IsOpen(prev) + || !IsHotEdge(e) + || !IsHotEdge(prev)) + { + return; + } + + // Avoid trivial joins + if ((pt.Y < e.Top.Y + 2 || pt.Y < prev.Top.Y + 2) + && ((e.Bot.Y > pt.Y) || (prev.Bot.Y > pt.Y))) + { + return; + } + + if (checkCurrX) + { + if (ClipperUtils.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25) + { + return; + } + } + else if (e.CurX != prev.CurX) + { + return; + } + + if (ClipperUtils.CrossProduct(e.Top, pt, prev.Top) != 0) + { + return; + } + + if (e.Outrec.Idx == prev.Outrec.Idx) + { + this.AddLocalMaxPoly(prev, e, pt); + } + else if (e.Outrec.Idx < prev.Outrec.Idx) + { + JoinOutrecPaths(e, prev); + } + else + { + JoinOutrecPaths(prev, e); + } + + prev.JoinWith = JoinWith.Right; + e.JoinWith = JoinWith.Left; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false) + { + Active next = e.NextInAEL; + if (IsOpen(e) + || !IsHotEdge(e) + || IsJoined(e) + || next == null + || IsOpen(next) + || !IsHotEdge(next)) + { + return; + } + + // Avoid trivial joins + if ((pt.Y < e.Top.Y + 2 || pt.Y < next.Top.Y + 2) + && ((e.Bot.Y > pt.Y) || (next.Bot.Y > pt.Y))) + { + return; + } + + if (checkCurrX) + { + if (ClipperUtils.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25) + { + return; + } + } + else if (e.CurX != next.CurX) + { + return; + } + + if (ClipperUtils.CrossProduct(e.Top, pt, next.Top) != 0) + { + return; + } + + if (e.Outrec.Idx == next.Outrec.Idx) + { + this.AddLocalMaxPoly(e, next, pt); + } + else if (e.Outrec.Idx < next.Outrec.Idx) + { + JoinOutrecPaths(e, next); + } + else + { + JoinOutrecPaths(next, e); + } + + e.JoinWith = JoinWith.Right; + next.JoinWith = JoinWith.Left; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void FixOutRecPts(OutRec outrec) + { + OutPt op = outrec.Pts; + do + { + op.OutRec = outrec; + op = op.Next; + } + while (op != outrec.Pts); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OutPt AddLocalMaxPoly(Active ae1, Active ae2, Vector2 pt) + { + if (IsJoined(ae1)) + { + this.Split(ae1, pt); + } + + if (IsJoined(ae2)) + { + this.Split(ae2, pt); + } + + if (IsFront(ae1) == IsFront(ae2)) + { + if (IsOpenEnd(ae1)) + { + SwapFrontBackSides(ae1.Outrec!); + } + else if (IsOpenEnd(ae2)) + { + SwapFrontBackSides(ae2.Outrec!); + } + else + { + return null; + } + } + + OutPt result = AddOutPt(ae1, pt); + if (ae1.Outrec == ae2.Outrec) + { + OutRec outrec = ae1.Outrec; + outrec.Pts = result; + UncoupleOutRec(ae1); + } + + // and to preserve the winding orientation of outrec ... + else if (IsOpen(ae1)) + { + if (ae1.WindDx < 0) + { + JoinOutrecPaths(ae1, ae2); + } + else + { + JoinOutrecPaths(ae2, ae1); + } + } + else if (ae1.Outrec.Idx < ae2.Outrec.Idx) + { + JoinOutrecPaths(ae1, ae2); + } + else + { + JoinOutrecPaths(ae2, ae1); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsJoined(Active e) + => e.JoinWith != JoinWith.None; + + private void Split(Active e, Vector2 currPt) + { + if (e.JoinWith == JoinWith.Right) + { + e.JoinWith = JoinWith.None; + e.NextInAEL.JoinWith = JoinWith.None; + this.AddLocalMinPoly(e, e.NextInAEL, currPt, true); + } + else + { + e.JoinWith = JoinWith.None; + e.PrevInAEL.JoinWith = JoinWith.None; + this.AddLocalMinPoly(e.PrevInAEL, e, currPt, true); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsFront(Active ae) + => ae == ae.Outrec.FrontEdge; + + private struct LocMinSorter : IComparer + { + public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2) + => locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y); + } + + private readonly struct LocalMinima + { + public readonly Vertex Vertex; + public readonly ClippingType Polytype; + public readonly bool IsOpen; + + public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false) + { + this.Vertex = vertex; + this.Polytype = polytype; + this.IsOpen = isOpen; + } + + public static bool operator ==(LocalMinima lm1, LocalMinima lm2) + + // TODO: Check this. Why ref equals. + => ReferenceEquals(lm1.Vertex, lm2.Vertex); + + public static bool operator !=(LocalMinima lm1, LocalMinima lm2) + => !(lm1 == lm2); + + public override bool Equals(object obj) + => obj is LocalMinima minima && this == minima; + + public override int GetHashCode() + => this.Vertex.GetHashCode(); + } + + // IntersectNode: a structure representing 2 intersecting edges. + // Intersections must be sorted so they are processed from the largest + // Y coordinates to the smallest while keeping edges adjacent. + private readonly struct IntersectNode + { + public readonly Vector2 Point; + public readonly Active Edge1; + public readonly Active Edge2; + + public IntersectNode(Vector2 pt, Active edge1, Active edge2) + { + this.Point = pt; + this.Edge1 = edge1; + this.Edge2 = edge2; + } + } + + private struct HorzSegSorter : IComparer + { + public readonly int Compare(HorzSegment hs1, HorzSegment hs2) + { + if (hs1 == null || hs2 == null) + { + return 0; + } + + if (hs1.RightOp == null) + { + return hs2.RightOp == null ? 0 : 1; + } + else if (hs2.RightOp == null) + { + return -1; + } + else + { + return hs1.LeftOp.Point.X.CompareTo(hs2.LeftOp.Point.X); + } + } + } + + private struct IntersectListSort : IComparer + { + public readonly int Compare(IntersectNode a, IntersectNode b) + { + if (a.Point.Y == b.Point.Y) + { + if (a.Point.X == b.Point.X) + { + return 0; + } + + return (a.Point.X < b.Point.X) ? -1 : 1; + } + + return (a.Point.Y > b.Point.Y) ? -1 : 1; + } + } + + private class HorzSegment + { + public HorzSegment(OutPt op) + { + this.LeftOp = op; + this.RightOp = null; + this.LeftToRight = true; + } + + public OutPt LeftOp { get; set; } + + public OutPt RightOp { get; set; } + + public bool LeftToRight { get; set; } + } + + private class HorzJoin + { + public HorzJoin(OutPt ltor, OutPt rtol) + { + this.Op1 = ltor; + this.Op2 = rtol; + } + + public OutPt Op1 { get; } + + public OutPt Op2 { get; } + } + + // OutPt: vertex data structure for clipping solutions + private class OutPt + { + public OutPt(Vector2 pt, OutRec outrec) + { + this.Point = pt; + this.OutRec = outrec; + this.Next = this; + this.Prev = this; + this.HorizSegment = null; + } + + public Vector2 Point { get; } + + public OutPt Next { get; set; } + + public OutPt Prev { get; set; } + + public OutRec OutRec { get; set; } + + public HorzSegment HorizSegment { get; set; } + } + + // OutRec: path data structure for clipping solutions + private class OutRec + { + public int Idx { get; set; } + + public OutRec Owner { get; set; } + + public Active FrontEdge { get; set; } + + public Active BackEdge { get; set; } + + public OutPt Pts { get; set; } + + public PolyPathF PolyPath { get; set; } + + public BoundsF Bounds { get; set; } + + public PathF Path { get; set; } = []; + + public bool IsOpen { get; set; } + + public List Splits { get; set; } + } + + private class Vertex + { + public Vertex(Vector2 pt, VertexFlags flags, Vertex prev) + { + this.Point = pt; + this.Flags = flags; + this.Next = null; + this.Prev = prev; + } + + public Vector2 Point { get; } + + public Vertex Next { get; set; } + + public Vertex Prev { get; set; } + + public VertexFlags Flags { get; set; } + } + + private class Active + { + public Vector2 Bot { get; set; } + + public Vector2 Top { get; set; } + + public float CurX { get; set; } // current (updated at every new scanline) + + public float Dx { get; set; } + + public int WindDx { get; set; } // 1 or -1 depending on winding direction + + public int WindCount { get; set; } + + public int WindCount2 { get; set; } // winding count of the opposite polytype + + public OutRec Outrec { get; set; } + + // AEL: 'active edge list' (Vatti's AET - active edge table) + // a linked list of all edges (from left to right) that are present + // (or 'active') within the current scanbeam (a horizontal 'beam' that + // sweeps from bottom to top over the paths in the clipping operation). + public Active PrevInAEL { get; set; } + + public Active NextInAEL { get; set; } + + // SEL: 'sorted edge list' (Vatti's ST - sorted table) + // linked list used when sorting edges into their new positions at the + // top of scanbeams, but also (re)used to process horizontals. + public Active PrevInSEL { get; set; } + + public Active NextInSEL { get; set; } + + public Active Jump { get; set; } + + public Vertex VertexTop { get; set; } + + public LocalMinima LocalMin { get; set; } // the bottom of an edge 'bound' (also Vatti) + + public bool IsLeftBound { get; set; } + + public JoinWith JoinWith { get; set; } + } +} + +internal class PolyPathF : IEnumerable +{ + private readonly PolyPathF parent; + private readonly List items = []; + + public PolyPathF(PolyPathF parent = null) + => this.parent = parent; + + public PathF Polygon { get; private set; } // polytree root's polygon == null + + public int Level => this.GetLevel(); + + public bool IsHole => this.GetIsHole(); + + public int Count => this.items.Count; + + public PolyPathF this[int index] => this.items[index]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PolyPathF AddChild(PathF p) + { + PolyPathF child = new(this) + { + Polygon = p + }; + + this.items.Add(child); + return child; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Area() + { + float result = this.Polygon == null ? 0 : ClipperUtils.Area(this.Polygon); + for (int i = 0; i < this.items.Count; i++) + { + PolyPathF child = this.items[i]; + result += child.Area(); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() => this.items.Clear(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool GetIsHole() + { + int lvl = this.Level; + return lvl != 0 && (lvl & 1) == 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetLevel() + { + int result = 0; + PolyPathF pp = this.parent; + while (pp != null) + { + ++result; + pp = pp.parent; + } + + return result; + } + + public IEnumerator GetEnumerator() => this.items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.items.GetEnumerator(); +} + +internal class PolyTreeF : PolyPathF +{ +} + +internal class PathsF : List +{ + public PathsF() + { + } + + public PathsF(IEnumerable items) + : base(items) + { + } + + public PathsF(int capacity) + : base(capacity) + { + } +} + +internal class PathF : List +{ + public PathF() + { + } + + public PathF(IEnumerable items) + : base(items) + { + } + + public PathF(int capacity) + : base(capacity) + { + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs deleted file mode 100644 index f904629e..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs +++ /dev/null @@ -1,384 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using SixLabors.PolygonClipper; -using ClipperPolygon = SixLabors.PolygonClipper.Polygon; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// Builders for from ImageSharp paths. -/// PolygonClipper requires explicit orientation and nesting of contours ImageSharp polygons do not contain that information -/// so we must derive that from the input. -/// -internal static class PolygonClipperFactory -{ - /// - /// Creates a new polygon by combining multiple paths using the specified intersection rule. - /// - /// Use this method to construct complex polygons from multiple input paths, such as when - /// importing shapes from vector graphics or combining user-drawn segments. The resulting polygon's structure - /// depends on the order and geometry of the input paths as well as the chosen intersection rule. - /// - /// - /// A collection of paths that define the shapes to be combined into a single polygon. Each path is expected to - /// represent a simple or complex shape. - /// - /// Containment rule for nesting, or . - /// A representing the union of all input paths, combined according to the specified intersection rule. - public static ClipperPolygon FromPaths(IEnumerable paths, IntersectionRule rule) - { - // Accumulate all paths of the complex shape into a single polygon. - ClipperPolygon polygon = []; - - foreach (IPath path in paths) - { - polygon = FromSimplePaths(path.Flatten(), rule, polygon); - } - - return polygon; - } - - /// - /// Builds a from closed rings. - /// - /// - /// Pipeline: - /// 1) Filter to closed paths with ≥3 unique points, copy to rings. - /// 2) Compute signed area via the shoelace formula to get orientation and magnitude. - /// 3) For each ring, pick its lexicographic bottom-left vertex. - /// 4) Parent assignment: for ring i, shoot a conceptual vertical ray downward from its bottom-left point - /// and test containment against all other rings using the selected . - /// The parent is the smallest-area ring that contains the point. - /// 5) Depth is the number of ancestors by repeated parent lookup. - /// 6) Materialize s, enforce even depth CCW and odd depth CW, - /// set and , add to and wire holes. - /// Notes: - /// - Step 4 mirrors the parent-detection approach formalized in Martínez–Rueda 2013. - /// - Containment uses Even-Odd or Non-Zero consistently, so glyph-like inputs can use Non-Zero. - /// - Boundary handling: points exactly on edges are not special-cased here, which is typical for nesting. - /// - /// Closed simple paths. - /// Containment rule for nesting, or . - /// Optional existing polygon to populate. - /// The constructed . - public static ClipperPolygon FromSimplePaths(IEnumerable paths, IntersectionRule rule, ClipperPolygon? polygon = null) - { - // Gather rings as Vertex lists (explicitly closed), plus per-ring metadata. - List> rings = []; - List areas = []; - List bottomLeft = []; - - foreach (ISimplePath p in paths) - { - if (!p.IsClosed) - { - // TODO: could append first point to close, but that fabricates geometry. - continue; - } - - ReadOnlySpan s = p.Points.Span; - int n = s.Length; - - // Need at least 3 points to form area. - if (n < 3) - { - continue; - } - - // Copy all points as-is. - List ring = new(n); - for (int i = 0; i < n; i++) - { - ring.Add(new Vertex(s[i].X, s[i].Y)); - } - - // Ensure explicit closure: start == end. - if (ring.Count > 0) - { - Vertex first = ring[0]; - Vertex last = ring[^1]; - if (first.X != last.X || first.Y != last.Y) - { - ring.Add(first); - } - } - - // After closure, still require at least 3 unique vertices. - if (ring.Count < 4) // 3 unique + repeated first == last - { - continue; - } - - rings.Add(ring); - - // SignedArea must handle a closed ring (last == first). - areas.Add(SignedArea(ring)); - - // Choose lexicographic bottom-left vertex index for nesting test. - bottomLeft.Add(IndexOfBottomLeft(ring)); - } - - int m = rings.Count; - if (m == 0) - { - return []; - } - - // Parent assignment: pick the smallest-area ring that contains the bottom-left vertex. - // TODO: We can use pooling here if we care about large numbers of rings. - int[] parent = new int[m]; - Array.Fill(parent, -1); - - for (int i = 0; i < m; i++) - { - Vertex q = rings[i][bottomLeft[i]]; - int best = -1; - double bestArea = double.MaxValue; - - for (int j = 0; j < m; j++) - { - if (i == j) - { - continue; - } - - if (IsPointInPolygon(q, rings[j], rule)) - { - double a = Math.Abs(areas[j]); - if (a < bestArea) - { - bestArea = a; - best = j; - } - } - } - - parent[i] = best; - } - - // Depth = number of ancestors by following Parent links. - // TODO: We can pool this if we care about large numbers of rings. - int[] depth = new int[m]; - for (int i = 0; i < m; i++) - { - int d = 0; - for (int pIdx = parent[i]; pIdx >= 0; pIdx = parent[pIdx]) - { - d++; - } - - depth[i] = d; - } - - // Emit contours, enforce orientation by depth, and wire into polygon. - polygon ??= []; - for (int i = 0; i < m; i++) - { - Contour c = new(); - - // Stream vertices into the contour. Ring is already explicitly closed. - foreach (Vertex v in rings[i]) - { - c.AddVertex(v); - } - - // Orientation convention: even depth = outer => CCW, odd depth = hole => CW. - if ((depth[i] & 1) == 0) - { - c.SetCounterClockwise(); - } - else - { - c.SetClockwise(); - } - - // Topology annotations. - c.ParentIndex = parent[i] >= 0 ? parent[i] : null; - c.Depth = depth[i]; - - polygon.Add(c); - } - - // Record hole indices for parents now that indices are stable. - for (int i = 0; i < m; i++) - { - int pIdx = parent[i]; - if (pIdx >= 0) - { - polygon[pIdx].AddHoleIndex(i); - } - } - - return polygon; - } - - /// - /// Computes the signed area of a closed ring using the shoelace formula. - /// - /// Ring of vertices. - /// - /// Formula: - /// - /// A = 0.5 * Σ cross(v[j], v[i]) with j = (i - 1) mod n - /// - /// where cross(a,b) = a.X * b.Y - a.Y * b.X. - /// Interpretation: - /// - A > 0 means counter-clockwise orientation. - /// - A < 0 means clockwise orientation. - /// - private static double SignedArea(List r) - { - double area = 0d; - - for (int i = 0, j = r.Count - 1; i < r.Count; j = i, i++) - { - area += Vertex.Cross(r[j], r[i]); - } - - return 0.5d * area; - } - - /// - /// Returns the index of the lexicographically bottom-left vertex. - /// - /// Ring of vertices. - /// - /// Lexicographic order (X then Y) yields a unique seed for nesting tests and matches - /// common parent-detection proofs that cast a ray from the lowest-leftmost point. - /// - private static int IndexOfBottomLeft(List r) - { - int k = 0; - - for (int i = 1; i < r.Count; i++) - { - Vertex a = r[i]; - Vertex b = r[k]; - - if (a.X < b.X || (a.X == b.X && a.Y < b.Y)) - { - k = i; - } - } - - return k; - } - - /// - /// Dispatches to the selected point-in-polygon implementation. - /// - /// Query point. - /// Closed ring. - /// Fill rule. - private static bool IsPointInPolygon(in Vertex p, List ring, IntersectionRule rule) - { - if (rule == IntersectionRule.EvenOdd) - { - return PointInPolygonEvenOdd(p, ring); - } - - return PointInPolygonNonZero(p, ring); - } - - /// - /// Even-odd point-in-polygon via ray casting. - /// - /// Query point. - /// Closed ring. - /// - /// Let a horizontal ray start at and extend to +∞ in X. - /// For each edge (a→b), count an intersection if the edge straddles the ray’s Y - /// and the ray’s X is strictly less than the edge’s X at that Y: - /// - /// intersects = ((b.Y > p.Y) != (a.Y > p.Y)) amp;& p.X < x_at_pY(a,b) - /// - /// Parity of the count determines interior. - /// Horizontal edges contribute zero because the straddle test excludes equal Y. - /// Using a half-open interval on Y prevents double-counting shared vertices. - /// - private static bool PointInPolygonEvenOdd(in Vertex p, List ring) - { - bool inside = false; - int n = ring.Count; - int j = n - 1; - - for (int i = 0; i < n; j = i, i++) - { - Vertex a = ring[j]; - Vertex b = ring[i]; - - bool straddles = (b.Y > p.Y) != (a.Y > p.Y); - - if (straddles) - { - double ySpan = a.Y - b.Y; - double xAtPY = (((a.X - b.X) * (p.Y - b.Y)) / (ySpan == 0d ? double.Epsilon : ySpan)) + b.X; - - if (p.X < xAtPY) - { - inside = !inside; - } - } - } - - return inside; - } - - /// - /// Non-zero winding point-in-polygon. - /// - /// Query point. - /// Closed ring. - /// - /// Scan all edges (a→b). - /// - If the edge crosses the scanline upward (a.Y ≤ p.Y && b.Y > p.Y) and - /// lies strictly to the left of the edge, increment the winding. - /// - If it crosses downward (a.Y > p.Y && b.Y ≤ p.Y) and - /// lies strictly to the right, decrement the winding. - /// The point is inside iff the winding number is non-zero. - /// Left/right is decided by the sign of the cross product of vectors a→b and a→p. - /// - private static bool PointInPolygonNonZero(in Vertex p, List ring) - { - int winding = 0; - int n = ring.Count; - - for (int i = 0, j = n - 1; i < n; j = i, i++) - { - Vertex a = ring[j]; - Vertex b = ring[i]; - - if (a.Y <= p.Y) - { - if (b.Y > p.Y && IsLeft(a, b, p)) - { - winding++; - } - } - else if (b.Y <= p.Y && !IsLeft(a, b, p)) - { - winding--; - } - } - - return winding != 0; - } - - /// - /// Returns true if is strictly left of the directed edge a→b. - /// - /// Edge start. - /// Edge end. - /// Query point. - /// - /// Tests the sign of the 2D cross product: - /// - /// cross = (b - a) × (p - a) = (b.X - a.X)*(p.Y - a.Y) - (b.Y - a.Y)*(p.X - a.X) - /// - /// Left if cross > 0, right if cross < 0, collinear if cross == 0. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsLeft(Vertex a, Vertex b, Vertex p) => Vertex.Cross(b - a, p - a) > 0d; -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs index 9d5ac054..14ea9c70 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Drawing.Shapes.Helpers; +using SixLabors.ImageSharp.Drawing.Processing; #pragma warning disable SA1201 // Elements should appear in the correct order namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; @@ -28,7 +28,6 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// internal sealed class PolygonStroker - { private ArrayBuilder outVertices = new(1); private ArrayBuilder srcVertices = new(16); @@ -42,18 +41,56 @@ internal sealed class PolygonStroker 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.BevelJoin; - - public LineCap LineCap { get; set; } = LineCap.Butt; - - public InnerJoin InnerJoin { get; set; } = InnerJoin.InnerMiter; + /// + /// Initializes a new instance of the class with the specified stroke options. + /// + /// + /// The stroke options to use for configuring line joins, caps, miter limits, and approximation scale. + /// Cannot be . + /// + public PolygonStroker(StrokeOptions options) + { + this.LineJoin = options.LineJoin; + this.InnerJoin = options.InnerJoin; + this.LineCap = options.LineCap; + this.MiterLimit = options.MiterLimit; + this.InnerMiterLimit = options.InnerMiterLimit; + this.ApproximationScale = options.ApproximationScale; + } + /// + /// Gets the miter limit used to clamp outer miter joins. + /// + public double MiterLimit { get; } + + /// + /// Gets the inner miter limit used to clamp joins on acute interior angles. + /// + public double InnerMiterLimit { get; } + + /// + /// Gets the arc approximation scale used for round joins and caps. + /// + public double ApproximationScale { get; } + + /// + /// Gets the outer line join style used for stroking corners. + /// + public LineJoin LineJoin { get; } + + /// + /// Gets the line cap style used for open path ends. + /// + public LineCap LineCap { get; } + + /// + /// Gets the join style used for sharp interior angles. + /// + public InnerJoin InnerJoin { get; } + + /// + /// Gets or sets the stroke width in the caller's coordinate space. + /// public double Width { get => this.strokeWidth * 2.0; @@ -75,6 +112,12 @@ public double Width } } + /// + /// Strokes the provided polyline or polygon and returns the outline vertices. + /// + /// The input points to stroke. + /// Whether the input is a closed ring. + /// The stroked outline as a closed point array. public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) { if (linePoints.Length < 2) @@ -95,6 +138,10 @@ public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) return [.. results]; } + /// + /// Adds a sequence of line segments to the current stroker state. + /// + /// The input points to add as line segments. public void AddLinePath(ReadOnlySpan linePoints) { for (int i = 0; i < linePoints.Length; i++) @@ -104,6 +151,9 @@ public void AddLinePath(ReadOnlySpan linePoints) } } + /// + /// Marks the current path as closed before finishing the outline. + /// public void ClosePath() { // Mark the current src path as closed; no geometry is pushed here. @@ -111,6 +161,10 @@ public void ClosePath() this.status = Status.Initial; } + /// + /// Finalizes stroking and appends output points to the provided list. + /// + /// The list that receives the stroked outline vertices. public void FinishPath(List results) { PointF currentPoint = new(0, 0); @@ -137,6 +191,9 @@ public void FinishPath(List results) } } + /// + /// Resets the stroker state so it can be reused for a new path. + /// public void Reset() { this.srcVertices.Clear(); @@ -483,14 +540,14 @@ private void CalcMiter( switch (lj) { - case LineJoin.MiterJoinRevert: + case LineJoin.MiterRevert: this.AddPoint(v1.X + dx1, v1.Y - dy1); this.AddPoint(v1.X + dx2, v1.Y - dy2); break; - case LineJoin.MiterJoinRound: + case LineJoin.MiterRound: this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); break; @@ -629,19 +686,19 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi break; - case InnerJoin.InnerMiter: - this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterJoinRevert, limit, 0); + case InnerJoin.Miter: + this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0); break; - case InnerJoin.InnerJag: - case InnerJoin.InnerRound: + case InnerJoin.Jag: + case InnerJoin.Round: 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); + this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0); } - else if (this.InnerJoin == InnerJoin.InnerJag) + else if (this.InnerJoin == InnerJoin.Jag) { this.AddPoint(v1.X + dx1, v1.Y - dy1); this.AddPoint(v1.X, v1.Y); @@ -665,7 +722,7 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi 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 (this.LineJoin is LineJoin.Round or LineJoin.Bevel && 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)) { @@ -681,14 +738,14 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi switch (this.LineJoin) { - case LineJoin.MiterJoin: - case LineJoin.MiterJoinRevert: - case LineJoin.MiterJoinRound: + case LineJoin.Miter: + case LineJoin.MiterRevert: + case LineJoin.MiterRound: this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, this.LineJoin, this.MiterLimit, dbevel); break; - case LineJoin.RoundJoin: + case LineJoin.Round: this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); break; diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs index 917ccb96..f04f9a04 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs @@ -2,8 +2,8 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Utilities; -using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; @@ -17,15 +17,8 @@ internal sealed class StrokedShapeGenerator /// /// Initializes a new instance of the class. /// - /// meter limit - /// arc tolerance - public StrokedShapeGenerator(float meterLimit = 2F, float arcTolerance = .25F) - { - // TODO: We need to consume the joint type properties here. - // to do so we need to replace the existing ones with our new enums and update - // the overloads and pens. - this.polygonStroker = new PolygonStroker(); - } + public StrokedShapeGenerator(StrokeOptions options) + => this.polygonStroker = new PolygonStroker(options); /// /// Strokes a collection of dashed polyline spans and returns a merged outline. @@ -43,24 +36,17 @@ public StrokedShapeGenerator(float meterLimit = 2F, float arcTolerance = .25F) /// /// /// This method streams each dashed span through the internal stroker as an open polyline, - /// producing closed stroke rings. To clean self overlaps, the rings are split between - /// subject and clip sets and a is performed. - /// The split ensures at least two operands so the union resolves overlaps. + /// producing closed stroke rings. To clean self overlaps, all rings are added as subject + /// paths and a is performed. /// The union uses to preserve winding density. /// public IPath[] GenerateStrokedShapes(List spans, float width) { - // PolygonClipper is not designed to clean up self-intersecting geometry within a single polygon. - // It operates strictly on two polygon operands (subject and clip) and only resolves overlaps - // between them. To force cleanup of dashed stroke overlaps, we alternate assigning each - // stroked segment to subject or clip, ensuring at least two operands exist so the union - // operation performs a true merge rather than a no-op on a single polygon. - // 1) Stroke each dashed span as open. this.polygonStroker.Width = width; List ringPoints = new(spans.Count); - List rings = new(spans.Count); + List rings = new(spans.Count); foreach (PointF[] span in spans) { if (span == null || span.Length < 2) @@ -89,37 +75,10 @@ public IPath[] GenerateStrokedShapes(List spans, float width) return count == 1 ? [rings[0]] : [.. rings]; } - // 2) Partition so the first and last are on different polygons - List subjectRings = new(count); - List clipRings = new(count); - - // First => subject - subjectRings.Add(rings[0]); - - // Middle by alternation using a single bool flag - bool assignToSubject = false; // start with clip for i=1 - for (int i = 1; i < count - 1; i++) - { - if (assignToSubject) - { - subjectRings.Add(rings[i]); - } - else - { - clipRings.Add(rings[i]); - } - - assignToSubject = !assignToSubject; - } - - // Last => opposite of first (i.e., clip) - clipRings.Add(rings[count - 1]); - - // 3) Union subject vs clip + // 2) Union all rings as subject paths ClippedShapeGenerator clipper = new(IntersectionRule.NonZero); - clipper.AddPaths(subjectRings, ClippingType.Subject); - clipper.AddPaths(clipRings, ClippingType.Clip); - return clipper.GenerateClippedShapes(BooleanOperation.Union); + clipper.AddPaths(rings, ClippingType.Subject); + return clipper.GenerateClippedShapes(BooleanOperation.Union, true); } /// @@ -135,16 +94,15 @@ public IPath[] GenerateStrokedShapes(List spans, float width) /// /// Each flattened simple path is streamed through the internal stroker as open or closed /// according to . The resulting stroke rings are split - /// between subject and clip sets and combined using . - /// This split is required because the Martinez based clipper resolves overlaps only between - /// two operands. Using preserves fill across overlaps - /// and prevents unintended holes in the merged outline. + /// paths and combined using . Using + /// preserves fill across overlaps and prevents + /// unintended holes in the merged outline. /// public IPath[] GenerateStrokedShapes(IPath path, float width) { // 1) Stroke the input path into closed rings List ringPoints = []; - List rings = []; + List rings = []; this.polygonStroker.Width = width; foreach (ISimplePath p in path.Flatten()) @@ -170,43 +128,12 @@ public IPath[] GenerateStrokedShapes(IPath path, float width) return count == 1 ? [rings[0]] : [.. rings]; } - // 2) Partition so the first and last are on different polygons - // PolygonClipper is not designed to clean up self-intersecting geometry within a single polygon. - // It operates strictly on two polygon operands (subject and clip) and only resolves overlaps - // between them. To force cleanup of overlaps, we alternate assigning each stroked ring to - // subject or clip, ensuring at least two operands exist so the union performs a true merge. - List subjectRings = new(count); - List clipRings = new(count); - - // First => subject - subjectRings.Add(rings[0]); - - // Middle by alternation using a single bool flag - bool assignToSubject = false; // start with clip for i=1 - for (int i = 1; i < count - 1; i++) - { - if (assignToSubject) - { - subjectRings.Add(rings[i]); - } - else - { - clipRings.Add(rings[i]); - } - - assignToSubject = !assignToSubject; - } - - // Last => opposite of first (i.e., clip) - clipRings.Add(rings[count - 1]); - - // 3) Union subject vs clip + // 2) Union all rings as subject paths ClippedShapeGenerator clipper = new(IntersectionRule.NonZero); - clipper.AddPaths(subjectRings, ClippingType.Subject); - clipper.AddPaths(clipRings, ClippingType.Clip); + clipper.AddPaths(rings, ClippingType.Subject); - // 4) Return the cleaned, merged outline - return clipper.GenerateClippedShapes(BooleanOperation.Union); + // 3) Return the cleaned, merged outline + return clipper.GenerateClippedShapes(BooleanOperation.Union, true); } /// diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs new file mode 100644 index 00000000..fd038b4a --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +[Flags] +internal enum VertexFlags +{ + None = 0, + OpenStart = 1, + OpenEnd = 2, + LocalMax = 4, + LocalMin = 8 +} diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs index 8734440e..2966e3f7 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs @@ -6,7 +6,6 @@ using SixLabors.Fonts; using SixLabors.Fonts.Rendering; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Text; @@ -221,7 +220,7 @@ void IGlyphRenderer.EndLayer() ShapeOptions options = new() { - ClippingOperation = BooleanOperation.Intersection, + BooleanOperation = BooleanOperation.Intersection, IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule) }; diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs index 375fb2d5..b2ba8752 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs @@ -99,7 +99,10 @@ public void DrawLines_EndCapRound(TestImageProvider provider, st where TPixel : unmanaged, IPixel { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) { EndCapStyle = EndCapStyle.Round }); + PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Round }, + }); DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } @@ -110,7 +113,10 @@ public void DrawLines_EndCapButt(TestImageProvider provider, str where TPixel : unmanaged, IPixel { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) { EndCapStyle = EndCapStyle.Butt }); + PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Butt }, + }); DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } @@ -121,7 +127,10 @@ public void DrawLines_EndCapSquare(TestImageProvider provider, s where TPixel : unmanaged, IPixel { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) { EndCapStyle = EndCapStyle.Square }); + PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Square }, + }); DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } @@ -132,7 +141,10 @@ public void DrawLines_JointStyleRound(TestImageProvider provider where TPixel : unmanaged, IPixel { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) { JointStyle = JointStyle.Round }); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round }, + }); DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } @@ -143,7 +155,10 @@ public void DrawLines_JointStyleSquare(TestImageProvider provide where TPixel : unmanaged, IPixel { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) { JointStyle = JointStyle.Square }); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Bevel }, + }); DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } @@ -154,7 +169,10 @@ public void DrawLines_JointStyleMiter(TestImageProvider provider where TPixel : unmanaged, IPixel { Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) { JointStyle = JointStyle.Miter }); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter }, + }); DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index f4f45bc9..b86078fc 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -6,7 +6,6 @@ using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; @@ -192,7 +191,7 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi // See http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/ClipType.htm for reference. foreach (BooleanOperation operation in (BooleanOperation[])Enum.GetValues(typeof(BooleanOperation))) { - ShapeOptions options = new() { ClippingOperation = operation }; + ShapeOptions options = new() { BooleanOperation = operation }; IPath shape = star.Clip(options, circle); provider.RunValidatingProcessorTest( diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs index ef268520..91d56671 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs @@ -121,8 +121,8 @@ public void JointAndEndCapStyle() Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); this.VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } [Fact] @@ -135,7 +135,7 @@ public void JointAndEndCapStyleDefaultOptions() Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); this.VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs index 6cdb5c25..5ab5ae86 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs @@ -117,8 +117,8 @@ public void JointAndEndCapStyle() Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); this.VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } [Fact] @@ -131,7 +131,7 @@ public void JointAndEndCapStyleDefaultOptions() Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); this.VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs index d57bf36d..8c283ed2 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs @@ -104,8 +104,8 @@ public void JointAndEndCapStyle() Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); Assert.Equal(this.path, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } [Fact] @@ -118,7 +118,7 @@ public void JointAndEndCapStyleDefaultOptions() Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); Assert.Equal(this.path, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs index df0bbf1f..cb104bbb 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs @@ -10,8 +10,6 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; public class DrawPathCollection : BaseImageOperationsExtensionTest { - private readonly GraphicsOptions nonDefault = new() { Antialias = false }; - private readonly Color color = Color.HotPink; private readonly SolidPen pen = Pens.Solid(Color.HotPink, 1); private readonly IPath path1 = new Path(new LinearLineSegment( [ @@ -162,8 +160,8 @@ public void JointAndEndCapStyle() { Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(this.pen.JointStyle, pPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, pPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap); }); Assert.Collection( @@ -182,8 +180,8 @@ public void JointAndEndCapStyleDefaultOptions() { Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(this.pen.JointStyle, pPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, pPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap); }); Assert.Collection( diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs index 0b6900cc..fbc3cbee 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs @@ -19,7 +19,7 @@ public class DrawPolygon : BaseImageOperationsExtensionTest new PointF(25, 10) ]; - private void VerifyPoints(PointF[] expectedPoints, IPath path) + private static void VerifyPoints(PointF[] expectedPoints, IPath path) { ISimplePath simplePath = Assert.Single(path.Flatten()); Assert.True(simplePath.IsClosed); @@ -34,7 +34,7 @@ public void Pen() DrawPathProcessor processor = this.Verify(); Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); Assert.Equal(this.pen, processor.Pen); } @@ -46,7 +46,7 @@ public void PenDefaultOptions() DrawPathProcessor processor = this.Verify(); Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); Assert.Equal(this.pen, processor.Pen); } @@ -58,7 +58,7 @@ public void BrushAndThickness() DrawPathProcessor processor = this.Verify(); Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); Assert.Equal(10, processorPen.StrokeWidth); @@ -72,7 +72,7 @@ public void BrushAndThicknessDefaultOptions() DrawPathProcessor processor = this.Verify(); Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); Assert.Equal(10, processorPen.StrokeWidth); @@ -86,7 +86,7 @@ public void ColorAndThickness() DrawPathProcessor processor = this.Verify(); Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); SolidPen processorPen = Assert.IsType(processor.Pen); Assert.Equal(Color.Red, brush.Color); @@ -101,7 +101,7 @@ public void ColorAndThicknessDefaultOptions() DrawPathProcessor processor = this.Verify(); Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); Assert.Equal(Color.Red, brush.Color); SolidPen processorPen = Assert.IsType(processor.Pen); @@ -116,10 +116,10 @@ public void JointAndEndCapStyle() DrawPathProcessor processor = this.Verify(); Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } [Fact] @@ -130,9 +130,9 @@ public void JointAndEndCapStyleDefaultOptions() DrawPathProcessor processor = this.Verify(); Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); + VerifyPoints(this.points, processor.Path); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs index b40b41c1..5e5ed330 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs @@ -112,7 +112,7 @@ public void JointAndEndCapStyle() Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); Assert.NotEqual(this.pen, processor.Pen); SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.JointStyle, processorPen.JointStyle); - Assert.Equal(this.pen.EndCapStyle, processorPen.EndCapStyle); + Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); + Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs index 631058a5..c77648fe 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs @@ -22,19 +22,19 @@ public void DrawPolygonMustDrawoutlineOnly(TestImageProvider pro x => x.DrawPolygon( color, scale, - new PointF[] { + [ new(5, 5), new(5, 150), new(190, 150), - }), + ]), new { scale }); } [Theory] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 3f)] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 1f)] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.3f)] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.7f)] + //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 3f)] + //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 1f)] + //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.3f)] + //[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.7f)] [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.003f)] public void DrawPolygonMustDrawoutlineOnly_Pattern(TestImageProvider provider, float scale) where TPixel : unmanaged, IPixel @@ -44,11 +44,11 @@ public void DrawPolygonMustDrawoutlineOnly_Pattern(TestImageProvider x.DrawPolygon( pen, - new PointF[] { - new(5, 5), - new(5, 150), - new(190, 150), - }), + [ + new(5, 5), + new(5, 150), + new(190, 150), + ]), new { scale }); } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs index 1853828e..766efb0e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs @@ -3,7 +3,6 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; -using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -28,7 +27,7 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance() { ShapeOptions option = new() { - ClippingOperation = BooleanOperation.Intersection, + BooleanOperation = BooleanOperation.Intersection, IntersectionRule = IntersectionRule.NonZero }; Configuration config = new(); @@ -37,18 +36,18 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance() context.SetShapeOptions(o => { - Assert.Equal(BooleanOperation.Intersection, o.ClippingOperation); // has original values + Assert.Equal(BooleanOperation.Intersection, o.BooleanOperation); // has original values Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule); - o.ClippingOperation = BooleanOperation.Xor; + o.BooleanOperation = BooleanOperation.Xor; o.IntersectionRule = IntersectionRule.EvenOdd; }); ShapeOptions returnedOption = context.GetShapeOptions(); - Assert.Equal(BooleanOperation.Xor, returnedOption.ClippingOperation); + Assert.Equal(BooleanOperation.Xor, returnedOption.BooleanOperation); Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule); - Assert.Equal(BooleanOperation.Intersection, option.ClippingOperation); // hasn't been mutated + Assert.Equal(BooleanOperation.Intersection, option.BooleanOperation); // hasn't been mutated Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule); } @@ -68,7 +67,7 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance() { ShapeOptions option = new() { - ClippingOperation = BooleanOperation.Intersection, + BooleanOperation = BooleanOperation.Intersection, IntersectionRule = IntersectionRule.NonZero }; Configuration config = new(); @@ -76,16 +75,16 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance() config.SetShapeOptions(o => { - Assert.Equal(BooleanOperation.Intersection, o.ClippingOperation); // has original values + Assert.Equal(BooleanOperation.Intersection, o.BooleanOperation); // has original values Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule); - o.ClippingOperation = BooleanOperation.Xor; + o.BooleanOperation = BooleanOperation.Xor; o.IntersectionRule = IntersectionRule.EvenOdd; }); ShapeOptions returnedOption = config.GetShapeOptions(); - Assert.Equal(BooleanOperation.Xor, returnedOption.ClippingOperation); + Assert.Equal(BooleanOperation.Xor, returnedOption.BooleanOperation); Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule); - Assert.Equal(BooleanOperation.Intersection, option.ClippingOperation); // hasn't been mutated + Assert.Equal(BooleanOperation.Intersection, option.BooleanOperation); // hasn't been mutated Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule); } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index 6a48e823..c8a84d34 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs @@ -4,7 +4,6 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; -using SixLabors.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.Tests.PolygonClipper; From 52312a1e5ae007cdf1921459c9211259ba937819 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Feb 2026 14:12:30 +1000 Subject: [PATCH 13/35] Clean PolygonClipperUtilities --- .../Shapes/PolygonGeometry/PolygonClipper.cs | 38 ++--- ...perUtils.cs => PolygonClipperUtilities.cs} | 152 ++++++------------ 2 files changed, 64 insertions(+), 126 deletions(-) rename src/ImageSharp.Drawing/Shapes/PolygonGeometry/{ClipperUtils.cs => PolygonClipperUtilities.cs} (54%) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs index cd4c4363..4cb04247 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs @@ -153,7 +153,7 @@ private void DisposeIntersectNodes() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddNewIntersectNode(Active ae1, Active ae2, float topY) { - if (!ClipperUtils.GetIntersectPt(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip)) + if (!PolygonClipperUtilities.GetLineIntersectPoint(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip)) { ip = new Vector2(ae1.CurX, topY); } @@ -168,20 +168,20 @@ private void AddNewIntersectNode(Active ae1, Active ae2, float topY) { if (absDx1 > absDx2) { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); + ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); } else { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); + ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); } } else if (absDx1 > 100) { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); + ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); } else if (absDx2 > 100) { - ip = ClipperUtils.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); + ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); } else { @@ -989,8 +989,8 @@ private void CleanCollinear(OutRec outrec) do { // NB if preserveCollinear == true, then only remove 180 deg. spikes - if ((ClipperUtils.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0) - && ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (ClipperUtils.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0))) + if ((PolygonClipperUtilities.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0) + && ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (PolygonClipperUtilities.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0))) { if (op2 == outrec.Pts) { @@ -1024,7 +1024,7 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) OutPt nextNextOp = splitOp.Next.Next; outrec.Pts = prevOp; - ClipperUtils.GetIntersectPoint( + PolygonClipperUtilities.GetIntersectPoint( prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip); float area1 = Area(prevOp); @@ -1085,7 +1085,7 @@ private void FixSelfIntersects(OutRec outrec) // triangles can't self-intersect while (op2.Prev != op2.Next.Next) { - if (ClipperUtils.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point)) + if (PolygonClipperUtilities.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point)) { this.DoSplitOp(outrec, op2); if (outrec.Pts == null) @@ -2453,7 +2453,7 @@ private static bool IsValidAelOrder(Active resident, Active newcomer) } // get the turning direction a1.top, a2.bot, a2.top - float d = ClipperUtils.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top); + float d = PolygonClipperUtilities.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top); if (d != 0) { return d < 0; @@ -2465,12 +2465,12 @@ private static bool IsValidAelOrder(Active resident, Active newcomer) // the direction they're about to turn if (!IsMaxima(resident) && (resident.Top.Y > newcomer.Top.Y)) { - return ClipperUtils.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0; + return PolygonClipperUtilities.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0; } if (!IsMaxima(newcomer) && (newcomer.Top.Y > resident.Top.Y)) { - return ClipperUtils.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0; + return PolygonClipperUtilities.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0; } float y = newcomer.Bot.Y; @@ -2487,13 +2487,13 @@ private static bool IsValidAelOrder(Active resident, Active newcomer) return newcomerIsLeft; } - if (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0) + if (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0) { return true; } // compare turning direction of the alternate bound - return (ClipperUtils.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft; + return (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -2933,7 +2933,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false) if (checkCurrX) { - if (ClipperUtils.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25) + if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25) { return; } @@ -2943,7 +2943,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false) return; } - if (ClipperUtils.CrossProduct(e.Top, pt, prev.Top) != 0) + if (PolygonClipperUtilities.CrossProduct(e.Top, pt, prev.Top) != 0) { return; } @@ -2988,7 +2988,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false) if (checkCurrX) { - if (ClipperUtils.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25) + if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25) { return; } @@ -2998,7 +2998,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false) return; } - if (ClipperUtils.CrossProduct(e.Top, pt, next.Top) != 0) + if (PolygonClipperUtilities.CrossProduct(e.Top, pt, next.Top) != 0) { return; } @@ -3383,7 +3383,7 @@ public PolyPathF AddChild(PathF p) [MethodImpl(MethodImplOptions.AggressiveInlining)] public float Area() { - float result = this.Polygon == null ? 0 : ClipperUtils.Area(this.Polygon); + float result = this.Polygon == null ? 0 : PolygonClipperUtilities.SignedArea(this.Polygon); for (int i = 0; i < this.items.Count; i++) { PolyPathF child = this.items[i]; diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs similarity index 54% rename from src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs index 6ed77da4..dae10d2d 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperUtils.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs @@ -6,18 +6,16 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; -internal static class ClipperUtils +internal static class PolygonClipperUtilities { - public const float DefaultArcTolerance = .25F; - public const float FloatingPointTolerance = 1e-05F; - public const float DefaultMinimumEdgeLength = .1F; - - // TODO: rename to Pow2? - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Sqr(float value) => value * value; - + /// + /// Computes the signed area of a path using the shoelace formula. + /// + /// + /// Positive values indicate clockwise orientation in screen space. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Area(PathF path) + public static float SignedArea(PathF path) { // https://en.wikipedia.org/wiki/Shoelace_formula float a = 0F; @@ -26,7 +24,8 @@ public static float Area(PathF path) return a; } - Vector2 prevPt = path[path.Count - 1]; + // Sum over edges (prev -> current). + Vector2 prevPt = path[^1]; for (int i = 0; i < path.Count; i++) { Vector2 pt = path[i]; @@ -37,87 +36,34 @@ public static float Area(PathF path) return a * .5F; } - public static PathF StripDuplicates(PathF path, bool isClosedPath) - { - int cnt = path.Count; - PathF result = new(cnt); - if (cnt == 0) - { - return result; - } - - PointF lastPt = path[0]; - result.Add(lastPt); - for (int i = 1; i < cnt; i++) - { - if (lastPt != path[i]) - { - lastPt = path[i]; - result.Add(lastPt); - } - } - - if (isClosedPath && lastPt == result[0]) - { - result.RemoveAt(result.Count - 1); - } - - return result; - } - - public static PathF Ellipse(Vector2 center, float radiusX, float radiusY = 0, int steps = 0) - { - if (radiusX <= 0) - { - return []; - } - - if (radiusY <= 0) - { - radiusY = radiusX; - } - - if (steps <= 2) - { - steps = (int)MathF.Ceiling(MathF.PI * MathF.Sqrt((radiusX + radiusY) * .5F)); - } - - float si = MathF.Sin(2 * MathF.PI / steps); - float co = MathF.Cos(2 * MathF.PI / steps); - float dx = co, dy = si; - PathF result = new(steps) { new Vector2(center.X + radiusX, center.Y) }; - Vector2 radiusXY = new(radiusX, radiusY); - for (int i = 1; i < steps; ++i) - { - result.Add(center + (radiusXY * new Vector2(dx, dy))); - float x = (dx * co) - (dy * si); - dy = (dy * co) + (dx * si); - dx = x; - } - - return result; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float DotProduct(Vector2 vec1, Vector2 vec2) => Vector2.Dot(vec1, vec2); + /// + /// Returns the dot product of the segments (pt1->pt2) and (pt2->pt3). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) + => Vector2.Dot(pt2 - pt1, pt3 - pt2); + + /// + /// Returns the 2D cross product magnitude of and . + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float CrossProduct(Vector2 vec1, Vector2 vec2) => (vec1.Y * vec2.X) - (vec2.Y * vec1.X); + /// + /// Returns the cross product of the segments (pt1->pt2) and (pt2->pt3). + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float CrossProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) => ((pt2.X - pt1.X) * (pt3.Y - pt2.Y)) - ((pt2.Y - pt1.Y) * (pt3.X - pt2.X)); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) - => Vector2.Dot(pt2 - pt1, pt3 - pt2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsAlmostZero(float value) - => MathF.Abs(value) <= FloatingPointTolerance; - + /// + /// Returns the squared perpendicular distance from a point to a line segment. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 line2) { @@ -128,9 +74,18 @@ public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 return 0; } - return Sqr(CrossProduct(cd, ab)) / DotProduct(cd, cd); + float cross = CrossProduct(cd, ab); + return (cross * cross) / DotProduct(cd, cd); } + /// + /// Returns true when two segments intersect. + /// + /// First endpoint of segment 1. + /// Second endpoint of segment 1. + /// First endpoint of segment 2. + /// Second endpoint of segment 2. + /// If true, allows shared endpoints; if false, requires a proper intersection. public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Vector2 seg2b, bool inclusive = false) { if (inclusive) @@ -158,26 +113,7 @@ public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Ve } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool GetIntersectPt(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) - { - Vector2 dxy1 = ln1b - ln1a; - Vector2 dxy2 = ln2b - ln2a; - float cp = CrossProduct(dxy1, dxy2); - if (cp == 0F) - { - ip = default; - return false; - } - - float qx = CrossProduct(ln1a, dxy1); - float qy = CrossProduct(ln2a, dxy2); - - ip = ((dxy1 * qy) - (dxy2 * qx)) / cp; - return ip != new Vector2(float.MaxValue); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) + public static bool GetLineIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) { Vector2 dxy1 = ln1b - ln1a; Vector2 dxy2 = ln2b - ln2a; @@ -189,6 +125,8 @@ public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, V } float t = (((ln1a.X - ln2a.X) * dxy2.Y) - ((ln1a.Y - ln2a.Y) * dxy2.X)) / det; + + // Clamp intersection to the segment endpoints. if (t <= 0F) { ip = ln1a; @@ -205,6 +143,12 @@ public static bool GetIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, V return true; } + /// + /// Returns the closest point on a segment to an external point. + /// + /// The point to project onto the segment. + /// First endpoint of the segment. + /// Second endpoint of the segment. public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 seg2) { if (seg1 == seg2) @@ -227,10 +171,4 @@ public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 return seg1 + (dxy * q); } - - public static PathF ReversePath(PathF path) - { - path.Reverse(); - return path; - } } From 49aaf2583d893993143db50ed8793f259d1b70ed Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Feb 2026 14:16:41 +1000 Subject: [PATCH 14/35] Fix build, more cleanup --- .../Shapes/PolygonGeometry/Clipper.cs | 111 ------------------ .../Shapes/PolygonGeometry/ClippingType.cs | 2 +- .../Shapes/PolygonGeometry/JoinWith.cs | 32 +++-- .../Shapes/PolygonGeometry/PolygonClipper.cs | 2 +- 4 files changed, 16 insertions(+), 131 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs deleted file mode 100644 index 71b26112..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/Clipper.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// Library to clip polygons. -/// -internal class Clipper -{ - private readonly PolygonClipper polygonClipper; - - /// - /// Initializes a new instance of the class. - /// - public Clipper() - => this.polygonClipper = new PolygonClipper() { PreserveCollinear = true }; - - /// - /// Generates the clipped shapes from the previously provided paths. - /// - /// The clipping operation. - /// The intersection rule. - /// The . - public IPath[] GenerateClippedShapes(BooleanOperation operation, IntersectionRule rule) - { - PathsF closedPaths = []; - PathsF openPaths = []; - - ClipperFillRule fillRule = rule == IntersectionRule.EvenOdd ? ClipperFillRule.EvenOdd : ClipperFillRule.NonZero; - this.polygonClipper.Execute(operation, fillRule, closedPaths, openPaths); - - IPath[] shapes = new IPath[closedPaths.Count + openPaths.Count]; - - int index = 0; - for (int i = 0; i < closedPaths.Count; i++) - { - PathF path = closedPaths[i]; - PointF[] points = new PointF[path.Count]; - - for (int j = 0; j < path.Count; j++) - { - points[j] = path[j]; - } - - shapes[index++] = new Polygon(points); - } - - for (int i = 0; i < openPaths.Count; i++) - { - PathF path = openPaths[i]; - PointF[] points = new PointF[path.Count]; - - for (int j = 0; j < path.Count; j++) - { - points[j] = path[j]; - } - - shapes[index++] = new Polygon(points); - } - - return shapes; - } - - /// - /// Adds the shapes. - /// - /// The paths. - /// The clipping type. - public void AddPaths(IEnumerable paths, ClippingType clippingType) - { - Guard.NotNull(paths, nameof(paths)); - - foreach (IPath p in paths) - { - this.AddPath(p, clippingType); - } - } - - /// - /// Adds the path. - /// - /// The path. - /// The clipping type. - public void AddPath(IPath path, ClippingType clippingType) - { - Guard.NotNull(path, nameof(path)); - - foreach (ISimplePath p in path.Flatten()) - { - this.AddPath(p, clippingType); - } - } - - /// - /// Adds the path. - /// - /// The path. - /// Type of the poly. - internal void AddPath(ISimplePath path, ClippingType clippingType) - { - ReadOnlySpan vectors = path.Points.Span; - PathF points = new(vectors.Length); - for (int i = 0; i < vectors.Length; i++) - { - points.Add(vectors[i]); - } - - this.polygonClipper.AddPath(points, clippingType, !path.IsClosed); - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs index f2e252f2..2ac4ef90 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Defines the polygon clipping type. /// -public enum ClippingType +internal enum ClippingType { /// /// Represents a shape to act as a subject which will be clipped or merged. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs index 83ca61ad..ee3272a8 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs @@ -3,27 +3,23 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; +/// +/// Specifies how a vertex should be joined with adjacent paths during polygon operations. +/// internal enum JoinWith { + /// + /// No joining operation. + /// None, - Left, - Right -} -internal enum HorzPosition -{ - Bottom, - Middle, - Top -} + /// + /// Join with the left adjacent path. + /// + Left, -// Vertex: a pre-clipping data structure. It is used to separate polygons -// into ascending and descending 'bounds' (or sides) that start at local -// minima and ascend to a local maxima, before descending again. -[Flags] -internal enum PointInPolygonResult -{ - IsOn = 0, - IsInside = 1, - IsOutside = 2 + /// + /// Join with the right adjacent path. + /// + Right } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs index 4cb04247..e9101592 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs @@ -1024,7 +1024,7 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) OutPt nextNextOp = splitOp.Next.Next; outrec.Pts = prevOp; - PolygonClipperUtilities.GetIntersectPoint( + _ = PolygonClipperUtilities.GetLineIntersectPoint( prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip); float area1 = Area(prevOp); From 10066315aa1e41cb4a68a48017c213154a74a436 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Feb 2026 14:55:07 +1000 Subject: [PATCH 15/35] Document PolygonClipper --- .../Shapes/PolygonGeometry/PolygonClipper.cs | 503 ++++++++++++++++-- 1 file changed, 464 insertions(+), 39 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs index e9101592..f8a9fea8 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs @@ -14,24 +14,32 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// Ported from and originally licensed /// under /// +/// +/// This class implements the Vatti clipping algorithm using a scanline sweep approach. +/// It processes polygon edges by sweeping a horizontal line from bottom to top, +/// maintaining an active edge list (AEL) of edges that intersect the current scanline. +/// internal sealed class PolygonClipper { private BooleanOperation clipType; private ClipperFillRule fillRule; - private Active actives; - private Active flaggedHorizontal; - private readonly List minimaList; - private readonly List intersectList; - private readonly List vertexList; - private readonly List outrecList; - private readonly List scanlineList; - private readonly List horzSegList; - private readonly List horzJoinList; - private int currentLocMin; - private float currentBotY; - private bool isSortedMinimaList; - private bool hasOpenPaths; - + private Active actives; // Head of the active edge list + private Active flaggedHorizontal; // Linked list of horizontal edges awaiting processing + private readonly List minimaList; // Local minima sorted by Y coordinate + private readonly List intersectList; // Intersections at current scanbeam + private readonly List vertexList; // All vertices from input paths + private readonly List outrecList; // Output polygon records + private readonly List scanlineList; // Y coordinates requiring processing + private readonly List horzSegList; // Horizontal segments for joining + private readonly List horzJoinList; // Horizontal joins to process + private int currentLocMin; // Index of current local minimum being processed + private float currentBotY; // Y coordinate of current scanbeam bottom + private bool isSortedMinimaList; // Whether minimaList has been sorted + private bool hasOpenPaths; // Whether any input paths are open (not closed) + + /// + /// Initializes a new instance of the class. + /// public PolygonClipper() { this.minimaList = []; @@ -44,13 +52,31 @@ public PolygonClipper() this.PreserveCollinear = true; } + /// + /// Gets or sets a value indicating whether collinear vertices should be preserved in the output. + /// When true, only 180-degree spikes are removed. When false, all collinear vertices are removed. + /// public bool PreserveCollinear { get; set; } + /// + /// Gets or sets a value indicating whether the output polygon orientation should be reversed. + /// public bool ReverseSolution { get; set; } + /// + /// Adds subject paths to the clipping operation. + /// Subject paths are the primary polygons being clipped. + /// + /// The subject paths to add. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddSubject(PathsF paths) => this.AddPaths(paths, ClippingType.Subject); + /// + /// Adds a single path to the clipping operation. + /// + /// The path to add. + /// Whether this is a subject or clip path. + /// Whether the path is open (polyline) or closed (polygon). [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddPath(PathF path, ClippingType polytype, bool isOpen = false) { @@ -58,6 +84,12 @@ public void AddPath(PathF path, ClippingType polytype, bool isOpen = false) this.AddPaths(tmp, polytype, isOpen); } + /// + /// Adds multiple paths to the clipping operation. + /// + /// The paths to add. + /// Whether these are subject or clip paths. + /// Whether the paths are open (polylines) or closed (polygons). [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false) { @@ -70,10 +102,24 @@ public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false) this.AddPathsToVertexList(paths, polytype, isOpen); } + /// + /// Executes the clipping operation and returns only closed paths. + /// + /// The boolean operation to perform. + /// The fill rule to use for polygon interiors. + /// Output collection for closed solution paths. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed) => this.Execute(clipType, fillRule, solutionClosed, []); + /// + /// Executes the clipping operation and returns both closed and open paths. + /// + /// The boolean operation to perform (union, intersection, difference, xor). + /// The fill rule to determine polygon interiors (even-odd, non-zero, positive, negative). + /// Output collection for closed solution paths (polygons). + /// Output collection for open solution paths (polylines). + /// Thrown when an error occurs during clipping. public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed, PathsF solutionOpen) { solutionClosed.Clear(); @@ -94,11 +140,19 @@ public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF } } + /// + /// Executes the core clipping algorithm using the Vatti scanbeam sweep. + /// Processes all edges from bottom to top, handling intersections and building output polygons. + /// + /// The boolean operation type. + /// The fill rule for determining polygon interiors. private void ExecuteInternal(BooleanOperation ct, ClipperFillRule fillRule) { this.fillRule = fillRule; this.clipType = ct; this.Reset(); + + // Get the first scanline Y coordinate if (!this.PopScanline(out float y)) { return; @@ -106,36 +160,53 @@ private void ExecuteInternal(BooleanOperation ct, ClipperFillRule fillRule) while (true) { + // Add local minima edges that start at current Y this.InsertLocalMinimaIntoAEL(y); + + // Process all horizontal edges at this Y Active ae; while (this.PopHorz(out ae)) { this.DoHorizontal(ae); } + // Convert horizontal segments to joins for later processing if (this.horzSegList.Count > 0) { this.ConvertHorzSegsToJoins(); this.horzSegList.Clear(); } - this.currentBotY = y; // bottom of scanbeam + this.currentBotY = y; // bottom of current scanbeam + + // Get next scanline; break if no more if (!this.PopScanline(out y)) { - break; // y new top of scanbeam + break; } + // Process intersections between current and next scanline this.DoIntersections(y); + + // Update edges at top of scanbeam this.DoTopOfScanbeam(y); + + // Process any horizontal edges that emerged while (this.PopHorz(out ae)) { this.DoHorizontal(ae!); } } + // Complete horizontal joins this.ProcessHorzJoins(); } + /// + /// Processes edge intersections at the top of the current scanbeam. + /// Builds intersection list, processes intersections in order, then cleans up. + /// + /// The Y coordinate of the top of the scanbeam. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DoIntersections(float topY) { @@ -146,23 +217,37 @@ private void DoIntersections(float topY) } } + /// + /// Clears the intersection node list. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DisposeIntersectNodes() => this.intersectList.Clear(); + /// + /// Adds a new intersection node for two edges at the specified Y coordinate. + /// Calculates the exact intersection point, adjusting for numerical precision when needed. + /// + /// First edge. + /// Second edge. + /// Top Y coordinate of the scanbeam. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddNewIntersectNode(Active ae1, Active ae2, float topY) { + // Calculate line intersection point if (!PolygonClipperUtilities.GetLineIntersectPoint(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip)) { + // Lines are parallel; use current X position ip = new Vector2(ae1.CurX, topY); } + // Adjust intersection point if it's outside the scanbeam bounds if (ip.Y > this.currentBotY || ip.Y < topY) { float absDx1 = MathF.Abs(ae1.Dx); float absDx2 = MathF.Abs(ae2.Dx); + // For very steep edges, project the point onto the edge // TODO: Check threshold here once we remove upscaling. if (absDx1 > 100 && absDx2 > 100) { @@ -185,6 +270,7 @@ private void AddNewIntersectNode(Active ae1, Active ae2, float topY) } else { + // Clamp Y to scanbeam bounds if (ip.Y < topY) { ip.Y = topY; @@ -194,6 +280,7 @@ private void AddNewIntersectNode(Active ae1, Active ae2, float topY) ip.Y = this.currentBotY; } + // Use the less steep edge to determine X if (absDx1 < absDx2) { ip.X = TopX(ae1, ip.Y); @@ -209,6 +296,13 @@ private void AddNewIntersectNode(Active ae1, Active ae2, float topY) this.intersectList.Add(node); } + /// + /// Sets the heading direction for a horizontal segment based on two output points. + /// + /// The horizontal segment to configure. + /// Previous output point. + /// Next output point. + /// True if the segment has a valid direction; false if the points have the same X coordinate. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt opN) { @@ -233,6 +327,11 @@ private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt op return true; } + /// + /// Updates a horizontal segment by extending it to include all consecutive horizontal output points. + /// + /// The horizontal segment to update. + /// True if the segment was successfully updated; false otherwise. private static bool UpdateHorzSegment(HorzSegment hs) { OutPt op = hs.LeftOp; @@ -240,6 +339,8 @@ private static bool UpdateHorzSegment(HorzSegment hs) bool outrecHasEdges = outrec.FrontEdge != null; float curr_y = op.Point.Y; OutPt opP = op, opN = op; + + // Extend the segment backwards and forwards along the horizontal line if (outrecHasEdges) { OutPt opA = outrec.Pts!, opZ = opA.Next; @@ -274,12 +375,18 @@ private static bool UpdateHorzSegment(HorzSegment hs) } else { - hs.RightOp = null; // (for sorting) + hs.RightOp = null; // Mark as invalid for sorting } return result; } + /// + /// Duplicates an output point, inserting it either after or before the original. + /// + /// The output point to duplicate. + /// If true, insert after op; otherwise insert before. + /// The newly created output point. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static OutPt DuplicateOp(OutPt op, bool insert_after) { @@ -302,9 +409,15 @@ private static OutPt DuplicateOp(OutPt op, bool insert_after) return result; } + /// + /// Converts horizontal segments into join operations. + /// Finds overlapping horizontal segments and creates joins between them. + /// private void ConvertHorzSegsToJoins() { int k = 0; + + // Update all segments and count valid ones foreach (HorzSegment hs in this.horzSegList) { if (UpdateHorzSegment(hs)) @@ -315,19 +428,23 @@ private void ConvertHorzSegsToJoins() if (k < 2) { - return; + return; // Need at least 2 segments to join } + // Sort segments by left X coordinate this.horzSegList.Sort(default(HorzSegSorter)); + // Find overlapping segments and create joins for (int i = 0; i < k - 1; i++) { HorzSegment hs1 = this.horzSegList[i]; - // for each HorzSegment, find others that overlap + // Check each subsequent segment for overlap for (int j = i + 1; j < k; j++) { HorzSegment hs2 = this.horzSegList[j]; + + // Skip if no overlap or same direction if ((hs2.LeftOp.Point.X >= hs1.RightOp.Point.X) || (hs2.LeftToRight == hs1.LeftToRight) || (hs2.RightOp.Point.X <= hs1.LeftOp.Point.X)) @@ -336,6 +453,8 @@ private void ConvertHorzSegsToJoins() } float curr_y = hs1.LeftOp.Point.Y; + + // Adjust segment endpoints to find join points if (hs1.LeftToRight) { while (hs1.LeftOp.Next.Point.Y == curr_y && @@ -374,6 +493,9 @@ private void ConvertHorzSegsToJoins() } } + /// + /// Clears the solution data while preserving input paths. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ClearSolutionOnly() { @@ -389,6 +511,13 @@ private void ClearSolutionOnly() this.horzJoinList.Clear(); } + /// + /// Builds output paths from the output record list. + /// Processes each output record, cleaning collinear points and building the final paths. + /// + /// Collection to receive closed paths (polygons). + /// Collection to receive open paths (polylines). + /// True if paths were successfully built. private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) { solutionClosed.Clear(); @@ -398,7 +527,7 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) int i = 0; - // _outrecList.Count is not static here because + // Note: outrecList.Count is not static here because // CleanCollinear can indirectly add additional OutRec while (i < this.outrecList.Count) { @@ -418,9 +547,10 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) } else { + // Clean collinear points from closed paths this.CleanCollinear(outrec); - // closed paths should always return a Positive orientation + // Closed paths should always return a positive orientation // except when ReverseSolution == true if (BuildPath(outrec.Pts, this.ReverseSolution, false, path)) { @@ -432,8 +562,17 @@ private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) return true; } + /// + /// Builds a path from an output point list. + /// + /// Starting output point. + /// Whether to traverse the list in reverse. + /// Whether this is an open path. + /// The path to populate. + /// True if a valid path was created. private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path) { + // Validate minimum path requirements if (op == null || op.Next == op || (!isOpen && op.Next == op.Prev)) { return false; @@ -443,6 +582,8 @@ private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path) Vector2 lastPt; OutPt op2; + + // Set starting point and direction if (reverse) { lastPt = op.Point; @@ -457,6 +598,7 @@ private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path) path.Add(lastPt); + // Traverse the output point list, adding unique points while (op2 != op) { if (op2.Point != lastPt) @@ -475,6 +617,7 @@ private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path) } } + // Filter out very small triangles return path.Count != 3 || !IsVerySmallTriangle(op2); } @@ -3117,18 +3260,42 @@ private void Split(Active e, Vector2 currPt) private static bool IsFront(Active ae) => ae == ae.Outrec.FrontEdge; + /// + /// Comparer for sorting local minima by Y coordinate (descending). + /// private struct LocMinSorter : IComparer { public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2) => locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y); } + /// + /// Represents a local minimum in a polygon path. + /// A local minimum is a vertex where the path changes from descending to ascending. + /// private readonly struct LocalMinima { + /// + /// Gets the vertex at the local minimum. + /// public readonly Vertex Vertex; + + /// + /// Gets the polygon type (subject or clip). + /// public readonly ClippingType Polytype; + + /// + /// Gets a value indicating whether this is an open path (polyline). + /// public readonly bool IsOpen; + /// + /// Initializes a new instance of the struct. + /// + /// The vertex at the local minimum. + /// The polygon type. + /// Whether this is an open path. public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false) { this.Vertex = vertex; @@ -3137,7 +3304,7 @@ public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false) } public static bool operator ==(LocalMinima lm1, LocalMinima lm2) - + // Use reference equality for vertex comparison // TODO: Check this. Why ref equals. => ReferenceEquals(lm1.Vertex, lm2.Vertex); @@ -3151,15 +3318,34 @@ public override int GetHashCode() => this.Vertex.GetHashCode(); } - // IntersectNode: a structure representing 2 intersecting edges. - // Intersections must be sorted so they are processed from the largest - // Y coordinates to the smallest while keeping edges adjacent. + /// + /// Represents an intersection between two active edges. + /// Intersections must be sorted so they are processed from the largest + /// Y coordinates to the smallest while keeping edges adjacent. + /// private readonly struct IntersectNode { + /// + /// Gets the intersection point. + /// public readonly Vector2 Point; + + /// + /// Gets the first intersecting edge. + /// public readonly Active Edge1; + + /// + /// Gets the second intersecting edge. + /// public readonly Active Edge2; + /// + /// Initializes a new instance of the struct. + /// + /// The intersection point. + /// The first intersecting edge. + /// The second intersecting edge. public IntersectNode(Vector2 pt, Active edge1, Active edge2) { this.Point = pt; @@ -3168,6 +3354,9 @@ public IntersectNode(Vector2 pt, Active edge1, Active edge2) } } + /// + /// Comparer for sorting horizontal segments by left X coordinate. + /// private struct HorzSegSorter : IComparer { public readonly int Compare(HorzSegment hs1, HorzSegment hs2) @@ -3192,6 +3381,9 @@ public readonly int Compare(HorzSegment hs1, HorzSegment hs2) } } + /// + /// Comparer for sorting intersection nodes by Y coordinate (descending), then X coordinate. + /// private struct IntersectListSort : IComparer { public readonly int Compare(IntersectNode a, IntersectNode b) @@ -3210,8 +3402,16 @@ public readonly int Compare(IntersectNode a, IntersectNode b) } } + /// + /// Represents a horizontal segment in the output polygon. + /// Used to identify and join horizontal edges. + /// private class HorzSegment { + /// + /// Initializes a new instance of the class. + /// + /// The starting output point. public HorzSegment(OutPt op) { this.LeftOp = op; @@ -3219,29 +3419,60 @@ public HorzSegment(OutPt op) this.LeftToRight = true; } + /// + /// Gets or sets the left output point. + /// public OutPt LeftOp { get; set; } + /// + /// Gets or sets the right output point. + /// public OutPt RightOp { get; set; } + /// + /// Gets or sets a value indicating whether the segment is oriented left-to-right. + /// public bool LeftToRight { get; set; } } + /// + /// Represents a horizontal join operation between two output points. + /// private class HorzJoin { + /// + /// Initializes a new instance of the class. + /// + /// Left-to-right output point. + /// Right-to-left output point. public HorzJoin(OutPt ltor, OutPt rtol) { this.Op1 = ltor; this.Op2 = rtol; } + /// + /// Gets the first output point in the join. + /// public OutPt Op1 { get; } + /// + /// Gets the second output point in the join. + /// public OutPt Op2 { get; } } - // OutPt: vertex data structure for clipping solutions + /// + /// Output point: represents a vertex in a clipping solution polygon. + /// Forms a circular doubly-linked list of vertices. + /// private class OutPt { + /// + /// Initializes a new instance of the class. + /// + /// The point coordinates. + /// The output record this point belongs to. public OutPt(Vector2 pt, OutRec outrec) { this.Point = pt; @@ -3251,43 +3482,101 @@ public OutPt(Vector2 pt, OutRec outrec) this.HorizSegment = null; } + /// + /// Gets the point coordinates. + /// public Vector2 Point { get; } + /// + /// Gets or sets the next output point in the circular list. + /// public OutPt Next { get; set; } + /// + /// Gets or sets the previous output point in the circular list. + /// public OutPt Prev { get; set; } + /// + /// Gets or sets the output record this point belongs to. + /// public OutRec OutRec { get; set; } + /// + /// Gets or sets the horizontal segment this point is part of (if any). + /// public HorzSegment HorizSegment { get; set; } } - // OutRec: path data structure for clipping solutions + /// + /// Output record: represents a complete output polygon path. + /// Contains a circular doubly-linked list of output points. + /// private class OutRec { + /// + /// Gets or sets the index of this output record in the output list. + /// public int Idx { get; set; } + /// + /// Gets or sets the parent output record (for holes). + /// public OutRec Owner { get; set; } + /// + /// Gets or sets the front (ascending) edge of the output polygon. + /// public Active FrontEdge { get; set; } + /// + /// Gets or sets the back (descending) edge of the output polygon. + /// public Active BackEdge { get; set; } + /// + /// Gets or sets the starting point in the circular output point list. + /// public OutPt Pts { get; set; } + /// + /// Gets or sets the polytree path (for hierarchical output). + /// public PolyPathF PolyPath { get; set; } + /// + /// Gets or sets the bounding rectangle. + /// public BoundsF Bounds { get; set; } + /// + /// Gets or sets the final output path. + /// public PathF Path { get; set; } = []; + /// + /// Gets or sets a value indicating whether this is an open path. + /// public bool IsOpen { get; set; } + /// + /// Gets or sets the list of split indices (for self-intersecting polygons). + /// public List Splits { get; set; } } + /// + /// Represents a vertex in an input polygon path. + /// Forms a circular doubly-linked list of vertices. + /// private class Vertex { + /// + /// Initializes a new instance of the class. + /// + /// The point coordinates. + /// Vertex flags (local min/max, open start/end). + /// The previous vertex in the list. public Vertex(Vector2 pt, VertexFlags flags, Vertex prev) { this.Point = pt; @@ -3296,78 +3585,172 @@ public Vertex(Vector2 pt, VertexFlags flags, Vertex prev) this.Prev = prev; } + /// + /// Gets the point coordinates. + /// public Vector2 Point { get; } + /// + /// Gets or sets the next vertex in the circular list. + /// public Vertex Next { get; set; } + /// + /// Gets or sets the previous vertex in the circular list. + /// public Vertex Prev { get; set; } + /// + /// Gets or sets the vertex flags indicating properties like local minima/maxima. + /// public VertexFlags Flags { get; set; } } + /// + /// Active edge: represents an edge currently intersecting the scanline. + /// Stored in the Active Edge List (AEL) during scanline processing. + /// private class Active { + /// + /// Gets or sets the bottom point of the edge. + /// public Vector2 Bot { get; set; } + /// + /// Gets or sets the top point of the edge. + /// public Vector2 Top { get; set; } - public float CurX { get; set; } // current (updated at every new scanline) + /// + /// Gets or sets the current X coordinate at the scanline (updated at every scanline). + /// + public float CurX { get; set; } + /// + /// Gets or sets the edge's reciprocal slope (dx/dy). + /// public float Dx { get; set; } - public int WindDx { get; set; } // 1 or -1 depending on winding direction + /// + /// Gets or sets the winding direction (1 for ascending, -1 for descending). + /// + public int WindDx { get; set; } + /// + /// Gets or sets the winding count for this edge's polygon type. + /// public int WindCount { get; set; } - public int WindCount2 { get; set; } // winding count of the opposite polytype + /// + /// Gets or sets the winding count for the opposite polygon type. + /// + public int WindCount2 { get; set; } + /// + /// Gets or sets the output record this edge contributes to. + /// public OutRec Outrec { get; set; } - // AEL: 'active edge list' (Vatti's AET - active edge table) - // a linked list of all edges (from left to right) that are present - // (or 'active') within the current scanbeam (a horizontal 'beam' that - // sweeps from bottom to top over the paths in the clipping operation). + /// + /// Gets or sets the previous edge in the Active Edge List. + /// The AEL is a doubly-linked list of all edges intersecting the current scanbeam, + /// ordered from left to right. + /// public Active PrevInAEL { get; set; } + /// + /// Gets or sets the next edge in the Active Edge List. + /// public Active NextInAEL { get; set; } - // SEL: 'sorted edge list' (Vatti's ST - sorted table) - // linked list used when sorting edges into their new positions at the - // top of scanbeams, but also (re)used to process horizontals. + /// + /// Gets or sets the previous edge in the Sorted Edge List. + /// The SEL is used when sorting edges into their new positions at scanbeam tops, + /// and is also reused to process horizontal edges. + /// public Active PrevInSEL { get; set; } + /// + /// Gets or sets the next edge in the Sorted Edge List. + /// public Active NextInSEL { get; set; } + /// + /// Gets or sets the jump pointer used during merge sort operations. + /// public Active Jump { get; set; } + /// + /// Gets or sets the vertex at the top of this edge segment. + /// public Vertex VertexTop { get; set; } - public LocalMinima LocalMin { get; set; } // the bottom of an edge 'bound' (also Vatti) + /// + /// Gets or sets the local minimum this edge belongs to. + /// + public LocalMinima LocalMin { get; set; } + /// + /// Gets or sets a value indicating whether this is a left bound edge. + /// public bool IsLeftBound { get; set; } + /// + /// Gets or sets the join status indicating if this edge is joined with an adjacent edge. + /// public JoinWith JoinWith { get; set; } } } +/// +/// Represents a node in a hierarchical polygon tree structure. +/// Can contain child paths representing holes or nested polygons. +/// internal class PolyPathF : IEnumerable { private readonly PolyPathF parent; private readonly List items = []; + /// + /// Initializes a new instance of the class. + /// + /// The parent path, or null for the root. public PolyPathF(PolyPathF parent = null) => this.parent = parent; - public PathF Polygon { get; private set; } // polytree root's polygon == null + /// + /// Gets the polygon path. The polytree root's polygon is null. + /// + public PathF Polygon { get; private set; } + /// + /// Gets the nesting level in the tree (0 for root). + /// public int Level => this.GetLevel(); + /// + /// Gets a value indicating whether this path represents a hole. + /// public bool IsHole => this.GetIsHole(); + /// + /// Gets the number of child paths. + /// public int Count => this.items.Count; + /// + /// Gets the child path at the specified index. + /// + /// The child index. + /// The child path. public PolyPathF this[int index] => this.items[index]; + /// + /// Adds a child path to this polytree node. + /// + /// The polygon path to add. + /// The created child node. [MethodImpl(MethodImplOptions.AggressiveInlining)] public PolyPathF AddChild(PathF p) { @@ -3380,6 +3763,10 @@ public PolyPathF AddChild(PathF p) return child; } + /// + /// Calculates the total area of this polygon and all its children. + /// + /// The signed area. [MethodImpl(MethodImplOptions.AggressiveInlining)] public float Area() { @@ -3393,6 +3780,9 @@ public float Area() return result; } + /// + /// Removes all child paths. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear() => this.items.Clear(); @@ -3417,43 +3807,78 @@ private int GetLevel() return result; } + /// + /// Returns an enumerator that iterates through the child paths. + /// + /// An enumerator for the children. public IEnumerator GetEnumerator() => this.items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.items.GetEnumerator(); } +/// +/// Root of a polytree structure containing hierarchical polygon data. +/// internal class PolyTreeF : PolyPathF { } +/// +/// Collection of polygon paths. +/// internal class PathsF : List { + /// + /// Initializes a new instance of the class. + /// public PathsF() { } + /// + /// Initializes a new instance of the class with items. + /// + /// Initial paths. public PathsF(IEnumerable items) : base(items) { } + /// + /// Initializes a new instance of the class with capacity. + /// + /// Initial capacity. public PathsF(int capacity) : base(capacity) { } } +/// +/// Represents a polygon path as a list of points. +/// internal class PathF : List { + /// + /// Initializes a new instance of the class. + /// public PathF() { } + /// + /// Initializes a new instance of the class with items. + /// + /// Initial points. public PathF(IEnumerable items) : base(items) { } + /// + /// Initializes a new instance of the class with capacity. + /// + /// Initial capacity. public PathF(int capacity) : base(capacity) { From 3b1b88d69e7812122eac82054faddbba22a2c980 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Feb 2026 15:42:24 +1000 Subject: [PATCH 16/35] Handle degenerate inbound path. --- .../Shapes/PolygonGeometry/PolygonStroker.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs index 14ea9c70..c75d115e 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs @@ -118,6 +118,11 @@ public double Width /// The input points to stroke. /// Whether the input is a closed ring. /// The stroked outline as a closed point array. + /// + /// When a 2-point input contains identical points (degenerate case), this method generates + /// a cap shape at that point: a circle for round caps or a square for square/butt caps. + /// This ensures that even degenerate input produces visible output when stroked. + /// public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) { if (linePoints.Length < 2) @@ -125,6 +130,21 @@ public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) return []; } + // Special case: for 2-point inputs, check if both points are identical (degenerate case) + // This avoids overhead for longer paths where the filtering logic handles near-duplicates + if (linePoints.Length == 2) + { + PointF p0 = linePoints[0]; + PointF p1 = linePoints[1]; + + if (Math.Abs(p1.X - p0.X) <= Constants.Misc.VertexDistanceEpsilon && + Math.Abs(p1.Y - p0.Y) <= Constants.Misc.VertexDistanceEpsilon) + { + // Both points are identical - generate a point cap shape + return this.GeneratePointCap(p0.X, p0.Y); + } + } + this.Reset(); this.AddLinePath(linePoints); @@ -762,6 +782,52 @@ private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDi [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddPoint(double x, double y) => this.outVertices.Add(new PointF((float)x, (float)y)); + /// + /// Generates a cap shape for a degenerate point (when all input points are identical). + /// Creates a circle for round caps or a square for square/butt caps. + /// + /// The X coordinate of the point. + /// The Y coordinate of the point. + /// The vertices forming the cap shape. + private PointF[] GeneratePointCap(double x, double y) + { + if (this.LineCap == LineCap.Round) + { + // Generate a circle with radius = strokeWidth + double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125 / this.ApproximationScale))) * 2; + int n = Math.Max(4, (int)(Constants.Misc.PiMul2 / da)); + double angleStep = Constants.Misc.PiMul2 / n; + + PointF[] points = new PointF[n + 1]; + + for (int i = 0; i < n; i++) + { + double angle = i * angleStep; + points[i] = new PointF( + (float)(x + (Math.Cos(angle) * this.strokeWidth)), + (float)(y + (Math.Sin(angle) * this.strokeWidth))); + } + + // Close the circle + points[n] = points[0]; + + return points; + } + else + { + // Generate a square cap (used for both Square and Butt caps) + double w = this.strokeWidth; + return + [ + new PointF((float)(x - w), (float)(y - w)), + new PointF((float)(x + w), (float)(y - w)), + new PointF((float)(x + w), (float)(y + w)), + new PointF((float)(x - w), (float)(y + w)), + new PointF((float)(x - w), (float)(y - w)) // Close the square + ]; + } + } + private enum Status { Initial, From f41dc743819cdc9d8f57cba1381b1b526e4912a3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Feb 2026 16:31:02 +1000 Subject: [PATCH 17/35] Remove unnecessary checks --- .../Shapes/PolygonGeometry/PolygonClipper.cs | 64 +++---------------- .../Shapes/PolygonGeometry/PolygonStroker.cs | 6 -- 2 files changed, 9 insertions(+), 61 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs index f8a9fea8..87f3291c 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs @@ -63,14 +63,6 @@ public PolygonClipper() /// public bool ReverseSolution { get; set; } - /// - /// Adds subject paths to the clipping operation. - /// Subject paths are the primary polygons being clipped. - /// - /// The subject paths to add. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddSubject(PathsF paths) => this.AddPaths(paths, ClippingType.Subject); - /// /// Adds a single path to the clipping operation. /// @@ -244,52 +236,13 @@ private void AddNewIntersectNode(Active ae1, Active ae2, float topY) // Adjust intersection point if it's outside the scanbeam bounds if (ip.Y > this.currentBotY || ip.Y < topY) { + // Clamp Y to scanbeam bounds + ip.Y = ip.Y < topY ? topY : this.currentBotY; + + // Use the more vertical edge (smaller |Dx|) to compute X for numerical stability float absDx1 = MathF.Abs(ae1.Dx); float absDx2 = MathF.Abs(ae2.Dx); - - // For very steep edges, project the point onto the edge - // TODO: Check threshold here once we remove upscaling. - if (absDx1 > 100 && absDx2 > 100) - { - if (absDx1 > absDx2) - { - ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); - } - else - { - ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); - } - } - else if (absDx1 > 100) - { - ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae1.Bot, ae1.Top); - } - else if (absDx2 > 100) - { - ip = PolygonClipperUtilities.GetClosestPtOnSegment(ip, ae2.Bot, ae2.Top); - } - else - { - // Clamp Y to scanbeam bounds - if (ip.Y < topY) - { - ip.Y = topY; - } - else - { - ip.Y = this.currentBotY; - } - - // Use the less steep edge to determine X - if (absDx1 < absDx2) - { - ip.X = TopX(ae1, ip.Y); - } - else - { - ip.X = TopX(ae2, ip.Y); - } - } + ip.X = absDx1 < absDx2 ? TopX(ae1, ip.Y) : TopX(ae2, ip.Y); } IntersectNode node = new(ip, ae1, ae2); @@ -1107,9 +1060,10 @@ private void ProcessHorzJoins() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool PtsReallyClose(Vector2 pt1, Vector2 pt2) - - // TODO: Check scale once we can remove upscaling. - => (Math.Abs(pt1.X - pt2.X) < 2F) && (Math.Abs(pt1.Y - pt2.Y) < 2F); + { + Vector2 delta = Vector2.Abs(pt1 - pt2); + return delta.X < 1e-6f && delta.Y < 1e-6f; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CleanCollinear(OutRec outrec) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs index c75d115e..772c37ca 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs @@ -118,11 +118,6 @@ public double Width /// The input points to stroke. /// Whether the input is a closed ring. /// The stroked outline as a closed point array. - /// - /// When a 2-point input contains identical points (degenerate case), this method generates - /// a cap shape at that point: a circle for round caps or a square for square/butt caps. - /// This ensures that even degenerate input produces visible output when stroked. - /// public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) { if (linePoints.Length < 2) @@ -131,7 +126,6 @@ public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) } // Special case: for 2-point inputs, check if both points are identical (degenerate case) - // This avoids overhead for longer paths where the filtering logic handles near-duplicates if (linePoints.Length == 2) { PointF p0 = linePoints[0]; From 6b42ee3fc16d4c68379c2d6b52eb44c518a3edfd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 5 Feb 2026 16:44:16 +1000 Subject: [PATCH 18/35] Use scale appropriate tolerances --- .../Shapes/PolygonGeometry/PolygonClipper.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs index 87f3291c..58a6fd2c 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs @@ -21,6 +21,11 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// internal sealed class PolygonClipper { + private const float MinimumDistanceThreshold = 1e-6f; + private const float MinimumAreaThreshold = 1e-6f; + private const float JoinYTolerance = 1e-6f; + private const float JoinDistanceSqrdThreshold = 1e-12f; + private BooleanOperation clipType; private ClipperFillRule fillRule; private Active actives; // Head of the active edge list @@ -1062,7 +1067,8 @@ private void ProcessHorzJoins() private static bool PtsReallyClose(Vector2 pt1, Vector2 pt2) { Vector2 delta = Vector2.Abs(pt1 - pt2); - return delta.X < 1e-6f && delta.Y < 1e-6f; + return delta.X < MinimumDistanceThreshold && + delta.Y < MinimumDistanceThreshold; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -1125,16 +1131,16 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip); float area1 = Area(prevOp); - float absArea1 = Math.Abs(area1); + float absArea1 = MathF.Abs(area1); - if (absArea1 < 2) + if (absArea1 < MinimumAreaThreshold) { outrec.Pts = null; return; } float area2 = AreaTriangle(ip, splitOp.Point, splitOp.Next.Point); - float absArea2 = Math.Abs(area2); + float absArea2 = MathF.Abs(area2); // de-link splitOp and splitOp.next from the path // while inserting the intersection point @@ -1160,7 +1166,7 @@ private void DoSplitOp(OutRec outrec, OutPt splitOp) // So the only way for these areas to have the same sign is if // the split triangle is larger than the path containing prevOp or // if there's more than one self=intersection. - if (absArea2 > 1 && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0)))) + if (absArea2 > MinimumAreaThreshold && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0)))) { OutRec newOutRec = this.NewOutRec(); newOutRec.Owner = outrec.Owner; @@ -3022,7 +3028,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false) } // Avoid trivial joins - if ((pt.Y < e.Top.Y + 2 || pt.Y < prev.Top.Y + 2) + if ((pt.Y < e.Top.Y + JoinYTolerance || pt.Y < prev.Top.Y + JoinYTolerance) && ((e.Bot.Y > pt.Y) || (prev.Bot.Y > pt.Y))) { return; @@ -3030,7 +3036,7 @@ private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false) if (checkCurrX) { - if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > 0.25) + if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > JoinDistanceSqrdThreshold) { return; } @@ -3077,7 +3083,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false) } // Avoid trivial joins - if ((pt.Y < e.Top.Y + 2 || pt.Y < next.Top.Y + 2) + if ((pt.Y < e.Top.Y + JoinYTolerance || pt.Y < next.Top.Y + JoinYTolerance) && ((e.Bot.Y > pt.Y) || (next.Bot.Y > pt.Y))) { return; @@ -3085,7 +3091,7 @@ private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false) if (checkCurrX) { - if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > 0.25) + if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > JoinDistanceSqrdThreshold) { return; } From e5ada72f82caf58a41c06aa1e5bf7e9d14222932 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 17 Feb 2026 22:15:38 +1000 Subject: [PATCH 19/35] Use PolygonClipper --- ImageSharp.Drawing.sln | 8 + .../ImageSharp.Drawing.csproj | 3 + .../Processors/Drawing/DrawPathProcessor.cs | 15 +- .../Processing/StrokeOptions.cs | 36 +- .../Shapes/ClipPathExtensions.cs | 19 +- .../Shapes/ComplexPolygon.cs | 3 +- .../Shapes/OutlinePathExtensions.cs | 12 +- .../Shapes/PolygonGeometry/ArrayBuilder{T}.cs | 156 - .../Shapes/PolygonGeometry/BoundsF.cs | 90 - .../PolygonGeometry/ClippedShapeGenerator.cs | 127 +- .../Shapes/PolygonGeometry/ClipperFillRule.cs | 23 - .../Shapes/PolygonGeometry/ClippingType.cs | 20 - .../Shapes/PolygonGeometry/JoinWith.cs | 25 - .../Shapes/PolygonGeometry/PolygonClipper.cs | 3846 ----------------- .../PolygonGeometry/PolygonClipperFactory.cs | 79 + .../PolygonClipperUtilities.cs | 174 - .../Shapes/PolygonGeometry/PolygonStroker.cs | 913 ---- .../PolygonGeometry/StrokedShapeGenerator.cs | 247 +- .../Shapes/PolygonGeometry/VertexDistance.cs | 98 - .../Shapes/PolygonGeometry/VertexFlags.cs | 14 - .../Utilities/ThreadLocalBlenderBuffers.cs | 2 +- .../Drawing/DrawPolygon.cs | 118 +- .../ImageSharp.Drawing.Benchmarks/Program.cs | 6 +- .../Drawing/DrawComplexPolygonTests.cs | 4 +- .../Drawing/DrawingRobustnessTests.cs | 70 + .../Issues/Issue_330.cs | 16 + .../Processing/FillPathProcessorTests.cs | 57 + .../Shapes/PolygonClipper/ClipperTests.cs | 52 +- 28 files changed, 533 insertions(+), 5700 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs create mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 74e8e154..827d45f5 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -337,6 +337,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolygonClipper", "..\..\SixLabors\PolygonClipper\src\PolygonClipper\PolygonClipper.csproj", "{5ED54794-99BF-5E50-A861-0BAAAC794E44}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -359,6 +361,10 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU + {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -386,12 +392,14 @@ Global {68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} + {5ED54794-99BF-5E50-A861-0BAAAC794E44} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 + ..\..\SixLabors\PolygonClipper\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{5ed54794-99bf-5e50-a861-0baaac794e44}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection GlobalSection(Performance) = preSolution diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 488180d6..9eccafbf 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -47,5 +47,8 @@ + + + diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs index d565bd72..5b3a5cc8 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs @@ -48,7 +48,20 @@ public IImageProcessor CreatePixelSpecificProcessor(Configuratio // The global transform is applied in the FillPathProcessor. IPath outline = this.Pen.GeneratePath(this.Path.Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); - return new FillPathProcessor(this.Options, this.Pen.StrokeFill, outline) + DrawingOptions effectiveOptions = this.Options; + + // Non-normalized stroked output can contain overlaps/self-intersections. + // Rasterizing these contours with non-zero winding matches the intended stroke semantics. + if (!this.Pen.StrokeOptions.NormalizeOutput && + this.Options.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + { + ShapeOptions shapeOptions = this.Options.ShapeOptions.DeepClone(); + shapeOptions.IntersectionRule = IntersectionRule.NonZero; + + effectiveOptions = new DrawingOptions(this.Options.GraphicsOptions, shapeOptions, this.Options.Transform); + } + + return new FillPathProcessor(effectiveOptions, this.Pen.StrokeFill, outline) .CreatePixelSpecificProcessor(configuration, source, sourceRectangle); } } diff --git a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs index 4e4b34e8..51886f91 100644 --- a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs @@ -8,20 +8,32 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class StrokeOptions : IEquatable { + /// + /// Gets or sets a value indicating whether stroked contours should be normalized by + /// resolving self-intersections and overlaps before returning. + /// + /// + /// Defaults to for maximum throughput. + /// When disabled, callers should rasterize with a non-zero winding fill rule. + /// + public bool NormalizeOutput { get; set; } + /// /// Gets or sets the miter limit used to clamp outer miter joins. /// - public double MiterLimit { get; set; } = 4; + public double MiterLimit { get; set; } = 4D; /// /// Gets or sets the inner miter limit used to clamp joins on acute interior angles. /// - public double InnerMiterLimit { get; set; } = 1.01; + public double InnerMiterLimit { get; set; } = 1.01D; /// - /// Gets or sets the arc approximation scale used for round joins and caps. + /// Gets or sets the tessellation detail scale for round joins and round caps. + /// Higher values produce more vertices (smoother curves, more work). + /// Lower values produce fewer vertices. /// - public double ApproximationScale { get; set; } = 1.0; + public double ArcDetailScale { get; set; } = 1D; /// /// Gets or sets the outer line join style used for stroking corners. @@ -44,19 +56,21 @@ public sealed class StrokeOptions : IEquatable /// public bool Equals(StrokeOptions? other) => other is not null && - this.MiterLimit == other.MiterLimit && - this.InnerMiterLimit == other.InnerMiterLimit && - this.ApproximationScale == other.ApproximationScale && - this.LineJoin == other.LineJoin && - this.LineCap == other.LineCap && - this.InnerJoin == other.InnerJoin; + this.NormalizeOutput == other.NormalizeOutput && + this.MiterLimit == other.MiterLimit && + this.InnerMiterLimit == other.InnerMiterLimit && + this.ArcDetailScale == other.ArcDetailScale && + this.LineJoin == other.LineJoin && + this.LineCap == other.LineCap && + this.InnerJoin == other.InnerJoin; /// public override int GetHashCode() => HashCode.Combine( + this.NormalizeOutput, this.MiterLimit, this.InnerMiterLimit, - this.ApproximationScale, + this.ArcDetailScale, this.LineJoin, this.LineCap, this.InnerJoin); diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs index a1101853..b9b3ccde 100644 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs @@ -11,6 +11,8 @@ namespace SixLabors.ImageSharp.Drawing; /// public static class ClipPathExtensions { + private static readonly ShapeOptions DefaultOptions = new(); + /// /// Clips the specified subject path with the provided clipping paths. /// @@ -18,7 +20,7 @@ public static class ClipPathExtensions /// The clipping paths. /// The clipped . public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths) - => subjectPath.Clip((IEnumerable)clipPaths); + => subjectPath.Clip(DefaultOptions, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. @@ -31,7 +33,7 @@ public static IPath Clip( this IPath subjectPath, ShapeOptions options, params IPath[] clipPaths) - => subjectPath.Clip(options, (IEnumerable)clipPaths); + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. @@ -40,7 +42,7 @@ public static IPath Clip( /// The clipping paths. /// The clipped . public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) - => subjectPath.Clip(new ShapeOptions(), clipPaths); + => subjectPath.Clip(DefaultOptions, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. @@ -53,14 +55,5 @@ public static IPath Clip( this IPath subjectPath, ShapeOptions options, IEnumerable clipPaths) - { - ClippedShapeGenerator clipper = new(options.IntersectionRule); - - clipper.AddPath(subjectPath, ClippingType.Subject); - clipper.AddPaths(clipPaths, ClippingType.Clip); - - IPath[] result = clipper.GenerateClippedShapes(options.BooleanOperation); - - return new ComplexPolygon(result); - } + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); } diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs index 6cfb4319..59c404f4 100644 --- a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Diagnostics.CodeAnalysis; using System.Numerics; @@ -34,7 +33,7 @@ public ComplexPolygon(PointF[] contour, PointF[] hole) /// /// The paths. public ComplexPolygon(IEnumerable paths) - : this(paths.ToArray()) + : this([.. paths]) { } diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs index 466a597f..78ff4fc1 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Drawing; /// public static class OutlinePathExtensions { + private static readonly StrokeOptions DefaultOptions = new(); + /// /// Generates an outline of the path. /// @@ -19,7 +21,7 @@ public static class OutlinePathExtensions /// The outline width. /// A new representing the outline. public static IPath GenerateOutline(this IPath path, float width) - => GenerateOutline(path, width, new StrokeOptions()); + => GenerateOutline(path, width, DefaultOptions); /// /// Generates an outline of the path. @@ -35,8 +37,7 @@ public static IPath GenerateOutline(this IPath path, float width, StrokeOptions return Path.Empty; } - StrokedShapeGenerator generator = new(strokeOptions); - return new ComplexPolygon(generator.GenerateStrokedShapes(path, width)); + return StrokedShapeGenerator.GenerateStrokedShapes(path, width, strokeOptions); } /// @@ -69,7 +70,7 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpanWhether the first item in the pattern is on or off. /// A new representing the outline. public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) - => GenerateOutline(path, width, pattern, startOff, new StrokeOptions()); + => GenerateOutline(path, width, pattern, startOff, DefaultOptions); /// /// Generates an outline of the path with alternating on and off segments based on the pattern. @@ -235,7 +236,6 @@ public static IPath GenerateOutline( } // Each outline span is stroked as an open polyline; the union cleans overlaps. - StrokedShapeGenerator generator = new(strokeOptions); - return new ComplexPolygon(generator.GenerateStrokedShapes(outlines, width)); + return StrokedShapeGenerator.GenerateStrokedShapes(outlines, width, strokeOptions); } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs deleted file mode 100644 index 6654102c..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ArrayBuilder{T}.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// 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/PolygonGeometry/BoundsF.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs deleted file mode 100644 index 14ac870b..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/BoundsF.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -internal struct BoundsF -{ - public float Left; - public float Top; - public float Right; - public float Bottom; - - public BoundsF(float l, float t, float r, float b) - { - this.Left = l; - this.Top = t; - this.Right = r; - this.Bottom = b; - } - - public BoundsF(BoundsF bounds) - { - this.Left = bounds.Left; - this.Top = bounds.Top; - this.Right = bounds.Right; - this.Bottom = bounds.Bottom; - } - - public BoundsF(bool isValid) - { - if (isValid) - { - this.Left = 0; - this.Top = 0; - this.Right = 0; - this.Bottom = 0; - } - else - { - this.Left = float.MaxValue; - this.Top = float.MaxValue; - this.Right = -float.MaxValue; - this.Bottom = -float.MaxValue; - } - } - - public float Width - { - readonly get => this.Right - this.Left; - set => this.Right = this.Left + value; - } - - public float Height - { - readonly get => this.Bottom - this.Top; - set => this.Bottom = this.Top + value; - } - - public readonly bool IsEmpty() - => this.Bottom <= this.Top || this.Right <= this.Left; - - public readonly Vector2 MidPoint() - => new Vector2(this.Left + this.Right, this.Top + this.Bottom) * .5F; - - public readonly bool Contains(Vector2 pt) - => pt.X > this.Left - && pt.X < this.Right - && pt.Y > this.Top && pt.Y < this.Bottom; - - public readonly bool Contains(BoundsF bounds) - => bounds.Left >= this.Left - && bounds.Right <= this.Right - && bounds.Top >= this.Top - && bounds.Bottom <= this.Bottom; - - public readonly bool Intersects(BoundsF bounds) - => (Math.Max(this.Left, bounds.Left) < Math.Min(this.Right, bounds.Right)) - && (Math.Max(this.Top, bounds.Top) < Math.Min(this.Bottom, bounds.Bottom)); - - public readonly PathF AsPath() - => - [ - new Vector2(this.Left, this.Top), - new Vector2(this.Right, this.Top), - new Vector2(this.Right, this.Bottom), - new Vector2(this.Left, this.Bottom) - ]; -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs index 7fff6b6e..286dfb01 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs @@ -1,32 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.PolygonClipper; +using PCPolygon = SixLabors.PolygonClipper.Polygon; +using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; + namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Generates clipped shapes from one or more input paths using polygon boolean operations. /// /// -/// This class provides a high-level wrapper around the low-level . +/// This class provides a high-level wrapper around the low-level . /// It accumulates subject and clip polygons, applies the specified , -/// and converts the resulting polygon contours back into instances suitable +/// and converts the resulting polygon contours back into instances suitable /// for rendering or further processing. /// -internal sealed class ClippedShapeGenerator +internal static class ClippedShapeGenerator { - private readonly PolygonClipper polygonClipper; - private readonly IntersectionRule rule; - - /// - /// Initializes a new instance of the class. - /// - /// The intersection rule. - public ClippedShapeGenerator(IntersectionRule rule) - { - this.rule = rule; - this.polygonClipper = new PolygonClipper() { PreserveCollinear = true }; - } - /// /// Generates the final clipped shapes from the previously provided subject and clip paths. /// @@ -34,101 +25,47 @@ public ClippedShapeGenerator(IntersectionRule rule) /// The boolean operation to perform, such as , /// , or . /// - /// TEMP. Remove when we update IntersectionRule to add missing entries. + /// The subject path. + /// The clipping paths. /// - /// An array of instances representing the result of the boolean operation. + /// The representing the result of the boolean operation. /// - public IPath[] GenerateClippedShapes(BooleanOperation operation, bool? positive = null) + public static ComplexPolygon GenerateClippedShapes( + BooleanOperation operation, + IPath subject, + IEnumerable clip) { - PathsF closedPaths = []; - PathsF openPaths = []; + Guard.NotNull(subject); + Guard.NotNull(clip); - ClipperFillRule fillRule = this.rule == IntersectionRule.EvenOdd ? ClipperFillRule.EvenOdd : ClipperFillRule.NonZero; + PCPolygon s = PolygonClipperFactory.FromSimpleClosedPaths(subject.Flatten()); + PCPolygon c = PolygonClipperFactory.FromClosedPaths(clip); - if (positive.HasValue) + PCPolygon result = operation switch { - fillRule = positive.Value ? ClipperFillRule.Positive : ClipperFillRule.Negative; - } - - this.polygonClipper.Execute(operation, fillRule, closedPaths, openPaths); + BooleanOperation.Xor => PolygonClipperAction.Xor(s, c), + BooleanOperation.Difference => PolygonClipperAction.Difference(s, c), + BooleanOperation.Union => PolygonClipperAction.Union(s, c), + _ => PolygonClipperAction.Intersection(s, c), + }; - IPath[] shapes = new IPath[closedPaths.Count + openPaths.Count]; + IPath[] shapes = new IPath[result.Count]; int index = 0; - for (int i = 0; i < closedPaths.Count; i++) + for (int i = 0; i < result.Count; i++) { - PathF path = closedPaths[i]; - PointF[] points = new PointF[path.Count]; + Contour contour = result[i]; + PointF[] points = new PointF[contour.Count]; - for (int j = 0; j < path.Count; j++) + for (int j = 0; j < contour.Count; j++) { - points[j] = path[j]; + Vertex vertex = contour[j]; + points[j] = new PointF((float)vertex.X, (float)vertex.Y); } shapes[index++] = new Polygon(points); } - for (int i = 0; i < openPaths.Count; i++) - { - PathF path = openPaths[i]; - PointF[] points = new PointF[path.Count]; - - for (int j = 0; j < path.Count; j++) - { - points[j] = path[j]; - } - - shapes[index++] = new Polygon(points); - } - - return shapes; - } - - /// - /// Adds a collection of paths to the current clipping operation. - /// - /// - /// The paths to add. Each path may represent a simple or complex polygon. - /// - /// - /// Determines whether the paths are assigned to the subject or clip polygon. - /// - public void AddPaths(IEnumerable paths, ClippingType clippingType) - { - Guard.NotNull(paths, nameof(paths)); - - foreach (IPath p in paths) - { - this.AddPath(p, clippingType); - } - } - - /// - /// Adds a single path to the current clipping operation. - /// - /// The path to add. - /// - /// Determines whether the path is assigned to the subject or clip polygon. - /// - public void AddPath(IPath path, ClippingType clippingType) - { - Guard.NotNull(path, nameof(path)); - - foreach (ISimplePath p in path.Flatten()) - { - this.AddPath(p, clippingType); - } - } - - private void AddPath(ISimplePath path, ClippingType clippingType) - { - ReadOnlySpan vectors = path.Points.Span; - PathF points = new(vectors.Length); - for (int i = 0; i < vectors.Length; i++) - { - points.Add(vectors[i]); - } - - this.polygonClipper.AddPath(points, clippingType, !path.IsClosed); + return new(shapes); } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs deleted file mode 100644 index 90d1c614..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperFillRule.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// By far the most widely used filling rules for polygons are EvenOdd -/// and NonZero, sometimes called Alternate and Winding respectively. -/// -/// -/// -/// TODO: This overlaps with the enum. -/// We should see if we can enhance the to support all these rules. -/// -internal enum ClipperFillRule -{ - EvenOdd, - NonZero, - Positive, - Negative -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs deleted file mode 100644 index 2ac4ef90..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippingType.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// Defines the polygon clipping type. -/// -internal enum ClippingType -{ - /// - /// Represents a shape to act as a subject which will be clipped or merged. - /// - Subject, - - /// - /// Represents a shape to act as a clipped path. - /// - Clip -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs deleted file mode 100644 index ee3272a8..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/JoinWith.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// Specifies how a vertex should be joined with adjacent paths during polygon operations. -/// -internal enum JoinWith -{ - /// - /// No joining operation. - /// - None, - - /// - /// Join with the left adjacent path. - /// - Left, - - /// - /// Join with the right adjacent path. - /// - Right -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs deleted file mode 100644 index 58a6fd2c..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipper.cs +++ /dev/null @@ -1,3846 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -#nullable disable - -using System.Collections; -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// Contains functions that cover most polygon boolean and offsetting needs. -/// Ported from and originally licensed -/// under -/// -/// -/// This class implements the Vatti clipping algorithm using a scanline sweep approach. -/// It processes polygon edges by sweeping a horizontal line from bottom to top, -/// maintaining an active edge list (AEL) of edges that intersect the current scanline. -/// -internal sealed class PolygonClipper -{ - private const float MinimumDistanceThreshold = 1e-6f; - private const float MinimumAreaThreshold = 1e-6f; - private const float JoinYTolerance = 1e-6f; - private const float JoinDistanceSqrdThreshold = 1e-12f; - - private BooleanOperation clipType; - private ClipperFillRule fillRule; - private Active actives; // Head of the active edge list - private Active flaggedHorizontal; // Linked list of horizontal edges awaiting processing - private readonly List minimaList; // Local minima sorted by Y coordinate - private readonly List intersectList; // Intersections at current scanbeam - private readonly List vertexList; // All vertices from input paths - private readonly List outrecList; // Output polygon records - private readonly List scanlineList; // Y coordinates requiring processing - private readonly List horzSegList; // Horizontal segments for joining - private readonly List horzJoinList; // Horizontal joins to process - private int currentLocMin; // Index of current local minimum being processed - private float currentBotY; // Y coordinate of current scanbeam bottom - private bool isSortedMinimaList; // Whether minimaList has been sorted - private bool hasOpenPaths; // Whether any input paths are open (not closed) - - /// - /// Initializes a new instance of the class. - /// - public PolygonClipper() - { - this.minimaList = []; - this.intersectList = []; - this.vertexList = []; - this.outrecList = []; - this.scanlineList = []; - this.horzSegList = []; - this.horzJoinList = []; - this.PreserveCollinear = true; - } - - /// - /// Gets or sets a value indicating whether collinear vertices should be preserved in the output. - /// When true, only 180-degree spikes are removed. When false, all collinear vertices are removed. - /// - public bool PreserveCollinear { get; set; } - - /// - /// Gets or sets a value indicating whether the output polygon orientation should be reversed. - /// - public bool ReverseSolution { get; set; } - - /// - /// Adds a single path to the clipping operation. - /// - /// The path to add. - /// Whether this is a subject or clip path. - /// Whether the path is open (polyline) or closed (polygon). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddPath(PathF path, ClippingType polytype, bool isOpen = false) - { - PathsF tmp = [path]; - this.AddPaths(tmp, polytype, isOpen); - } - - /// - /// Adds multiple paths to the clipping operation. - /// - /// The paths to add. - /// Whether these are subject or clip paths. - /// Whether the paths are open (polylines) or closed (polygons). - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddPaths(PathsF paths, ClippingType polytype, bool isOpen = false) - { - if (isOpen) - { - this.hasOpenPaths = true; - } - - this.isSortedMinimaList = false; - this.AddPathsToVertexList(paths, polytype, isOpen); - } - - /// - /// Executes the clipping operation and returns only closed paths. - /// - /// The boolean operation to perform. - /// The fill rule to use for polygon interiors. - /// Output collection for closed solution paths. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed) - => this.Execute(clipType, fillRule, solutionClosed, []); - - /// - /// Executes the clipping operation and returns both closed and open paths. - /// - /// The boolean operation to perform (union, intersection, difference, xor). - /// The fill rule to determine polygon interiors (even-odd, non-zero, positive, negative). - /// Output collection for closed solution paths (polygons). - /// Output collection for open solution paths (polylines). - /// Thrown when an error occurs during clipping. - public void Execute(BooleanOperation clipType, ClipperFillRule fillRule, PathsF solutionClosed, PathsF solutionOpen) - { - solutionClosed.Clear(); - solutionOpen.Clear(); - - try - { - this.ExecuteInternal(clipType, fillRule); - this.BuildPaths(solutionClosed, solutionOpen); - } - catch (Exception ex) - { - throw new ClipperException("An error occurred while attempting to clip the polygon. See the inner exception for details.", ex); - } - finally - { - this.ClearSolutionOnly(); - } - } - - /// - /// Executes the core clipping algorithm using the Vatti scanbeam sweep. - /// Processes all edges from bottom to top, handling intersections and building output polygons. - /// - /// The boolean operation type. - /// The fill rule for determining polygon interiors. - private void ExecuteInternal(BooleanOperation ct, ClipperFillRule fillRule) - { - this.fillRule = fillRule; - this.clipType = ct; - this.Reset(); - - // Get the first scanline Y coordinate - if (!this.PopScanline(out float y)) - { - return; - } - - while (true) - { - // Add local minima edges that start at current Y - this.InsertLocalMinimaIntoAEL(y); - - // Process all horizontal edges at this Y - Active ae; - while (this.PopHorz(out ae)) - { - this.DoHorizontal(ae); - } - - // Convert horizontal segments to joins for later processing - if (this.horzSegList.Count > 0) - { - this.ConvertHorzSegsToJoins(); - this.horzSegList.Clear(); - } - - this.currentBotY = y; // bottom of current scanbeam - - // Get next scanline; break if no more - if (!this.PopScanline(out y)) - { - break; - } - - // Process intersections between current and next scanline - this.DoIntersections(y); - - // Update edges at top of scanbeam - this.DoTopOfScanbeam(y); - - // Process any horizontal edges that emerged - while (this.PopHorz(out ae)) - { - this.DoHorizontal(ae!); - } - } - - // Complete horizontal joins - this.ProcessHorzJoins(); - } - - /// - /// Processes edge intersections at the top of the current scanbeam. - /// Builds intersection list, processes intersections in order, then cleans up. - /// - /// The Y coordinate of the top of the scanbeam. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoIntersections(float topY) - { - if (this.BuildIntersectList(topY)) - { - this.ProcessIntersectList(); - this.DisposeIntersectNodes(); - } - } - - /// - /// Clears the intersection node list. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DisposeIntersectNodes() - => this.intersectList.Clear(); - - /// - /// Adds a new intersection node for two edges at the specified Y coordinate. - /// Calculates the exact intersection point, adjusting for numerical precision when needed. - /// - /// First edge. - /// Second edge. - /// Top Y coordinate of the scanbeam. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddNewIntersectNode(Active ae1, Active ae2, float topY) - { - // Calculate line intersection point - if (!PolygonClipperUtilities.GetLineIntersectPoint(ae1.Bot, ae1.Top, ae2.Bot, ae2.Top, out Vector2 ip)) - { - // Lines are parallel; use current X position - ip = new Vector2(ae1.CurX, topY); - } - - // Adjust intersection point if it's outside the scanbeam bounds - if (ip.Y > this.currentBotY || ip.Y < topY) - { - // Clamp Y to scanbeam bounds - ip.Y = ip.Y < topY ? topY : this.currentBotY; - - // Use the more vertical edge (smaller |Dx|) to compute X for numerical stability - float absDx1 = MathF.Abs(ae1.Dx); - float absDx2 = MathF.Abs(ae2.Dx); - ip.X = absDx1 < absDx2 ? TopX(ae1, ip.Y) : TopX(ae2, ip.Y); - } - - IntersectNode node = new(ip, ae1, ae2); - this.intersectList.Add(node); - } - - /// - /// Sets the heading direction for a horizontal segment based on two output points. - /// - /// The horizontal segment to configure. - /// Previous output point. - /// Next output point. - /// True if the segment has a valid direction; false if the points have the same X coordinate. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool SetHorzSegHeadingForward(HorzSegment hs, OutPt opP, OutPt opN) - { - if (opP.Point.X == opN.Point.X) - { - return false; - } - - if (opP.Point.X < opN.Point.X) - { - hs.LeftOp = opP; - hs.RightOp = opN; - hs.LeftToRight = true; - } - else - { - hs.LeftOp = opN; - hs.RightOp = opP; - hs.LeftToRight = false; - } - - return true; - } - - /// - /// Updates a horizontal segment by extending it to include all consecutive horizontal output points. - /// - /// The horizontal segment to update. - /// True if the segment was successfully updated; false otherwise. - private static bool UpdateHorzSegment(HorzSegment hs) - { - OutPt op = hs.LeftOp; - OutRec outrec = GetRealOutRec(op.OutRec); - bool outrecHasEdges = outrec.FrontEdge != null; - float curr_y = op.Point.Y; - OutPt opP = op, opN = op; - - // Extend the segment backwards and forwards along the horizontal line - if (outrecHasEdges) - { - OutPt opA = outrec.Pts!, opZ = opA.Next; - while (opP != opZ && opP.Prev.Point.Y == curr_y) - { - opP = opP.Prev; - } - - while (opN != opA && opN.Next.Point.Y == curr_y) - { - opN = opN.Next; - } - } - else - { - while (opP.Prev != opN && opP.Prev.Point.Y == curr_y) - { - opP = opP.Prev; - } - - while (opN.Next != opP && opN.Next.Point.Y == curr_y) - { - opN = opN.Next; - } - } - - bool result = SetHorzSegHeadingForward(hs, opP, opN) && hs.LeftOp.HorizSegment == null; - - if (result) - { - hs.LeftOp.HorizSegment = hs; - } - else - { - hs.RightOp = null; // Mark as invalid for sorting - } - - return result; - } - - /// - /// Duplicates an output point, inserting it either after or before the original. - /// - /// The output point to duplicate. - /// If true, insert after op; otherwise insert before. - /// The newly created output point. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt DuplicateOp(OutPt op, bool insert_after) - { - OutPt result = new(op.Point, op.OutRec); - if (insert_after) - { - result.Next = op.Next; - result.Next.Prev = result; - result.Prev = op; - op.Next = result; - } - else - { - result.Prev = op.Prev; - result.Prev.Next = result; - result.Next = op; - op.Prev = result; - } - - return result; - } - - /// - /// Converts horizontal segments into join operations. - /// Finds overlapping horizontal segments and creates joins between them. - /// - private void ConvertHorzSegsToJoins() - { - int k = 0; - - // Update all segments and count valid ones - foreach (HorzSegment hs in this.horzSegList) - { - if (UpdateHorzSegment(hs)) - { - k++; - } - } - - if (k < 2) - { - return; // Need at least 2 segments to join - } - - // Sort segments by left X coordinate - this.horzSegList.Sort(default(HorzSegSorter)); - - // Find overlapping segments and create joins - for (int i = 0; i < k - 1; i++) - { - HorzSegment hs1 = this.horzSegList[i]; - - // Check each subsequent segment for overlap - for (int j = i + 1; j < k; j++) - { - HorzSegment hs2 = this.horzSegList[j]; - - // Skip if no overlap or same direction - if ((hs2.LeftOp.Point.X >= hs1.RightOp.Point.X) || - (hs2.LeftToRight == hs1.LeftToRight) || - (hs2.RightOp.Point.X <= hs1.LeftOp.Point.X)) - { - continue; - } - - float curr_y = hs1.LeftOp.Point.Y; - - // Adjust segment endpoints to find join points - if (hs1.LeftToRight) - { - while (hs1.LeftOp.Next.Point.Y == curr_y && - hs1.LeftOp.Next.Point.X <= hs2.LeftOp.Point.X) - { - hs1.LeftOp = hs1.LeftOp.Next; - } - - while (hs2.LeftOp.Prev.Point.Y == curr_y && - hs2.LeftOp.Prev.Point.X <= hs1.LeftOp.Point.X) - { - hs2.LeftOp = hs2.LeftOp.Prev; - } - - HorzJoin join = new(DuplicateOp(hs1.LeftOp, true), DuplicateOp(hs2.LeftOp, false)); - this.horzJoinList.Add(join); - } - else - { - while (hs1.LeftOp.Prev.Point.Y == curr_y && - hs1.LeftOp.Prev.Point.X <= hs2.LeftOp.Point.X) - { - hs1.LeftOp = hs1.LeftOp.Prev; - } - - while (hs2.LeftOp.Next.Point.Y == curr_y && - hs2.LeftOp.Next.Point.X <= hs1.LeftOp.Point.X) - { - hs2.LeftOp = hs2.LeftOp.Next; - } - - HorzJoin join = new(DuplicateOp(hs2.LeftOp, true), DuplicateOp(hs1.LeftOp, false)); - this.horzJoinList.Add(join); - } - } - } - } - - /// - /// Clears the solution data while preserving input paths. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ClearSolutionOnly() - { - while (this.actives != null) - { - this.DeleteFromAEL(this.actives); - } - - this.scanlineList.Clear(); - this.DisposeIntersectNodes(); - this.outrecList.Clear(); - this.horzSegList.Clear(); - this.horzJoinList.Clear(); - } - - /// - /// Builds output paths from the output record list. - /// Processes each output record, cleaning collinear points and building the final paths. - /// - /// Collection to receive closed paths (polygons). - /// Collection to receive open paths (polylines). - /// True if paths were successfully built. - private bool BuildPaths(PathsF solutionClosed, PathsF solutionOpen) - { - solutionClosed.Clear(); - solutionOpen.Clear(); - solutionClosed.EnsureCapacity(this.outrecList.Count); - solutionOpen.EnsureCapacity(this.outrecList.Count); - - int i = 0; - - // Note: outrecList.Count is not static here because - // CleanCollinear can indirectly add additional OutRec - while (i < this.outrecList.Count) - { - OutRec outrec = this.outrecList[i++]; - if (outrec.Pts == null) - { - continue; - } - - PathF path = []; - if (outrec.IsOpen) - { - if (BuildPath(outrec.Pts, this.ReverseSolution, true, path)) - { - solutionOpen.Add(path); - } - } - else - { - // Clean collinear points from closed paths - this.CleanCollinear(outrec); - - // Closed paths should always return a positive orientation - // except when ReverseSolution == true - if (BuildPath(outrec.Pts, this.ReverseSolution, false, path)) - { - solutionClosed.Add(path); - } - } - } - - return true; - } - - /// - /// Builds a path from an output point list. - /// - /// Starting output point. - /// Whether to traverse the list in reverse. - /// Whether this is an open path. - /// The path to populate. - /// True if a valid path was created. - private static bool BuildPath(OutPt op, bool reverse, bool isOpen, PathF path) - { - // Validate minimum path requirements - if (op == null || op.Next == op || (!isOpen && op.Next == op.Prev)) - { - return false; - } - - path.Clear(); - - Vector2 lastPt; - OutPt op2; - - // Set starting point and direction - if (reverse) - { - lastPt = op.Point; - op2 = op.Prev; - } - else - { - op = op.Next; - lastPt = op.Point; - op2 = op.Next; - } - - path.Add(lastPt); - - // Traverse the output point list, adding unique points - while (op2 != op) - { - if (op2.Point != lastPt) - { - lastPt = op2.Point; - path.Add(lastPt); - } - - if (reverse) - { - op2 = op2.Prev; - } - else - { - op2 = op2.Next; - } - } - - // Filter out very small triangles - return path.Count != 3 || !IsVerySmallTriangle(op2); - } - - private void DoHorizontal(Active horz) - /******************************************************************************* - * Notes: Horizontal edges (HEs) at scanline intersections (i.e. at the top or * - * bottom of a scanbeam) are processed as if layered.The order in which HEs * - * are processed doesn't matter. HEs intersect with the bottom vertices of * - * other HEs[#] and with non-horizontal edges [*]. Once these intersections * - * are completed, intermediate HEs are 'promoted' to the next edge in their * - * bounds, and they in turn may be intersected[%] by other HEs. * - * * - * eg: 3 horizontals at a scanline: / | / / * - * | / | (HE3)o ========%========== o * - * o ======= o(HE2) / | / / * - * o ============#=========*======*========#=========o (HE1) * - * / | / | / * - *******************************************************************************/ - { - Vector2 pt; - bool horzIsOpen = IsOpen(horz); - float y = horz.Bot.Y; - - Vertex vertex_max = horzIsOpen ? GetCurrYMaximaVertex_Open(horz) : GetCurrYMaximaVertex(horz); - - // remove 180 deg.spikes and also simplify - // consecutive horizontals when PreserveCollinear = true - if (vertex_max != null && - !horzIsOpen && vertex_max != horz.VertexTop) - { - TrimHorz(horz, this.PreserveCollinear); - } - - bool isLeftToRight = ResetHorzDirection(horz, vertex_max, out float leftX, out float rightX); - - if (IsHotEdge(horz)) - { - OutPt op = AddOutPt(horz, new Vector2(horz.CurX, y)); - this.AddToHorzSegList(op); - } - - OutRec currOutrec = horz.Outrec; - - while (true) - { - // loops through consec. horizontal edges (if open) - Active ae = isLeftToRight ? horz.NextInAEL : horz.PrevInAEL; - - while (ae != null) - { - if (ae.VertexTop == vertex_max) - { - // do this first!! - if (IsHotEdge(horz) && IsJoined(ae!)) - { - this.Split(ae, ae.Top); - } - - if (IsHotEdge(horz)) - { - while (horz.VertexTop != vertex_max) - { - AddOutPt(horz, horz.Top); - this.UpdateEdgeIntoAEL(horz); - } - - if (isLeftToRight) - { - this.AddLocalMaxPoly(horz, ae, horz.Top); - } - else - { - this.AddLocalMaxPoly(ae, horz, horz.Top); - } - } - - this.DeleteFromAEL(ae); - this.DeleteFromAEL(horz); - return; - } - - // if horzEdge is a maxima, keep going until we reach - // its maxima pair, otherwise check for break conditions - if (vertex_max != horz.VertexTop || IsOpenEnd(horz)) - { - // otherwise stop when 'ae' is beyond the end of the horizontal line - if ((isLeftToRight && ae.CurX > rightX) || (!isLeftToRight && ae.CurX < leftX)) - { - break; - } - - if (ae.CurX == horz.Top.X && !IsHorizontal(ae)) - { - pt = NextVertex(horz).Point; - - // to maximize the possibility of putting open edges into - // solutions, we'll only break if it's past HorzEdge's end - if (IsOpen(ae) && !IsSamePolyType(ae, horz) && !IsHotEdge(ae)) - { - if ((isLeftToRight && (TopX(ae, pt.Y) > pt.X)) || - (!isLeftToRight && (TopX(ae, pt.Y) < pt.X))) - { - break; - } - } - - // otherwise for edges at horzEdge's end, only stop when horzEdge's - // outslope is greater than e's slope when heading right or when - // horzEdge's outslope is less than e's slope when heading left. - else if ((isLeftToRight && (TopX(ae, pt.Y) >= pt.X)) || (!isLeftToRight && (TopX(ae, pt.Y) <= pt.X))) - { - break; - } - } - } - - pt = new Vector2(ae.CurX, y); - - if (isLeftToRight) - { - this.IntersectEdges(horz, ae, pt); - this.SwapPositionsInAEL(horz, ae); - horz.CurX = ae.CurX; - ae = horz.NextInAEL; - } - else - { - this.IntersectEdges(ae, horz, pt); - this.SwapPositionsInAEL(ae, horz); - horz.CurX = ae.CurX; - ae = horz.PrevInAEL; - } - - if (IsHotEdge(horz) && (horz.Outrec != currOutrec)) - { - currOutrec = horz.Outrec; - this.AddToHorzSegList(GetLastOp(horz)); - } - - // we've reached the end of this horizontal - } - - // check if we've finished looping - // through consecutive horizontals - // ie open at top - if (horzIsOpen && IsOpenEnd(horz)) - { - if (IsHotEdge(horz)) - { - AddOutPt(horz, horz.Top); - if (IsFront(horz)) - { - horz.Outrec.FrontEdge = null; - } - else - { - horz.Outrec.BackEdge = null; - } - - horz.Outrec = null; - } - - this.DeleteFromAEL(horz); - return; - } - else if (NextVertex(horz).Point.Y != horz.Top.Y) - { - break; - } - - // still more horizontals in bound to process ... - if (IsHotEdge(horz)) - { - AddOutPt(horz, horz.Top); - } - - this.UpdateEdgeIntoAEL(horz); - - if (this.PreserveCollinear && !horzIsOpen && HorzIsSpike(horz)) - { - TrimHorz(horz, true); - } - - isLeftToRight = ResetHorzDirection(horz, vertex_max, out leftX, out rightX); - - // end for loop and end of (possible consecutive) horizontals - } - - if (IsHotEdge(horz)) - { - this.AddToHorzSegList(AddOutPt(horz, horz.Top)); - } - - this.UpdateEdgeIntoAEL(horz); // this is the end of an intermediate horiz. - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoTopOfScanbeam(float y) - { - this.flaggedHorizontal = null; // sel_ is reused to flag horizontals (see PushHorz below) - Active ae = this.actives; - while (ae != null) - { - // NB 'ae' will never be horizontal here - if (ae.Top.Y == y) - { - ae.CurX = ae.Top.X; - if (IsMaxima(ae)) - { - ae = this.DoMaxima(ae); // TOP OF BOUND (MAXIMA) - continue; - } - - // INTERMEDIATE VERTEX ... - if (IsHotEdge(ae)) - { - AddOutPt(ae, ae.Top); - } - - this.UpdateEdgeIntoAEL(ae); - if (IsHorizontal(ae)) - { - this.PushHorz(ae); // horizontals are processed later - } - } - else - { - // i.e. not the top of the edge - ae.CurX = TopX(ae, y); - } - - ae = ae.NextInAEL; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Active DoMaxima(Active ae) - { - Active prevE; - Active nextE, maxPair; - prevE = ae.PrevInAEL; - nextE = ae.NextInAEL; - - if (IsOpenEnd(ae)) - { - if (IsHotEdge(ae)) - { - AddOutPt(ae, ae.Top); - } - - if (!IsHorizontal(ae)) - { - if (IsHotEdge(ae)) - { - if (IsFront(ae)) - { - ae.Outrec.FrontEdge = null; - } - else - { - ae.Outrec.BackEdge = null; - } - - ae.Outrec = null; - } - - this.DeleteFromAEL(ae); - } - - return nextE; - } - - maxPair = GetMaximaPair(ae); - if (maxPair == null) - { - return nextE; // eMaxPair is horizontal - } - - if (IsJoined(ae)) - { - this.Split(ae, ae.Top); - } - - if (IsJoined(maxPair)) - { - this.Split(maxPair, maxPair.Top); - } - - // only non-horizontal maxima here. - // process any edges between maxima pair ... - while (nextE != maxPair) - { - this.IntersectEdges(ae, nextE!, ae.Top); - this.SwapPositionsInAEL(ae, nextE!); - nextE = ae.NextInAEL; - } - - if (IsOpen(ae)) - { - if (IsHotEdge(ae)) - { - this.AddLocalMaxPoly(ae, maxPair, ae.Top); - } - - this.DeleteFromAEL(maxPair); - this.DeleteFromAEL(ae); - return prevE != null ? prevE.NextInAEL : this.actives; - } - - // here ae.nextInAel == ENext == EMaxPair ... - if (IsHotEdge(ae)) - { - this.AddLocalMaxPoly(ae, maxPair, ae.Top); - } - - this.DeleteFromAEL(ae); - this.DeleteFromAEL(maxPair); - return prevE != null ? prevE.NextInAEL : this.actives; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void TrimHorz(Active horzEdge, bool preserveCollinear) - { - bool wasTrimmed = false; - Vector2 pt = NextVertex(horzEdge).Point; - - while (pt.Y == horzEdge.Top.Y) - { - // always trim 180 deg. spikes (in closed paths) - // but otherwise break if preserveCollinear = true - if (preserveCollinear && (pt.X < horzEdge.Top.X) != (horzEdge.Bot.X < horzEdge.Top.X)) - { - break; - } - - horzEdge.VertexTop = NextVertex(horzEdge); - horzEdge.Top = pt; - wasTrimmed = true; - if (IsMaxima(horzEdge)) - { - break; - } - - pt = NextVertex(horzEdge).Point; - } - - if (wasTrimmed) - { - SetDx(horzEdge); // +/-infinity - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddToHorzSegList(OutPt op) - { - if (op.OutRec.IsOpen) - { - return; - } - - this.horzSegList.Add(new HorzSegment(op)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt GetLastOp(Active hotEdge) - { - OutRec outrec = hotEdge.Outrec; - return (hotEdge == outrec.FrontEdge) ? outrec.Pts : outrec.Pts.Next; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex GetCurrYMaximaVertex_Open(Active ae) - { - Vertex result = ae.VertexTop; - if (ae.WindDx > 0) - { - while (result.Next.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None)) - { - result = result.Next; - } - } - else - { - while (result.Prev.Point.Y == result.Point.Y && ((result.Flags & (VertexFlags.OpenEnd | VertexFlags.LocalMax)) == VertexFlags.None)) - { - result = result.Prev; - } - } - - if (!IsMaxima(result)) - { - result = null; // not a maxima - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex GetCurrYMaximaVertex(Active ae) - { - Vertex result = ae.VertexTop; - if (ae.WindDx > 0) - { - while (result.Next.Point.Y == result.Point.Y) - { - result = result.Next; - } - } - else - { - while (result.Prev.Point.Y == result.Point.Y) - { - result = result.Prev; - } - } - - if (!IsMaxima(result)) - { - result = null; // not a maxima - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsVerySmallTriangle(OutPt op) - => op.Next.Next == op.Prev - && (PtsReallyClose(op.Prev.Point, op.Next.Point) - || PtsReallyClose(op.Point, op.Next.Point) - || PtsReallyClose(op.Point, op.Prev.Point)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsValidClosedPath(OutPt op) - => op != null && op.Next != op && (op.Next != op.Prev || !IsVerySmallTriangle(op)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt DisposeOutPt(OutPt op) - { - OutPt result = op.Next == op ? null : op.Next; - op.Prev.Next = op.Next; - op.Next.Prev = op.Prev; - - return result; - } - - private void ProcessHorzJoins() - { - foreach (HorzJoin j in this.horzJoinList) - { - OutRec or1 = GetRealOutRec(j.Op1.OutRec); - OutRec or2 = GetRealOutRec(j.Op2.OutRec); - - OutPt op1b = j.Op1.Next; - OutPt op2b = j.Op2.Prev; - j.Op1.Next = j.Op2; - j.Op2.Prev = j.Op1; - op1b.Prev = op2b; - op2b.Next = op1b; - - // 'join' is really a split - if (or1 == or2) - { - or2 = new OutRec - { - Pts = op1b - }; - - FixOutRecPts(or2); - - if (or1.Pts.OutRec == or2) - { - or1.Pts = j.Op1; - or1.Pts.OutRec = or1; - } - - or2.Owner = or1; - - this.outrecList.Add(or2); - } - else - { - or2.Pts = null; - or2.Owner = or1; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool PtsReallyClose(Vector2 pt1, Vector2 pt2) - { - Vector2 delta = Vector2.Abs(pt1 - pt2); - return delta.X < MinimumDistanceThreshold && - delta.Y < MinimumDistanceThreshold; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CleanCollinear(OutRec outrec) - { - outrec = GetRealOutRec(outrec); - - if (outrec?.IsOpen != false) - { - return; - } - - if (!IsValidClosedPath(outrec.Pts)) - { - outrec.Pts = null; - return; - } - - OutPt startOp = outrec.Pts; - OutPt op2 = startOp; - do - { - // NB if preserveCollinear == true, then only remove 180 deg. spikes - if ((PolygonClipperUtilities.CrossProduct(op2.Prev.Point, op2.Point, op2.Next.Point) == 0) - && ((op2.Point == op2.Prev.Point) || (op2.Point == op2.Next.Point) || !this.PreserveCollinear || (PolygonClipperUtilities.DotProduct(op2.Prev.Point, op2.Point, op2.Next.Point) < 0))) - { - if (op2 == outrec.Pts) - { - outrec.Pts = op2.Prev; - } - - op2 = DisposeOutPt(op2); - if (!IsValidClosedPath(op2)) - { - outrec.Pts = null; - return; - } - - startOp = op2; - continue; - } - - op2 = op2.Next; - } - while (op2 != startOp); - - this.FixSelfIntersects(outrec); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DoSplitOp(OutRec outrec, OutPt splitOp) - { - // splitOp.prev <=> splitOp && - // splitOp.next <=> splitOp.next.next are intersecting - OutPt prevOp = splitOp.Prev; - OutPt nextNextOp = splitOp.Next.Next; - outrec.Pts = prevOp; - - _ = PolygonClipperUtilities.GetLineIntersectPoint( - prevOp.Point, splitOp.Point, splitOp.Next.Point, nextNextOp.Point, out Vector2 ip); - - float area1 = Area(prevOp); - float absArea1 = MathF.Abs(area1); - - if (absArea1 < MinimumAreaThreshold) - { - outrec.Pts = null; - return; - } - - float area2 = AreaTriangle(ip, splitOp.Point, splitOp.Next.Point); - float absArea2 = MathF.Abs(area2); - - // de-link splitOp and splitOp.next from the path - // while inserting the intersection point - if (ip == prevOp.Point || ip == nextNextOp.Point) - { - nextNextOp.Prev = prevOp; - prevOp.Next = nextNextOp; - } - else - { - OutPt newOp2 = new(ip, outrec) - { - Prev = prevOp, - Next = nextNextOp - }; - - nextNextOp.Prev = newOp2; - prevOp.Next = newOp2; - } - - // nb: area1 is the path's area *before* splitting, whereas area2 is - // the area of the triangle containing splitOp & splitOp.next. - // So the only way for these areas to have the same sign is if - // the split triangle is larger than the path containing prevOp or - // if there's more than one self=intersection. - if (absArea2 > MinimumAreaThreshold && (absArea2 > absArea1 || ((area2 > 0) == (area1 > 0)))) - { - OutRec newOutRec = this.NewOutRec(); - newOutRec.Owner = outrec.Owner; - splitOp.OutRec = newOutRec; - splitOp.Next.OutRec = newOutRec; - - OutPt newOp = new(ip, newOutRec) { Prev = splitOp.Next, Next = splitOp }; - newOutRec.Pts = newOp; - splitOp.Prev = newOp; - splitOp.Next.Next = newOp; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void FixSelfIntersects(OutRec outrec) - { - OutPt op2 = outrec.Pts; - - // triangles can't self-intersect - while (op2.Prev != op2.Next.Next) - { - if (PolygonClipperUtilities.SegsIntersect(op2.Prev.Point, op2.Point, op2.Next.Point, op2.Next.Next.Point)) - { - this.DoSplitOp(outrec, op2); - if (outrec.Pts == null) - { - return; - } - - op2 = outrec.Pts; - continue; - } - else - { - op2 = op2.Next; - } - - if (op2 == outrec.Pts) - { - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Reset() - { - if (!this.isSortedMinimaList) - { - this.minimaList.Sort(default(LocMinSorter)); - this.isSortedMinimaList = true; - } - - 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); - } - - this.currentBotY = 0; - this.currentLocMin = 0; - this.actives = null; - this.flaggedHorizontal = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InsertScanline(float y) - { - int index = this.scanlineList.BinarySearch(y); - if (index >= 0) - { - return; - } - - index = ~index; - this.scanlineList.Insert(index, y); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool PopScanline(out float y) - { - int cnt = this.scanlineList.Count - 1; - if (cnt < 0) - { - y = 0; - return false; - } - - y = this.scanlineList[cnt]; - this.scanlineList.RemoveAt(cnt--); - while (cnt >= 0 && y == this.scanlineList[cnt]) - { - this.scanlineList.RemoveAt(cnt--); - } - - return true; - } - - private void InsertLocalMinimaIntoAEL(float botY) - { - LocalMinima localMinima; - Active leftBound, rightBound; - - // Add any local minima (if any) at BotY - // NB horizontal local minima edges should contain locMin.vertex.prev - while (this.HasLocMinAtY(botY)) - { - localMinima = this.PopLocalMinima(); - if ((localMinima.Vertex.Flags & VertexFlags.OpenStart) != VertexFlags.None) - { - leftBound = null; - } - else - { - leftBound = new Active - { - Bot = localMinima.Vertex.Point, - CurX = localMinima.Vertex.Point.X, - WindDx = -1, - VertexTop = localMinima.Vertex.Prev, - Top = localMinima.Vertex.Prev.Point, - Outrec = null, - LocalMin = localMinima - }; - SetDx(leftBound); - } - - if ((localMinima.Vertex.Flags & VertexFlags.OpenEnd) != VertexFlags.None) - { - rightBound = null; - } - else - { - rightBound = new Active - { - Bot = localMinima.Vertex.Point, - CurX = localMinima.Vertex.Point.X, - WindDx = 1, - VertexTop = localMinima.Vertex.Next, // i.e. ascending - Top = localMinima.Vertex.Next.Point, - Outrec = null, - LocalMin = localMinima - }; - SetDx(rightBound); - } - - // Currently LeftB is just the descending bound and RightB is the ascending. - // Now if the LeftB isn't on the left of RightB then we need swap them. - if (leftBound != null && rightBound != null) - { - if (IsHorizontal(leftBound)) - { - if (IsHeadingRightHorz(leftBound)) - { - SwapActives(ref leftBound, ref rightBound); - } - } - else if (IsHorizontal(rightBound)) - { - if (IsHeadingLeftHorz(rightBound)) - { - SwapActives(ref leftBound, ref rightBound); - } - } - else if (leftBound.Dx < rightBound.Dx) - { - SwapActives(ref leftBound, ref rightBound); - } - - // so when leftBound has windDx == 1, the polygon will be oriented - // counter-clockwise in Cartesian coords (clockwise with inverted Y). - } - else if (leftBound == null) - { - leftBound = rightBound; - rightBound = null; - } - - bool contributing; - leftBound.IsLeftBound = true; - this.InsertLeftEdge(leftBound); - - if (IsOpen(leftBound)) - { - this.SetWindCountForOpenPathEdge(leftBound); - contributing = this.IsContributingOpen(leftBound); - } - else - { - this.SetWindCountForClosedPathEdge(leftBound); - contributing = this.IsContributingClosed(leftBound); - } - - if (rightBound != null) - { - rightBound.WindCount = leftBound.WindCount; - rightBound.WindCount2 = leftBound.WindCount2; - InsertRightEdge(leftBound, rightBound); /////// - - if (contributing) - { - this.AddLocalMinPoly(leftBound, rightBound, leftBound.Bot, true); - if (!IsHorizontal(leftBound)) - { - this.CheckJoinLeft(leftBound, leftBound.Bot); - } - } - - while (rightBound.NextInAEL != null && IsValidAelOrder(rightBound.NextInAEL, rightBound)) - { - this.IntersectEdges(rightBound, rightBound.NextInAEL, rightBound.Bot); - this.SwapPositionsInAEL(rightBound, rightBound.NextInAEL); - } - - if (IsHorizontal(rightBound)) - { - this.PushHorz(rightBound); - } - else - { - this.CheckJoinRight(rightBound, rightBound.Bot); - this.InsertScanline(rightBound.Top.Y); - } - } - else if (contributing) - { - this.StartOpenPath(leftBound, leftBound.Bot); - } - - if (IsHorizontal(leftBound)) - { - this.PushHorz(leftBound); - } - else - { - this.InsertScanline(leftBound.Top.Y); - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active ExtractFromSEL(Active ae) - { - Active res = ae.NextInSEL; - if (res != null) - { - res.PrevInSEL = ae.PrevInSEL; - } - - ae.PrevInSEL.NextInSEL = res; - return res; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Insert1Before2InSEL(Active ae1, Active ae2) - { - ae1.PrevInSEL = ae2.PrevInSEL; - if (ae1.PrevInSEL != null) - { - ae1.PrevInSEL.NextInSEL = ae1; - } - - ae1.NextInSEL = ae2; - ae2.PrevInSEL = ae1; - } - - private bool BuildIntersectList(float topY) - { - if (this.actives == null || this.actives.NextInAEL == null) - { - return false; - } - - // Calculate edge positions at the top of the current scanbeam, and from this - // we will determine the intersections required to reach these new positions. - this.AdjustCurrXAndCopyToSEL(topY); - - // Find all edge intersections in the current scanbeam using a stable merge - // sort that ensures only adjacent edges are intersecting. Intersect info is - // stored in FIntersectList ready to be processed in ProcessIntersectList. - // Re merge sorts see https://stackoverflow.com/a/46319131/359538 - Active left = this.flaggedHorizontal; - Active right; - Active lEnd; - Active rEnd; - Active currBase; - Active prevBase; - Active tmp; - - while (left.Jump != null) - { - prevBase = null; - while (left?.Jump != null) - { - currBase = left; - right = left.Jump; - lEnd = right; - rEnd = right.Jump; - left.Jump = rEnd; - while (left != lEnd && right != rEnd) - { - if (right.CurX < left.CurX) - { - tmp = right.PrevInSEL; - while (true) - { - this.AddNewIntersectNode(tmp, right, topY); - if (tmp == left) - { - break; - } - - tmp = tmp.PrevInSEL; - } - - tmp = right; - right = ExtractFromSEL(tmp); - lEnd = right; - Insert1Before2InSEL(tmp, left); - if (left == currBase) - { - currBase = tmp; - currBase.Jump = rEnd; - if (prevBase == null) - { - this.flaggedHorizontal = currBase; - } - else - { - prevBase.Jump = currBase; - } - } - } - else - { - left = left.NextInSEL; - } - } - - prevBase = currBase; - left = rEnd; - } - - left = this.flaggedHorizontal; - } - - return this.intersectList.Count > 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ProcessIntersectList() - { - // We now have a list of intersections required so that edges will be - // correctly positioned at the top of the scanbeam. However, it's important - // that edge intersections are processed from the bottom up, but it's also - // crucial that intersections only occur between adjacent edges. - - // First we do a quicksort so intersections proceed in a bottom up order ... - this.intersectList.Sort(default(IntersectListSort)); - - // Now as we process these intersections, we must sometimes adjust the order - // to ensure that intersecting edges are always adjacent ... - for (int i = 0; i < this.intersectList.Count; ++i) - { - if (!EdgesAdjacentInAEL(this.intersectList[i])) - { - int j = i + 1; - while (!EdgesAdjacentInAEL(this.intersectList[j])) - { - j++; - } - - // swap - (this.intersectList[j], this.intersectList[i]) = - (this.intersectList[i], this.intersectList[j]); - } - - IntersectNode node = this.intersectList[i]; - this.IntersectEdges(node.Edge1, node.Edge2, node.Point); - this.SwapPositionsInAEL(node.Edge1, node.Edge2); - - node.Edge1.CurX = node.Point.X; - node.Edge2.CurX = node.Point.X; - this.CheckJoinLeft(node.Edge2, node.Point, true); - this.CheckJoinRight(node.Edge1, node.Point, true); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SwapPositionsInAEL(Active ae1, Active ae2) - { - // preconditon: ae1 must be immediately to the left of ae2 - Active next = ae2.NextInAEL; - if (next != null) - { - next.PrevInAEL = ae1; - } - - Active prev = ae1.PrevInAEL; - if (prev != null) - { - prev.NextInAEL = ae2; - } - - ae2.PrevInAEL = prev; - ae2.NextInAEL = ae1; - ae1.PrevInAEL = ae2; - ae1.NextInAEL = next; - if (ae2.PrevInAEL == null) - { - this.actives = ae2; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ResetHorzDirection(Active horz, Vertex vertexMax, out float leftX, out float rightX) - { - if (horz.Bot.X == horz.Top.X) - { - // the horizontal edge is going nowhere ... - leftX = horz.CurX; - rightX = horz.CurX; - Active ae = horz.NextInAEL; - while (ae != null && ae.VertexTop != vertexMax) - { - ae = ae.NextInAEL; - } - - return ae != null; - } - - if (horz.CurX < horz.Top.X) - { - leftX = horz.CurX; - rightX = horz.Top.X; - return true; - } - - leftX = horz.Top.X; - rightX = horz.CurX; - return false; // right to left - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool HorzIsSpike(Active horz) - { - Vector2 nextPt = NextVertex(horz).Point; - return (horz.Bot.X < horz.Top.X) != (horz.Top.X < nextPt.X); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active FindEdgeWithMatchingLocMin(Active e) - { - Active result = e.NextInAEL; - while (result != null) - { - if (result.LocalMin == e.LocalMin) - { - return result; - } - - if (!IsHorizontal(result) && e.Bot != result.Bot) - { - result = null; - } - else - { - result = result.NextInAEL; - } - } - - result = e.PrevInAEL; - while (result != null) - { - if (result.LocalMin == e.LocalMin) - { - return result; - } - - if (!IsHorizontal(result) && e.Bot != result.Bot) - { - return null; - } - - result = result.PrevInAEL; - } - - return result; - } - - private OutPt IntersectEdges(Active ae1, Active ae2, Vector2 pt) - { - OutPt resultOp = null; - - // MANAGE OPEN PATH INTERSECTIONS SEPARATELY ... - if (this.hasOpenPaths && (IsOpen(ae1) || IsOpen(ae2))) - { - if (IsOpen(ae1) && IsOpen(ae2)) - { - return null; - } - - // the following line avoids duplicating quite a bit of code - if (IsOpen(ae2)) - { - SwapActives(ref ae1, ref ae2); - } - - if (IsJoined(ae2)) - { - this.Split(ae2, pt); // needed for safety - } - - if (this.clipType == BooleanOperation.Union) - { - if (!IsHotEdge(ae2)) - { - return null; - } - } - else if (ae2.LocalMin.Polytype == ClippingType.Subject) - { - return null; - } - - switch (this.fillRule) - { - case ClipperFillRule.Positive: - if (ae2.WindCount != 1) - { - return null; - } - - break; - case ClipperFillRule.Negative: - if (ae2.WindCount != -1) - { - return null; - } - - break; - default: - if (Math.Abs(ae2.WindCount) != 1) - { - return null; - } - - break; - } - - // toggle contribution ... - if (IsHotEdge(ae1)) - { - resultOp = AddOutPt(ae1, pt); - if (IsFront(ae1)) - { - ae1.Outrec.FrontEdge = null; - } - else - { - ae1.Outrec.BackEdge = null; - } - - ae1.Outrec = null; - } - - // horizontal edges can pass under open paths at a LocMins - else if (pt == ae1.LocalMin.Vertex.Point && !IsOpenEnd(ae1.LocalMin.Vertex)) - { - // find the other side of the LocMin and - // if it's 'hot' join up with it ... - Active ae3 = FindEdgeWithMatchingLocMin(ae1); - if (ae3 != null && IsHotEdge(ae3)) - { - ae1.Outrec = ae3.Outrec; - if (ae1.WindDx > 0) - { - SetSides(ae3.Outrec!, ae1, ae3); - } - else - { - SetSides(ae3.Outrec!, ae3, ae1); - } - - return ae3.Outrec.Pts; - } - - resultOp = this.StartOpenPath(ae1, pt); - } - else - { - resultOp = this.StartOpenPath(ae1, pt); - } - - return resultOp; - } - - // MANAGING CLOSED PATHS FROM HERE ON - if (IsJoined(ae1)) - { - this.Split(ae1, pt); - } - - if (IsJoined(ae2)) - { - this.Split(ae2, pt); - } - - // UPDATE WINDING COUNTS... - int oldE1WindCount, oldE2WindCount; - if (ae1.LocalMin.Polytype == ae2.LocalMin.Polytype) - { - if (this.fillRule == ClipperFillRule.EvenOdd) - { - oldE1WindCount = ae1.WindCount; - ae1.WindCount = ae2.WindCount; - ae2.WindCount = oldE1WindCount; - } - else - { - if (ae1.WindCount + ae2.WindDx == 0) - { - ae1.WindCount = -ae1.WindCount; - } - else - { - ae1.WindCount += ae2.WindDx; - } - - if (ae2.WindCount - ae1.WindDx == 0) - { - ae2.WindCount = -ae2.WindCount; - } - else - { - ae2.WindCount -= ae1.WindDx; - } - } - } - else - { - if (this.fillRule != ClipperFillRule.EvenOdd) - { - ae1.WindCount2 += ae2.WindDx; - } - else - { - ae1.WindCount2 = ae1.WindCount2 == 0 ? 1 : 0; - } - - if (this.fillRule != ClipperFillRule.EvenOdd) - { - ae2.WindCount2 -= ae1.WindDx; - } - else - { - ae2.WindCount2 = ae2.WindCount2 == 0 ? 1 : 0; - } - } - - switch (this.fillRule) - { - case ClipperFillRule.Positive: - oldE1WindCount = ae1.WindCount; - oldE2WindCount = ae2.WindCount; - break; - case ClipperFillRule.Negative: - oldE1WindCount = -ae1.WindCount; - oldE2WindCount = -ae2.WindCount; - break; - default: - oldE1WindCount = Math.Abs(ae1.WindCount); - oldE2WindCount = Math.Abs(ae2.WindCount); - break; - } - - bool e1WindCountIs0or1 = oldE1WindCount is 0 or 1; - bool e2WindCountIs0or1 = oldE2WindCount is 0 or 1; - - if ((!IsHotEdge(ae1) && !e1WindCountIs0or1) || (!IsHotEdge(ae2) && !e2WindCountIs0or1)) - { - return null; - } - - // NOW PROCESS THE INTERSECTION ... - - // if both edges are 'hot' ... - if (IsHotEdge(ae1) && IsHotEdge(ae2)) - { - if ((oldE1WindCount != 0 && oldE1WindCount != 1) || (oldE2WindCount != 0 && oldE2WindCount != 1) || - (ae1.LocalMin.Polytype != ae2.LocalMin.Polytype && this.clipType != BooleanOperation.Xor)) - { - resultOp = this.AddLocalMaxPoly(ae1, ae2, pt); - } - else if (IsFront(ae1) || (ae1.Outrec == ae2.Outrec)) - { - // this 'else if' condition isn't strictly needed but - // it's sensible to split polygons that ony touch at - // a common vertex (not at common edges). - resultOp = this.AddLocalMaxPoly(ae1, ae2, pt); - this.AddLocalMinPoly(ae1, ae2, pt); - } - else - { - // can't treat as maxima & minima - resultOp = AddOutPt(ae1, pt); - AddOutPt(ae2, pt); - SwapOutrecs(ae1, ae2); - } - } - - // if one or other edge is 'hot' ... - else if (IsHotEdge(ae1)) - { - resultOp = AddOutPt(ae1, pt); - SwapOutrecs(ae1, ae2); - } - else if (IsHotEdge(ae2)) - { - resultOp = AddOutPt(ae2, pt); - SwapOutrecs(ae1, ae2); - } - - // neither edge is 'hot' - else - { - float e1Wc2, e2Wc2; - switch (this.fillRule) - { - case ClipperFillRule.Positive: - e1Wc2 = ae1.WindCount2; - e2Wc2 = ae2.WindCount2; - break; - case ClipperFillRule.Negative: - e1Wc2 = -ae1.WindCount2; - e2Wc2 = -ae2.WindCount2; - break; - default: - e1Wc2 = Math.Abs(ae1.WindCount2); - e2Wc2 = Math.Abs(ae2.WindCount2); - break; - } - - if (!IsSamePolyType(ae1, ae2)) - { - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - } - else if (oldE1WindCount == 1 && oldE2WindCount == 1) - { - resultOp = null; - switch (this.clipType) - { - case BooleanOperation.Union: - if (e1Wc2 > 0 && e2Wc2 > 0) - { - return null; - } - - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - break; - - case BooleanOperation.Difference: - if (((GetPolyType(ae1) == ClippingType.Clip) && (e1Wc2 > 0) && (e2Wc2 > 0)) - || ((GetPolyType(ae1) == ClippingType.Subject) && (e1Wc2 <= 0) && (e2Wc2 <= 0))) - { - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - } - - break; - - case BooleanOperation.Xor: - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - break; - - default: // ClipType.Intersection: - if (e1Wc2 <= 0 || e2Wc2 <= 0) - { - return null; - } - - resultOp = this.AddLocalMinPoly(ae1, ae2, pt); - break; - } - } - } - - return resultOp; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DeleteFromAEL(Active ae) - { - Active prev = ae.PrevInAEL; - Active next = ae.NextInAEL; - if (prev == null && next == null && (ae != this.actives)) - { - return; // already deleted - } - - if (prev != null) - { - prev.NextInAEL = next; - } - else - { - this.actives = next; - } - - if (next != null) - { - next.PrevInAEL = prev; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AdjustCurrXAndCopyToSEL(float topY) - { - Active ae = this.actives; - this.flaggedHorizontal = ae; - while (ae != null) - { - ae.PrevInSEL = ae.PrevInAEL; - ae.NextInSEL = ae.NextInAEL; - ae.Jump = ae.NextInSEL; - if (ae.JoinWith == JoinWith.Left) - { - ae.CurX = ae.PrevInAEL.CurX; // this also avoids complications - } - else - { - ae.CurX = TopX(ae, topY); - } - - // NB don't update ae.curr.Y yet (see AddNewIntersectNode) - ae = ae.NextInAEL; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool HasLocMinAtY(float y) - => this.currentLocMin < this.minimaList.Count && this.minimaList[this.currentLocMin].Vertex.Point.Y == y; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private LocalMinima PopLocalMinima() - => this.minimaList[this.currentLocMin++]; - - private void AddPathsToVertexList(PathsF paths, ClippingType polytype, bool isOpen) - { - int totalVertCnt = 0; - for (int i = 0; i < paths.Count; i++) - { - PathF path = paths[i]; - totalVertCnt += path.Count; - } - - this.vertexList.EnsureCapacity(this.vertexList.Count + totalVertCnt); - - foreach (PathF path in paths) - { - Vertex v0 = null, prev_v = null, curr_v; - foreach (Vector2 pt in path) - { - if (v0 == null) - { - v0 = new Vertex(pt, VertexFlags.None, null); - this.vertexList.Add(v0); - prev_v = v0; - } - else if (prev_v.Point != pt) - { - // ie skips duplicates - curr_v = new Vertex(pt, VertexFlags.None, prev_v); - this.vertexList.Add(curr_v); - prev_v.Next = curr_v; - prev_v = curr_v; - } - } - - if (prev_v == null || prev_v.Prev == null) - { - continue; - } - - if (!isOpen && prev_v.Point == v0.Point) - { - prev_v = prev_v.Prev; - } - - prev_v.Next = v0; - v0.Prev = prev_v; - if (!isOpen && prev_v.Next == prev_v) - { - continue; - } - - // OK, we have a valid path - bool going_up, going_up0; - if (isOpen) - { - curr_v = v0.Next; - while (curr_v != v0 && curr_v.Point.Y == v0.Point.Y) - { - curr_v = curr_v.Next; - } - - going_up = curr_v.Point.Y <= v0.Point.Y; - if (going_up) - { - v0.Flags = VertexFlags.OpenStart; - this.AddLocMin(v0, polytype, true); - } - else - { - v0.Flags = VertexFlags.OpenStart | VertexFlags.LocalMax; - } - } - else - { - // closed path - prev_v = v0.Prev; - while (prev_v != v0 && prev_v.Point.Y == v0.Point.Y) - { - prev_v = prev_v.Prev; - } - - if (prev_v == v0) - { - continue; // only open paths can be completely flat - } - - going_up = prev_v.Point.Y > v0.Point.Y; - } - - going_up0 = going_up; - prev_v = v0; - curr_v = v0.Next; - while (curr_v != v0) - { - if (curr_v.Point.Y > prev_v.Point.Y && going_up) - { - prev_v.Flags |= VertexFlags.LocalMax; - going_up = false; - } - else if (curr_v.Point.Y < prev_v.Point.Y && !going_up) - { - going_up = true; - this.AddLocMin(prev_v, polytype, isOpen); - } - - prev_v = curr_v; - curr_v = curr_v.Next; - } - - if (isOpen) - { - prev_v.Flags |= VertexFlags.OpenEnd; - if (going_up) - { - prev_v.Flags |= VertexFlags.LocalMax; - } - else - { - this.AddLocMin(prev_v, polytype, isOpen); - } - } - else if (going_up != going_up0) - { - if (going_up0) - { - this.AddLocMin(prev_v, polytype, false); - } - else - { - prev_v.Flags |= VertexFlags.LocalMax; - } - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddLocMin(Vertex vert, ClippingType polytype, bool isOpen) - { - // make sure the vertex is added only once. - if ((vert.Flags & VertexFlags.LocalMin) != VertexFlags.None) - { - return; - } - - vert.Flags |= VertexFlags.LocalMin; - - LocalMinima lm = new(vert, polytype, isOpen); - this.minimaList.Add(lm); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PushHorz(Active ae) - { - ae.NextInSEL = this.flaggedHorizontal; - this.flaggedHorizontal = ae; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool PopHorz(out Active ae) - { - ae = this.flaggedHorizontal; - if (this.flaggedHorizontal == null) - { - return false; - } - - this.flaggedHorizontal = this.flaggedHorizontal.NextInSEL; - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutPt AddLocalMinPoly(Active ae1, Active ae2, Vector2 pt, bool isNew = false) - { - OutRec outrec = this.NewOutRec(); - ae1.Outrec = outrec; - ae2.Outrec = outrec; - - if (IsOpen(ae1)) - { - outrec.Owner = null; - outrec.IsOpen = true; - if (ae1.WindDx > 0) - { - SetSides(outrec, ae1, ae2); - } - else - { - SetSides(outrec, ae2, ae1); - } - } - else - { - outrec.IsOpen = false; - Active prevHotEdge = GetPrevHotEdge(ae1); - - // e.windDx is the winding direction of the **input** paths - // and unrelated to the winding direction of output polygons. - // Output orientation is determined by e.outrec.frontE which is - // the ascending edge (see AddLocalMinPoly). - if (prevHotEdge != null) - { - outrec.Owner = prevHotEdge.Outrec; - if (OutrecIsAscending(prevHotEdge) == isNew) - { - SetSides(outrec, ae2, ae1); - } - else - { - SetSides(outrec, ae1, ae2); - } - } - else - { - outrec.Owner = null; - if (isNew) - { - SetSides(outrec, ae1, ae2); - } - else - { - SetSides(outrec, ae2, ae1); - } - } - } - - OutPt op = new(pt, outrec); - outrec.Pts = op; - return op; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetDx(Active ae) - => ae.Dx = GetDx(ae.Bot, ae.Top); - - /******************************************************************************* - * Dx: 0(90deg) * - * | * - * +inf (180deg) <--- o --. -inf (0deg) * - *******************************************************************************/ - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float GetDx(Vector2 pt1, Vector2 pt2) - { - float dy = pt2.Y - pt1.Y; - if (dy != 0) - { - return (pt2.X - pt1.X) / dy; - } - - if (pt2.X > pt1.X) - { - return float.NegativeInfinity; - } - - return float.PositiveInfinity; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float TopX(Active ae, float currentY) - { - Vector2 top = ae.Top; - Vector2 bottom = ae.Bot; - - if ((currentY == top.Y) || (top.X == bottom.X)) - { - return top.X; - } - - if (currentY == bottom.Y) - { - return bottom.X; - } - - return bottom.X + (ae.Dx * (currentY - bottom.Y)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHorizontal(Active ae) - => ae.Top.Y == ae.Bot.Y; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHeadingRightHorz(Active ae) - => float.IsNegativeInfinity(ae.Dx); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHeadingLeftHorz(Active ae) - => float.IsPositiveInfinity(ae.Dx); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SwapActives(ref Active ae1, ref Active ae2) - => (ae2, ae1) = (ae1, ae2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ClippingType GetPolyType(Active ae) - => ae.LocalMin.Polytype; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsSamePolyType(Active ae1, Active ae2) - => ae1.LocalMin.Polytype == ae2.LocalMin.Polytype; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContributingClosed(Active ae) - { - switch (this.fillRule) - { - case ClipperFillRule.Positive: - if (ae.WindCount != 1) - { - return false; - } - - break; - case ClipperFillRule.Negative: - if (ae.WindCount != -1) - { - return false; - } - - break; - case ClipperFillRule.NonZero: - if (Math.Abs(ae.WindCount) != 1) - { - return false; - } - - break; - } - - switch (this.clipType) - { - case BooleanOperation.Intersection: - return this.fillRule switch - { - ClipperFillRule.Positive => ae.WindCount2 > 0, - ClipperFillRule.Negative => ae.WindCount2 < 0, - _ => ae.WindCount2 != 0, - }; - - case BooleanOperation.Union: - return this.fillRule switch - { - ClipperFillRule.Positive => ae.WindCount2 <= 0, - ClipperFillRule.Negative => ae.WindCount2 >= 0, - _ => ae.WindCount2 == 0, - }; - - case BooleanOperation.Difference: - bool result = this.fillRule switch - { - ClipperFillRule.Positive => ae.WindCount2 <= 0, - ClipperFillRule.Negative => ae.WindCount2 >= 0, - _ => ae.WindCount2 == 0, - }; - return (GetPolyType(ae) == ClippingType.Subject) ? result : !result; - - case BooleanOperation.Xor: - return true; // XOr is always contributing unless open - - default: - return false; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool IsContributingOpen(Active ae) - { - bool isInClip, isInSubj; - switch (this.fillRule) - { - case ClipperFillRule.Positive: - isInSubj = ae.WindCount > 0; - isInClip = ae.WindCount2 > 0; - break; - case ClipperFillRule.Negative: - isInSubj = ae.WindCount < 0; - isInClip = ae.WindCount2 < 0; - break; - default: - isInSubj = ae.WindCount != 0; - isInClip = ae.WindCount2 != 0; - break; - } - - bool result = this.clipType switch - { - BooleanOperation.Intersection => isInClip, - BooleanOperation.Union => !isInSubj && !isInClip, - _ => !isInClip - }; - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetWindCountForClosedPathEdge(Active ae) - { - // Wind counts refer to polygon regions not edges, so here an edge's WindCnt - // indicates the higher of the wind counts for the two regions touching the - // edge. (nb: Adjacent regions can only ever have their wind counts differ by - // one. Also, open paths have no meaningful wind directions or counts.) - Active ae2 = ae.PrevInAEL; - - // find the nearest closed path edge of the same PolyType in AEL (heading left) - ClippingType pt = GetPolyType(ae); - while (ae2 != null && (GetPolyType(ae2) != pt || IsOpen(ae2))) - { - ae2 = ae2.PrevInAEL; - } - - if (ae2 == null) - { - ae.WindCount = ae.WindDx; - ae2 = this.actives; - } - else if (this.fillRule == ClipperFillRule.EvenOdd) - { - ae.WindCount = ae.WindDx; - ae.WindCount2 = ae2.WindCount2; - ae2 = ae2.NextInAEL; - } - else - { - // NonZero, positive, or negative filling here ... - // when e2's WindCnt is in the SAME direction as its WindDx, - // then polygon will fill on the right of 'e2' (and 'e' will be inside) - // nb: neither e2.WindCnt nor e2.WindDx should ever be 0. - if (ae2.WindCount * ae2.WindDx < 0) - { - // opposite directions so 'ae' is outside 'ae2' ... - if (Math.Abs(ae2.WindCount) > 1) - { - // outside prev poly but still inside another. - if (ae2.WindDx * ae.WindDx < 0) - { - // reversing direction so use the same WC - ae.WindCount = ae2.WindCount; - } - else - { - // otherwise keep 'reducing' the WC by 1 (i.e. towards 0) ... - ae.WindCount = ae2.WindCount + ae.WindDx; - } - } - else - { - // now outside all polys of same polytype so set own WC ... - ae.WindCount = IsOpen(ae) ? 1 : ae.WindDx; - } - } - else - { - // 'ae' must be inside 'ae2' - if (ae2.WindDx * ae.WindDx < 0) - { - // reversing direction so use the same WC - ae.WindCount = ae2.WindCount; - } - else - { - // otherwise keep 'increasing' the WC by 1 (i.e. away from 0) ... - ae.WindCount = ae2.WindCount + ae.WindDx; - } - } - - ae.WindCount2 = ae2.WindCount2; - ae2 = ae2.NextInAEL; // i.e. get ready to calc WindCnt2 - } - - // update windCount2 ... - if (this.fillRule == ClipperFillRule.EvenOdd) - { - while (ae2 != ae) - { - if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) - { - ae.WindCount2 = ae.WindCount2 == 0 ? 1 : 0; - } - - ae2 = ae2.NextInAEL; - } - } - else - { - while (ae2 != ae) - { - if (GetPolyType(ae2!) != pt && !IsOpen(ae2!)) - { - ae.WindCount2 += ae2.WindDx; - } - - ae2 = ae2.NextInAEL; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetWindCountForOpenPathEdge(Active ae) - { - Active ae2 = this.actives; - if (this.fillRule == ClipperFillRule.EvenOdd) - { - int cnt1 = 0, cnt2 = 0; - while (ae2 != ae) - { - if (GetPolyType(ae2!) == ClippingType.Clip) - { - cnt2++; - } - else if (!IsOpen(ae2!)) - { - cnt1++; - } - - ae2 = ae2.NextInAEL; - } - - ae.WindCount = IsOdd(cnt1) ? 1 : 0; - ae.WindCount2 = IsOdd(cnt2) ? 1 : 0; - } - else - { - while (ae2 != ae) - { - if (GetPolyType(ae2!) == ClippingType.Clip) - { - ae.WindCount2 += ae2.WindDx; - } - else if (!IsOpen(ae2!)) - { - ae.WindCount += ae2.WindDx; - } - - ae2 = ae2.NextInAEL; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsValidAelOrder(Active resident, Active newcomer) - { - if (newcomer.CurX != resident.CurX) - { - return newcomer.CurX > resident.CurX; - } - - // get the turning direction a1.top, a2.bot, a2.top - float d = PolygonClipperUtilities.CrossProduct(resident.Top, newcomer.Bot, newcomer.Top); - if (d != 0) - { - return d < 0; - } - - // edges must be collinear to get here - - // for starting open paths, place them according to - // the direction they're about to turn - if (!IsMaxima(resident) && (resident.Top.Y > newcomer.Top.Y)) - { - return PolygonClipperUtilities.CrossProduct(newcomer.Bot, resident.Top, NextVertex(resident).Point) <= 0; - } - - if (!IsMaxima(newcomer) && (newcomer.Top.Y > resident.Top.Y)) - { - return PolygonClipperUtilities.CrossProduct(newcomer.Bot, newcomer.Top, NextVertex(newcomer).Point) >= 0; - } - - float y = newcomer.Bot.Y; - bool newcomerIsLeft = newcomer.IsLeftBound; - - if (resident.Bot.Y != y || resident.LocalMin.Vertex.Point.Y != y) - { - return newcomer.IsLeftBound; - } - - // resident must also have just been inserted - if (resident.IsLeftBound != newcomerIsLeft) - { - return newcomerIsLeft; - } - - if (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, resident.Bot, resident.Top) == 0) - { - return true; - } - - // compare turning direction of the alternate bound - return (PolygonClipperUtilities.CrossProduct(PrevPrevVertex(resident).Point, newcomer.Bot, PrevPrevVertex(newcomer).Point) > 0) == newcomerIsLeft; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void InsertLeftEdge(Active ae) - { - Active ae2; - - if (this.actives == null) - { - ae.PrevInAEL = null; - ae.NextInAEL = null; - this.actives = ae; - } - else if (!IsValidAelOrder(this.actives, ae)) - { - ae.PrevInAEL = null; - ae.NextInAEL = this.actives; - this.actives.PrevInAEL = ae; - this.actives = ae; - } - else - { - ae2 = this.actives; - while (ae2.NextInAEL != null && IsValidAelOrder(ae2.NextInAEL, ae)) - { - ae2 = ae2.NextInAEL; - } - - // don't separate joined edges - if (ae2.JoinWith == JoinWith.Right) - { - ae2 = ae2.NextInAEL; - } - - ae.NextInAEL = ae2.NextInAEL; - if (ae2.NextInAEL != null) - { - ae2.NextInAEL.PrevInAEL = ae; - } - - ae.PrevInAEL = ae2; - ae2.NextInAEL = ae; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void InsertRightEdge(Active ae, Active ae2) - { - ae2.NextInAEL = ae.NextInAEL; - if (ae.NextInAEL != null) - { - ae.NextInAEL.PrevInAEL = ae2; - } - - ae2.PrevInAEL = ae; - ae.NextInAEL = ae2; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex NextVertex(Active ae) - { - if (ae.WindDx > 0) - { - return ae.VertexTop.Next; - } - - return ae.VertexTop.Prev; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vertex PrevPrevVertex(Active ae) - { - if (ae.WindDx > 0) - { - return ae.VertexTop.Prev.Prev; - } - - return ae.VertexTop.Next.Next; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsMaxima(Vertex vertex) - => (vertex.Flags & VertexFlags.LocalMax) != VertexFlags.None; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsMaxima(Active ae) - => IsMaxima(ae.VertexTop); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active GetMaximaPair(Active ae) - { - Active ae2; - ae2 = ae.NextInAEL; - while (ae2 != null) - { - if (ae2.VertexTop == ae.VertexTop) - { - return ae2; // Found! - } - - ae2 = ae2.NextInAEL; - } - - return null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOdd(int val) - => (val & 1) != 0; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHotEdge(Active ae) - => ae.Outrec != null; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOpen(Active ae) - => ae.LocalMin.IsOpen; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOpenEnd(Active ae) - => ae.LocalMin.IsOpen && IsOpenEnd(ae.VertexTop); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsOpenEnd(Vertex v) - => (v.Flags & (VertexFlags.OpenStart | VertexFlags.OpenEnd)) != VertexFlags.None; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Active GetPrevHotEdge(Active ae) - { - Active prev = ae.PrevInAEL; - while (prev != null && (IsOpen(prev) || !IsHotEdge(prev))) - { - prev = prev.PrevInAEL; - } - - return prev; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void JoinOutrecPaths(Active ae1, Active ae2) - { - // join ae2 outrec path onto ae1 outrec path and then delete ae2 outrec path - // pointers. (NB Only very rarely do the joining ends share the same coords.) - OutPt p1Start = ae1.Outrec.Pts; - OutPt p2Start = ae2.Outrec.Pts; - OutPt p1End = p1Start.Next; - OutPt p2End = p2Start.Next; - if (IsFront(ae1)) - { - p2End.Prev = p1Start; - p1Start.Next = p2End; - p2Start.Next = p1End; - p1End.Prev = p2Start; - ae1.Outrec.Pts = p2Start; - - // nb: if IsOpen(e1) then e1 & e2 must be a 'maximaPair' - ae1.Outrec.FrontEdge = ae2.Outrec.FrontEdge; - if (ae1.Outrec.FrontEdge != null) - { - ae1.Outrec.FrontEdge.Outrec = ae1.Outrec; - } - } - else - { - p1End.Prev = p2Start; - p2Start.Next = p1End; - p1Start.Next = p2End; - p2End.Prev = p1Start; - - ae1.Outrec.BackEdge = ae2.Outrec.BackEdge; - if (ae1.Outrec.BackEdge != null) - { - ae1.Outrec.BackEdge.Outrec = ae1.Outrec; - } - } - - // after joining, the ae2.OutRec must contains no vertices ... - ae2.Outrec.FrontEdge = null; - ae2.Outrec.BackEdge = null; - ae2.Outrec.Pts = null; - SetOwner(ae2.Outrec, ae1.Outrec); - - if (IsOpenEnd(ae1)) - { - ae2.Outrec.Pts = ae1.Outrec.Pts; - ae1.Outrec.Pts = null; - } - - // and ae1 and ae2 are maxima and are about to be dropped from the Actives list. - ae1.Outrec = null; - ae2.Outrec = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutPt AddOutPt(Active ae, Vector2 pt) - { - // Outrec.OutPts: a circular doubly-linked-list of POutPt where ... - // opFront[.Prev]* ~~~> opBack & opBack == opFront.Next - OutRec outrec = ae.Outrec; - bool toFront = IsFront(ae); - OutPt opFront = outrec.Pts; - OutPt opBack = opFront.Next; - - if (toFront && (pt == opFront.Point)) - { - return opFront; - } - else if (!toFront && (pt == opBack.Point)) - { - return opBack; - } - - OutPt newOp = new(pt, outrec); - opBack.Prev = newOp; - newOp.Prev = opFront; - newOp.Next = opBack; - opFront.Next = newOp; - if (toFront) - { - outrec.Pts = newOp; - } - - return newOp; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutRec NewOutRec() - { - OutRec result = new() - { - Idx = this.outrecList.Count - }; - this.outrecList.Add(result); - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutPt StartOpenPath(Active ae, Vector2 pt) - { - OutRec outrec = this.NewOutRec(); - outrec.IsOpen = true; - if (ae.WindDx > 0) - { - outrec.FrontEdge = ae; - outrec.BackEdge = null; - } - else - { - outrec.FrontEdge = null; - outrec.BackEdge = ae; - } - - ae.Outrec = outrec; - OutPt op = new(pt, outrec); - outrec.Pts = op; - return op; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateEdgeIntoAEL(Active ae) - { - ae.Bot = ae.Top; - ae.VertexTop = NextVertex(ae); - ae.Top = ae.VertexTop.Point; - ae.CurX = ae.Bot.X; - SetDx(ae); - - if (IsJoined(ae)) - { - this.Split(ae, ae.Bot); - } - - if (IsHorizontal(ae)) - { - return; - } - - this.InsertScanline(ae.Top.Y); - - this.CheckJoinLeft(ae, ae.Bot); - this.CheckJoinRight(ae, ae.Bot, true); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetSides(OutRec outrec, Active startEdge, Active endEdge) - { - outrec.FrontEdge = startEdge; - outrec.BackEdge = endEdge; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SwapOutrecs(Active ae1, Active ae2) - { - OutRec or1 = ae1.Outrec; // at least one edge has - OutRec or2 = ae2.Outrec; // an assigned outrec - if (or1 == or2) - { - (or1.BackEdge, or1.FrontEdge) = (or1.FrontEdge, or1.BackEdge); - return; - } - - if (or1 != null) - { - if (ae1 == or1.FrontEdge) - { - or1.FrontEdge = ae2; - } - else - { - or1.BackEdge = ae2; - } - } - - if (or2 != null) - { - if (ae2 == or2.FrontEdge) - { - or2.FrontEdge = ae1; - } - else - { - or2.BackEdge = ae1; - } - } - - ae1.Outrec = or2; - ae2.Outrec = or1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetOwner(OutRec outrec, OutRec newOwner) - { - // precondition1: new_owner is never null - while (newOwner.Owner != null && newOwner.Owner.Pts == null) - { - newOwner.Owner = newOwner.Owner.Owner; - } - - // make sure that outrec isn't an owner of newOwner - OutRec tmp = newOwner; - while (tmp != null && tmp != outrec) - { - tmp = tmp.Owner; - } - - if (tmp != null) - { - newOwner.Owner = outrec.Owner; - } - - outrec.Owner = newOwner; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float Area(OutPt op) - { - // https://en.wikipedia.org/wiki/Shoelace_formula - float area = 0; - OutPt op2 = op; - do - { - area += (op2.Prev.Point.Y + op2.Point.Y) * (op2.Prev.Point.X - op2.Point.X); - op2 = op2.Next; - } - while (op2 != op); - return area * .5F; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static float AreaTriangle(Vector2 pt1, Vector2 pt2, Vector2 pt3) - => ((pt3.Y + pt1.Y) * (pt3.X - pt1.X)) - + ((pt1.Y + pt2.Y) * (pt1.X - pt2.X)) - + ((pt2.Y + pt3.Y) * (pt2.X - pt3.X)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static OutRec GetRealOutRec(OutRec outRec) - { - while ((outRec != null) && (outRec.Pts == null)) - { - outRec = outRec.Owner; - } - - return outRec; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void UncoupleOutRec(Active ae) - { - OutRec outrec = ae.Outrec; - if (outrec == null) - { - return; - } - - outrec.FrontEdge.Outrec = null; - outrec.BackEdge.Outrec = null; - outrec.FrontEdge = null; - outrec.BackEdge = null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool OutrecIsAscending(Active hotEdge) - => hotEdge == hotEdge.Outrec.FrontEdge; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SwapFrontBackSides(OutRec outrec) - { - // while this proc. is needed for open paths - // it's almost never needed for closed paths - (outrec.BackEdge, outrec.FrontEdge) = (outrec.FrontEdge, outrec.BackEdge); - outrec.Pts = outrec.Pts.Next; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool EdgesAdjacentInAEL(IntersectNode inode) - => (inode.Edge1.NextInAEL == inode.Edge2) || (inode.Edge1.PrevInAEL == inode.Edge2); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CheckJoinLeft(Active e, Vector2 pt, bool checkCurrX = false) - { - Active prev = e.PrevInAEL; - if (prev == null - || IsOpen(e) - || IsOpen(prev) - || !IsHotEdge(e) - || !IsHotEdge(prev)) - { - return; - } - - // Avoid trivial joins - if ((pt.Y < e.Top.Y + JoinYTolerance || pt.Y < prev.Top.Y + JoinYTolerance) - && ((e.Bot.Y > pt.Y) || (prev.Bot.Y > pt.Y))) - { - return; - } - - if (checkCurrX) - { - if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, prev.Bot, prev.Top) > JoinDistanceSqrdThreshold) - { - return; - } - } - else if (e.CurX != prev.CurX) - { - return; - } - - if (PolygonClipperUtilities.CrossProduct(e.Top, pt, prev.Top) != 0) - { - return; - } - - if (e.Outrec.Idx == prev.Outrec.Idx) - { - this.AddLocalMaxPoly(prev, e, pt); - } - else if (e.Outrec.Idx < prev.Outrec.Idx) - { - JoinOutrecPaths(e, prev); - } - else - { - JoinOutrecPaths(prev, e); - } - - prev.JoinWith = JoinWith.Right; - e.JoinWith = JoinWith.Left; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CheckJoinRight(Active e, Vector2 pt, bool checkCurrX = false) - { - Active next = e.NextInAEL; - if (IsOpen(e) - || !IsHotEdge(e) - || IsJoined(e) - || next == null - || IsOpen(next) - || !IsHotEdge(next)) - { - return; - } - - // Avoid trivial joins - if ((pt.Y < e.Top.Y + JoinYTolerance || pt.Y < next.Top.Y + JoinYTolerance) - && ((e.Bot.Y > pt.Y) || (next.Bot.Y > pt.Y))) - { - return; - } - - if (checkCurrX) - { - if (PolygonClipperUtilities.PerpendicDistFromLineSqrd(pt, next.Bot, next.Top) > JoinDistanceSqrdThreshold) - { - return; - } - } - else if (e.CurX != next.CurX) - { - return; - } - - if (PolygonClipperUtilities.CrossProduct(e.Top, pt, next.Top) != 0) - { - return; - } - - if (e.Outrec.Idx == next.Outrec.Idx) - { - this.AddLocalMaxPoly(e, next, pt); - } - else if (e.Outrec.Idx < next.Outrec.Idx) - { - JoinOutrecPaths(e, next); - } - else - { - JoinOutrecPaths(next, e); - } - - e.JoinWith = JoinWith.Right; - next.JoinWith = JoinWith.Left; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void FixOutRecPts(OutRec outrec) - { - OutPt op = outrec.Pts; - do - { - op.OutRec = outrec; - op = op.Next; - } - while (op != outrec.Pts); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private OutPt AddLocalMaxPoly(Active ae1, Active ae2, Vector2 pt) - { - if (IsJoined(ae1)) - { - this.Split(ae1, pt); - } - - if (IsJoined(ae2)) - { - this.Split(ae2, pt); - } - - if (IsFront(ae1) == IsFront(ae2)) - { - if (IsOpenEnd(ae1)) - { - SwapFrontBackSides(ae1.Outrec!); - } - else if (IsOpenEnd(ae2)) - { - SwapFrontBackSides(ae2.Outrec!); - } - else - { - return null; - } - } - - OutPt result = AddOutPt(ae1, pt); - if (ae1.Outrec == ae2.Outrec) - { - OutRec outrec = ae1.Outrec; - outrec.Pts = result; - UncoupleOutRec(ae1); - } - - // and to preserve the winding orientation of outrec ... - else if (IsOpen(ae1)) - { - if (ae1.WindDx < 0) - { - JoinOutrecPaths(ae1, ae2); - } - else - { - JoinOutrecPaths(ae2, ae1); - } - } - else if (ae1.Outrec.Idx < ae2.Outrec.Idx) - { - JoinOutrecPaths(ae1, ae2); - } - else - { - JoinOutrecPaths(ae2, ae1); - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsJoined(Active e) - => e.JoinWith != JoinWith.None; - - private void Split(Active e, Vector2 currPt) - { - if (e.JoinWith == JoinWith.Right) - { - e.JoinWith = JoinWith.None; - e.NextInAEL.JoinWith = JoinWith.None; - this.AddLocalMinPoly(e, e.NextInAEL, currPt, true); - } - else - { - e.JoinWith = JoinWith.None; - e.PrevInAEL.JoinWith = JoinWith.None; - this.AddLocalMinPoly(e.PrevInAEL, e, currPt, true); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsFront(Active ae) - => ae == ae.Outrec.FrontEdge; - - /// - /// Comparer for sorting local minima by Y coordinate (descending). - /// - private struct LocMinSorter : IComparer - { - public readonly int Compare(LocalMinima locMin1, LocalMinima locMin2) - => locMin2.Vertex.Point.Y.CompareTo(locMin1.Vertex.Point.Y); - } - - /// - /// Represents a local minimum in a polygon path. - /// A local minimum is a vertex where the path changes from descending to ascending. - /// - private readonly struct LocalMinima - { - /// - /// Gets the vertex at the local minimum. - /// - public readonly Vertex Vertex; - - /// - /// Gets the polygon type (subject or clip). - /// - public readonly ClippingType Polytype; - - /// - /// Gets a value indicating whether this is an open path (polyline). - /// - public readonly bool IsOpen; - - /// - /// Initializes a new instance of the struct. - /// - /// The vertex at the local minimum. - /// The polygon type. - /// Whether this is an open path. - public LocalMinima(Vertex vertex, ClippingType polytype, bool isOpen = false) - { - this.Vertex = vertex; - this.Polytype = polytype; - this.IsOpen = isOpen; - } - - public static bool operator ==(LocalMinima lm1, LocalMinima lm2) - // Use reference equality for vertex comparison - // TODO: Check this. Why ref equals. - => ReferenceEquals(lm1.Vertex, lm2.Vertex); - - public static bool operator !=(LocalMinima lm1, LocalMinima lm2) - => !(lm1 == lm2); - - public override bool Equals(object obj) - => obj is LocalMinima minima && this == minima; - - public override int GetHashCode() - => this.Vertex.GetHashCode(); - } - - /// - /// Represents an intersection between two active edges. - /// Intersections must be sorted so they are processed from the largest - /// Y coordinates to the smallest while keeping edges adjacent. - /// - private readonly struct IntersectNode - { - /// - /// Gets the intersection point. - /// - public readonly Vector2 Point; - - /// - /// Gets the first intersecting edge. - /// - public readonly Active Edge1; - - /// - /// Gets the second intersecting edge. - /// - public readonly Active Edge2; - - /// - /// Initializes a new instance of the struct. - /// - /// The intersection point. - /// The first intersecting edge. - /// The second intersecting edge. - public IntersectNode(Vector2 pt, Active edge1, Active edge2) - { - this.Point = pt; - this.Edge1 = edge1; - this.Edge2 = edge2; - } - } - - /// - /// Comparer for sorting horizontal segments by left X coordinate. - /// - private struct HorzSegSorter : IComparer - { - public readonly int Compare(HorzSegment hs1, HorzSegment hs2) - { - if (hs1 == null || hs2 == null) - { - return 0; - } - - if (hs1.RightOp == null) - { - return hs2.RightOp == null ? 0 : 1; - } - else if (hs2.RightOp == null) - { - return -1; - } - else - { - return hs1.LeftOp.Point.X.CompareTo(hs2.LeftOp.Point.X); - } - } - } - - /// - /// Comparer for sorting intersection nodes by Y coordinate (descending), then X coordinate. - /// - private struct IntersectListSort : IComparer - { - public readonly int Compare(IntersectNode a, IntersectNode b) - { - if (a.Point.Y == b.Point.Y) - { - if (a.Point.X == b.Point.X) - { - return 0; - } - - return (a.Point.X < b.Point.X) ? -1 : 1; - } - - return (a.Point.Y > b.Point.Y) ? -1 : 1; - } - } - - /// - /// Represents a horizontal segment in the output polygon. - /// Used to identify and join horizontal edges. - /// - private class HorzSegment - { - /// - /// Initializes a new instance of the class. - /// - /// The starting output point. - public HorzSegment(OutPt op) - { - this.LeftOp = op; - this.RightOp = null; - this.LeftToRight = true; - } - - /// - /// Gets or sets the left output point. - /// - public OutPt LeftOp { get; set; } - - /// - /// Gets or sets the right output point. - /// - public OutPt RightOp { get; set; } - - /// - /// Gets or sets a value indicating whether the segment is oriented left-to-right. - /// - public bool LeftToRight { get; set; } - } - - /// - /// Represents a horizontal join operation between two output points. - /// - private class HorzJoin - { - /// - /// Initializes a new instance of the class. - /// - /// Left-to-right output point. - /// Right-to-left output point. - public HorzJoin(OutPt ltor, OutPt rtol) - { - this.Op1 = ltor; - this.Op2 = rtol; - } - - /// - /// Gets the first output point in the join. - /// - public OutPt Op1 { get; } - - /// - /// Gets the second output point in the join. - /// - public OutPt Op2 { get; } - } - - /// - /// Output point: represents a vertex in a clipping solution polygon. - /// Forms a circular doubly-linked list of vertices. - /// - private class OutPt - { - /// - /// Initializes a new instance of the class. - /// - /// The point coordinates. - /// The output record this point belongs to. - public OutPt(Vector2 pt, OutRec outrec) - { - this.Point = pt; - this.OutRec = outrec; - this.Next = this; - this.Prev = this; - this.HorizSegment = null; - } - - /// - /// Gets the point coordinates. - /// - public Vector2 Point { get; } - - /// - /// Gets or sets the next output point in the circular list. - /// - public OutPt Next { get; set; } - - /// - /// Gets or sets the previous output point in the circular list. - /// - public OutPt Prev { get; set; } - - /// - /// Gets or sets the output record this point belongs to. - /// - public OutRec OutRec { get; set; } - - /// - /// Gets or sets the horizontal segment this point is part of (if any). - /// - public HorzSegment HorizSegment { get; set; } - } - - /// - /// Output record: represents a complete output polygon path. - /// Contains a circular doubly-linked list of output points. - /// - private class OutRec - { - /// - /// Gets or sets the index of this output record in the output list. - /// - public int Idx { get; set; } - - /// - /// Gets or sets the parent output record (for holes). - /// - public OutRec Owner { get; set; } - - /// - /// Gets or sets the front (ascending) edge of the output polygon. - /// - public Active FrontEdge { get; set; } - - /// - /// Gets or sets the back (descending) edge of the output polygon. - /// - public Active BackEdge { get; set; } - - /// - /// Gets or sets the starting point in the circular output point list. - /// - public OutPt Pts { get; set; } - - /// - /// Gets or sets the polytree path (for hierarchical output). - /// - public PolyPathF PolyPath { get; set; } - - /// - /// Gets or sets the bounding rectangle. - /// - public BoundsF Bounds { get; set; } - - /// - /// Gets or sets the final output path. - /// - public PathF Path { get; set; } = []; - - /// - /// Gets or sets a value indicating whether this is an open path. - /// - public bool IsOpen { get; set; } - - /// - /// Gets or sets the list of split indices (for self-intersecting polygons). - /// - public List Splits { get; set; } - } - - /// - /// Represents a vertex in an input polygon path. - /// Forms a circular doubly-linked list of vertices. - /// - private class Vertex - { - /// - /// Initializes a new instance of the class. - /// - /// The point coordinates. - /// Vertex flags (local min/max, open start/end). - /// The previous vertex in the list. - public Vertex(Vector2 pt, VertexFlags flags, Vertex prev) - { - this.Point = pt; - this.Flags = flags; - this.Next = null; - this.Prev = prev; - } - - /// - /// Gets the point coordinates. - /// - public Vector2 Point { get; } - - /// - /// Gets or sets the next vertex in the circular list. - /// - public Vertex Next { get; set; } - - /// - /// Gets or sets the previous vertex in the circular list. - /// - public Vertex Prev { get; set; } - - /// - /// Gets or sets the vertex flags indicating properties like local minima/maxima. - /// - public VertexFlags Flags { get; set; } - } - - /// - /// Active edge: represents an edge currently intersecting the scanline. - /// Stored in the Active Edge List (AEL) during scanline processing. - /// - private class Active - { - /// - /// Gets or sets the bottom point of the edge. - /// - public Vector2 Bot { get; set; } - - /// - /// Gets or sets the top point of the edge. - /// - public Vector2 Top { get; set; } - - /// - /// Gets or sets the current X coordinate at the scanline (updated at every scanline). - /// - public float CurX { get; set; } - - /// - /// Gets or sets the edge's reciprocal slope (dx/dy). - /// - public float Dx { get; set; } - - /// - /// Gets or sets the winding direction (1 for ascending, -1 for descending). - /// - public int WindDx { get; set; } - - /// - /// Gets or sets the winding count for this edge's polygon type. - /// - public int WindCount { get; set; } - - /// - /// Gets or sets the winding count for the opposite polygon type. - /// - public int WindCount2 { get; set; } - - /// - /// Gets or sets the output record this edge contributes to. - /// - public OutRec Outrec { get; set; } - - /// - /// Gets or sets the previous edge in the Active Edge List. - /// The AEL is a doubly-linked list of all edges intersecting the current scanbeam, - /// ordered from left to right. - /// - public Active PrevInAEL { get; set; } - - /// - /// Gets or sets the next edge in the Active Edge List. - /// - public Active NextInAEL { get; set; } - - /// - /// Gets or sets the previous edge in the Sorted Edge List. - /// The SEL is used when sorting edges into their new positions at scanbeam tops, - /// and is also reused to process horizontal edges. - /// - public Active PrevInSEL { get; set; } - - /// - /// Gets or sets the next edge in the Sorted Edge List. - /// - public Active NextInSEL { get; set; } - - /// - /// Gets or sets the jump pointer used during merge sort operations. - /// - public Active Jump { get; set; } - - /// - /// Gets or sets the vertex at the top of this edge segment. - /// - public Vertex VertexTop { get; set; } - - /// - /// Gets or sets the local minimum this edge belongs to. - /// - public LocalMinima LocalMin { get; set; } - - /// - /// Gets or sets a value indicating whether this is a left bound edge. - /// - public bool IsLeftBound { get; set; } - - /// - /// Gets or sets the join status indicating if this edge is joined with an adjacent edge. - /// - public JoinWith JoinWith { get; set; } - } -} - -/// -/// Represents a node in a hierarchical polygon tree structure. -/// Can contain child paths representing holes or nested polygons. -/// -internal class PolyPathF : IEnumerable -{ - private readonly PolyPathF parent; - private readonly List items = []; - - /// - /// Initializes a new instance of the class. - /// - /// The parent path, or null for the root. - public PolyPathF(PolyPathF parent = null) - => this.parent = parent; - - /// - /// Gets the polygon path. The polytree root's polygon is null. - /// - public PathF Polygon { get; private set; } - - /// - /// Gets the nesting level in the tree (0 for root). - /// - public int Level => this.GetLevel(); - - /// - /// Gets a value indicating whether this path represents a hole. - /// - public bool IsHole => this.GetIsHole(); - - /// - /// Gets the number of child paths. - /// - public int Count => this.items.Count; - - /// - /// Gets the child path at the specified index. - /// - /// The child index. - /// The child path. - public PolyPathF this[int index] => this.items[index]; - - /// - /// Adds a child path to this polytree node. - /// - /// The polygon path to add. - /// The created child node. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PolyPathF AddChild(PathF p) - { - PolyPathF child = new(this) - { - Polygon = p - }; - - this.items.Add(child); - return child; - } - - /// - /// Calculates the total area of this polygon and all its children. - /// - /// The signed area. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public float Area() - { - float result = this.Polygon == null ? 0 : PolygonClipperUtilities.SignedArea(this.Polygon); - for (int i = 0; i < this.items.Count; i++) - { - PolyPathF child = this.items[i]; - result += child.Area(); - } - - return result; - } - - /// - /// Removes all child paths. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Clear() => this.items.Clear(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool GetIsHole() - { - int lvl = this.Level; - return lvl != 0 && (lvl & 1) == 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetLevel() - { - int result = 0; - PolyPathF pp = this.parent; - while (pp != null) - { - ++result; - pp = pp.parent; - } - - return result; - } - - /// - /// Returns an enumerator that iterates through the child paths. - /// - /// An enumerator for the children. - public IEnumerator GetEnumerator() => this.items.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => this.items.GetEnumerator(); -} - -/// -/// Root of a polytree structure containing hierarchical polygon data. -/// -internal class PolyTreeF : PolyPathF -{ -} - -/// -/// Collection of polygon paths. -/// -internal class PathsF : List -{ - /// - /// Initializes a new instance of the class. - /// - public PathsF() - { - } - - /// - /// Initializes a new instance of the class with items. - /// - /// Initial paths. - public PathsF(IEnumerable items) - : base(items) - { - } - - /// - /// Initializes a new instance of the class with capacity. - /// - /// Initial capacity. - public PathsF(int capacity) - : base(capacity) - { - } -} - -/// -/// Represents a polygon path as a list of points. -/// -internal class PathF : List -{ - /// - /// Initializes a new instance of the class. - /// - public PathF() - { - } - - /// - /// Initializes a new instance of the class with items. - /// - /// Initial points. - public PathF(IEnumerable items) - : base(items) - { - } - - /// - /// Initializes a new instance of the class with capacity. - /// - /// Initial capacity. - public PathF(int capacity) - : base(capacity) - { - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs new file mode 100644 index 00000000..dfe11f4d --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.PolygonClipper; +using PCPolygon = SixLabors.PolygonClipper.Polygon; + +namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; + +/// +/// Builders for from ImageSharp paths. +/// Converts ImageSharp paths to the format required by PolygonClipper. +/// +/// +/// PolygonClipper computes parent-child relationships, depth, and orientation during its +/// sweep line algorithm, so we only need to provide contours with vertices. +/// +internal static class PolygonClipperFactory +{ + /// + /// Creates a polygon from multiple paths. + /// + /// The paths to convert. + /// A containing all flattened paths as contours. + public static PCPolygon FromClosedPaths(IEnumerable paths) + { + PCPolygon polygon = []; + + foreach (IPath path in paths) + { + polygon = FromSimpleClosedPaths(path.Flatten(), polygon); + } + + return polygon; + } + + /// + /// Converts closed simple paths to PolygonClipper contours. + /// + /// Closed simple paths. + /// Optional existing polygon to populate. + /// The constructed . + /// + /// This method simply converts ImageSharp paths to PolygonClipper contours by copying vertices. + /// PolygonClipper's sweep line algorithm will determine parent-child relationships, depth, + /// and proper orientation during clipping operations. We only need to ensure paths are + /// closed and have sufficient vertices. + /// + public static PCPolygon FromSimpleClosedPaths(IEnumerable paths, PCPolygon? polygon = null) + { + polygon ??= []; + + foreach (ISimplePath p in paths) + { + if (!p.IsClosed) + { + continue; + } + + ReadOnlySpan points = p.Points.Span; + if (points.Length < 3) + { + continue; + } + + Contour contour = []; + + // Copy all vertices + for (int i = 0; i < points.Length; i++) + { + contour.Add(new Vertex(points[i].X, points[i].Y)); + } + + // Add the contour - PolygonClipper will determine parent/depth/orientation during sweep + polygon.Add(contour); + } + + return polygon; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs deleted file mode 100644 index dae10d2d..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperUtilities.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -internal static class PolygonClipperUtilities -{ - /// - /// Computes the signed area of a path using the shoelace formula. - /// - /// - /// Positive values indicate clockwise orientation in screen space. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float SignedArea(PathF path) - { - // https://en.wikipedia.org/wiki/Shoelace_formula - float a = 0F; - if (path.Count < 3) - { - return a; - } - - // Sum over edges (prev -> current). - Vector2 prevPt = path[^1]; - for (int i = 0; i < path.Count; i++) - { - Vector2 pt = path[i]; - a += (prevPt.Y + pt.Y) * (prevPt.X - pt.X); - prevPt = pt; - } - - return a * .5F; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DotProduct(Vector2 vec1, Vector2 vec2) - => Vector2.Dot(vec1, vec2); - - /// - /// Returns the dot product of the segments (pt1->pt2) and (pt2->pt3). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DotProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) - => Vector2.Dot(pt2 - pt1, pt3 - pt2); - - /// - /// Returns the 2D cross product magnitude of and . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float CrossProduct(Vector2 vec1, Vector2 vec2) - => (vec1.Y * vec2.X) - (vec2.Y * vec1.X); - - /// - /// Returns the cross product of the segments (pt1->pt2) and (pt2->pt3). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float CrossProduct(Vector2 pt1, Vector2 pt2, Vector2 pt3) - => ((pt2.X - pt1.X) * (pt3.Y - pt2.Y)) - ((pt2.Y - pt1.Y) * (pt3.X - pt2.X)); - - /// - /// Returns the squared perpendicular distance from a point to a line segment. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float PerpendicDistFromLineSqrd(Vector2 pt, Vector2 line1, Vector2 line2) - { - Vector2 ab = pt - line1; - Vector2 cd = line2 - line1; - if (cd == Vector2.Zero) - { - return 0; - } - - float cross = CrossProduct(cd, ab); - return (cross * cross) / DotProduct(cd, cd); - } - - /// - /// Returns true when two segments intersect. - /// - /// First endpoint of segment 1. - /// Second endpoint of segment 1. - /// First endpoint of segment 2. - /// Second endpoint of segment 2. - /// If true, allows shared endpoints; if false, requires a proper intersection. - public static bool SegsIntersect(Vector2 seg1a, Vector2 seg1b, Vector2 seg2a, Vector2 seg2b, bool inclusive = false) - { - if (inclusive) - { - float res1 = CrossProduct(seg1a, seg2a, seg2b); - float res2 = CrossProduct(seg1b, seg2a, seg2b); - if (res1 * res2 > 0) - { - return false; - } - - float res3 = CrossProduct(seg2a, seg1a, seg1b); - float res4 = CrossProduct(seg2b, seg1a, seg1b); - if (res3 * res4 > 0) - { - return false; - } - - // ensure NOT collinear - return res1 != 0 || res2 != 0 || res3 != 0 || res4 != 0; - } - - return (CrossProduct(seg1a, seg2a, seg2b) * CrossProduct(seg1b, seg2a, seg2b) < 0) - && (CrossProduct(seg2a, seg1a, seg1b) * CrossProduct(seg2b, seg1a, seg1b) < 0); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetLineIntersectPoint(Vector2 ln1a, Vector2 ln1b, Vector2 ln2a, Vector2 ln2b, out Vector2 ip) - { - Vector2 dxy1 = ln1b - ln1a; - Vector2 dxy2 = ln2b - ln2a; - float det = CrossProduct(dxy1, dxy2); - if (det == 0F) - { - ip = default; - return false; - } - - float t = (((ln1a.X - ln2a.X) * dxy2.Y) - ((ln1a.Y - ln2a.Y) * dxy2.X)) / det; - - // Clamp intersection to the segment endpoints. - if (t <= 0F) - { - ip = ln1a; - } - else if (t >= 1F) - { - ip = ln1b; - } - else - { - ip = ln1a + (t * dxy1); - } - - return true; - } - - /// - /// Returns the closest point on a segment to an external point. - /// - /// The point to project onto the segment. - /// First endpoint of the segment. - /// Second endpoint of the segment. - public static Vector2 GetClosestPtOnSegment(Vector2 offPt, Vector2 seg1, Vector2 seg2) - { - if (seg1 == seg2) - { - return seg1; - } - - Vector2 dxy = seg2 - seg1; - Vector2 oxy = (offPt - seg1) * dxy; - float q = (oxy.X + oxy.Y) / DotProduct(dxy, dxy); - - if (q < 0) - { - q = 0; - } - else if (q > 1) - { - q = 1; - } - - return seg1 + (dxy * q); - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs deleted file mode 100644 index 772c37ca..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonStroker.cs +++ /dev/null @@ -1,913 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Drawing.Processing; - -#pragma warning disable SA1201 // Elements should appear in the correct order -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// Generates polygonal stroke outlines for vector paths using analytic joins and caps. -/// -/// -/// -/// This class performs geometric stroking of input paths, producing an explicit polygonal -/// outline suitable for filling or clipping. It replicates the behavior of analytic stroking -/// as implemented in vector renderers (e.g., AGG, Skia), without relying on rasterization. -/// -/// -/// The stroker supports multiple join and cap styles, adjustable miter limits, and an -/// approximation scale for arc and round joins. It operates entirely in double precision -/// for numerical stability, emitting coordinates for downstream use -/// in polygon merging or clipping operations. -/// -/// -/// Used by higher-level utility to produce consistent, -/// merged outlines for stroked paths and dashed spans. -/// -/// -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; - - /// - /// Initializes a new instance of the class with the specified stroke options. - /// - /// - /// The stroke options to use for configuring line joins, caps, miter limits, and approximation scale. - /// Cannot be . - /// - public PolygonStroker(StrokeOptions options) - { - this.LineJoin = options.LineJoin; - this.InnerJoin = options.InnerJoin; - this.LineCap = options.LineCap; - this.MiterLimit = options.MiterLimit; - this.InnerMiterLimit = options.InnerMiterLimit; - this.ApproximationScale = options.ApproximationScale; - } - - /// - /// Gets the miter limit used to clamp outer miter joins. - /// - public double MiterLimit { get; } - - /// - /// Gets the inner miter limit used to clamp joins on acute interior angles. - /// - public double InnerMiterLimit { get; } - - /// - /// Gets the arc approximation scale used for round joins and caps. - /// - public double ApproximationScale { get; } - - /// - /// Gets the outer line join style used for stroking corners. - /// - public LineJoin LineJoin { get; } - - /// - /// Gets the line cap style used for open path ends. - /// - public LineCap LineCap { get; } - - /// - /// Gets the join style used for sharp interior angles. - /// - public InnerJoin InnerJoin { get; } - - /// - /// Gets or sets the stroke width in the caller's coordinate space. - /// - 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; - } - } - - /// - /// Strokes the provided polyline or polygon and returns the outline vertices. - /// - /// The input points to stroke. - /// Whether the input is a closed ring. - /// The stroked outline as a closed point array. - public PointF[] ProcessPath(ReadOnlySpan linePoints, bool isClosed) - { - if (linePoints.Length < 2) - { - return []; - } - - // Special case: for 2-point inputs, check if both points are identical (degenerate case) - if (linePoints.Length == 2) - { - PointF p0 = linePoints[0]; - PointF p1 = linePoints[1]; - - if (Math.Abs(p1.X - p0.X) <= Constants.Misc.VertexDistanceEpsilon && - Math.Abs(p1.Y - p0.Y) <= Constants.Misc.VertexDistanceEpsilon) - { - // Both points are identical - generate a point cap shape - return this.GeneratePointCap(p0.X, p0.Y); - } - } - - this.Reset(); - this.AddLinePath(linePoints); - - if (isClosed) - { - this.ClosePath(); - } - - List results = new(linePoints.Length * 3); - this.FinishPath(results); - return [.. results]; - } - - /// - /// Adds a sequence of line segments to the current stroker state. - /// - /// The input points to add as line segments. - 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); - } - } - - /// - /// Marks the current path as closed before finishing the outline. - /// - public void ClosePath() - { - // Mark the current src path as closed; no geometry is pushed here. - this.closed = (int)PathFlags.Close; - this.status = Status.Initial; - } - - /// - /// Finalizes stroking and appends output points to the provided list. - /// - /// The list that receives the stroked outline vertices. - 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; - } - } - } - } - - /// - /// Resets the stroker state so it can be reused for a new path. - /// - 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(); - } - - // Remove the tail pair (vd2 and its predecessor vd1) and re-add the tail 't'. - // Re-adding forces a fresh Measure() against the new predecessor, collapsing zero-length edges. - if (this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - - this.AddVertex(t.X, t.Y, t.Distance); - } - - if (!closed) - { - return; - } - - // TODO: Why check again? Doesn't the while loop above already ensure this? - 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.MiterRevert: - - this.AddPoint(v1.X + dx1, v1.Y - dy1); - this.AddPoint(v1.X + dx2, v1.Y - dy2); - - break; - - case LineJoin.MiterRound: - 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(); - - if (len < Constants.Misc.VertexDistanceEpsilon) - { - // Degenerate cap: emit a symmetric butt cap of zero span. - // This avoids div-by-zero in direction computation. - this.AddPoint(v0.X, v0.Y); - this.AddPoint(v1.X, v1.Y); - return; - } - - 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) - { - const double eps = Constants.Misc.VertexDistanceEpsilon; - if (len1 < eps || len2 < eps) - { - // Degenerate join: reuse the non-degenerate edge length for both offsets - // to emit a simple bevel and avoid unstable direction math. - this.outVertices.Clear(); - - double l1 = len1 >= eps ? len1 : len2; - double l2 = len2 >= eps ? len2 : len1; - - double offX1 = this.strokeWidth * (v1.Y - v0.Y) / l1; - double offY1 = this.strokeWidth * (v1.X - v0.X) / l1; - double offX2 = this.strokeWidth * (v2.Y - v1.Y) / l2; - double offY2 = this.strokeWidth * (v2.X - v1.X) / l2; - - this.AddPoint(v1.X + offX1, v1.Y - offY1); - this.AddPoint(v1.X + offX2, v1.Y - offY2); - return; - } - - 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.Miter: - this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterRevert, limit, 0); - - break; - - case InnerJoin.Jag: - case InnerJoin.Round: - 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.MiterRevert, limit, 0); - } - else if (this.InnerJoin == InnerJoin.Jag) - { - 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.Round or LineJoin.Bevel && 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.Miter: - case LineJoin.MiterRevert: - case LineJoin.MiterRound: - this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, this.LineJoin, this.MiterLimit, dbevel); - - break; - - case LineJoin.Round: - 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)); - - /// - /// Generates a cap shape for a degenerate point (when all input points are identical). - /// Creates a circle for round caps or a square for square/butt caps. - /// - /// The X coordinate of the point. - /// The Y coordinate of the point. - /// The vertices forming the cap shape. - private PointF[] GeneratePointCap(double x, double y) - { - if (this.LineCap == LineCap.Round) - { - // Generate a circle with radius = strokeWidth - double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125 / this.ApproximationScale))) * 2; - int n = Math.Max(4, (int)(Constants.Misc.PiMul2 / da)); - double angleStep = Constants.Misc.PiMul2 / n; - - PointF[] points = new PointF[n + 1]; - - for (int i = 0; i < n; i++) - { - double angle = i * angleStep; - points[i] = new PointF( - (float)(x + (Math.Cos(angle) * this.strokeWidth)), - (float)(y + (Math.Sin(angle) * this.strokeWidth))); - } - - // Close the circle - points[n] = points[0]; - - return points; - } - else - { - // Generate a square cap (used for both Square and Butt caps) - double w = this.strokeWidth; - return - [ - new PointF((float)(x - w), (float)(y - w)), - new PointF((float)(x + w), (float)(y - w)), - new PointF((float)(x + w), (float)(y + w)), - new PointF((float)(x - w), (float)(y + w)), - new PointF((float)(x - w), (float)(y - w)) // Close the square - ]; - } - } - - 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/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs index f04f9a04..a3dc7583 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs @@ -1,25 +1,18 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.PolygonClipper; + +using PCPolygon = SixLabors.PolygonClipper.Polygon; +using StrokeOptions = SixLabors.ImageSharp.Drawing.Processing.StrokeOptions; namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; /// /// Generates stroked and merged shapes using polygon stroking and boolean clipping. /// -internal sealed class StrokedShapeGenerator +internal static class StrokedShapeGenerator { - private readonly PolygonStroker polygonStroker; - - /// - /// Initializes a new instance of the class. - /// - public StrokedShapeGenerator(StrokeOptions options) - => this.polygonStroker = new PolygonStroker(options); - /// /// Strokes a collection of dashed polyline spans and returns a merged outline. /// @@ -28,25 +21,15 @@ public StrokedShapeGenerator(StrokeOptions options) /// and is stroked using the current stroker settings. /// Spans that are null or contain fewer than 2 points are ignored. /// - /// The stroke width in the caller’s coordinate space. + /// The stroke width in the caller's coordinate space. + /// The stroke geometry options. /// - /// An array of closed paths representing the stroked outline after boolean merge. - /// Returns an empty array when no valid spans are provided. Returns a single path - /// when only one valid stroked ring is produced. + /// A representing the stroked outline after boolean merge. /// - /// - /// This method streams each dashed span through the internal stroker as an open polyline, - /// producing closed stroke rings. To clean self overlaps, all rings are added as subject - /// paths and a is performed. - /// The union uses to preserve winding density. - /// - public IPath[] GenerateStrokedShapes(List spans, float width) + public static ComplexPolygon GenerateStrokedShapes(List spans, float width, StrokeOptions options) { // 1) Stroke each dashed span as open. - this.polygonStroker.Width = width; - - List ringPoints = new(spans.Count); - List rings = new(spans.Count); + PCPolygon rings = new(spans.Count); foreach (PointF[] span in spans) { if (span == null || span.Length < 2) @@ -54,173 +37,141 @@ public IPath[] GenerateStrokedShapes(List spans, float width) continue; } - PointF[] stroked = this.polygonStroker.ProcessPath(span, isClosed: false); - if (stroked.Length < 3) + Contour ring = new(span.Length); + for (int i = 0; i < span.Length; i++) { - continue; + PointF p = span[i]; + ring.Add(new Vertex(p.X, p.Y)); } - ringPoints.Add(stroked); - rings.Add(new Polygon(new LinearLineSegment(stroked))); + rings.Add(ring); } int count = rings.Count; if (count == 0) { - return []; + return new([]); } - if (!HasIntersections(ringPoints)) + PCPolygon result = PolygonStroker.Stroke(rings, width, CreateStrokeOptions(options)); + + IPath[] shapes = new IPath[result.Count]; + int index = 0; + for (int i = 0; i < result.Count; i++) { - return count == 1 ? [rings[0]] : [.. rings]; + Contour contour = result[i]; + PointF[] points = new PointF[contour.Count]; + + for (int j = 0; j < contour.Count; j++) + { + Vertex vertex = contour[j]; + points[j] = new PointF((float)vertex.X, (float)vertex.Y); + } + + shapes[index++] = new Polygon(points); } - // 2) Union all rings as subject paths - ClippedShapeGenerator clipper = new(IntersectionRule.NonZero); - clipper.AddPaths(rings, ClippingType.Subject); - return clipper.GenerateClippedShapes(BooleanOperation.Union, true); + return new(shapes); } /// /// Strokes a path and returns a merged outline from its flattened segments. /// /// The source path. It is flattened using the current flattening settings. - /// The stroke width in the caller’s coordinate space. + /// The stroke width in the caller's coordinate space. + /// The stroke geometry options. /// - /// An array of closed paths representing the stroked outline after boolean merge. - /// Returns an empty array when no valid rings are produced. Returns a single path - /// when only one valid stroked ring exists. + /// A representing the stroked outline after boolean merge. /// - /// - /// Each flattened simple path is streamed through the internal stroker as open or closed - /// according to . The resulting stroke rings are split - /// paths and combined using . Using - /// preserves fill across overlaps and prevents - /// unintended holes in the merged outline. - /// - public IPath[] GenerateStrokedShapes(IPath path, float width) + public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, StrokeOptions options) { - // 1) Stroke the input path into closed rings - List ringPoints = []; - List rings = []; - this.polygonStroker.Width = width; + // 1) Stroke the input path as open or closed. + PCPolygon rings = []; - foreach (ISimplePath p in path.Flatten()) + foreach (ISimplePath sp in path.Flatten()) { - PointF[] stroked = this.polygonStroker.ProcessPath(p.Points.Span, p.IsClosed); - if (stroked.Length < 3) + ReadOnlySpan span = sp.Points.Span; + + if (span.Length < 2) { - continue; // skip degenerate outputs + continue; } - ringPoints.Add(stroked); - rings.Add(new Polygon(new LinearLineSegment(stroked))); + Contour ring = new(span.Length); + for (int i = 0; i < span.Length; i++) + { + PointF p = span[i]; + ring.Add(new Vertex(p.X, p.Y)); + } + + if (sp.IsClosed) + { + ring.Add(ring[0]); + } + + rings.Add(ring); } int count = rings.Count; if (count == 0) { - return []; + return new([]); } - if (!HasIntersections(ringPoints)) + PCPolygon result = PolygonStroker.Stroke(rings, width, CreateStrokeOptions(options)); + + IPath[] shapes = new IPath[result.Count]; + int index = 0; + for (int i = 0; i < result.Count; i++) { - return count == 1 ? [rings[0]] : [.. rings]; - } + Contour contour = result[i]; + PointF[] points = new PointF[contour.Count]; - // 2) Union all rings as subject paths - ClippedShapeGenerator clipper = new(IntersectionRule.NonZero); - clipper.AddPaths(rings, ClippingType.Subject); + for (int j = 0; j < contour.Count; j++) + { + Vertex vertex = contour[j]; + points[j] = new PointF((float)vertex.X, (float)vertex.Y); + } + + shapes[index++] = new Polygon(points); + } - // 3) Return the cleaned, merged outline - return clipper.GenerateClippedShapes(BooleanOperation.Union, true); + return new(shapes); } - /// - /// Determines whether any of the provided rings contain self-intersections or intersect with other rings. - /// - /// - /// This method performs a conservative scan to detect intersections among the provided rings. It - /// checks for both self-intersections within each ring and intersections between different rings. Rings are treated - /// as polylines; if a ring is closed (its first and last points are equal), the closing segment is included in the - /// intersection checks. This method is intended for fast intersection detection and may be used to determine - /// whether further geometric processing, such as clipping, is necessary. - /// - /// - /// A list of rings, where each ring is represented as an array of points defining its vertices. Each ring is - /// expected to be a sequence of points forming a polyline or polygon. - /// - /// if any ring self-intersects or any two rings intersect; otherwise, . - private static bool HasIntersections(List rings) + private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions options) { - // Detect whether any stroked ring self-intersects or intersects another ring. - // This is a fast, conservative scan used to decide whether we can skip clipping. - Vector2 intersection = default; - - for (int r = 0; r < rings.Count; r++) + PolygonClipper.StrokeOptions o = new() { - PointF[] ring = rings[r]; - int segmentCount = ring.Length - 1; - if (segmentCount < 2) + ArcDetailScale = options.ArcDetailScale, + MiterLimit = options.MiterLimit, + InnerMiterLimit = options.InnerMiterLimit, + NormalizeOutput = options.NormalizeOutput, + LineJoin = options.LineJoin switch { - continue; - } - - // 1) Self-intersection scan for the current ring. - // Adjacent segments share a vertex and are skipped to avoid trivial hits. - bool isClosed = ring[0] == ring[^1]; - for (int i = 0; i < segmentCount; i++) + LineJoin.MiterRound => PolygonClipper.LineJoin.MiterRound, + LineJoin.Bevel => PolygonClipper.LineJoin.Bevel, + LineJoin.Round => PolygonClipper.LineJoin.Round, + LineJoin.MiterRevert => PolygonClipper.LineJoin.MiterRevert, + _ => PolygonClipper.LineJoin.Miter, + }, + + InnerJoin = options.InnerJoin switch { - Vector2 a0 = new(ring[i].X, ring[i].Y); - Vector2 a1 = new(ring[i + 1].X, ring[i + 1].Y); - - for (int j = i + 1; j < segmentCount; j++) - { - // Skip neighbors and the closing edge pair in a closed ring. - if (j == i + 1 || (isClosed && i == 0 && j == segmentCount - 1)) - { - continue; - } - - Vector2 b0 = new(ring[j].X, ring[j].Y); - Vector2 b1 = new(ring[j + 1].X, ring[j + 1].Y); - if (Intersect.LineSegmentToLineSegmentIgnoreCollinear(a0, a1, b0, b1, ref intersection)) - { - return true; - } - } - } + InnerJoin.Round => PolygonClipper.InnerJoin.Round, + InnerJoin.Miter => PolygonClipper.InnerJoin.Miter, + InnerJoin.Jag => PolygonClipper.InnerJoin.Jag, + _ => PolygonClipper.InnerJoin.Bevel, + }, - // 2) Cross-ring intersection scan against later rings only. - // This avoids double work while checking all ring pairs. - for (int s = r + 1; s < rings.Count; s++) + LineCap = options.LineCap switch { - PointF[] other = rings[s]; - int otherSegmentCount = other.Length - 1; - if (otherSegmentCount < 1) - { - continue; - } - - for (int i = 0; i < segmentCount; i++) - { - Vector2 a0 = new(ring[i].X, ring[i].Y); - Vector2 a1 = new(ring[i + 1].X, ring[i + 1].Y); - - for (int j = 0; j < otherSegmentCount; j++) - { - Vector2 b0 = new(other[j].X, other[j].Y); - Vector2 b1 = new(other[j + 1].X, other[j + 1].Y); - if (Intersect.LineSegmentToLineSegmentIgnoreCollinear(a0, a1, b0, b1, ref intersection)) - { - return true; - } - } - } + LineCap.Round => PolygonClipper.LineCap.Round, + LineCap.Square => PolygonClipper.LineCap.Square, + _ => PolygonClipper.LineCap.Butt, } - } + }; - // No intersections detected. - return false; + return o; } } diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs deleted file mode 100644 index 8dd0e724..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexDistance.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -// TODO: We can improve the performance of some of the operations here by using unsafe casting to Vector128 -// Like we do in 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/PolygonGeometry/VertexFlags.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs deleted file mode 100644 index fd038b4a..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/VertexFlags.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -[Flags] -internal enum VertexFlags -{ - None = 0, - OpenStart = 1, - OpenEnd = 2, - LocalMax = 4, - LocalMin = 8 -} diff --git a/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs b/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs index d073429c..c3a07c11 100644 --- a/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs +++ b/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs @@ -52,7 +52,7 @@ public Span OverlaySpan return this.overlayBuffer.Memory.Span; } - return Span.Empty; + return []; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 380ce246..2305594e 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -23,12 +23,20 @@ public abstract class DrawPolygon private Image image; - private SDPointF[][] sdPoints; private Bitmap sdBitmap; private Graphics sdGraphics; + private GraphicsPath sdPath; + private Pen sdPen; private SKPath skPath; private SKSurface skSurface; + private SKPaint skPaint; + + private SolidPen isPen; + + private IPath imageSharpPath; + + private IPath strokedImageSharpPath; protected abstract int Width { get; } @@ -43,55 +51,107 @@ protected virtual PointF[][] GetPoints(FeatureCollection features) => public void Setup() { string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); - FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); this.points = this.GetPoints(featureCollection); - this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray(); + // Prebuild a single multi-subpath geometry for each library so the benchmark focuses on stroking/rasterization. + this.sdPath = new GraphicsPath(FillMode.Winding); this.skPath = new SKPath(); - foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2)) + PathBuilder pb = new(); + + foreach (PointF[] loop in this.points) { - this.skPath.MoveTo(ptArr[0].X, ptArr[1].Y); - for (int i = 1; i < ptArr.Length; i++) + if (loop.Length < 3) + { + continue; + } + + // System.Drawing: one GraphicsPath with multiple closed figures. + SDPointF firstSd = new(loop[0].X, loop[0].Y); + SDPointF[] sdPoly = new SDPointF[loop.Length]; + for (int i = 0; i < loop.Length; i++) + { + sdPoly[i] = new SDPointF(loop[i].X, loop[i].Y); + } + + this.sdPath.StartFigure(); + this.sdPath.AddPolygon(sdPoly); + this.sdPath.CloseFigure(); + + // Skia: one SKPath with multiple closed contours. + this.skPath.MoveTo(loop[0].X, loop[0].Y); + for (int i = 1; i < loop.Length; i++) { - this.skPath.LineTo(ptArr[i].X, ptArr[i].Y); + this.skPath.LineTo(loop[i].X, loop[i].Y); } - this.skPath.LineTo(ptArr[0].X, ptArr[1].Y); + this.skPath.Close(); + + // ImageSharp: one IPath with multiple closed figures. + pb.StartFigure(); + pb.AddLines(loop); + pb.CloseFigure(); } + this.imageSharpPath = pb.Build(); + this.image = new Image(this.Width, this.Height); + this.isPen = new SolidPen(Color.White, this.Thickness); + this.strokedImageSharpPath = this.isPen.GeneratePath(this.imageSharpPath); + this.sdBitmap = new Bitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); this.sdGraphics.InterpolationMode = InterpolationMode.Default; this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias; + this.sdGraphics.PixelOffsetMode = PixelOffsetMode.Default; + this.sdGraphics.CompositingMode = CompositingMode.SourceOver; + + this.sdPen = new Pen(System.Drawing.Color.White, this.Thickness); + this.skSurface = SKSurface.Create(new SKImageInfo(this.Width, this.Height)); + this.skPaint = new SKPaint + { + Style = SKPaintStyle.Stroke, + Color = SKColors.White, + StrokeWidth = this.Thickness, + IsAntialias = true, + }; + } + + [IterationSetup] + public void IterationSetup() + { + // Clear all targets to avoid overdraw effects influencing results. + this.sdGraphics.Clear(System.Drawing.Color.Transparent); + this.skSurface.Canvas.Clear(SKColors.Transparent); } [GlobalCleanup] public void Cleanup() { - this.image.Dispose(); + this.sdPen.Dispose(); + this.sdPath.Dispose(); this.sdGraphics.Dispose(); this.sdBitmap.Dispose(); + + this.skPaint.Dispose(); this.skSurface.Dispose(); this.skPath.Dispose(); + + this.image.Dispose(); } [Benchmark] public void SystemDrawing() - { - using Pen pen = new(System.Drawing.Color.White, this.Thickness); + => this.sdGraphics.DrawPath(this.sdPen, this.sdPath); - foreach (SDPointF[] loop in this.sdPoints) - { - this.sdGraphics.DrawPolygon(pen, loop); - } - } + [Benchmark] + public void ImageSharpCombinedPaths() + => this.image.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); [Benchmark] - public void ImageSharp() + public void ImageSharpSeparatePaths() => this.image.Mutate( c => { @@ -103,16 +163,18 @@ public void ImageSharp() [Benchmark(Baseline = true)] public void SkiaSharp() + => this.skSurface.Canvas.DrawPath(this.skPath, this.skPaint); + + [Benchmark] + public IPath ImageSharpStrokeAndClip() { - using SKPaint paint = new() - { - Style = SKPaintStyle.Stroke, - Color = SKColors.White, - StrokeWidth = this.Thickness, - IsAntialias = true, - }; + return this.isPen.GeneratePath(this.imageSharpPath); + } - this.skSurface.Canvas.DrawPath(this.skPath, paint); + [Benchmark] + public void FillPolygon() + { + this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); } } @@ -122,7 +184,7 @@ public class DrawPolygonAll : DrawPolygon protected override int Height => 4800; - protected override float Thickness => 2f; + protected override float Thickness => 2F; } public class DrawPolygonMediumThin : DrawPolygon @@ -131,7 +193,7 @@ public class DrawPolygonMediumThin : DrawPolygon protected override int Height => 1000; - protected override float Thickness => 1f; + protected override float Thickness => 1F; protected override PointF[][] GetPoints(FeatureCollection features) { @@ -145,5 +207,5 @@ protected override PointF[][] GetPoints(FeatureCollection features) public class DrawPolygonMediumThick : DrawPolygonMediumThin { - protected override float Thickness => 10f; + protected override float Thickness => 10F; } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Program.cs b/tests/ImageSharp.Drawing.Benchmarks/Program.cs index 97bb7feb..199bdd2b 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Program.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Program.cs @@ -16,11 +16,11 @@ public class InProcessConfig : ManualConfig { public InProcessConfig() { - AddLogger(ConsoleLogger.Default); + this.AddLogger(ConsoleLogger.Default); - AddColumnProvider(DefaultColumnProviders.Instance); + this.AddColumnProvider(DefaultColumnProviders.Instance); - AddExporter(DefaultExporters.Html, DefaultExporters.Csv); + this.AddExporter(DefaultExporters.Html, DefaultExporters.Csv); this.AddJob(Job.MediumRun .WithToolchain(InProcessEmitToolchain.Instance)); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs index 86779df3..34026ab5 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 diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index be892373..9898ccc7 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.PolygonClipper; using SkiaSharp; namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; @@ -255,4 +256,73 @@ public void Missisippi_Skia(int offset) using FileStream fs = File.Create(fn); data.SaveTo(fs); } + + [Theory] + [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] + public void LargeGeoJson_States_Benchmark(TestImageProvider provider, int thickness) + { + 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(); + + image.Mutate( + c => + { + foreach (PointF[] loop in points) + { + c.DrawPolygon(Color.White, thickness, loop); + } + }); + + image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] + public void LargeStar_Benchmark(TestImageProvider provider, int thickness) + { + List points = CreateStarPolygon(1001, 100F); + Matrix3x2 transform = Matrix3x2.CreateTranslation(250, 250); + + using Image image = provider.GetImage(); + + image.Mutate( + c => + { + foreach (PointF[] loop in points) + { + c.SetDrawingTransform(transform); + c.DrawPolygon(Color.White, thickness, loop); + } + }); + + image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + } + + private static List CreateStarPolygon(int vertexCount, float radius) + { + if (vertexCount < 5 || (vertexCount & 1) == 0) + { + throw new ArgumentOutOfRangeException(nameof(vertexCount), "Vertex count must be an odd number >= 5."); + } + + int step = (vertexCount - 1) / 2; + List contour = new(vertexCount + 1); + for (int i = 0; i < vertexCount; i++) + { + int index = (i * step) % vertexCount; + float angle = (index * MathF.PI * 2) / vertexCount; + contour.Add(new PointF(MathF.Cos(angle) * radius, MathF.Sin(angle) * radius)); + } + + contour.Add(contour[0]); + return [[.. contour]]; + } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs index 26e151dd..7d261782 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs @@ -21,6 +21,22 @@ public void OffsetTextOutlines(TestImageProvider provider) provider.RunValidatingProcessorTest(p => { + + //p.DrawText( + // new RichTextOptions(namefont) + // { + // VerticalAlignment = VerticalAlignment.Center, + // HorizontalAlignment = HorizontalAlignment.Center, + // TextAlignment = TextAlignment.Center, + // TextDirection = TextDirection.LeftToRight, + // Origin = new Point(1156, 713), + // }, + // "O", + // Brushes.Solid(Color.White), + // Pens.Solid(Color.Black, 5)); + + //return; + p.DrawText( new RichTextOptions(bibfont) { diff --git a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs index 48d61f96..3ebbeb67 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; +using SixLabors.ImageSharp.Drawing.Shapes; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; @@ -163,6 +164,56 @@ public void DoesNotThrowFillingTriangle() image.Mutate(ctx => ctx.Fill(Color.White, path)); } } + + [Fact] + public void DrawPathProcessor_UsesNonZeroRule_WhenStrokeNormalizationIsDisabled() + { + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + SolidPen pen = new(Color.Black, 3F) + { + StrokeOptions = { NormalizeOutput = false } + }; + + DrawPathProcessor processor = new(options, pen, new RectangularPolygon(2F, 2F, 8F, 8F)); + + using Image image = new(20, 20); + IImageProcessor pixelProcessor = + processor.CreatePixelSpecificProcessor(image.Configuration, image, image.Bounds); + + FillPathProcessor fillProcessor = Assert.IsType>(pixelProcessor); + FillPathProcessor definition = fillProcessor.GetPrivateFieldValue("definition"); + + Assert.Equal(IntersectionRule.NonZero, definition.Options.ShapeOptions.IntersectionRule); + } + + [Fact] + public void DrawPathProcessor_PreservesRule_WhenStrokeNormalizationIsEnabled() + { + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + SolidPen pen = new(Color.Black, 3F) + { + StrokeOptions = { NormalizeOutput = true } + }; + + DrawPathProcessor processor = new(options, pen, new RectangularPolygon(2F, 2F, 8F, 8F)); + + using Image image = new(20, 20); + IImageProcessor pixelProcessor = + processor.CreatePixelSpecificProcessor(image.Configuration, image, image.Bounds); + + FillPathProcessor fillProcessor = Assert.IsType>(pixelProcessor); + FillPathProcessor definition = fillProcessor.GetPrivateFieldValue("definition"); + + Assert.Equal(IntersectionRule.EvenOdd, definition.Options.ShapeOptions.IntersectionRule); + } } internal static class ReflectionHelpers @@ -172,4 +223,10 @@ internal static T GetProtectedValue(this object obj, string name) .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) .Single(x => x.Name == name) .GetValue(obj); + + internal static T GetPrivateFieldValue(this object obj, string name) + => (T)obj.GetType() + .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy) + .Single(x => x.Name == name) + .GetValue(obj); } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index c8a84d34..7943ac2d 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs @@ -25,18 +25,8 @@ public class ClipperTests new Vector2(130, 40), new Vector2(65, 137))); - private IEnumerable Clip(IPath shape, params IPath[] hole) - { - ClippedShapeGenerator clipper = new(IntersectionRule.EvenOdd); - - clipper.AddPath(shape, ClippingType.Subject); - if (hole != null) - { - clipper.AddPaths(hole, ClippingType.Clip); - } - - return clipper.GenerateClippedShapes(BooleanOperation.Difference); - } + private static ComplexPolygon Clip(IPath shape, params IPath[] hole) + => ClippedShapeGenerator.GenerateClippedShapes(BooleanOperation.Difference, shape, hole); [Fact] public void OverlappingTriangleCutRightSide() @@ -52,19 +42,19 @@ public void OverlappingTriangleCutRightSide() new Vector2(70, 100), new Vector2(20, 100))); - IEnumerable shapes = this.Clip(triangle, cutout); - Assert.Single(shapes); - Assert.DoesNotContain(triangle, shapes); + ComplexPolygon shapes = Clip(triangle, cutout); + Assert.Single(shapes.Paths); + Assert.DoesNotContain(triangle, shapes.Paths); } [Fact] public void OverlappingTriangles() { - IEnumerable shapes = this.Clip(this.bigTriangle, this.littleTriangle); - Assert.Single(shapes); - IReadOnlyList path = shapes.Single().Flatten().First().Points.ToArray(); + ComplexPolygon shapes = Clip(this.bigTriangle, this.littleTriangle); + Assert.Single(shapes.Paths); + PointF[] path = shapes.Paths.Single().Flatten().First().Points.ToArray(); - Assert.Equal(7, path.Count); + Assert.Equal(7, path.Length); foreach (Vector2 p in this.bigTriangle.Flatten().First().Points.ToArray()) { Assert.Contains(p, path, new ApproximateFloatComparer(RectangularPolygonValueComparer.DefaultTolerance)); @@ -74,7 +64,7 @@ public void OverlappingTriangles() [Fact] public void NonOverlapping() { - IEnumerable shapes = this.Clip(this.topLeft, this.topRight) + IEnumerable shapes = Clip(this.topLeft, this.topRight).Paths .OfType().Select(x => (RectangularPolygon)x); Assert.Single(shapes); @@ -87,17 +77,17 @@ public void NonOverlapping() [Fact] public void OverLappingReturns1NewShape() { - IEnumerable shapes = this.Clip(this.bigSquare, this.topLeft); + ComplexPolygon shapes = Clip(this.bigSquare, this.topLeft); - Assert.Single(shapes); - Assert.DoesNotContain(shapes, x => RectangularPolygonValueComparer.Equals(this.bigSquare, x)); - Assert.DoesNotContain(shapes, x => RectangularPolygonValueComparer.Equals(this.topLeft, x)); + Assert.Single(shapes.Paths); + Assert.DoesNotContain(shapes.Paths, x => RectangularPolygonValueComparer.Equals(this.bigSquare, x)); + Assert.DoesNotContain(shapes.Paths, x => RectangularPolygonValueComparer.Equals(this.topLeft, x)); } [Fact] public void OverlappingButNotCrossingReturnsOrigionalShapes() { - IEnumerable shapes = this.Clip(this.bigSquare, this.hole) + IEnumerable shapes = Clip(this.bigSquare, this.hole).Paths .OfType().Select(x => (RectangularPolygon)x); Assert.Equal(2, shapes.Count()); @@ -109,10 +99,10 @@ public void OverlappingButNotCrossingReturnsOrigionalShapes() [Fact] public void TouchingButNotOverlapping() { - IEnumerable shapes = this.Clip(this.topMiddle, this.topLeft); - Assert.Single(shapes); - Assert.DoesNotContain(shapes, x => RectangularPolygonValueComparer.Equals(this.topMiddle, x)); - Assert.DoesNotContain(shapes, x => RectangularPolygonValueComparer.Equals(this.topLeft, x)); + ComplexPolygon shapes = Clip(this.topMiddle, this.topLeft); + Assert.Single(shapes.Paths); + Assert.DoesNotContain(shapes.Paths, x => RectangularPolygonValueComparer.Equals(this.topMiddle, x)); + Assert.DoesNotContain(shapes.Paths, x => RectangularPolygonValueComparer.Equals(this.topLeft, x)); } [Fact] @@ -123,8 +113,8 @@ public void ClippingRectanglesCreateCorrectNumberOfPoints() .Flatten(); Assert.Single(paths); - IReadOnlyList points = paths.First().Points.ToArray(); + PointF[] points = paths.First().Points.ToArray(); - Assert.Equal(8, points.Count); + Assert.Equal(8, points.Length); } } From a8be3f932e7b686fd5c7abdb7022aae42c75ee42 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 09:41:47 +1000 Subject: [PATCH 20/35] Use non-zero as default intersection rule. --- src/ImageSharp.Drawing/Processing/ShapeOptions.cs | 4 ++-- .../Processing/ShapeOptionsDefaultsExtensionsTests.cs | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs index 4df70625..bba986c0 100644 --- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs @@ -31,9 +31,9 @@ private ShapeOptions(ShapeOptions source) /// /// Gets or sets the rule for calculating intersection points. /// - /// Defaults to . + /// Defaults to . /// - public IntersectionRule IntersectionRule { get; set; } = IntersectionRule.EvenOdd; + public IntersectionRule IntersectionRule { get; set; } = IntersectionRule.NonZero; /// public ShapeOptions DeepClone() => new(this); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs index 766efb0e..ce7f3058 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs @@ -94,11 +94,13 @@ public void GetDefaultOptionsFromConfiguration_SettingNullThenReturnsNewInstance Configuration config = new(); ShapeOptions options = config.GetShapeOptions(); + Assert.Equal(IntersectionRule.NonZero, options.IntersectionRule); Assert.NotNull(options); config.SetShapeOptions((ShapeOptions)null); ShapeOptions options2 = config.GetShapeOptions(); Assert.NotNull(options2); + Assert.Equal(IntersectionRule.NonZero, options2.IntersectionRule); // we set it to null should now be a new instance Assert.NotEqual(options, options2); @@ -123,6 +125,7 @@ public void GetDefaultOptionsFromConfiguration_AlwaysReturnsInstance() Assert.DoesNotContain(typeof(ShapeOptions), config.Properties.Keys); ShapeOptions options = config.GetShapeOptions(); Assert.NotNull(options); + Assert.Equal(IntersectionRule.NonZero, options.IntersectionRule); } [Fact] @@ -143,6 +146,7 @@ public void GetDefaultOptionsFromProcessingContext_AlwaysReturnsInstance() ShapeOptions ctxOptions = context.GetShapeOptions(); Assert.NotNull(ctxOptions); + Assert.Equal(IntersectionRule.NonZero, ctxOptions.IntersectionRule); } [Fact] @@ -154,6 +158,7 @@ public void GetDefaultOptionsFromProcessingContext_AlwaysReturnsInstanceEvenIfSe context.SetShapeOptions((ShapeOptions)null); ShapeOptions ctxOptions = context.GetShapeOptions(); Assert.NotNull(ctxOptions); + Assert.Equal(IntersectionRule.NonZero, ctxOptions.IntersectionRule); } [Fact] From ce6997855be5cc464a4ea22f123adc5749c4f0c1 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 16:49:42 +1000 Subject: [PATCH 21/35] Blaze style works. --- ImageSharp.Drawing.sln | 8 + .../Processing/Backends/CpuDrawingBackend.cs | 72 ++ .../Processing/Backends/IDrawingBackend.cs | 34 + .../Processors/Drawing/FillPathProcessor.cs | 5 +- .../Drawing/FillPathProcessor{TPixel}.cs | 270 +++-- .../Text/DrawTextProcessor{TPixel}.cs | 2 + .../Processors/Text/RichTextGlyphRenderer.cs | 82 +- .../RasterizerDefaultsExtensions.cs | 170 +++ .../Shapes/Rasterization/ActiveEdgeList.cs | 2 + .../Shapes/Rasterization/DefaultRasterizer.cs | 92 ++ .../Shapes/Rasterization/IRasterizer.cs | 43 + .../Shapes/Rasterization/PolygonScanner.cs | 25 + .../Shapes/Rasterization/RasterizerOptions.cs | 75 ++ .../Shapes/Rasterization/SharpBlazeScanner.cs | 983 ++++++++++++++++++ .../Shapes/Rasterization/TiledRasterizer.cs | 254 +++++ .../Drawing/DrawPolygon.cs | 17 + .../Drawing/DrawingRobustnessTests.cs | 45 +- .../Drawing/FillPolygonTests.cs | 2 +- .../Drawing/Paths/Clear.cs | 3 +- .../GraphicsOptionsTests.cs | 10 - .../BaseImageOperationsExtensionTest.cs | 1 - .../Processing/FillPathProcessorTests.cs | 67 ++ .../RasterizerDefaultsExtensionsTests.cs | 122 +++ .../Shapes/Scan/TiledRasterizerTests.cs | 102 ++ .../TestUtilities/GraphicsOptionsComparer.cs | 4 +- 25 files changed, 2362 insertions(+), 128 deletions(-) create mode 100644 src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs create mode 100644 src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs create mode 100644 src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/TiledRasterizerTests.cs diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 827d45f5..07251836 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -339,6 +339,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolygonClipper", "..\..\SixLabors\PolygonClipper\src\PolygonClipper\PolygonClipper.csproj", "{5ED54794-99BF-5E50-A861-0BAAAC794E44}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp", "..\..\SixLabors\ImageSharp\src\ImageSharp\ImageSharp.csproj", "{AAF7501C-8537-7F13-5193-B538318BD071}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -365,6 +367,10 @@ Global {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Debug|Any CPU.Build.0 = Debug|Any CPU {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Release|Any CPU.ActiveCfg = Release|Any CPU {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Release|Any CPU.Build.0 = Release|Any CPU + {AAF7501C-8537-7F13-5193-B538318BD071}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAF7501C-8537-7F13-5193-B538318BD071}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAF7501C-8537-7F13-5193-B538318BD071}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAF7501C-8537-7F13-5193-B538318BD071}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -393,6 +399,7 @@ Global {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} {5ED54794-99BF-5E50-A861-0BAAAC794E44} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {AAF7501C-8537-7F13-5193-B538318BD071} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} @@ -401,6 +408,7 @@ Global shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 ..\..\SixLabors\PolygonClipper\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{5ed54794-99bf-5e50-a861-0baaac794e44}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 + ..\..\SixLabors\ImageSharp\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{aaf7501c-8537-7f13-5193-b538318bd071}*SharedItemsImports = 5 EndGlobalSection GlobalSection(Performance) = preSolution HasPerformanceSessions = true diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs new file mode 100644 index 00000000..ad98387e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Default CPU drawing backend. +/// +/// +/// This backend currently dispatches to the existing scanline rasterizer pipeline. +/// A tiled rasterizer path is wired behind an AppContext switch for incremental rollout. +/// +internal sealed class CpuDrawingBackend : IDrawingBackend +{ + private const string ExperimentalTiledRasterizerSwitch = "SixLabors.ImageSharp.Drawing.ExperimentalTiledRasterizer"; + + private readonly IRasterizer defaultRasterizer; + private readonly TiledRasterizer tiledRasterizer; + + private CpuDrawingBackend(IRasterizer defaultRasterizer) + { + Guard.NotNull(defaultRasterizer, nameof(defaultRasterizer)); + this.defaultRasterizer = defaultRasterizer; + this.tiledRasterizer = TiledRasterizer.Instance; + } + + /// + /// Gets the default backend instance. + /// + public static CpuDrawingBackend Instance { get; } = new(DefaultRasterizer.Instance); + + /// + /// Gets the primary rasterizer used by this backend. + /// + public IRasterizer PrimaryRasterizer => this.defaultRasterizer; + + /// + /// Creates a backend that uses the given rasterizer as the primary implementation. + /// + /// Primary rasterizer. + /// A backend instance. + public static CpuDrawingBackend Create(IRasterizer rasterizer) + { + Guard.NotNull(rasterizer, nameof(rasterizer)); + return ReferenceEquals(rasterizer, DefaultRasterizer.Instance) ? Instance : new CpuDrawingBackend(rasterizer); + } + + /// + public void RasterizePath( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + if (UseExperimentalTiledRasterizer()) + { + this.tiledRasterizer.Rasterize(path, options, allocator, ref state, scanlineHandler); + return; + } + + this.defaultRasterizer.Rasterize(path, options, allocator, ref state, scanlineHandler); + } + + private static bool UseExperimentalTiledRasterizer() + => AppContext.TryGetSwitch(ExperimentalTiledRasterizerSwitch, out bool enabled) && enabled; +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs new file mode 100644 index 00000000..b82f1ef1 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Internal drawing backend abstraction used by processors. +/// +/// +/// This boundary allows processor logic to stay stable while the implementation evolves +/// (for example: alternate CPU rasterizers or eventual non-CPU backends). +/// +internal interface IDrawingBackend +{ + /// + /// Rasterizes a path into scanline coverage. + /// + /// The caller-provided mutable state type. + /// The path to rasterize. + /// Rasterizer options. + /// Allocator for temporary data. + /// Caller-owned mutable state passed to the scanline callback. + /// Scanline callback. + void RasterizePath( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct; +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs index 477c3325..755683e7 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs @@ -12,9 +12,10 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; public class FillPathProcessor : IImageProcessor { /// - /// Minimum subpixel count for rasterization, being applied even if antialiasing is off. + /// Fixed subpixel sampling density used by the CPU rasterizer for both antialiased and + /// quantized-aliased rendering. /// - internal const int MinimumSubpixelCount = 8; + internal const int FixedRasterizerSubpixelCount = 16; /// /// Initializes a new instance of the class. diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index 5da06dec..2621e211 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors; @@ -63,95 +63,233 @@ protected override void OnFrameApply(ImageFrame source) } int minX = interest.Left; - int subpixelCount = FillPathProcessor.MinimumSubpixelCount; - - // 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 - // region to align with the pixel grid. - if (graphicsOptions.Antialias) - { - subpixelCount = Math.Max(subpixelCount, graphicsOptions.AntialiasSubpixelDepth); - } + // The rasterizer always computes continuous coverage, then aliased mode quantizes coverage + // in ProcessRasterizedScanline(). + int subpixelCount = FillPathProcessor.FixedRasterizerSubpixelCount; using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, this.bounds); - int scanlineWidth = interest.Width; MemoryAllocator allocator = this.Configuration.MemoryAllocator; - bool scanlineDirty = true; - - PolygonScanner scanner = PolygonScanner.Create( - this.path, - interest.Top, - interest.Bottom, + IDrawingBackend drawingBackend = configuration.GetDrawingBackend(); + RasterizerOptions rasterizerOptions = new( + interest, subpixelCount, shapeOptions.IntersectionRule, - configuration.MemoryAllocator); + RasterizerSamplingOrigin.PixelBoundary); + + RasterizationState state = new( + source, + applicator, + minX, + graphicsOptions.Antialias, + isSolidBrushWithoutBlending, + solidBrushColor); + + drawingBackend.RasterizePath( + this.path, + rasterizerOptions, + allocator, + ref state, + ProcessRasterizedScanline); + } + + private static bool IsSolidBrushWithoutBlending(GraphicsOptions options, Brush inputBrush, [NotNullWhen(true)] out SolidBrush? solidBrush) + { + solidBrush = inputBrush as SolidBrush; - try + if (solidBrush == null) { - using IMemoryOwner bScanline = allocator.Allocate(scanlineWidth); - Span scanline = bScanline.Memory.Span; + return false; + } + + return options.IsOpaqueColorWithoutBlending(solidBrush.Color); + } - while (scanner.MoveToNextPixelLine()) + private static void ProcessRasterizedScanline(int y, Span scanline, ref RasterizationState state) + { + if (!state.Antialias) + { + bool hasOnes = false; + bool hasZeros = false; + for (int x = 0; x < scanline.Length; x++) { - if (scanlineDirty) + if (scanline[x] >= 0.5F) { - scanline.Clear(); + scanline[x] = 1F; + hasOnes = true; } + else + { + scanline[x] = 0F; + hasZeros = true; + } + } - scanlineDirty = scanner.ScanCurrentPixelLineInto(minX, 0F, scanline); - - if (scanlineDirty) + if (state.IsSolidBrushWithoutBlending && hasOnes != hasZeros) + { + if (hasOnes) { - int y = scanner.PixelLineY; - if (!graphicsOptions.Antialias) - { - bool hasOnes = false; - bool hasZeros = false; - for (int x = 0; x < scanline.Length; x++) - { - if (scanline[x] >= 0.5F) - { - scanline[x] = 1F; - hasOnes = true; - } - else - { - scanline[x] = 0F; - hasZeros = true; - } - } - - if (isSolidBrushWithoutBlending && hasOnes != hasZeros) - { - if (hasOnes) - { - source.PixelBuffer.DangerousGetRowSpan(y).Slice(minX, scanlineWidth).Fill(solidBrushColor); - } - - continue; - } - } - - applicator.Apply(scanline, minX, y); + state.Source.PixelBuffer.DangerousGetRowSpan(y).Slice(state.MinX, scanline.Length).Fill(state.SolidBrushColor); } + + return; + } + + if (state.IsSolidBrushWithoutBlending && hasOnes) + { + FillOpaqueRuns(state.Source, y, state.MinX, scanline, state.SolidBrushColor); + return; } } - finally + + if (state.IsSolidBrushWithoutBlending) + { + ApplyCoverageRunsForOpaqueSolidBrush(state.Source, state.Applicator, scanline, state.MinX, y, state.SolidBrushColor); + } + else { - scanner.Dispose(); + ApplyNonZeroCoverageRuns(state.Applicator, scanline, state.MinX, y); } } - private static bool IsSolidBrushWithoutBlending(GraphicsOptions options, Brush inputBrush, [NotNullWhen(true)] out SolidBrush? solidBrush) + private static void ApplyNonZeroCoverageRuns(BrushApplicator applicator, Span scanline, int minX, int y) { - solidBrush = inputBrush as SolidBrush; + int i = 0; + while (i < scanline.Length) + { + while (i < scanline.Length && scanline[i] <= 0F) + { + i++; + } - if (solidBrush == null) + int runStart = i; + while (i < scanline.Length && scanline[i] > 0F) + { + i++; + } + + int runLength = i - runStart; + if (runLength > 0) + { + applicator.Apply(scanline.Slice(runStart, runLength), minX + runStart, y); + } + } + } + + private static void ApplyCoverageRunsForOpaqueSolidBrush( + ImageFrame source, + BrushApplicator applicator, + Span scanline, + int minX, + int y, + TPixel solidBrushColor) + { + Span destinationRow = source.PixelBuffer.DangerousGetRowSpan(y).Slice(minX, scanline.Length); + int i = 0; + + while (i < scanline.Length) { - return false; + while (i < scanline.Length && scanline[i] <= 0F) + { + i++; + } + + int runStart = i; + while (i < scanline.Length && scanline[i] > 0F) + { + i++; + } + + int runEnd = i; + if (runEnd <= runStart) + { + continue; + } + + int opaqueStart = runStart; + while (opaqueStart < runEnd && scanline[opaqueStart] < 1F) + { + opaqueStart++; + } + + if (opaqueStart > runStart) + { + int prefixLength = opaqueStart - runStart; + applicator.Apply(scanline.Slice(runStart, prefixLength), minX + runStart, y); + } + + int opaqueEnd = runEnd; + while (opaqueEnd > opaqueStart && scanline[opaqueEnd - 1] < 1F) + { + opaqueEnd--; + } + + if (opaqueEnd > opaqueStart) + { + destinationRow.Slice(opaqueStart, opaqueEnd - opaqueStart).Fill(solidBrushColor); + } + + if (runEnd > opaqueEnd) + { + int suffixLength = runEnd - opaqueEnd; + applicator.Apply(scanline.Slice(opaqueEnd, suffixLength), minX + opaqueEnd, y); + } } + } - return options.IsOpaqueColorWithoutBlending(solidBrush.Color); + private static void FillOpaqueRuns(ImageFrame source, int y, int minX, Span scanline, TPixel solidBrushColor) + { + Span destinationRow = source.PixelBuffer.DangerousGetRowSpan(y).Slice(minX, scanline.Length); + int i = 0; + + while (i < scanline.Length) + { + while (i < scanline.Length && scanline[i] <= 0F) + { + i++; + } + + int runStart = i; + while (i < scanline.Length && scanline[i] > 0F) + { + i++; + } + + int runLength = i - runStart; + if (runLength > 0) + { + destinationRow.Slice(runStart, runLength).Fill(solidBrushColor); + } + } + } + + private readonly struct RasterizationState + { + public RasterizationState( + ImageFrame source, + BrushApplicator applicator, + int minX, + bool antialias, + bool isSolidBrushWithoutBlending, + TPixel solidBrushColor) + { + this.Source = source; + this.Applicator = applicator; + this.MinX = minX; + this.Antialias = antialias; + this.IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending; + this.SolidBrushColor = solidBrushColor; + } + + public ImageFrame Source { get; } + + public BrushApplicator Applicator { get; } + + public int MinX { get; } + + public bool Antialias { get; } + + public bool IsSolidBrushWithoutBlending { get; } + + public TPixel SolidBrushColor { get; } } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index e1a6e09a..86baf90b 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors; @@ -35,6 +36,7 @@ protected override void BeforeImageApply() textOptions, this.definition.DrawingOptions, this.Configuration.MemoryAllocator, + this.Configuration.GetDrawingBackend(), this.definition.Pen, this.definition.Brush); diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index ab29fc80..311ad7d7 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -6,6 +6,7 @@ using SixLabors.Fonts; using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Text; @@ -24,6 +25,7 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa private readonly DrawingOptions drawingOptions; private readonly MemoryAllocator memoryAllocator; + private readonly IDrawingBackend drawingBackend; private readonly Pen? defaultPen; private readonly Brush? defaultBrush; private readonly IPathInternals? path; @@ -55,12 +57,14 @@ public RichTextGlyphRenderer( RichTextOptions textOptions, DrawingOptions drawingOptions, MemoryAllocator memoryAllocator, + IDrawingBackend drawingBackend, Pen? pen, Brush? brush) : base(drawingOptions.Transform) { this.drawingOptions = drawingOptions; this.memoryAllocator = memoryAllocator; + this.drawingBackend = drawingBackend; this.defaultPen = pen; this.defaultBrush = brush; this.DrawingOperations = []; @@ -537,56 +541,43 @@ private Buffer2D Render(IPath path) // Pad to prevent edge clipping. size += new Size(2, 2); - int subpixelCount = FillPathProcessor.MinimumSubpixelCount; - float xOffset = .5F; + // Use one coverage rasterization path for both AA and aliased text. + // Aliased mode quantizes coverage in ProcessTextScanline(). + int subpixelCount = FillPathProcessor.FixedRasterizerSubpixelCount; + RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; GraphicsOptions graphicsOptions = this.drawingOptions.GraphicsOptions; - if (graphicsOptions.Antialias) - { - xOffset = 0F; // We are antialiasing. Skip offsetting as real antialiasing should take care of offset. - subpixelCount = Math.Max(subpixelCount, graphicsOptions.AntialiasSubpixelDepth); - } // Take the path inside the path builder, scan thing and generate a Buffer2D representing the glyph. Buffer2D buffer = this.memoryAllocator.Allocate2D(size.Width, size.Height, AllocationOptions.Clean); - - PolygonScanner scanner = PolygonScanner.Create( - offsetPath, - 0, - size.Height, + TextRasterizationState state = new(buffer, graphicsOptions.Antialias); + RasterizerOptions rasterizerOptions = new( + new Rectangle(0, 0, size.Width, size.Height), subpixelCount, TextUtilities.MapFillRule(this.currentFillRule), - this.memoryAllocator); + samplingOrigin); + + this.drawingBackend.RasterizePath( + offsetPath, + rasterizerOptions, + this.memoryAllocator, + ref state, + ProcessTextScanline); + + return buffer; + } + + private static void ProcessTextScanline(int y, Span scanline, ref TextRasterizationState state) + { + Span destination = state.Buffer.DangerousGetRowSpan(y); + scanline.CopyTo(destination); - try + if (!state.Antialias) { - while (scanner.MoveToNextPixelLine()) + for (int x = 0; x < destination.Length; x++) { - Span scanline = buffer.DangerousGetRowSpan(scanner.PixelLineY); - bool scanlineDirty = scanner.ScanCurrentPixelLineInto(0, xOffset, scanline); - - if (scanlineDirty && !graphicsOptions.Antialias) - { - for (int x = 0; x < size.Width; x++) - { - if (scanline[x] >= 0.5) - { - scanline[x] = 1; - } - else - { - scanline[x] = 0; - } - } - } + destination[x] = destination[x] >= 0.5F ? 1F : 0F; } } - finally - { - // Can't use ref struct as a 'ref' or 'out' value when 'using' so as it is readonly explicitly dispose. - scanner.Dispose(); - } - - return buffer; } private void Dispose(bool disposing) @@ -632,6 +623,19 @@ public readonly void Dispose() } } + private readonly struct TextRasterizationState + { + public TextRasterizationState(Buffer2D buffer, bool antialias) + { + this.Buffer = buffer; + this.Antialias = antialias; + } + + public Buffer2D Buffer { get; } + + public bool Antialias { get; } + } + private readonly struct CacheKey : IEquatable { public string Font { get; init; } diff --git a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs new file mode 100644 index 00000000..db2361cf --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Adds extensions that allow configuring the path rasterizer implementation. +/// +internal static class RasterizerDefaultsExtensions +{ + /// + /// Sets the drawing backend against the source image processing context. + /// + /// The image processing context to store the backend against. + /// The backend to use. + /// The passed in to allow chaining. + internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingContext context, IDrawingBackend backend) + { + Guard.NotNull(backend, nameof(backend)); + context.Properties[typeof(IDrawingBackend)] = backend; + + if (backend is CpuDrawingBackend cpuBackend) + { + context.Properties[typeof(IRasterizer)] = cpuBackend.PrimaryRasterizer; + } + + return context; + } + + /// + /// Sets the default drawing backend against the configuration. + /// + /// The configuration to store the backend against. + /// The backend to use. + internal static void SetDrawingBackend(this Configuration configuration, IDrawingBackend backend) + { + Guard.NotNull(backend, nameof(backend)); + configuration.Properties[typeof(IDrawingBackend)] = backend; + + if (backend is CpuDrawingBackend cpuBackend) + { + configuration.Properties[typeof(IRasterizer)] = cpuBackend.PrimaryRasterizer; + } + } + + /// + /// Gets the drawing backend from the source image processing context. + /// + /// The image processing context to retrieve the backend from. + /// The configured backend. + internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext context) + { + if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && + backend is IDrawingBackend configured) + { + return configured; + } + + if (context.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && + rasterizer is IRasterizer configuredRasterizer) + { + return CpuDrawingBackend.Create(configuredRasterizer); + } + + return context.Configuration.GetDrawingBackend(); + } + + /// + /// Gets the default drawing backend from the configuration. + /// + /// The configuration to retrieve the backend from. + /// The configured backend. + internal static IDrawingBackend GetDrawingBackend(this Configuration configuration) + { + if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && + backend is IDrawingBackend configured) + { + return configured; + } + + if (configuration.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && + rasterizer is IRasterizer configuredRasterizer) + { + IDrawingBackend rasterizerBackend = CpuDrawingBackend.Create(configuredRasterizer); + configuration.Properties[typeof(IDrawingBackend)] = rasterizerBackend; + return rasterizerBackend; + } + + IDrawingBackend defaultBackend = CpuDrawingBackend.Instance; + configuration.Properties[typeof(IDrawingBackend)] = defaultBackend; + return defaultBackend; + } + + /// + /// Sets the rasterizer against the source image processing context. + /// + /// The image processing context to store the rasterizer against. + /// The rasterizer to use. + /// The passed in to allow chaining. + internal static IImageProcessingContext SetRasterizer(this IImageProcessingContext context, IRasterizer rasterizer) + { + Guard.NotNull(rasterizer, nameof(rasterizer)); + context.Properties[typeof(IRasterizer)] = rasterizer; + context.Properties[typeof(IDrawingBackend)] = CpuDrawingBackend.Create(rasterizer); + return context; + } + + /// + /// Sets the default rasterizer against the configuration. + /// + /// The configuration to store the rasterizer against. + /// The rasterizer to use. + internal static void SetRasterizer(this Configuration configuration, IRasterizer rasterizer) + { + Guard.NotNull(rasterizer, nameof(rasterizer)); + configuration.Properties[typeof(IRasterizer)] = rasterizer; + configuration.Properties[typeof(IDrawingBackend)] = CpuDrawingBackend.Create(rasterizer); + } + + /// + /// Gets the rasterizer from the source image processing context. + /// + /// The image processing context to retrieve the rasterizer from. + /// The configured rasterizer. + internal static IRasterizer GetRasterizer(this IImageProcessingContext context) + { + if (context.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && + rasterizer is IRasterizer configured) + { + return configured; + } + + if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && + backend is CpuDrawingBackend cpuBackend) + { + return cpuBackend.PrimaryRasterizer; + } + + // Do not cache config fallback in the context so changes on configuration reflow. + return context.Configuration.GetRasterizer(); + } + + /// + /// Gets the default rasterizer from the configuration. + /// + /// The configuration to retrieve the rasterizer from. + /// The configured rasterizer. + internal static IRasterizer GetRasterizer(this Configuration configuration) + { + if (configuration.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && + rasterizer is IRasterizer configured) + { + return configured; + } + + if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && + backend is CpuDrawingBackend cpuBackend) + { + return cpuBackend.PrimaryRasterizer; + } + + IRasterizer defaultRasterizer = DefaultRasterizer.Instance; + configuration.Properties[typeof(IRasterizer)] = defaultRasterizer; + configuration.Properties[typeof(IDrawingBackend)] = CpuDrawingBackend.Instance; + return defaultRasterizer; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs index a58cce99..a760c521 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs @@ -37,6 +37,8 @@ public ActiveEdgeList(Span buffer) private readonly Span ActiveEdges => this.Buffer.Slice(0, this.count); + public readonly bool IsEmpty => this.count == 0; + public void EnterEdge(int edgeIdx) => this.Buffer[this.count++] = edgeIdx | EnteringEdgeFlag; public readonly void LeaveEdge(int edgeIdx) diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs new file mode 100644 index 00000000..588c4b7d --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// Default CPU scanline rasterizer used by ImageSharp.Drawing. +/// +internal sealed class DefaultRasterizer : IRasterizer +{ + /// + /// Gets the singleton default rasterizer instance. + /// + public static DefaultRasterizer Instance { get; } = new(); + + /// + public void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(allocator, nameof(allocator)); + Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); + + Rectangle interest = options.Interest; + if (interest.Equals(Rectangle.Empty)) + { + return; + } + + if (SharpBlazeScanner.TryRasterize(path, options, allocator, ref state, scanlineHandler)) + { + return; + } + + RasterizeWithPolygonScanner(path, options, allocator, ref state, scanlineHandler); + } + + private static void RasterizeWithPolygonScanner( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + Rectangle interest = options.Interest; + int minX = interest.Left; + int scanlineWidth = interest.Width; + float xOffset = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + bool scanlineDirty = true; + + PolygonScanner scanner = PolygonScanner.Create( + path, + interest.Top, + interest.Bottom, + options.SubpixelCount, + options.IntersectionRule, + allocator); + + try + { + using IMemoryOwner scanlineOwner = allocator.Allocate(scanlineWidth); + Span scanline = scanlineOwner.Memory.Span; + + while (scanner.MoveToNextPixelLine()) + { + if (scanlineDirty) + { + scanline.Clear(); + } + + scanlineDirty = scanner.ScanCurrentPixelLineInto(minX, xOffset, scanline); + if (scanlineDirty) + { + scanlineHandler(scanner.PixelLineY, scanline, ref state); + } + } + } + finally + { + scanner.Dispose(); + } + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs new file mode 100644 index 00000000..af642517 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// Delegate invoked for each rasterized scanline. +/// +/// The caller-provided state type. +/// The destination y coordinate. +/// Coverage values for the scanline. +/// Caller-provided mutable state. +internal delegate void RasterizerScanlineHandler(int y, Span scanline, ref TState state) + where TState : struct; + +/// +/// Defines a rasterizer capable of converting vector paths into per-pixel scanline coverage. +/// +internal interface IRasterizer +{ + /// + /// Rasterizes a path into scanline coverage and invokes + /// for each non-empty destination row. + /// + /// The caller-provided state type. + /// The path to rasterize. + /// Rasterization options. + /// The memory allocator used for temporary buffers. + /// Caller-provided mutable state passed to the callback. + /// + /// Callback invoked for each rasterized scanline. Implementations should invoke this callback + /// in ascending y order and not concurrently for a single invocation. + /// + void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct; +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs index 5ef039bc..daa0efec 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -176,6 +176,21 @@ private void SkipEdgesBeforeMinY() public bool MoveToNextPixelLine() { this.PixelLineY++; + + // When there are no active edges we can skip directly to the next row that may receive coverage. + if (this.activeEdges.IsEmpty) + { + if (this.idx0 < this.sorted0.Length) + { + float nextStartY = this.edges[this.sorted0[this.idx0]].Y0; + int nextRelevantPixelLine = (int)MathF.Floor(nextStartY); + if (nextRelevantPixelLine > this.PixelLineY) + { + this.PixelLineY = nextRelevantPixelLine; + } + } + } + this.yPlusOne = this.PixelLineY + 1; this.SubPixelY = this.PixelLineY - this.SubpixelDistance; return this.PixelLineY < this.maxY; @@ -183,6 +198,16 @@ public bool MoveToNextPixelLine() public bool MoveToNextSubpixelScanLine() { + // If the active edge list is empty and the next edge starts at or below the next pixel row, + // the current row cannot produce any intersections. + if (this.activeEdges.IsEmpty && + this.idx0 < this.sorted0.Length && + this.edges[this.sorted0[this.idx0]].Y0 >= this.yPlusOne) + { + this.SubPixelY = this.yPlusOne; + return false; + } + this.SubPixelY += this.SubpixelDistance; this.EnterEdges(); this.LeaveEdges(); diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs new file mode 100644 index 00000000..20517710 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// Describes where sample coverage is aligned relative to destination pixels. +/// +internal enum RasterizerSamplingOrigin +{ + /// + /// Samples are aligned to pixel boundaries. + /// + PixelBoundary = 0, + + /// + /// Samples are aligned to pixel centers. + /// + PixelCenter = 1 +} + +/// +/// Immutable options used by rasterizers when scan-converting vector geometry. +/// +internal readonly struct RasterizerOptions +{ + /// + /// Initializes a new instance of the struct. + /// + /// Destination bounds to rasterize into. + /// Subpixel sampling count. + /// Polygon intersection rule. + /// Sampling origin alignment. + public RasterizerOptions( + Rectangle interest, + int subpixelCount, + IntersectionRule intersectionRule, + RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary) + { + Guard.MustBeGreaterThan(subpixelCount, 0, nameof(subpixelCount)); + + this.Interest = interest; + this.SubpixelCount = subpixelCount; + this.IntersectionRule = intersectionRule; + this.SamplingOrigin = samplingOrigin; + } + + /// + /// Gets destination bounds to rasterize into. + /// + public Rectangle Interest { get; } + + /// + /// Gets the subpixel sampling count. + /// + public int SubpixelCount { get; } + + /// + /// Gets the polygon intersection rule. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the sampling origin alignment. + /// + public RasterizerSamplingOrigin SamplingOrigin { get; } + + /// + /// Creates a copy of the current options with a different interest rectangle. + /// + /// The replacement interest rectangle. + /// A new value. + public RasterizerOptions WithInterest(Rectangle interest) + => new(interest, this.SubpixelCount, this.IntersectionRule, this.SamplingOrigin); +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs new file mode 100644 index 00000000..205b8a51 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs @@ -0,0 +1,983 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// SharpBlaze-style fixed-point scanner that converts path segments into per-row coverage runs. +/// +internal static class SharpBlazeScanner +{ + private const int FixedShift = 8; + private const int FixedOne = 1 << FixedShift; + private static readonly int WordBitCount = IntPtr.Size * 8; + private const int AreaToCoverageShift = 9; + private const int CoverageStepCount = 256; + private const int EvenOddMask = (CoverageStepCount * 2) - 1; + private const int EvenOddPeriod = CoverageStepCount * 2; + private const float CoverageScale = 1F / CoverageStepCount; + + public static bool TryRasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + Rectangle interest = options.Interest; + int width = interest.Width; + int height = interest.Height; + if (width <= 0 || height <= 0) + { + return true; + } + + int wordsPerRow = BitVectorsForMaxBitCount(width); + long bitVectorCount = (long)wordsPerRow * height; + long coverStride = (long)width * 2; + long coverCount = coverStride * height; + if (bitVectorCount > int.MaxValue || coverCount > int.MaxValue) + { + return false; + } + + using IMemoryOwner bitVectorsOwner = allocator.Allocate((int)bitVectorCount, AllocationOptions.Clean); + using IMemoryOwner coverAreaOwner = allocator.Allocate((int)coverCount, AllocationOptions.Clean); + using IMemoryOwner startCoverOwner = allocator.Allocate(height, AllocationOptions.Clean); + using IMemoryOwner scanlineOwner = allocator.Allocate(width, AllocationOptions.Clean); + + float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + + Context context = new( + bitVectorsOwner.Memory.Span, + coverAreaOwner.Memory.Span, + startCoverOwner.Memory.Span, + width, + height, + wordsPerRow, + (int)coverStride, + options.IntersectionRule); + + context.RasterizePath(path, allocator, interest.Left, interest.Top, samplingOffsetX); + context.EmitScanlines(interest.Top, scanlineOwner.Memory.Span, ref state, scanlineHandler); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + + private static bool ClipToVerticalBounds(ref float x0, ref float y0, ref float x1, ref float y1, float minY, float maxY) + { + float t0 = 0F; + float t1 = 1F; + float dx = x1 - x0; + float dy = y1 - y0; + + if (!ClipTest(-dy, y0 - minY, ref t0, ref t1)) + { + return false; + } + + if (!ClipTest(dy, maxY - y0, ref t0, ref t1)) + { + return false; + } + + if (t1 < 1F) + { + x1 = x0 + (dx * t1); + y1 = y0 + (dy * t1); + } + + if (t0 > 0F) + { + x0 += dx * t0; + y0 += dy * t0; + } + + return y0 != y1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTest(float p, float q, ref float t0, ref float t1) + { + if (p == 0F) + { + return q >= 0F; + } + + float r = q / p; + if (p < 0F) + { + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } + } + else + { + if (r < t0) + { + return false; + } + + if (r < t1) + { + t1 = r; + } + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FindAdjustment(int value) + { + int lte0 = ~((value - 1) >> 31) & 1; + int divisibleBy256 = (((value & (FixedOne - 1)) - 1) >> 31) & 1; + return lte0 & divisibleBy256; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TrailingZeroCount(nuint value) + => IntPtr.Size == sizeof(ulong) + ? BitOperations.TrailingZeroCount((ulong)value) + : BitOperations.TrailingZeroCount((uint)value); + + private ref struct Context + { + private readonly Span bitVectors; + private readonly Span coverArea; + private readonly Span startCover; + private readonly int width; + private readonly int height; + private readonly int wordsPerRow; + private readonly int coverStride; + private readonly IntersectionRule intersectionRule; + + public Context( + Span bitVectors, + Span coverArea, + Span startCover, + int width, + int height, + int wordsPerRow, + int coverStride, + IntersectionRule intersectionRule) + { + this.bitVectors = bitVectors; + this.coverArea = coverArea; + this.startCover = startCover; + this.width = width; + this.height = height; + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.intersectionRule = intersectionRule; + } + + public void RasterizePath(IPath path, MemoryAllocator allocator, int minX, int minY, float samplingOffsetX) + { + using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); + foreach (TessellatedMultipolygon.Ring ring in multipolygon) + { + ReadOnlySpan vertices = ring.Vertices; + for (int i = 0; i < ring.VertexCount; i++) + { + PointF p0 = vertices[i]; + PointF p1 = vertices[i + 1]; + + float x0 = (p0.X - minX) + samplingOffsetX; + float y0 = p0.Y - minY; + float x1 = (p1.X - minX) + samplingOffsetX; + float y1 = p1.Y - minY; + + if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) + { + continue; + } + + if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, this.height)) + { + continue; + } + + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1); + if (fy0 == fy1) + { + continue; + } + + this.RasterizeLine(fx0, fy0, fx1, fy1); + } + } + } + + public void EmitScanlines(int destinationTop, Span scanline, ref TState state, RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + for (int row = 0; row < this.height; row++) + { + Span rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); + int rowCover = this.startCover[row]; + if (rowCover == 0 && IsRowEmpty(rowBitVectors)) + { + continue; + } + + scanline.Clear(); + bool scanlineDirty = this.EmitRowCoverage(rowBitVectors, row, rowCover, scanline); + if (scanlineDirty) + { + scanlineHandler(destinationTop + row, scanline, ref state); + } + } + } + + private static bool IsRowEmpty(ReadOnlySpan rowBitVectors) + { + for (int i = 0; i < rowBitVectors.Length; i++) + { + if (rowBitVectors[i] != 0) + { + return false; + } + } + + return true; + } + + private bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row, int cover, Span scanline) + { + int rowOffset = row * this.coverStride; + int spanStart = 0; + int spanEnd = 0; + float spanCoverage = 0F; + bool hasCoverage = false; + + for (int wordIndex = 0; wordIndex < rowBitVectors.Length; wordIndex++) + { + nuint bitset = rowBitVectors[wordIndex]; + while (bitset != 0) + { + int localBitIndex = TrailingZeroCount(bitset); + bitset &= bitset - 1; + + int x = (wordIndex * WordBitCount) + localBitIndex; + if ((uint)x >= (uint)this.width) + { + continue; + } + + int tableIndex = rowOffset + (x << 1); + int area = this.coverArea[tableIndex + 1] + (cover << AreaToCoverageShift); + float coverage = this.AreaToCoverage(area); + + if (spanEnd == x) + { + if (coverage <= 0F) + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + spanStart = x + 1; + spanEnd = spanStart; + spanCoverage = 0F; + } + else if (coverage == spanCoverage) + { + spanEnd = x + 1; + } + else + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + if (cover == 0) + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else + { + float gapCoverage = this.AreaToCoverage(cover << AreaToCoverageShift); + if (spanCoverage == gapCoverage) + { + if (coverage == gapCoverage) + { + spanEnd = x + 1; + } + else + { + hasCoverage |= FlushSpan(scanline, spanStart, x, spanCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + hasCoverage |= FlushSpan(scanline, spanEnd, x, gapCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + } + + cover += this.coverArea[tableIndex]; + } + } + + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + if (cover != 0 && spanEnd < this.width) + { + hasCoverage |= FlushSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift)); + } + + return hasCoverage; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float AreaToCoverage(int area) + { + int signedArea = area >> AreaToCoverageShift; + int absoluteArea = signedArea < 0 ? -signedArea : signedArea; + if (this.intersectionRule == IntersectionRule.NonZero) + { + if (absoluteArea >= CoverageStepCount) + { + return 1F; + } + + return absoluteArea * CoverageScale; + } + + int wrapped = absoluteArea & EvenOddMask; + if (wrapped > CoverageStepCount) + { + wrapped = EvenOddPeriod - wrapped; + } + + if (wrapped >= CoverageStepCount) + { + return 1F; + } + + return wrapped * CoverageScale; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool FlushSpan(Span scanline, int start, int end, float coverage) + { + if (coverage <= 0F || end <= start) + { + return false; + } + + scanline.Slice(start, end - start).Fill(coverage); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ConditionalSetBit(int row, int column) + { + int bitIndex = row * this.wordsPerRow; + int wordIndex = bitIndex + (column / WordBitCount); + nuint mask = (nuint)1 << (column % WordBitCount); + ref nuint word = ref this.bitVectors[wordIndex]; + bool newlySet = (word & mask) == 0; + word |= mask; + return newlySet; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddCell(int row, int column, int delta, int area) + { + if ((uint)row >= (uint)this.height) + { + return; + } + + if (column < 0) + { + this.startCover[row] += delta; + return; + } + + if ((uint)column >= (uint)this.width) + { + return; + } + + int index = (row * this.coverStride) + (column << 1); + if (this.ConditionalSetBit(row, column)) + { + this.coverArea[index] = delta; + this.coverArea[index + 1] = area; + } + else + { + this.coverArea[index] += delta; + this.coverArea[index + 1] += area; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CellVertical(int px, int py, int x, int y0, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x - x); + this.AddCell(py, px, delta, area); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Cell(int row, int px, int x0, int y0, int x1, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x0 - x1); + this.AddCell(row, px, delta, area); + } + + private void VerticalDown(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + this.CellVertical(columnIndex, rowIndex0, fx, fy0, FixedOne); + for (int row = rowIndex0 + 1; row < rowIndex1; row++) + { + this.CellVertical(columnIndex, row, fx, 0, FixedOne); + } + + this.CellVertical(columnIndex, rowIndex1, fx, 0, fy1); + } + + private void VerticalUp(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = (y0 - 1) >> FixedShift; + int rowIndex1 = y1 >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + this.CellVertical(columnIndex, rowIndex0, fx, fy0, 0); + for (int row = rowIndex0 - 1; row > rowIndex1; row--) + { + this.CellVertical(columnIndex, row, fx, FixedOne, 0); + } + + this.CellVertical(columnIndex, rowIndex1, fx, FixedOne, fy1); + } + + private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p1y - p0y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); + } + + private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowDownR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p0y - p1y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y - (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); + } + + private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowUpR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p0x - p1x; + int dy = p1y - p0y; + int pp = fx0 * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); + } + + private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x > p1x) + { + this.RowDownL(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p0x - p1x; + int dy = p0y - p1y; + int pp = fx0 * dy; + int cy = p0y - (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); + } + + private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x > p1x) + { + this.RowUpL(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowDownR_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowDownR_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownR_V(rowIndex1, cx, 0, x1, fy1); + } + + private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowUpR_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowUpR_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpR_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 - delta; + + this.RowDownL_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowDownL_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownL_V(rowIndex1, cx, 0, x1, fy1); + } + + private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 - delta; + + this.RowUpL_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowUpL_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpL_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + private void RasterizeLine(int x0, int y0, int x1, int y1) + { + if (x0 == x1) + { + int columnIndex = (x0 - FindAdjustment(x0)) >> FixedShift; + if (y0 < y1) + { + this.VerticalDown(columnIndex, y0, y1, x0); + } + else + { + this.VerticalUp(columnIndex, y0, y1, x0); + } + + return; + } + + if (y0 < y1) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + if (rowIndex0 == rowIndex1) + { + int rowBase = rowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowDownR(rowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowDownL(rowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineDownR(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + else + { + this.LineDownL(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + + return; + } + + int upRowIndex0 = (y0 - 1) >> FixedShift; + int upRowIndex1 = y1 >> FixedShift; + if (upRowIndex0 == upRowIndex1) + { + int rowBase = upRowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowUpR(upRowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowUpL(upRowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineUpR(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } + else + { + this.LineUpL(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } + } + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs new file mode 100644 index 00000000..9f045565 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs @@ -0,0 +1,254 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// Experimental tiled CPU rasterizer. +/// +/// +/// The implementation splits the Y range into independent bands, rasterizes each band into a +/// temporary coverage buffer, then emits scanlines in deterministic top-to-bottom order. +/// This keeps the external callback contract unchanged while enabling parallel work internally. +/// +internal sealed class TiledRasterizer : IRasterizer +{ + // Keep tiles reasonably tall so small text/glyph workloads stay on the scalar fast path. + private const int MinimumBandHeight = 96; + + // Minimum amount of work assigned to each band. + private const int MinimumPixelsPerBand = 196608; + + // Bounded temporary memory: one float coverage value per destination pixel. + private const int MaximumBufferedPixels = 16777216; // 4096 x 4096 + + private const int MaximumBandCount = 8; + + /// + /// Gets the singleton tiled rasterizer instance. + /// + public static TiledRasterizer Instance { get; } = new(); + + /// + public void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(allocator, nameof(allocator)); + Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); + + Rectangle interest = options.Interest; + if (interest.Equals(Rectangle.Empty)) + { + return; + } + + if (!TryCreateBandPlan(interest, out Band[]? plannedBands) || plannedBands is null) + { + DefaultRasterizer.Instance.Rasterize(path, options, allocator, ref state, scanlineHandler); + return; + } + + Band[] bands = plannedBands; + RasterizerOptions bandedOptions = options; + + // Most path implementations lazily materialize flattened point buffers. + // Priming this once avoids duplicated cache-building across worker threads. + PrimePathCaches(path); + + try + { + ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = bands.Length }; + Parallel.For( + 0, + bands.Length, + parallelOptions, + i => RasterizeBand(path, bandedOptions, allocator, bands[i])); + + EmitBands(bands, interest.Width, ref state, scanlineHandler); + } + finally + { + foreach (Band band in bands) + { + band.Dispose(); + } + } + } + + private static void PrimePathCaches(IPath path) + { + foreach (ISimplePath simplePath in path.Flatten()) + { + _ = simplePath.Points.Length; + } + } + + private static bool TryCreateBandPlan(Rectangle interest, out Band[]? bands) + { + bands = null; + + int width = interest.Width; + int height = interest.Height; + long totalPixels = (long)width * height; + if (totalPixels > MaximumBufferedPixels) + { + return false; + } + + int processorCount = Environment.ProcessorCount; + if (processorCount < 2 || height < (MinimumBandHeight * 2) || totalPixels < (MinimumPixelsPerBand * 2L)) + { + return false; + } + + int byHeight = height / MinimumBandHeight; + int byPixels = (int)(totalPixels / MinimumPixelsPerBand); + int bandCount = Math.Min(MaximumBandCount, Math.Min(processorCount, Math.Min(byHeight, byPixels))); + if (bandCount < 2) + { + return false; + } + + bands = new Band[bandCount]; + int baseHeight = height / bandCount; + int remainder = height % bandCount; + int y = interest.Top; + + for (int i = 0; i < bandCount; i++) + { + int bandHeight = baseHeight + (i < remainder ? 1 : 0); + bands[i] = new Band(y, bandHeight); + y += bandHeight; + } + + return true; + } + + private static void RasterizeBand( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + Band band) + { + int width = options.Interest.Width; + int coverageLength = checked(width * band.Height); + + IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); + IMemoryOwner dirtyRowsOwner = allocator.Allocate(band.Height, AllocationOptions.Clean); + + try + { + RasterizerOptions bandOptions = options.WithInterest( + new Rectangle(options.Interest.Left, band.Top, width, band.Height)); + + BandCaptureState captureState = new(band.Top, width, coverageOwner.Memory, dirtyRowsOwner.Memory); + DefaultRasterizer.Instance.Rasterize(path, bandOptions, allocator, ref captureState, CaptureBandScanline); + + band.SetBuffers(coverageOwner, dirtyRowsOwner); + } + catch + { + coverageOwner.Dispose(); + dirtyRowsOwner.Dispose(); + throw; + } + } + + private static void EmitBands( + Band[] bands, + int scanlineWidth, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + foreach (Band band in bands) + { + if (band.CoverageOwner is null || band.DirtyRowsOwner is null) + { + continue; + } + + Span coverage = band.CoverageOwner.Memory.Span; + Span dirtyRows = band.DirtyRowsOwner.Memory.Span; + + for (int row = 0; row < band.Height; row++) + { + if (dirtyRows[row] == 0) + { + continue; + } + + Span scanline = coverage.Slice(row * scanlineWidth, scanlineWidth); + scanlineHandler(band.Top + row, scanline, ref state); + } + } + } + + private static void CaptureBandScanline(int y, Span scanline, ref BandCaptureState state) + { + int row = y - state.Top; + Span coverage = state.Coverage.Span; + scanline.CopyTo(coverage.Slice(row * state.Width, state.Width)); + state.DirtyRows.Span[row] = 1; + } + + private struct BandCaptureState + { + public BandCaptureState(int top, int width, Memory coverage, Memory dirtyRows) + { + this.Top = top; + this.Width = width; + this.Coverage = coverage; + this.DirtyRows = dirtyRows; + } + + public int Top { get; } + + public int Width { get; } + + public Memory Coverage { get; } + + public Memory DirtyRows { get; } + } + + private sealed class Band : IDisposable + { + public Band(int top, int height) + { + this.Top = top; + this.Height = height; + } + + public int Top { get; } + + public int Height { get; } + + public IMemoryOwner? CoverageOwner { get; private set; } + + public IMemoryOwner? DirtyRowsOwner { get; private set; } + + public void SetBuffers(IMemoryOwner coverageOwner, IMemoryOwner dirtyRowsOwner) + { + this.CoverageOwner = coverageOwner; + this.DirtyRowsOwner = dirtyRowsOwner; + } + + public void Dispose() + { + this.CoverageOwner?.Dispose(); + this.DirtyRowsOwner?.Dispose(); + this.CoverageOwner = null; + this.DirtyRowsOwner = null; + } + } +} diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 2305594e..cb35b1b0 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -8,6 +8,7 @@ using GeoJSON.Net.Feature; using Newtonsoft.Json; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -161,6 +162,22 @@ public void ImageSharpSeparatePaths() } }); + [Benchmark] + public void ImageSharpCombinedPathsTiled() + => this.image.Mutate(c => c.SetRasterizer(TiledRasterizer.Instance).Draw(this.isPen, this.imageSharpPath)); + + [Benchmark] + public void ImageSharpSeparatePathsTiled() + => this.image.Mutate( + c => + { + c.SetRasterizer(TiledRasterizer.Instance); + foreach (PointF[] loop in this.points) + { + c.DrawPolygon(Color.White, this.Thickness, loop); + } + }); + [Benchmark(Baseline = true)] public void SkiaSharp() => this.skSurface.Canvas.DrawPath(this.skPath, this.skPaint); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index 9898ccc7..2405dace 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -6,6 +6,7 @@ using GeoJSON.Net.Feature; using Newtonsoft.Json; using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -87,7 +88,7 @@ public void LargeGeoJson_Lines(TestImageProvider provider, string geoJso using Image image = provider.GetImage(); DrawingOptions options = new() { - GraphicsOptions = new GraphicsOptions() { Antialias = aa > 0, AntialiasSubpixelDepth = aa }, + GraphicsOptions = new GraphicsOptions() { Antialias = aa > 0 }, }; foreach (PointF[] loop in points) { @@ -123,7 +124,7 @@ private Image FillGeoJsonPolygons(TestImageProvider provider, st Image image = provider.GetImage(); DrawingOptions options = new() { - GraphicsOptions = new GraphicsOptions() { Antialias = aa > 0, AntialiasSubpixelDepth = aa }, + GraphicsOptions = new GraphicsOptions() { Antialias = aa > 0 }, }; Random rnd = new(42); byte[] rgb = new byte[3]; @@ -201,7 +202,9 @@ public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provi image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } - [Theory]//(Skip = "For local experiments only")] +#pragma warning disable xUnit1004 // Test methods should not be skipped + [Theory(Skip = "For local experiments only")] +#pragma warning restore xUnit1004 // Test methods should not be skipped [InlineData(0)] [InlineData(5000)] [InlineData(9000)] @@ -259,7 +262,7 @@ public void Missisippi_Skia(int offset) [Theory] [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] - public void LargeGeoJson_States_Benchmark(TestImageProvider provider, int thickness) + public void LargeGeoJson_States_Separate_Benchmark(TestImageProvider provider, int thickness) { string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); @@ -284,6 +287,40 @@ public void LargeGeoJson_States_Benchmark(TestImageProvider provider, in image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } + [Theory] + [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] + public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider, int thickness) + { + 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); + + PathBuilder pb = new(); + foreach (PointF[] loop in points) + { + pb.StartFigure(); + pb.AddLines(loop); + pb.CloseFigure(); + } + + IPath path = pb.Build(); + + using Image image = provider.GetImage(); + + image.Mutate(c => + { + c.SetRasterizer(TiledRasterizer.Instance); + c.Draw(Color.White, thickness, path); + }); + + image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + } + [Theory] [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] public void LargeStar_Benchmark(TestImageProvider provider, int thickness) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index b86078fc..facad777 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -22,7 +22,7 @@ public void FillPolygon_Solid_Basic(TestImageProvider provider, PointF[] polygon1 = PolygonFactory.CreatePointArray((2, 2), (6, 2), (6, 4), (2, 4)); PointF[] polygon2 = PolygonFactory.CreatePointArray((2, 8), (4, 6), (6, 8), (4, 10)); - GraphicsOptions options = new() { Antialias = antialias > 0, AntialiasSubpixelDepth = antialias }; + GraphicsOptions options = new() { Antialias = antialias > 0 }; provider.RunValidatingProcessorTest( c => c.SetGraphicsOptions(options) .FillPolygon(Color.White, polygon1) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs index c4b54af3..178dfa48 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs @@ -15,8 +15,7 @@ public class Clear : BaseImageOperationsExtensionTest { AlphaCompositionMode = PixelFormats.PixelAlphaCompositionMode.Clear, BlendPercentage = 0.5f, - ColorBlendingMode = PixelFormats.PixelColorBlendingMode.Darken, - AntialiasSubpixelDepth = 99 + ColorBlendingMode = PixelFormats.PixelColorBlendingMode.Darken } }; diff --git a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs index a41be44b..6700b36d 100644 --- a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs @@ -22,14 +22,6 @@ public void DefaultGraphicsOptionsAntialias() Assert.True(this.cloneGraphicsOptions.Antialias); } - [Fact] - public void DefaultGraphicsOptionsAntialiasSuppixelDepth() - { - const int Expected = 16; - Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth); - Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth); - } - [Fact] public void DefaultGraphicsOptionsBlendPercentage() { @@ -61,7 +53,6 @@ public void NonDefaultClone() { AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, Antialias = false, - AntialiasSubpixelDepth = 23, BlendPercentage = .25F, ColorBlendingMode = PixelColorBlendingMode.HardLight, }; @@ -79,7 +70,6 @@ public void CloneIsDeep() actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.Antialias = false; - actual.AntialiasSubpixelDepth = 23; actual.BlendPercentage = .25F; actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/BaseImageOperationsExtensionTest.cs b/tests/ImageSharp.Drawing.Tests/Processing/BaseImageOperationsExtensionTest.cs index 926b355c..c0ab5fbe 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/BaseImageOperationsExtensionTest.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/BaseImageOperationsExtensionTest.cs @@ -23,7 +23,6 @@ public BaseImageOperationsExtensionTest() { this.graphicsOptions = new GraphicsOptions { - AntialiasSubpixelDepth = 99, Antialias = false, BlendPercentage = 0.9f, AlphaCompositionMode = PixelAlphaCompositionMode.DestOut, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs index 3ebbeb67..9f5e6fe1 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs @@ -8,6 +8,8 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; using SixLabors.ImageSharp.Drawing.Shapes; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; @@ -214,6 +216,71 @@ public void DrawPathProcessor_PreservesRule_WhenStrokeNormalizationIsEnabled() Assert.Equal(IntersectionRule.EvenOdd, definition.Options.ShapeOptions.IntersectionRule); } + + [Fact] + public void FillPathProcessor_UsesConfiguredRasterizer() + { + RecordingRasterizer rasterizer = new(); + Configuration configuration = new(); + configuration.SetRasterizer(rasterizer); + + FillPathProcessor processor = new( + new DrawingOptions(), + Brushes.Solid(Color.White), + new EllipsePolygon(6F, 6F, 4F)); + + using Image image = new(configuration, 20, 20); + processor.Execute(configuration, image, image.Bounds); + + Assert.True(rasterizer.CallCount > 0); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FillPathProcessor_UsesFixedSubpixelCount(bool antialias) + { + RecordingRasterizer rasterizer = new(); + Configuration configuration = new(); + configuration.SetRasterizer(rasterizer); + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = antialias + } + }; + + FillPathProcessor processor = new( + drawingOptions, + Brushes.Solid(Color.White), + new EllipsePolygon(6F, 6F, 4F)); + + using Image image = new(configuration, 20, 20); + processor.Execute(configuration, image, image.Bounds); + + Assert.Equal(FillPathProcessor.FixedRasterizerSubpixelCount, rasterizer.LastSubpixelCount); + } + + private sealed class RecordingRasterizer : IRasterizer + { + public int CallCount { get; private set; } + + public int LastSubpixelCount { get; private set; } + + public void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + this.CallCount++; + this.LastSubpixelCount = options.SubpixelCount; + } + } } internal static class ReflectionHelpers diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs new file mode 100644 index 00000000..10281b68 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class RasterizerDefaultsExtensionsTests +{ + [Fact] + public void GetDefaultRasterizerFromConfiguration_AlwaysReturnsDefaultInstance() + { + Configuration configuration = new(); + + IRasterizer first = configuration.GetRasterizer(); + IRasterizer second = configuration.GetRasterizer(); + + Assert.Same(first, second); + Assert.Same(DefaultRasterizer.Instance, first); + } + + [Fact] + public void GetDefaultDrawingBackendFromConfiguration_AlwaysReturnsDefaultInstance() + { + Configuration configuration = new(); + + IDrawingBackend first = configuration.GetDrawingBackend(); + IDrawingBackend second = configuration.GetDrawingBackend(); + + Assert.Same(first, second); + Assert.Same(CpuDrawingBackend.Instance, first); + } + + [Fact] + public void SetRasterizerOnConfiguration_RoundTrips() + { + Configuration configuration = new(); + RecordingRasterizer rasterizer = new(); + + configuration.SetRasterizer(rasterizer); + + Assert.Same(rasterizer, configuration.GetRasterizer()); + Assert.IsType(configuration.GetDrawingBackend()); + } + + [Fact] + public void SetRasterizerOnProcessingContext_RoundTrips() + { + Configuration configuration = new(); + FakeImageOperationsProvider.FakeImageOperations context = new(configuration, null, true); + RecordingRasterizer rasterizer = new(); + + context.SetRasterizer(rasterizer); + + Assert.Same(rasterizer, context.GetRasterizer()); + Assert.IsType(context.GetDrawingBackend()); + } + + [Fact] + public void GetRasterizerFromProcessingContext_FallsBackToConfiguration() + { + Configuration configuration = new(); + RecordingRasterizer rasterizer = new(); + configuration.SetRasterizer(rasterizer); + FakeImageOperationsProvider.FakeImageOperations context = new(configuration, null, true); + + Assert.Same(rasterizer, context.GetRasterizer()); + } + + [Fact] + public void SetDrawingBackendOnConfiguration_RoundTrips() + { + Configuration configuration = new(); + RecordingDrawingBackend backend = new(); + + configuration.SetDrawingBackend(backend); + + Assert.Same(backend, configuration.GetDrawingBackend()); + } + + [Fact] + public void SetDrawingBackendOnProcessingContext_RoundTrips() + { + Configuration configuration = new(); + FakeImageOperationsProvider.FakeImageOperations context = new(configuration, null, true); + RecordingDrawingBackend backend = new(); + + context.SetDrawingBackend(backend); + + Assert.Same(backend, context.GetDrawingBackend()); + } + + private sealed class RecordingRasterizer : IRasterizer + { + public void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + } + } + + private sealed class RecordingDrawingBackend : IDrawingBackend + { + public void RasterizePath( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TiledRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TiledRasterizerTests.cs new file mode 100644 index 00000000..de270faa --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TiledRasterizerTests.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; + +public class TiledRasterizerTests +{ + [Theory] + [InlineData(IntersectionRule.EvenOdd)] + [InlineData(IntersectionRule.NonZero)] + public void MatchesDefaultRasterizer_ForLargeSelfIntersectingPath(IntersectionRule rule) + { + IPath path = PolygonFactory.CreatePolygon( + (1, 4), + (1, 3), + (3, 3), + (3, 2), + (2, 2), + (2, 4), + (1, 4), + (1, 1), + (4, 1), + (4, 4), + (3, 4), + (3, 5), + (2, 5), + (2, 4), + (1, 4)) + .Transform(Matrix3x2.CreateScale(200F)); + + Rectangle interest = Rectangle.Ceiling(path.Bounds); + RasterizerOptions options = new(interest, 4, rule); + + float[] expected = Rasterize(DefaultRasterizer.Instance, path, options); + float[] actual = Rasterize(TiledRasterizer.Instance, path, options); + + AssertCoverageEqual(expected, actual); + } + + [Fact] + public void MatchesDefaultRasterizer_ForPixelCenterSampling() + { + RectangularPolygon path = new(20.2F, 30.4F, 700.1F, 540.6F); + Rectangle interest = Rectangle.Ceiling(path.Bounds); + RasterizerOptions options = new( + interest, + 1, + IntersectionRule.NonZero, + RasterizerSamplingOrigin.PixelCenter); + + float[] expected = Rasterize(DefaultRasterizer.Instance, path, options); + float[] actual = Rasterize(TiledRasterizer.Instance, path, options); + + AssertCoverageEqual(expected, actual); + } + + private static float[] Rasterize(IRasterizer rasterizer, IPath path, in RasterizerOptions options) + { + int width = options.Interest.Width; + int height = options.Interest.Height; + float[] coverage = new float[width * height]; + CaptureState state = new(coverage, width, options.Interest.Top); + + rasterizer.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + + return coverage; + } + + private static void CaptureScanline(int y, Span scanline, ref CaptureState state) + { + int row = y - state.Top; + scanline.CopyTo(state.Coverage.AsSpan(row * state.Width, state.Width)); + } + + private static void AssertCoverageEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i], 6); + } + } + + private readonly struct CaptureState + { + public CaptureState(float[] coverage, int width, int top) + { + this.Coverage = coverage; + this.Width = width; + this.Top = top; + } + + public float[] Coverage { get; } + + public int Width { get; } + + public int Top { get; } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs index 1d3d4ddd..fcdb3f9c 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs @@ -11,13 +11,11 @@ public bool Equals(GraphicsOptions x, GraphicsOptions y) { if (this.SkipClearOptions) { - return x.Antialias == y.Antialias - && x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth; + return x.Antialias == y.Antialias; } return x.AlphaCompositionMode == y.AlphaCompositionMode && x.Antialias == y.Antialias - && x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth && x.BlendPercentage == y.BlendPercentage && x.ColorBlendingMode == y.ColorBlendingMode; } From 2cf5aead50013872230d4c48bdef3449ca0721b0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 17:55:38 +1000 Subject: [PATCH 22/35] Use less memory --- .../Shapes/Rasterization/SharpBlazeScanner.cs | 137 +++++++++++++----- 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs index 205b8a51..c9f0c355 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs @@ -13,6 +13,10 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; /// internal static class SharpBlazeScanner { + // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows). + // Keeping this bounded prevents pathological full-image allocations on very large interests. + private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L; + private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; private static readonly int WordBitCount = IntPtr.Size * 8; @@ -39,39 +43,108 @@ public static bool TryRasterize( } int wordsPerRow = BitVectorsForMaxBitCount(width); - long bitVectorCount = (long)wordsPerRow * height; long coverStride = (long)width * 2; - long coverCount = coverStride * height; - if (bitVectorCount > int.MaxValue || coverCount > int.MaxValue) + if (coverStride > int.MaxValue || + !TryGetBandHeight(width, height, wordsPerRow, coverStride, out int maxBandRows)) { return false; } - using IMemoryOwner bitVectorsOwner = allocator.Allocate((int)bitVectorCount, AllocationOptions.Clean); - using IMemoryOwner coverAreaOwner = allocator.Allocate((int)coverCount, AllocationOptions.Clean); - using IMemoryOwner startCoverOwner = allocator.Allocate(height, AllocationOptions.Clean); - using IMemoryOwner scanlineOwner = allocator.Allocate(width, AllocationOptions.Clean); + int coverStrideInt = (int)coverStride; + int bitVectorCapacity = checked(wordsPerRow * maxBandRows); + int coverAreaCapacity = checked(coverStrideInt * maxBandRows); + using IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity); + using IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); + using IMemoryOwner startCoverOwner = allocator.Allocate(maxBandRows); + + // Per-row activity flags avoid scanning the full bit-vector row just to detect "empty row". + using IMemoryOwner rowHasBitsOwner = allocator.Allocate(maxBandRows); + using IMemoryOwner scanlineOwner = allocator.Allocate(width); + + Span bitVectorsBuffer = bitVectorsOwner.Memory.Span; + Span coverAreaBuffer = coverAreaOwner.Memory.Span; + Span startCoverBuffer = startCoverOwner.Memory.Span; + Span rowHasBitsBuffer = rowHasBitsOwner.Memory.Span; + Span scanline = scanlineOwner.Memory.Span; float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - Context context = new( - bitVectorsOwner.Memory.Span, - coverAreaOwner.Memory.Span, - startCoverOwner.Memory.Span, - width, - height, - wordsPerRow, - (int)coverStride, - options.IntersectionRule); - - context.RasterizePath(path, allocator, interest.Left, interest.Top, samplingOffsetX); - context.EmitScanlines(interest.Top, scanlineOwner.Memory.Span, ref state, scanlineHandler); + using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); + int bandTop = 0; + while (bandTop < height) + { + int bandHeight = Math.Min(maxBandRows, height - bandTop); + int bitVectorCount = wordsPerRow * bandHeight; + int coverCount = coverStrideInt * bandHeight; + + Span bitVectors = bitVectorsBuffer[..bitVectorCount]; + Span coverArea = coverAreaBuffer[..coverCount]; + Span startCover = startCoverBuffer[..bandHeight]; + Span rowHasBits = rowHasBitsBuffer[..bandHeight]; + + bitVectors.Clear(); + coverArea.Clear(); + startCover.Clear(); + rowHasBits.Clear(); + + Context context = new( + bitVectors, + coverArea, + startCover, + rowHasBits, + width, + bandHeight, + wordsPerRow, + coverStrideInt, + options.IntersectionRule); + + context.RasterizeMultipolygon( + multipolygon, + interest.Left, + interest.Top + bandTop, + samplingOffsetX); + + context.EmitScanlines(interest.Top + bandTop, scanline, ref state, scanlineHandler); + bandTop += bandHeight; + } + return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; + private static bool TryGetBandHeight(int width, int height, int wordsPerRow, long coverStride, out int bandHeight) + { + bandHeight = 0; + if (width <= 0 || height <= 0 || wordsPerRow <= 0 || coverStride <= 0) + { + return false; + } + + long bytesPerRow = + ((long)wordsPerRow * IntPtr.Size) + + (coverStride * sizeof(int)) + + sizeof(int); + + long rowsByBudget = BandMemoryBudgetBytes / bytesPerRow; + if (rowsByBudget < 1) + { + rowsByBudget = 1; + } + + long rowsByBitVectors = int.MaxValue / wordsPerRow; + long rowsByCoverArea = int.MaxValue / coverStride; + long maxRows = Math.Min(rowsByBudget, Math.Min(rowsByBitVectors, rowsByCoverArea)); + if (maxRows < 1) + { + return false; + } + + bandHeight = (int)Math.Min(height, maxRows); + return bandHeight > 0; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); @@ -163,6 +236,7 @@ private ref struct Context private readonly Span bitVectors; private readonly Span coverArea; private readonly Span startCover; + private readonly Span rowHasBits; private readonly int width; private readonly int height; private readonly int wordsPerRow; @@ -173,6 +247,7 @@ public Context( Span bitVectors, Span coverArea, Span startCover, + Span rowHasBits, int width, int height, int wordsPerRow, @@ -182,6 +257,7 @@ public Context( this.bitVectors = bitVectors; this.coverArea = coverArea; this.startCover = startCover; + this.rowHasBits = rowHasBits; this.width = width; this.height = height; this.wordsPerRow = wordsPerRow; @@ -189,9 +265,8 @@ public Context( this.intersectionRule = intersectionRule; } - public void RasterizePath(IPath path, MemoryAllocator allocator, int minX, int minY, float samplingOffsetX) + public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX, int minY, float samplingOffsetX) { - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); foreach (TessellatedMultipolygon.Ring ring in multipolygon) { ReadOnlySpan vertices = ring.Vertices; @@ -234,13 +309,13 @@ public void EmitScanlines(int destinationTop, Span scanline, ref { for (int row = 0; row < this.height; row++) { - Span rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); int rowCover = this.startCover[row]; - if (rowCover == 0 && IsRowEmpty(rowBitVectors)) + if (rowCover == 0 && this.rowHasBits[row] == 0) { continue; } + Span rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); scanline.Clear(); bool scanlineDirty = this.EmitRowCoverage(rowBitVectors, row, rowCover, scanline); if (scanlineDirty) @@ -250,19 +325,6 @@ public void EmitScanlines(int destinationTop, Span scanline, ref } } - private static bool IsRowEmpty(ReadOnlySpan rowBitVectors) - { - for (int i = 0; i < rowBitVectors.Length; i++) - { - if (rowBitVectors[i] != 0) - { - return false; - } - } - - return true; - } - private bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row, int cover, Span scanline) { int rowOffset = row * this.coverStride; @@ -397,7 +459,7 @@ private static bool FlushSpan(Span scanline, int start, int end, float co return false; } - scanline.Slice(start, end - start).Fill(coverage); + scanline[start..end].Fill(coverage); return true; } @@ -410,6 +472,7 @@ private bool ConditionalSetBit(int row, int column) ref nuint word = ref this.bitVectors[wordIndex]; bool newlySet = (word & mask) == 0; word |= mask; + this.rowHasBits[row] = 1; return newlySet; } From 815b1e62a551b6bea7534bd7ef6adfdbe9d1e81d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 18:03:03 +1000 Subject: [PATCH 23/35] Use nint --- .../Shapes/Rasterization/SharpBlazeScanner.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs index c9f0c355..8c9f9abe 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs @@ -19,7 +19,7 @@ internal static class SharpBlazeScanner private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; - private static readonly int WordBitCount = IntPtr.Size * 8; + private static readonly int WordBitCount = nint.Size * 8; private const int AreaToCoverageShift = 9; private const int CoverageStepCount = 256; private const int EvenOddMask = (CoverageStepCount * 2) - 1; @@ -123,7 +123,7 @@ private static bool TryGetBandHeight(int width, int height, int wordsPerRow, lon } long bytesPerRow = - ((long)wordsPerRow * IntPtr.Size) + + ((long)wordsPerRow * nint.Size) + (coverStride * sizeof(int)) + sizeof(int); @@ -227,11 +227,11 @@ private static int FindAdjustment(int value) [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int TrailingZeroCount(nuint value) - => IntPtr.Size == sizeof(ulong) + => nint.Size == sizeof(ulong) ? BitOperations.TrailingZeroCount((ulong)value) : BitOperations.TrailingZeroCount((uint)value); - private ref struct Context + private readonly ref struct Context { private readonly Span bitVectors; private readonly Span coverArea; From ed7c0429e6bdfd136c3593dfc01e6728c35150f3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 20:38:07 +1000 Subject: [PATCH 24/35] Remove old scanner/rasterizer --- .../Processing/Backends/CpuDrawingBackend.cs | 32 +- .../Processors/Drawing/FillPathProcessor.cs | 6 - .../Drawing/FillPathProcessor{TPixel}.cs | 72 +- .../Processors/Text/RichTextGlyphRenderer.cs | 24 +- .../Shapes/ComplexPolygon.cs | 19 +- .../Shapes/IInternalPathOwner.cs | 2 +- src/ImageSharp.Drawing/Shapes/Path.cs | 4 +- .../Shapes/Rasterization/ActiveEdgeList.cs | 283 ---- .../Shapes/Rasterization/DefaultRasterizer.cs | 348 ++++- .../Shapes/Rasterization/PolygonScanner.cs | 1198 ++++++++++++++--- .../Shapes/Rasterization/PolygonScanning.MD | 258 +++- .../Rasterization/RasterizerExtensions.cs | 70 - .../Shapes/Rasterization/RasterizerOptions.cs | 34 +- .../Shapes/Rasterization/ScanEdge.cs | 64 - .../Rasterization/ScanEdgeCollection.Build.cs | 434 ------ .../Rasterization/ScanEdgeCollection.cs | 43 - .../Rasterization/ScanlineRasterizer.cs | 44 + .../Shapes/Rasterization/SharpBlazeScanner.cs | 1046 -------------- .../Shapes/Rasterization/TiledRasterizer.cs | 254 ---- .../Shapes/TessellatedMultipolygon.cs | 2 +- .../Drawing/DrawPolygon.cs | 26 +- .../ImageSharp.Drawing.Benchmarks.csproj | 2 + .../ImageSharp.Drawing.Benchmarks/Program.cs | 10 +- .../Drawing/DrawingRobustnessTests.cs | 2 +- .../Processing/FillPathProcessorTests.cs | 13 +- ...izerTests.cs => DefaultRasterizerTests.cs} | 15 +- .../Shapes/Scan/NumericCornerCasePolygons.cs | 35 - .../Shapes/Scan/PolygonScannerTests.cs | 615 --------- .../Shapes/Scan/RasterizerExtensionsTests.cs | 48 - .../Shapes/Scan/ScanEdgeCollectionTests.cs | 172 --- .../Shapes/Scan/SharpBlazeRasterizerTests.cs | 128 ++ 31 files changed, 1801 insertions(+), 3502 deletions(-) delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerExtensions.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdge.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs create mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs delete mode 100644 src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs rename tests/ImageSharp.Drawing.Tests/Shapes/Scan/{TiledRasterizerTests.cs => DefaultRasterizerTests.cs} (84%) delete mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCasePolygons.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/PolygonScannerTests.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/RasterizerExtensionsTests.cs delete mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs create mode 100644 tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs index ad98387e..5c742ec7 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -10,22 +9,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Default CPU drawing backend. /// -/// -/// This backend currently dispatches to the existing scanline rasterizer pipeline. -/// A tiled rasterizer path is wired behind an AppContext switch for incremental rollout. -/// internal sealed class CpuDrawingBackend : IDrawingBackend { - private const string ExperimentalTiledRasterizerSwitch = "SixLabors.ImageSharp.Drawing.ExperimentalTiledRasterizer"; + private readonly IRasterizer primaryRasterizer; - private readonly IRasterizer defaultRasterizer; - private readonly TiledRasterizer tiledRasterizer; - - private CpuDrawingBackend(IRasterizer defaultRasterizer) + private CpuDrawingBackend(IRasterizer primaryRasterizer) { - Guard.NotNull(defaultRasterizer, nameof(defaultRasterizer)); - this.defaultRasterizer = defaultRasterizer; - this.tiledRasterizer = TiledRasterizer.Instance; + Guard.NotNull(primaryRasterizer, nameof(primaryRasterizer)); + this.primaryRasterizer = primaryRasterizer; } /// @@ -36,7 +27,7 @@ private CpuDrawingBackend(IRasterizer defaultRasterizer) /// /// Gets the primary rasterizer used by this backend. /// - public IRasterizer PrimaryRasterizer => this.defaultRasterizer; + public IRasterizer PrimaryRasterizer => this.primaryRasterizer; /// /// Creates a backend that uses the given rasterizer as the primary implementation. @@ -57,16 +48,5 @@ public void RasterizePath( ref TState state, RasterizerScanlineHandler scanlineHandler) where TState : struct - { - if (UseExperimentalTiledRasterizer()) - { - this.tiledRasterizer.Rasterize(path, options, allocator, ref state, scanlineHandler); - return; - } - - this.defaultRasterizer.Rasterize(path, options, allocator, ref state, scanlineHandler); - } - - private static bool UseExperimentalTiledRasterizer() - => AppContext.TryGetSwitch(ExperimentalTiledRasterizerSwitch, out bool enabled) && enabled; + => this.primaryRasterizer.Rasterize(path, options, allocator, ref state, scanlineHandler); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs index 755683e7..5dfadb97 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs @@ -11,12 +11,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; /// public class FillPathProcessor : IImageProcessor { - /// - /// Fixed subpixel sampling density used by the CPU rasterizer for both antialiased and - /// quantized-aliased rendering. - /// - internal const int FixedRasterizerSubpixelCount = 16; - /// /// Initializes a new instance of the class. /// diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index 2621e211..88b1a5a0 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -64,23 +64,20 @@ protected override void OnFrameApply(ImageFrame source) int minX = interest.Left; - // The rasterizer always computes continuous coverage, then aliased mode quantizes coverage - // in ProcessRasterizedScanline(). - int subpixelCount = FillPathProcessor.FixedRasterizerSubpixelCount; using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, this.bounds); MemoryAllocator allocator = this.Configuration.MemoryAllocator; IDrawingBackend drawingBackend = configuration.GetDrawingBackend(); + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; RasterizerOptions rasterizerOptions = new( interest, - subpixelCount, shapeOptions.IntersectionRule, + rasterizationMode, RasterizerSamplingOrigin.PixelBoundary); RasterizationState state = new( source, applicator, minX, - graphicsOptions.Antialias, isSolidBrushWithoutBlending, solidBrushColor); @@ -106,41 +103,6 @@ private static bool IsSolidBrushWithoutBlending(GraphicsOptions options, Brush i private static void ProcessRasterizedScanline(int y, Span scanline, ref RasterizationState state) { - if (!state.Antialias) - { - bool hasOnes = false; - bool hasZeros = false; - for (int x = 0; x < scanline.Length; x++) - { - if (scanline[x] >= 0.5F) - { - scanline[x] = 1F; - hasOnes = true; - } - else - { - scanline[x] = 0F; - hasZeros = true; - } - } - - if (state.IsSolidBrushWithoutBlending && hasOnes != hasZeros) - { - if (hasOnes) - { - state.Source.PixelBuffer.DangerousGetRowSpan(y).Slice(state.MinX, scanline.Length).Fill(state.SolidBrushColor); - } - - return; - } - - if (state.IsSolidBrushWithoutBlending && hasOnes) - { - FillOpaqueRuns(state.Source, y, state.MinX, scanline, state.SolidBrushColor); - return; - } - } - if (state.IsSolidBrushWithoutBlending) { ApplyCoverageRunsForOpaqueSolidBrush(state.Source, state.Applicator, scanline, state.MinX, y, state.SolidBrushColor); @@ -236,46 +198,18 @@ private static void ApplyCoverageRunsForOpaqueSolidBrush( } } - private static void FillOpaqueRuns(ImageFrame source, int y, int minX, Span scanline, TPixel solidBrushColor) - { - Span destinationRow = source.PixelBuffer.DangerousGetRowSpan(y).Slice(minX, scanline.Length); - int i = 0; - - while (i < scanline.Length) - { - while (i < scanline.Length && scanline[i] <= 0F) - { - i++; - } - - int runStart = i; - while (i < scanline.Length && scanline[i] > 0F) - { - i++; - } - - int runLength = i - runStart; - if (runLength > 0) - { - destinationRow.Slice(runStart, runLength).Fill(solidBrushColor); - } - } - } - private readonly struct RasterizationState { public RasterizationState( ImageFrame source, BrushApplicator applicator, int minX, - bool antialias, bool isSolidBrushWithoutBlending, TPixel solidBrushColor) { this.Source = source; this.Applicator = applicator; this.MinX = minX; - this.Antialias = antialias; this.IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending; this.SolidBrushColor = solidBrushColor; } @@ -286,8 +220,6 @@ public RasterizationState( public int MinX { get; } - public bool Antialias { get; } - public bool IsSolidBrushWithoutBlending { get; } public TPixel SolidBrushColor { get; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index 311ad7d7..f2b5e953 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -541,19 +541,17 @@ private Buffer2D Render(IPath path) // Pad to prevent edge clipping. size += new Size(2, 2); - // Use one coverage rasterization path for both AA and aliased text. - // Aliased mode quantizes coverage in ProcessTextScanline(). - int subpixelCount = FillPathProcessor.FixedRasterizerSubpixelCount; RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; GraphicsOptions graphicsOptions = this.drawingOptions.GraphicsOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; // Take the path inside the path builder, scan thing and generate a Buffer2D representing the glyph. Buffer2D buffer = this.memoryAllocator.Allocate2D(size.Width, size.Height, AllocationOptions.Clean); - TextRasterizationState state = new(buffer, graphicsOptions.Antialias); + TextRasterizationState state = new(buffer); RasterizerOptions rasterizerOptions = new( new Rectangle(0, 0, size.Width, size.Height), - subpixelCount, TextUtilities.MapFillRule(this.currentFillRule), + rasterizationMode, samplingOrigin); this.drawingBackend.RasterizePath( @@ -570,14 +568,6 @@ private static void ProcessTextScanline(int y, Span scanline, ref TextRas { Span destination = state.Buffer.DangerousGetRowSpan(y); scanline.CopyTo(destination); - - if (!state.Antialias) - { - for (int x = 0; x < destination.Length; x++) - { - destination[x] = destination[x] >= 0.5F ? 1F : 0F; - } - } } private void Dispose(bool disposing) @@ -625,15 +615,9 @@ public readonly void Dispose() private readonly struct TextRasterizationState { - public TextRasterizationState(Buffer2D buffer, bool antialias) - { - this.Buffer = buffer; - this.Antialias = antialias; - } + public TextRasterizationState(Buffer2D buffer) => this.Buffer = buffer; public Buffer2D Buffer { get; } - - public bool Antialias { get; } } private readonly struct CacheKey : IEquatable diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs index 59c404f4..dcdda406 100644 --- a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs @@ -117,10 +117,7 @@ public IPath AsClosedPath() /// SegmentInfo IPathInternals.PointAlongPath(float distance) { - if (this.internalPaths == null) - { - this.InitInternalPaths(); - } + this.EnsureInternalPaths(); distance %= this.length; foreach (InternalPath p in this.internalPaths) @@ -141,10 +138,21 @@ SegmentInfo IPathInternals.PointAlongPath(float distance) /// IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() { - this.InitInternalPaths(); + this.EnsureInternalPaths(); return this.internalPaths; } + [MemberNotNull(nameof(internalPaths))] + private void EnsureInternalPaths() + { + if (this.internalPaths is not null) + { + return; + } + + this.InitInternalPaths(); + } + /// /// Initializes and . /// @@ -152,6 +160,7 @@ IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() private void InitInternalPaths() { this.internalPaths = new List(this.paths.Length); + this.length = 0; foreach (IPath p in this.paths) { diff --git a/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs b/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs index d708153d..b9c54685 100644 --- a/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs +++ b/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs @@ -13,5 +13,5 @@ internal interface IInternalPathOwner /// Returns the rings as a readonly collection of elements. /// /// The . - IReadOnlyList GetRingsAsInternalPath(); + public IReadOnlyList GetRingsAsInternalPath(); } diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Shapes/Path.cs index 4b992568..4ddf0c42 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Shapes/Path.cs @@ -15,6 +15,7 @@ public class Path : IPath, ISimplePath, IPathInternals, IInternalPathOwner { private readonly ILineSegment[] lineSegments; private InternalPath? innerPath; + private IReadOnlyList? internalPathRings; /// /// Initializes a new instance of the class. @@ -131,7 +132,8 @@ SegmentInfo IPathInternals.PointAlongPath(float distance) => this.InnerPath.PointAlongPath(distance); /// - IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() => [this.InnerPath]; + IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() + => this.internalPathRings ??= [this.InnerPath]; /// /// Converts an SVG path string into an . diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs deleted file mode 100644 index a760c521..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -internal enum NonZeroIntersectionType -{ - Down, - Up, - Corner, - CornerDummy -} - -/// -/// The list of active edges as an index buffer into . -/// -internal ref struct ActiveEdgeList -{ - private const int EnteringEdgeFlag = 1 << 30; - private const int LeavingEdgeFlag = 1 << 31; - private const int MaxEdges = EnteringEdgeFlag - 1; - - private const int StripMask = ~(EnteringEdgeFlag | LeavingEdgeFlag); - - private const float NonzeroSortingHelperEpsilon = 1e-4f; - - private int count; - internal readonly Span Buffer; - - public ActiveEdgeList(Span buffer) - { - this.count = 0; - this.Buffer = buffer; - } - - private readonly Span ActiveEdges => this.Buffer.Slice(0, this.count); - - public readonly bool IsEmpty => this.count == 0; - - public void EnterEdge(int edgeIdx) => this.Buffer[this.count++] = edgeIdx | EnteringEdgeFlag; - - public readonly void LeaveEdge(int edgeIdx) - { - Span active = this.ActiveEdges; - for (int i = 0; i < active.Length; i++) - { - if (active[i] == edgeIdx) - { - active[i] |= LeavingEdgeFlag; - return; - } - } - } - - public void RemoveLeavingEdges() - { - int offset = 0; - - Span active = this.ActiveEdges; - - for (int i = 0; i < active.Length; i++) - { - int flaggedIdx = active[i]; - int edgeIdx = Strip(flaggedIdx); - if (IsLeaving(flaggedIdx)) - { - offset++; - } - else - { - // Unmask and offset: - active[i - offset] = edgeIdx; - } - } - - this.count -= offset; - } - - public Span ScanOddEven(float y, Span edges, Span intersections) - { - DebugGuard.MustBeLessThanOrEqualTo(edges.Length, MaxEdges, "edges.Length"); - - int intersectionCounter = 0; - int offset = 0; - - Span active = this.ActiveEdges; - - for (int i = 0; i < active.Length; i++) - { - int flaggedIdx = active[i]; - int edgeIdx = Strip(flaggedIdx); - ref ScanEdge edge = ref edges[edgeIdx]; - float x = edge.GetX(y); - if (IsEntering(flaggedIdx)) - { - Emit(x, edge.EmitV0, intersections, ref intersectionCounter); - } - else if (IsLeaving(flaggedIdx)) - { - Emit(x, edge.EmitV1, intersections, ref intersectionCounter); - - offset++; - - // Do not offset: - continue; - } - else - { - // Emit once: - intersections[intersectionCounter++] = x; - } - - // Unmask and offset: - active[i - offset] = edgeIdx; - } - - this.count -= offset; - - intersections = intersections.Slice(0, intersectionCounter); - intersections.Sort(); - - return intersections; - } - - public Span ScanNonZero( - float y, - Span edges, - Span intersections, - Span intersectionTypes) - { - DebugGuard.MustBeLessThanOrEqualTo(edges.Length, MaxEdges, "edges.Length"); - - int intersectionCounter = 0; - int offset = 0; - - Span active = this.ActiveEdges; - - for (int i = 0; i < active.Length; i++) - { - int flaggedIdx = active[i]; - int edgeIdx = Strip(flaggedIdx); - ref ScanEdge edge = ref edges[edgeIdx]; - bool edgeUp = edge.EdgeUp; - float x = edge.GetX(y); - if (IsEntering(flaggedIdx)) - { - EmitNonZero(x, edge.EmitV0, edgeUp, intersections, intersectionTypes, ref intersectionCounter); - } - else if (IsLeaving(flaggedIdx)) - { - EmitNonZero(x, edge.EmitV1, edgeUp, intersections, intersectionTypes, ref intersectionCounter); - - offset++; - - // Do not offset: - continue; - } - else - { - // Emit once: - if (edgeUp) - { - intersectionTypes[intersectionCounter] = NonZeroIntersectionType.Up; - intersections[intersectionCounter++] = x + NonzeroSortingHelperEpsilon; - } - else - { - intersectionTypes[intersectionCounter] = NonZeroIntersectionType.Down; - intersections[intersectionCounter++] = x - NonzeroSortingHelperEpsilon; - } - } - - // Unmask and offset: - active[i - offset] = edgeIdx; - } - - this.count -= offset; - - intersections = intersections.Slice(0, intersectionCounter); - intersectionTypes = intersectionTypes.Slice(0, intersectionCounter); - intersections.Sort(intersectionTypes); - - return ApplyNonzeroRule(intersections, intersectionTypes); - } - - private static Span ApplyNonzeroRule(Span intersections, Span intersectionTypes) - { - int offset = 0; - int tracker = 0; - - for (int i = 0; i < intersectionTypes.Length; i++) - { - NonZeroIntersectionType type = intersectionTypes[i]; - if (type == NonZeroIntersectionType.CornerDummy) - { - // we skip this one so we can emit twice on actual "Corner" - offset++; - } - else if (type == NonZeroIntersectionType.Corner) - { - // Assume a Down, Up serie - NonzeroEmitIfNeeded(intersections, i, -1, intersections[i], ref tracker, ref offset); - offset -= 1; - NonzeroEmitIfNeeded(intersections, i, 1, intersections[i], ref tracker, ref offset); - } - else - { - int diff = type == NonZeroIntersectionType.Up ? 1 : -1; - float emitVal = intersections[i] + (NonzeroSortingHelperEpsilon * diff * -1); - NonzeroEmitIfNeeded(intersections, i, diff, emitVal, ref tracker, ref offset); - } - } - - return intersections.Slice(0, intersections.Length - offset); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void NonzeroEmitIfNeeded(Span intersections, int i, int diff, float emitVal, ref int tracker, ref int offset) - { - bool emit = (tracker == 0 && diff != 0) || tracker * diff == -1; - tracker += diff; - - if (emit) - { - intersections[i - offset] = emitVal; - } - else - { - offset++; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Emit(float x, int times, Span emitSpan, ref int emitCounter) - { - if (times > 1) - { - emitSpan[emitCounter++] = x; - } - - if (times > 0) - { - emitSpan[emitCounter++] = x; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void EmitNonZero(float x, int times, bool edgeUp, Span emitSpan, Span intersectionTypes, ref int emitCounter) - { - if (times == 2) - { - intersectionTypes[emitCounter] = NonZeroIntersectionType.CornerDummy; - emitSpan[emitCounter++] = x - NonzeroSortingHelperEpsilon; // To make sure the "dummy" point precedes the actual one - - intersectionTypes[emitCounter] = NonZeroIntersectionType.Corner; - emitSpan[emitCounter++] = x; - } - else if (times == 1) - { - if (edgeUp) - { - intersectionTypes[emitCounter] = NonZeroIntersectionType.Up; - emitSpan[emitCounter++] = x + NonzeroSortingHelperEpsilon; - } - else - { - intersectionTypes[emitCounter] = NonZeroIntersectionType.Down; - emitSpan[emitCounter++] = x - NonzeroSortingHelperEpsilon; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Strip(int flaggedIdx) => flaggedIdx & StripMask; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsEntering(int flaggedIdx) => (flaggedIdx & EnteringEdgeFlag) == EnteringEdgeFlag; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsLeaving(int flaggedIdx) => (flaggedIdx & LeavingEdgeFlag) == LeavingEdgeFlag; -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs index 588c4b7d..f650e798 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs @@ -7,10 +7,34 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; /// -/// Default CPU scanline rasterizer used by ImageSharp.Drawing. +/// Default CPU rasterizer that processes large paths in parallel vertical bands. /// +/// +/// The algorithm preserves the public scanline callback contract (top-to-bottom emission) while +/// parallelizing internal work: +/// 1. Partition the interest rectangle into Y-bands. +/// 2. Rasterize each band independently into temporary coverage buffers. +/// 3. Emit bands back in deterministic top-to-bottom order. +/// +/// This design avoids concurrent writes to destination pixels and keeps per-band work isolated. +/// It also lets the implementation fall back to the single-pass scanner when tiling would not pay +/// off (small workloads, huge temporary buffers, or low core counts). +/// internal sealed class DefaultRasterizer : IRasterizer { + // Keep bands reasonably tall so the overhead of per-band setup does not dominate tiny draws. + private const int MinimumBandHeight = 96; + + // Require a minimum pixel workload per band so thread scheduling overhead stays amortized. + private const int MinimumPixelsPerBand = 196608; + + // Hard cap on buffered pixels across all bands for a single rasterization invocation. + // One float is buffered per pixel plus a dirty-row byte map per band. + private const int MaximumBufferedPixels = 16777216; // 4096 x 4096 + + // Bounding band count limits task fan-out and keeps allocator pressure predictable. + private const int MaximumBandCount = 8; + /// /// Gets the singleton default rasterizer instance. /// @@ -25,6 +49,7 @@ public void Rasterize( RasterizerScanlineHandler scanlineHandler) where TState : struct { + // Fast argument validation at entry keeps failure behavior consistent with other rasterizers. Guard.NotNull(path, nameof(path)); Guard.NotNull(allocator, nameof(allocator)); Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); @@ -32,61 +57,322 @@ public void Rasterize( Rectangle interest = options.Interest; if (interest.Equals(Rectangle.Empty)) { + // Nothing intersects the destination; skip all work. return; } - if (SharpBlazeScanner.TryRasterize(path, options, allocator, ref state, scanlineHandler)) + if (!TryCreateBandPlan(interest, out Band[]? plannedBands) || plannedBands is null) + { + // For small or extreme workloads, single-pass rasterization is cheaper and avoids + // temporary band buffers. + ScanlineRasterizer.Instance.Rasterize(path, options, allocator, ref state, scanlineHandler); + return; + } + + Band[] bands = plannedBands; + RasterizerOptions bandedOptions = options; + + // Prime lazy path state once on the caller thread to avoid N workers racing to + // materialize the same internal path structures. + PrimePathState(path); + + try + { + // Limit parallelism to planned band count. This keeps work partition deterministic + // and avoids oversubscribing worker threads for this operation. + ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = bands.Length }; + _ = Parallel.For( + 0, + bands.Length, + parallelOptions, + i => RasterizeBand(path, bandedOptions, allocator, bands[i])); + + // Emit in deterministic order so downstream compositing observes stable scanline order. + EmitBands(bands, interest.Width, ref state, scanlineHandler); + } + finally + { + foreach (Band band in bands) + { + band.Dispose(); + } + } + } + + /// + /// Forces lazy path materialization before worker threads start. + /// + /// The source path. + private static void PrimePathState(IPath path) + { + if (path is IInternalPathOwner owner) { + // Force ring extraction once for paths that expose internal rings. This is the + // hot path for ComplexPolygon and avoids repeated per-band conversion cost. + _ = owner.GetRingsAsInternalPath().Count; return; } - RasterizeWithPolygonScanner(path, options, allocator, ref state, scanlineHandler); + // Fallback for generic paths: force flattening once so lazy point arrays are available + // before worker threads begin. + foreach (ISimplePath simplePath in path.Flatten()) + { + _ = simplePath.Points.Length; + } } - private static void RasterizeWithPolygonScanner( + /// + /// Computes a band partitioning plan for the destination rectangle. + /// + /// Destination interest rectangle. + /// + /// When this method returns , contains the planned rasterization bands. + /// + /// + /// when banding should be used; otherwise . + /// + private static bool TryCreateBandPlan(Rectangle interest, out Band[]? bands) + { + bands = null; + + int width = interest.Width; + int height = interest.Height; + long totalPixels = (long)width * height; + if (totalPixels > MaximumBufferedPixels) + { + // Refuse banding for extremely large interests to cap temporary memory use. + return false; + } + + int processorCount = Environment.ProcessorCount; + if (processorCount < 2 || height < (MinimumBandHeight * 2) || totalPixels < (MinimumPixelsPerBand * 2L)) + { + // Not enough parallel work: prefer single-pass path. + return false; + } + + // Bound candidate band count by three limits: + // - image height (minimum band height), + // - total pixels (minimum pixels per band), + // - hardware + hard cap. + int byHeight = height / MinimumBandHeight; + int byPixels = (int)(totalPixels / MinimumPixelsPerBand); + int bandCount = Math.Min(MaximumBandCount, Math.Min(processorCount, Math.Min(byHeight, byPixels))); + if (bandCount < 2) + { + return false; + } + + bands = new Band[bandCount]; + int baseHeight = height / bandCount; + int remainder = height % bandCount; + int y = interest.Top; + + for (int i = 0; i < bandCount; i++) + { + // Distribute remainder rows to the earliest bands to keep shapes balanced. + int bandHeight = baseHeight + (i < remainder ? 1 : 0); + bands[i] = new Band(y, bandHeight); + y += bandHeight; + } + + return true; + } + + /// + /// Rasterizes a single band using the fallback scanline rasterizer into temporary buffers. + /// + /// Path to rasterize. + /// Rasterization options. + /// Memory allocator. + /// The destination band to populate. + private static void RasterizeBand( IPath path, in RasterizerOptions options, MemoryAllocator allocator, + Band band) + { + // Band-local buffers keep writes private to the worker and avoid shared state. + // coverageLength is width * bandHeight and is bounded by band planning constraints. + int width = options.Interest.Width; + int coverageLength = checked(width * band.Height); + + IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); + IMemoryOwner dirtyRowsOwner = allocator.Allocate(band.Height, AllocationOptions.Clean); + + try + { + RasterizerOptions bandOptions = options.WithInterest( + new Rectangle(options.Interest.Left, band.Top, width, band.Height)); + + // Capture state collects scanline output from the fallback scanner into local buffers. + BandCaptureState captureState = new(band.Top, width, coverageOwner.Memory, dirtyRowsOwner.Memory); + ScanlineRasterizer.Instance.Rasterize(path, bandOptions, allocator, ref captureState, CaptureBandScanline); + + band.SetBuffers(coverageOwner, dirtyRowsOwner); + } + catch + { + coverageOwner.Dispose(); + dirtyRowsOwner.Dispose(); + throw; + } + } + + /// + /// Emits all buffered bands in top-to-bottom scanline order. + /// + /// The rasterization callback state type. + /// Bands containing buffered coverage. + /// Width of each scanline. + /// Mutable callback state. + /// Scanline callback. + private static void EmitBands( + Band[] bands, + int scanlineWidth, ref TState state, RasterizerScanlineHandler scanlineHandler) where TState : struct { - Rectangle interest = options.Interest; - int minX = interest.Left; - int scanlineWidth = interest.Width; - float xOffset = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - bool scanlineDirty = true; - - PolygonScanner scanner = PolygonScanner.Create( - path, - interest.Top, - interest.Bottom, - options.SubpixelCount, - options.IntersectionRule, - allocator); - - try + // Serialize final emission in band order so callback consumers receive stable rows. + foreach (Band band in bands) { - using IMemoryOwner scanlineOwner = allocator.Allocate(scanlineWidth); - Span scanline = scanlineOwner.Memory.Span; + if (band.CoverageOwner is null || band.DirtyRowsOwner is null) + { + continue; + } + + Span coverage = band.CoverageOwner.Memory.Span; + Span dirtyRows = band.DirtyRowsOwner.Memory.Span; - while (scanner.MoveToNextPixelLine()) + for (int row = 0; row < band.Height; row++) { - if (scanlineDirty) + if (dirtyRows[row] == 0) { - scanline.Clear(); + // Sparse rows are skipped to avoid unnecessary callback invocations. + continue; } - scanlineDirty = scanner.ScanCurrentPixelLineInto(minX, xOffset, scanline); - if (scanlineDirty) - { - scanlineHandler(scanner.PixelLineY, scanline, ref state); - } + Span scanline = coverage.Slice(row * scanlineWidth, scanlineWidth); + scanlineHandler(band.Top + row, scanline, ref state); } } - finally + } + + /// + /// Captures one scanline from the fallback scanner into band-local storage. + /// + /// Absolute destination Y. + /// Coverage values for the row. + /// Band capture state. + private static void CaptureBandScanline(int y, Span scanline, ref BandCaptureState state) + { + // The fallback scanner writes one row at a time; copy into contiguous band storage. + int row = y - state.Top; + Span coverage = state.Coverage.Span; + scanline.CopyTo(coverage.Slice(row * state.Width, state.Width)); + state.DirtyRows.Span[row] = 1; + } + + /// + /// Mutable capture state used while rasterizing a single band. + /// + private readonly struct BandCaptureState + { + /// + /// Initializes a new instance of the struct. + /// + /// Top-most Y of the target band. + /// Scanline width for the band. + /// Contiguous storage for band coverage rows. + /// Row activity map for sparse emission. + public BandCaptureState(int top, int width, Memory coverage, Memory dirtyRows) + { + this.Top = top; + this.Width = width; + this.Coverage = coverage; + this.DirtyRows = dirtyRows; + } + + /// + /// Gets the top-most destination Y of the band. + /// + public int Top { get; } + + /// + /// Gets the number of pixels in each band row. + /// + public int Width { get; } + + /// + /// Gets contiguous per-row coverage storage for this band. + /// + public Memory Coverage { get; } + + /// + /// Gets the row activity map where non-zero indicates row data is present. + /// + public Memory DirtyRows { get; } + } + + /// + /// Owns temporary buffers and metadata for a single planned band. + /// + private sealed class Band : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// Top-most destination Y for the band. + /// Number of rows in the band. + public Band(int top, int height) + { + this.Top = top; + this.Height = height; + } + + /// + /// Gets the top-most destination Y for this band. + /// + public int Top { get; } + + /// + /// Gets the band height in rows. + /// + public int Height { get; } + + /// + /// Gets the owner of the coverage buffer for this band. + /// + public IMemoryOwner? CoverageOwner { get; private set; } + + /// + /// Gets the owner of the dirty-row map buffer for this band. + /// + public IMemoryOwner? DirtyRowsOwner { get; private set; } + + /// + /// Assigns buffer ownership to this band instance. + /// + /// Coverage buffer owner. + /// Dirty-row buffer owner. + public void SetBuffers(IMemoryOwner coverageOwner, IMemoryOwner dirtyRowsOwner) + { + // Ownership is transferred to the band container and released in Dispose(). + this.CoverageOwner = coverageOwner; + this.DirtyRowsOwner = dirtyRowsOwner; + } + + /// + /// Disposes all band-owned buffers. + /// + public void Dispose() { - scanner.Dispose(); + // Always release pooled buffers even if rasterization fails in other bands. + this.CoverageOwner?.Dispose(); + this.DirtyRowsOwner?.Dispose(); + this.CoverageOwner = null; + this.DirtyRowsOwner = null; } } } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs index daa0efec..29c6d2fc 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -2,256 +2,1102 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Runtime.InteropServices; +using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -internal ref struct PolygonScanner +/// +/// Fixed-point polygon scanner that converts path segments into per-row coverage runs. +/// +internal static class PolygonScanner { - private readonly int minY; - private readonly int maxY; - private readonly IntersectionRule intersectionRule; - private readonly ScanEdgeCollection edgeCollection; - private readonly Span edges; - - // Common contiguous buffer for sorted0, sorted1, intersections, activeEdges [,intersectionTypes] - private readonly IMemoryOwner dataBuffer; - - // | <- edgeCnt -> | <- edgeCnt -> | <- edgeCnt -> | <- maxIntersectionCount -> | <- maxIntersectionCount -> | - // |---------------|---------------|---------------|----------------------------|----------------------------| - // | sorted0 | sorted1 | activeEdges | intersections | intersectionTypes | - // |---------------|---------------|---------------|----------------------------|----------------------------| - private readonly Span sorted0; - private readonly Span sorted1; - private ActiveEdgeList activeEdges; - private readonly Span intersections; - private readonly Span intersectionTypes; - - private int idx0; - private int idx1; - private float yPlusOne; - - public readonly float SubpixelDistance; - public readonly float SubpixelArea; - public int PixelLineY; - public float SubPixelY; - - private PolygonScanner( - ScanEdgeCollection edgeCollection, - int maxIntersectionCount, - int minY, - int maxY, - int subsampling, - IntersectionRule intersectionRule, - MemoryAllocator allocator) + // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows). + // Keeping this bounded prevents pathological full-image allocations on very large interests. + private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L; + + private const int FixedShift = 8; + private const int FixedOne = 1 << FixedShift; + private static readonly int WordBitCount = nint.Size * 8; + private const int AreaToCoverageShift = 9; + private const int CoverageStepCount = 256; + private const int EvenOddMask = (CoverageStepCount * 2) - 1; + private const int EvenOddPeriod = CoverageStepCount * 2; + private const float CoverageScale = 1F / CoverageStepCount; + + public static void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct { - this.minY = minY; - this.maxY = maxY; - this.SubpixelDistance = 1f / subsampling; - this.SubpixelArea = this.SubpixelDistance / subsampling; - this.intersectionRule = intersectionRule; - this.edgeCollection = edgeCollection; - this.edges = edgeCollection.Edges; - int edgeCount = this.edges.Length; - int dataBufferSize = (edgeCount * 3) + maxIntersectionCount; + Rectangle interest = options.Interest; + int width = interest.Width; + int height = interest.Height; + if (width <= 0 || height <= 0) + { + return; + } - // In case of IntersectionRule.Nonzero, we need more space for intersectionTypes: - if (intersectionRule == IntersectionRule.NonZero) + int wordsPerRow = BitVectorsForMaxBitCount(width); + long coverStride = (long)width * 2; + if (coverStride > int.MaxValue || + !TryGetBandHeight(width, height, wordsPerRow, coverStride, out int maxBandRows)) { - dataBufferSize += maxIntersectionCount; + throw new ImageProcessingException("The rasterizer interest bounds are too large for PolygonScanner buffers."); } - this.dataBuffer = allocator.Allocate(dataBufferSize); - Span dataBufferInt32Span = this.dataBuffer.Memory.Span; - Span dataBufferFloatSpan = MemoryMarshal.Cast(dataBufferInt32Span); + int coverStrideInt = (int)coverStride; + int bitVectorCapacity = checked(wordsPerRow * maxBandRows); + int coverAreaCapacity = checked(coverStrideInt * maxBandRows); + using IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); + using IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); + using IMemoryOwner startCoverOwner = allocator.Allocate(maxBandRows, AllocationOptions.Clean); + + // Per-row activity flags avoid scanning the full bit-vector row just to detect "empty row". + using IMemoryOwner rowHasBitsOwner = allocator.Allocate(maxBandRows, AllocationOptions.Clean); + using IMemoryOwner rowTouchedOwner = allocator.Allocate(maxBandRows, AllocationOptions.Clean); + using IMemoryOwner touchedRowsOwner = allocator.Allocate(maxBandRows); + using IMemoryOwner scanlineOwner = allocator.Allocate(width); - this.sorted0 = dataBufferInt32Span.Slice(0, edgeCount); - this.sorted1 = dataBufferInt32Span.Slice(edgeCount, edgeCount); - this.activeEdges = new ActiveEdgeList(dataBufferInt32Span.Slice(edgeCount * 2, edgeCount)); - this.intersections = dataBufferFloatSpan.Slice(edgeCount * 3, maxIntersectionCount); - if (intersectionRule == IntersectionRule.NonZero) + Span bitVectorsBuffer = bitVectorsOwner.Memory.Span; + Span coverAreaBuffer = coverAreaOwner.Memory.Span; + Span startCoverBuffer = startCoverOwner.Memory.Span; + Span rowHasBitsBuffer = rowHasBitsOwner.Memory.Span; + Span rowTouchedBuffer = rowTouchedOwner.Memory.Span; + Span touchedRowsBuffer = touchedRowsOwner.Memory.Span; + Span scanline = scanlineOwner.Memory.Span; + + float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + + using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); + int bandTop = 0; + while (bandTop < height) { - Span remainder = - dataBufferInt32Span.Slice((edgeCount * 3) + maxIntersectionCount, maxIntersectionCount); - this.intersectionTypes = MemoryMarshal.Cast(remainder); + int bandHeight = Math.Min(maxBandRows, height - bandTop); + int bitVectorCount = wordsPerRow * bandHeight; + int coverCount = coverStrideInt * bandHeight; + + Span bitVectors = bitVectorsBuffer[..bitVectorCount]; + Span coverArea = coverAreaBuffer[..coverCount]; + Span startCover = startCoverBuffer[..bandHeight]; + Span rowHasBits = rowHasBitsBuffer[..bandHeight]; + Span rowTouched = rowTouchedBuffer[..bandHeight]; + Span touchedRows = touchedRowsBuffer[..bandHeight]; + + Context context = new( + bitVectors, + coverArea, + startCover, + rowHasBits, + rowTouched, + touchedRows, + width, + bandHeight, + wordsPerRow, + coverStrideInt, + options.IntersectionRule, + options.RasterizationMode); + + context.RasterizeMultipolygon( + multipolygon, + interest.Left, + interest.Top + bandTop, + samplingOffsetX); + + context.EmitScanlines(interest.Top + bandTop, scanline, ref state, scanlineHandler); + context.ResetTouchedRows(); + bandTop += bandHeight; } - else + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; + + private static bool TryGetBandHeight(int width, int height, int wordsPerRow, long coverStride, out int bandHeight) + { + bandHeight = 0; + if (width <= 0 || height <= 0 || wordsPerRow <= 0 || coverStride <= 0) + { + return false; + } + + long bytesPerRow = + ((long)wordsPerRow * nint.Size) + + (coverStride * sizeof(int)) + + sizeof(int); + + long rowsByBudget = BandMemoryBudgetBytes / bytesPerRow; + if (rowsByBudget < 1) + { + rowsByBudget = 1; + } + + long rowsByBitVectors = int.MaxValue / wordsPerRow; + long rowsByCoverArea = int.MaxValue / coverStride; + long maxRows = Math.Min(rowsByBudget, Math.Min(rowsByBitVectors, rowsByCoverArea)); + if (maxRows < 1) { - this.intersectionTypes = default; + return false; } - this.idx0 = 0; - this.idx1 = 0; - this.PixelLineY = minY - 1; - this.SubPixelY = default; - this.yPlusOne = default; + bandHeight = (int)Math.Min(height, maxRows); + return bandHeight > 0; } - public static PolygonScanner Create( - IPath polygon, - int minY, - int maxY, - int subsampling, - IntersectionRule intersectionRule, - MemoryAllocator allocator) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + + private static bool ClipToVerticalBounds(ref float x0, ref float y0, ref float x1, ref float y1, float minY, float maxY) { - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, allocator); - ScanEdgeCollection edges = ScanEdgeCollection.Create(multipolygon, allocator, subsampling); - PolygonScanner scanner = new(edges, multipolygon.TotalVertexCount * 2, minY, maxY, subsampling, intersectionRule, allocator); - scanner.Init(); - return scanner; + float t0 = 0F; + float t1 = 1F; + float dx = x1 - x0; + float dy = y1 - y0; + + if (!ClipTest(-dy, y0 - minY, ref t0, ref t1)) + { + return false; + } + + if (!ClipTest(dy, maxY - y0, ref t0, ref t1)) + { + return false; + } + + if (t1 < 1F) + { + x1 = x0 + (dx * t1); + y1 = y0 + (dy * t1); + } + + if (t0 > 0F) + { + x0 += dx * t0; + y0 += dy * t0; + } + + return y0 != y1; } - private void Init() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTest(float p, float q, ref float t0, ref float t1) { - // Reuse memory buffers of 'intersections' and 'activeEdges' for key-value sorting, - // since that region is unused at initialization time. - Span keys0 = this.intersections.Slice(0, this.sorted0.Length); - Span keys1 = MemoryMarshal.Cast(this.activeEdges.Buffer); + if (p == 0F) + { + return q >= 0F; + } - for (int i = 0; i < this.edges.Length; i++) + float r = q / p; + if (p < 0F) { - ref ScanEdge edge = ref this.edges[i]; - keys0[i] = edge.Y0; - keys1[i] = edge.Y1; - this.sorted0[i] = i; - this.sorted1[i] = i; + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } } + else + { + if (r < t0) + { + return false; + } - keys0.Sort(this.sorted0); - keys1.Sort(this.sorted1); + if (r < t1) + { + t1 = r; + } + } + + return true; + } - this.SkipEdgesBeforeMinY(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FindAdjustment(int value) + { + int lte0 = ~((value - 1) >> 31) & 1; + int divisibleBy256 = (((value & (FixedOne - 1)) - 1) >> 31) & 1; + return lte0 & divisibleBy256; } - private void SkipEdgesBeforeMinY() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int TrailingZeroCount(nuint value) + => nint.Size == sizeof(ulong) + ? BitOperations.TrailingZeroCount((ulong)value) + : BitOperations.TrailingZeroCount((uint)value); + + private ref struct Context { - if (this.edges.Length == 0) + private readonly Span bitVectors; + private readonly Span coverArea; + private readonly Span startCover; + private readonly Span rowHasBits; + private readonly Span rowTouched; + private readonly Span touchedRows; + private readonly int width; + private readonly int height; + private readonly int wordsPerRow; + private readonly int coverStride; + private readonly IntersectionRule intersectionRule; + private readonly RasterizationMode rasterizationMode; + private int touchedRowCount; + + public Context( + Span bitVectors, + Span coverArea, + Span startCover, + Span rowHasBits, + Span rowTouched, + Span touchedRows, + int width, + int height, + int wordsPerRow, + int coverStride, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode) { - return; + this.bitVectors = bitVectors; + this.coverArea = coverArea; + this.startCover = startCover; + this.rowHasBits = rowHasBits; + this.rowTouched = rowTouched; + this.touchedRows = touchedRows; + this.width = width; + this.height = height; + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.intersectionRule = intersectionRule; + this.rasterizationMode = rasterizationMode; + this.touchedRowCount = 0; } - this.SubPixelY = this.edges[this.sorted0[0]].Y0; + public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX, int minY, float samplingOffsetX) + { + foreach (TessellatedMultipolygon.Ring ring in multipolygon) + { + ReadOnlySpan vertices = ring.Vertices; + for (int i = 0; i < ring.VertexCount; i++) + { + PointF p0 = vertices[i]; + PointF p1 = vertices[i + 1]; + + float x0 = (p0.X - minX) + samplingOffsetX; + float y0 = p0.Y - minY; + float x1 = (p1.X - minX) + samplingOffsetX; + float y1 = p1.Y - minY; - int i0 = 1; - int i1 = 0; + if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) + { + continue; + } - // Do fake scans of the lines that start before minY. - // Instead of fake scanning at every possible subpixel Y location, - // only "scan" at start edge Y positions (defined by values in sorted0) and end Y positions (defined by values in sorted1). - // Walk the two lists simultaneously following mergesort logic. - while (this.SubPixelY < this.minY) + if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, this.height)) + { + continue; + } + + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1); + if (fy0 == fy1) + { + continue; + } + + this.RasterizeLine(fx0, fy0, fx1, fy1); + } + } + } + + public readonly void EmitScanlines(int destinationTop, Span scanline, ref TState state, RasterizerScanlineHandler scanlineHandler) + where TState : struct { - this.EnterEdges(); - this.LeaveEdges(); - this.activeEdges.RemoveLeavingEdges(); + for (int row = 0; row < this.height; row++) + { + int rowCover = this.startCover[row]; + if (rowCover == 0 && this.rowHasBits[row] == 0) + { + continue; + } - bool hasMore0 = i0 < this.sorted0.Length; - bool hasMore1 = i1 < this.sorted1.Length; + Span rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); + scanline.Clear(); + bool scanlineDirty = this.EmitRowCoverage(rowBitVectors, row, rowCover, scanline); + if (scanlineDirty) + { + scanlineHandler(destinationTop + row, scanline, ref state); + } + } + } - if (!hasMore0 && !hasMore1) + public void ResetTouchedRows() + { + // Reset only rows that received contributions in this band. This avoids clearing + // full temporary buffers when geometry is sparse relative to the interest bounds. + for (int i = 0; i < this.touchedRowCount; i++) { - // The entire polygon is outside the scan region, we skipped all edges, - // scanning will not find any intersections. - break; + int row = this.touchedRows[i]; + this.startCover[row] = 0; + this.rowTouched[row] = 0; + + if (this.rowHasBits[row] == 0) + { + continue; + } + + this.rowHasBits[row] = 0; + this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow).Clear(); } - float y0 = hasMore0 ? this.edges[this.sorted0[i0]].Y0 : float.MaxValue; - float y1 = hasMore1 ? this.edges[this.sorted1[i1]].Y1 : float.MaxValue; + this.touchedRowCount = 0; + } - if (y0 < y1) + private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row, int cover, Span scanline) + { + int rowOffset = row * this.coverStride; + int spanStart = 0; + int spanEnd = 0; + float spanCoverage = 0F; + bool hasCoverage = false; + + for (int wordIndex = 0; wordIndex < rowBitVectors.Length; wordIndex++) { - this.SubPixelY = y0; - i0++; + nuint bitset = rowBitVectors[wordIndex]; + while (bitset != 0) + { + int localBitIndex = TrailingZeroCount(bitset); + bitset &= bitset - 1; + + int x = (wordIndex * WordBitCount) + localBitIndex; + if ((uint)x >= (uint)this.width) + { + continue; + } + + int tableIndex = rowOffset + (x << 1); + int area = this.coverArea[tableIndex + 1] + (cover << AreaToCoverageShift); + float coverage = this.AreaToCoverage(area); + + if (spanEnd == x) + { + if (coverage <= 0F) + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + spanStart = x + 1; + spanEnd = spanStart; + spanCoverage = 0F; + } + else if (coverage == spanCoverage) + { + spanEnd = x + 1; + } + else + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + if (cover == 0) + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else + { + float gapCoverage = this.AreaToCoverage(cover << AreaToCoverageShift); + if (spanCoverage == gapCoverage) + { + if (coverage == gapCoverage) + { + spanEnd = x + 1; + } + else + { + hasCoverage |= FlushSpan(scanline, spanStart, x, spanCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + hasCoverage |= FlushSpan(scanline, spanEnd, x, gapCoverage); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + } + + cover += this.coverArea[tableIndex]; + } + } + + hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); + if (cover != 0 && spanEnd < this.width) + { + hasCoverage |= FlushSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift)); + } + + return hasCoverage; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly float AreaToCoverage(int area) + { + int signedArea = area >> AreaToCoverageShift; + int absoluteArea = signedArea < 0 ? -signedArea : signedArea; + float coverage; + if (this.intersectionRule == IntersectionRule.NonZero) + { + if (absoluteArea >= CoverageStepCount) + { + coverage = 1F; + } + else + { + coverage = absoluteArea * CoverageScale; + } } else { - this.SubPixelY = y1; - i1++; + int wrapped = absoluteArea & EvenOddMask; + if (wrapped > CoverageStepCount) + { + wrapped = EvenOddPeriod - wrapped; + } + + coverage = wrapped >= CoverageStepCount ? 1F : wrapped * CoverageScale; } + + if (this.rasterizationMode == RasterizationMode.Aliased) + { + return coverage >= 0.5F ? 1F : 0F; + } + + return coverage; } - } - public bool MoveToNextPixelLine() - { - this.PixelLineY++; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool FlushSpan(Span scanline, int start, int end, float coverage) + { + if (coverage <= 0F || end <= start) + { + return false; + } + + scanline[start..end].Fill(coverage); + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly bool ConditionalSetBit(int row, int column) + { + int bitIndex = row * this.wordsPerRow; + int wordIndex = bitIndex + (column / WordBitCount); + nuint mask = (nuint)1 << (column % WordBitCount); + ref nuint word = ref this.bitVectors[wordIndex]; + bool newlySet = (word & mask) == 0; + word |= mask; + this.rowHasBits[row] = 1; + return newlySet; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddCell(int row, int column, int delta, int area) + { + if ((uint)row >= (uint)this.height) + { + return; + } + + this.MarkRowTouched(row); + + if (column < 0) + { + this.startCover[row] += delta; + return; + } + + if ((uint)column >= (uint)this.width) + { + return; + } + + int index = (row * this.coverStride) + (column << 1); + if (this.ConditionalSetBit(row, column)) + { + this.coverArea[index] = delta; + this.coverArea[index + 1] = area; + } + else + { + this.coverArea[index] += delta; + this.coverArea[index + 1] += area; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void MarkRowTouched(int row) + { + if (this.rowTouched[row] != 0) + { + return; + } + + this.rowTouched[row] = 1; + this.touchedRows[this.touchedRowCount++] = row; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CellVertical(int px, int py, int x, int y0, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x - x); + this.AddCell(py, px, delta, area); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Cell(int row, int px, int x0, int y0, int x1, int y1) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x0 - x1); + this.AddCell(row, px, delta, area); + } + + private void VerticalDown(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + this.CellVertical(columnIndex, rowIndex0, fx, fy0, FixedOne); + for (int row = rowIndex0 + 1; row < rowIndex1; row++) + { + this.CellVertical(columnIndex, row, fx, 0, FixedOne); + } + + this.CellVertical(columnIndex, rowIndex1, fx, 0, fy1); + } + + private void VerticalUp(int columnIndex, int y0, int y1, int x) + { + int rowIndex0 = (y0 - 1) >> FixedShift; + int rowIndex1 = y1 >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + if (rowIndex0 == rowIndex1) + { + this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); + return; + } + + this.CellVertical(columnIndex, rowIndex0, fx, fy0, 0); + for (int row = rowIndex0 - 1; row > rowIndex1; row--) + { + this.CellVertical(columnIndex, row, fx, FixedOne, 0); + } - // When there are no active edges we can skip directly to the next row that may receive coverage. - if (this.activeEdges.IsEmpty) + this.CellVertical(columnIndex, rowIndex1, fx, FixedOne, fy1); + } + + private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) { - if (this.idx0 < this.sorted0.Length) + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) { - float nextStartY = this.edges[this.sorted0[this.idx0]].Y0; - int nextRelevantPixelLine = (int)MathF.Floor(nextStartY); - if (nextRelevantPixelLine > this.PixelLineY) + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p1y - p0y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) { - this.PixelLineY = nextRelevantPixelLine; + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; } } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); } - this.yPlusOne = this.PixelLineY + 1; - this.SubPixelY = this.PixelLineY - this.SubpixelDistance; - return this.PixelLineY < this.maxY; - } + private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowDownR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } - public bool MoveToNextSubpixelScanLine() - { - // If the active edge list is empty and the next edge starts at or below the next pixel row, - // the current row cannot produce any intersections. - if (this.activeEdges.IsEmpty && - this.idx0 < this.sorted0.Length && - this.edges[this.sorted0[this.idx0]].Y0 >= this.yPlusOne) + private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) { - this.SubPixelY = this.yPlusOne; - return false; + int columnIndex0 = p0x >> FixedShift; + int columnIndex1 = (p1x - 1) >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p1x - p0x; + int dy = p0y - p1y; + int pp = (FixedOne - fx0) * dy; + int cy = p0y - (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); + + int idx = columnIndex0 + 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx++) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); } - this.SubPixelY += this.SubpixelDistance; - this.EnterEdges(); - this.LeaveEdges(); - return this.SubPixelY < this.yPlusOne; - } + private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + if (p0x < p1x) + { + this.RowUpR(rowIndex, p0x, p0y, p1x, p1y); + } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } - public ReadOnlySpan ScanCurrentLine() - => this.intersectionRule == IntersectionRule.EvenOdd - ? this.activeEdges.ScanOddEven(this.SubPixelY, this.edges, this.intersections) - : this.activeEdges.ScanNonZero(this.SubPixelY, this.edges, this.intersections, this.intersectionTypes); + private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); - public readonly void Dispose() - { - this.edgeCollection.Dispose(); - this.dataBuffer.Dispose(); - } + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } - private void EnterEdges() - { - while (this.idx0 < this.sorted0.Length) + int dx = p0x - p1x; + int dy = p1y - p0y; + int pp = fx0 * dy; + int cy = p0y + (pp / dx); + + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy + delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); + } + + private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { - int edge0 = this.sorted0[this.idx0]; - if (this.edges[edge0].Y0 > this.SubPixelY) + if (p0x > p1x) + { + this.RowDownL(rowIndex, p0x, p0y, p1x, p1y); + } + else { - break; + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); } + } + + private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) + { + int columnIndex0 = (p0x - 1) >> FixedShift; + int columnIndex1 = p1x >> FixedShift; + int fx0 = p0x - (columnIndex0 << FixedShift); + int fx1 = p1x - (columnIndex1 << FixedShift); + + if (columnIndex0 == columnIndex1) + { + this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); + return; + } + + int dx = p0x - p1x; + int dy = p0y - p1y; + int pp = fx0 * dy; + int cy = p0y - (pp / dx); - this.activeEdges.EnterEdge(edge0); - this.idx0++; + this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); + + int idx = columnIndex0 - 1; + if (idx != columnIndex1) + { + int mod = (pp % dx) - dx; + int p = FixedOne * dy; + int lift = p / dx; + int rem = p % dx; + + for (; idx != columnIndex1; idx--) + { + int delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dx; + delta++; + } + + int ny = cy - delta; + this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); + cy = ny; + } + } + + this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); } - } - private void LeaveEdges() - { - while (this.idx1 < this.sorted1.Length) + private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { - int edge1 = this.sorted1[this.idx1]; - if (this.edges[edge1].Y1 > this.SubPixelY) + if (p0x > p1x) { - break; + this.RowUpL(rowIndex, p0x, p0y, p1x, p1y); } + else + { + int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; + int x = p0x - (columnIndex << FixedShift); + this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); + } + } + + private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowDownR_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowDownR_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownR_V(rowIndex1, cx, 0, x1, fy1); + } + + private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x1 - x0; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 + delta; + + this.RowUpR_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx + delta; + this.RowUpR_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpR_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y1 - y0; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = (FixedOne - fy0) * dx; + int delta = p / dy; + int cx = x0 - delta; + + this.RowDownL_V(rowIndex0, x0, fy0, cx, FixedOne); + + int row = rowIndex0 + 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row++) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowDownL_V(row, cx, 0, nx, FixedOne); + cx = nx; + } + } + + this.RowDownL_V(rowIndex1, cx, 0, x1, fy1); + } + + private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) + { + int dx = x0 - x1; + int dy = y0 - y1; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int p = fy0 * dx; + int delta = p / dy; + int cx = x0 - delta; - this.activeEdges.LeaveEdge(edge1); - this.idx1++; + this.RowUpL_V(rowIndex0, x0, fy0, cx, 0); + + int row = rowIndex0 - 1; + if (row != rowIndex1) + { + int mod = (p % dy) - dy; + p = FixedOne * dx; + int lift = p / dy; + int rem = p % dy; + + for (; row != rowIndex1; row--) + { + delta = lift; + mod += rem; + if (mod >= 0) + { + mod -= dy; + delta++; + } + + int nx = cx - delta; + this.RowUpL_V(row, cx, FixedOne, nx, 0); + cx = nx; + } + } + + this.RowUpL_V(rowIndex1, cx, FixedOne, x1, fy1); + } + + private void RasterizeLine(int x0, int y0, int x1, int y1) + { + if (x0 == x1) + { + int columnIndex = (x0 - FindAdjustment(x0)) >> FixedShift; + if (y0 < y1) + { + this.VerticalDown(columnIndex, y0, y1, x0); + } + else + { + this.VerticalUp(columnIndex, y0, y1, x0); + } + + return; + } + + if (y0 < y1) + { + int rowIndex0 = y0 >> FixedShift; + int rowIndex1 = (y1 - 1) >> FixedShift; + if (rowIndex0 == rowIndex1) + { + int rowBase = rowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowDownR(rowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowDownL(rowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineDownR(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + else + { + this.LineDownL(rowIndex0, rowIndex1, x0, y0, x1, y1); + } + + return; + } + + int upRowIndex0 = (y0 - 1) >> FixedShift; + int upRowIndex1 = y1 >> FixedShift; + if (upRowIndex0 == upRowIndex1) + { + int rowBase = upRowIndex0 << FixedShift; + int localY0 = y0 - rowBase; + int localY1 = y1 - rowBase; + if (x0 < x1) + { + this.RowUpR(upRowIndex0, x0, localY0, x1, localY1); + } + else + { + this.RowUpL(upRowIndex0, x0, localY0, x1, localY1); + } + } + else if (x0 < x1) + { + this.LineUpR(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } + else + { + this.LineUpL(upRowIndex0, upRowIndex1, x0, y0, x1, y1); + } } } } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD index 4206223f..ff43cf14 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD @@ -1,76 +1,204 @@ -# Polygon Scanning with Active Edge List +# Polygon Scanner (Fixed-Point, Banded) -Scanning is done with a variant of the ["Active Edge Table" algorithm](https://en.wikipedia.org/wiki/Scanline_rendering#Algorithm), that doesn't build a table beforehand, just maintains the list of currently active edges. +This document describes the current `PolygonScanner` implementation in +`src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs`. -After rasterizing polygons a collection of non-horizontal edges (ScanEdge) is extracted into ScanEdgeCollection. These are then sorted by minimum and maximum y-coordinate, which enables the maintanance of the Active Edge List as we traverse the collection from `minY` to `maxY`. +The scanner is a fixed-point, area/cover rasterizer inspired by Blaze-style +scan conversion. -When intersecting a ScanEdge start (Y0) and end (Y1) intersections have special handling. Since these belong to vertices (connection points) sometimes we need to emit the intersection point 2 times. In other cases we do not want to emit it at all. +https://github.com/aurimasg/blaze (MIT-Licensed) -### Illustration +## Goals -Consider the following polygon with 4 non-horizontal ScanEdge-s, being intersected by scanlines `SCANLINE 1` and `SCANLINE 2`: +- Robustly rasterize arbitrary tessellated polygon rings (including self intersections). +- Support `EvenOdd` and `NonZero` fill rules. +- Keep temporary memory bounded for large targets. +- Emit coverage spans efficiently for blending. +## High-Level Pipeline + +``` +IPath + | + v +TessellatedMultipolygon.Create(...) + | + v +Band loop over interest rectangle Y + | + +--> Clear band-local buffers + | + +--> Rasterize all ring edges into cover/area accumulators + | + +--> Convert accumulators to coverage scanlines + | + +--> Invoke rasterizer callback per dirty row +``` + +## Coordinate System and Precision + +- Geometry is transformed to scanner-local coordinates: + - `xLocal = (x - interest.Left) + samplingOffsetX` + - `yLocal = y - (interest.Top + bandTop)` +- Scanner math uses signed 24.8 fixed point: + - `FixedShift = 8` + - `FixedOne = 256` +- Coverage is normalized to `[0..1]` with 256 steps: + - `CoverageStepCount = 256` + - `CoverageScale = 1 / 256` + +This means 1 fixed unit in Y equals 1/256 pixel row resolution. + +## Memory Model and Banding + +The scanner bounds temporary memory with a per-band budget: + +- `BandMemoryBudgetBytes = 64 MB` +- Rows per band are computed from per-row byte cost. + +Per-row temporary storage: + +``` +bitVectors: wordsPerRow * sizeof(nuint) +coverArea : (width * 2) * sizeof(int) +startCover: 1 * sizeof(int) +``` + +Band buffers are reused for each band: + +``` +bitVectors : [bandHeight][wordsPerRow] // bitset marks touched columns +coverArea : [bandHeight][width * 2] // per x: [deltaCover, deltaArea] +startCover : [bandHeight] // carry-in cover at x=0 +rowHasBits : [bandHeight] // fast "row touched" flag +scanline : [width] float // output coverage row +``` + +If width/height are too large for safe indexing math, `PolygonScanner.Rasterize` +throws `InvalidOperationException`. + +## Edge Rasterization Stage + +For each tessellated ring edge `(p0 -> p1)`: + +1. Translate to local band coordinates. +2. Reject non-finite coordinates. +3. Clip vertically to `[0, bandHeight]` (`ClipToVerticalBounds`). +4. Convert endpoints to 24.8 fixed. +5. Skip horizontal edges (`fy0 == fy1`). +6. Route to directional line walkers (`LineDownR`, `LineUpL`, etc.). + +The walkers decompose edges into affected cells and call: + +- `Cell(...)` for general segments +- `CellVertical(...)` for vertical segments + +Both end up in `AddCell(row, column, deltaCover, deltaArea)`. + +`AddCell` updates: + +- `coverArea[row, column * 2 + 0] += deltaCover` +- `coverArea[row, column * 2 + 1] += deltaArea` +- bit in `bitVectors[row]` for `column` +- `rowHasBits[row] = 1` + +If `column < 0`, the contribution is folded into `startCover[row]` so coverage +to the left of the interest rectangle still influences pixels at `x >= 0`. + +## Scanline Emission Stage + +For each row in the current band: + +1. Skip quickly if `startCover[row] == 0` and `rowHasBits[row] == 0`. +2. Iterate set bits in the row bitset (`TrailingZeroCount` walk). +3. Reconstruct area/cover state at each touched `x`. +4. Convert signed accumulated area to coverage via fill rule. +5. Coalesce equal coverage into spans. +6. Fill `scanline[start..end]` for each non-zero span. +7. Invoke callback for dirty rows only. + +Core conversion: + +``` +area = coverArea[deltaArea] + (cover << 9) +``` + +`cover` is updated incrementally by `deltaCover`. + +## Fill Rule Handling + +### NonZero + +``` +absArea = abs(signedArea) +coverage = min(absArea, 256) / 256 ``` - + - - - - - - - - - - - - - - - - + - | (1) (1)\ - | \ B - | \ - | (0) \ -SCANLINE 1 >>>> | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> | (1) >>>>>>>>> - | | - | A C | - | | - | (2) X | (1) -SCANLINE 2 >>>> | >>>>>>>>>>>>>> + - - - - - - - - - - + >>>>>>>>> - | | - | | - | D | - | | - | (1) | (1) - + - - - - - - - + +### EvenOdd + +``` +wrapped = absArea & 511 +if wrapped > 256: wrapped = 512 - wrapped +coverage = min(wrapped, 256) / 256 +``` + +This is done in `AreaToCoverage(int area)`. + +## Why This Handles Self Intersections + +The scanner does not require geometric boolean normalization first. +Overlaps are resolved by accumulated area/cover integration and final fill-rule +mapping (`EvenOdd` or `NonZero`), so winding/parity behavior is decided at +rasterization time. + +## Fast Paths and Practical Optimizations + +- One tessellation build per rasterization call (`TessellatedMultipolygon` reused across bands). +- Band-buffer reuse avoids per-band allocations. +- `rowHasBits` avoids scanning all words in empty rows. +- Bitset iteration visits only touched columns. +- Span coalescing reduces per-pixel operations before blending. + +## Notes on Public Options + +- `RasterizerOptions.RasterizationMode` controls whether scanner output is: + - `Antialiased`: continuous coverage in `[0, 1]` + - `Aliased`: binary coverage (`0` or `1`), thresholded in the scanner +- `RasterizerSamplingOrigin` still affects X alignment (`PixelBoundary` vs `PixelCenter`). + +## Data Flow Diagram (Row-Level) + +``` + per-edge writes + | + v + +----------------------+ + | coverArea[row][x,*] | deltaCover + deltaArea + +----------------------+ + | + +--> bitVectors[row] set bit x + | + +--> rowHasBits[row] = 1 + | + +--> startCover[row] (for x < 0 contributions) + +Then during emit: + +bitVectors[row] -> touched x list -> accumulate cover/area -> coverage spans + | + v + scanline[width] + | + v + Rasterizer callback ``` +## Failure Modes and Diagnostics -#### Intersections at SCANLINE 1 - -- Intersection with edge A is trivial, since it's being intersected on an internal point of the edge -- The second intersection is more tricky: the intersection point is at the connection (vertex) between edges B and C, but we do not want to emit the intersection 2 times. - - To avoid this, when checking the scanline's collision against edge B we emit 0 intersections at it's endpoint (Y1), when checking against edge C we emit 1 point at its start point (Y0) - -#### Intersections at SCANLINE 2 - -- Intersection with edge A is trivial, since it's being intersected on an internal point -- However the rest is tricky: We want to to emulate the intersection with the collinear edge X not being listed in `ScanEdgeCollection`. - - The easiest way is to emit a point pair for the line part between A-D and a second point pair for D-C (to emulate the intersection with X) - - To achieve this, we should emit the start point (Y0) of the D edge 2 times when intersecting it! - -### Edge emit rules - -The emit rules are there to provide a consistent way for intersecting scanlines as described in the previous "Illustration" part, handling all corner cases. -These rules only work well, when: -- The outline polygons are Clockwise in screen-space (= "has positive orientation" according to the terminlogy used in the repository) -- Holes have Counter-Clockwise ("negative") orientation. - -Most real-world inputs tend to follow these rules, however intersecting polygons which do not do so, leads to inaccuracies around horizontal edges. These inaccuracies are visually acceptable. - -The rules apply to vertices (edge connections). `⟶` and `⟵` edges are horizontal, `↑` and `↓` edges are non-horizontal. - -Edge In | Edge Out | Emit on "Edge In" | Emit on "Edge out" --- | -- | -- | -- -↑ | ↑ | 0 | 1 -↑ | ↓ | 1 | 1 -↑ | ⟵ | 2 | 0 -↑ | ⟶ | 1 | 0 -↓ | ↑ | 1 | 1 -↓ | ↓ | 0 | 1 -↓ | ⟵ | 1 | 0 -↓ | ⟶ | 2 | 0 -⟵ | ↑ | 0 | 1 -⟵ | ↓ | 0 | 2 -⟵ | ⟵ | 0 | 0 -⟵ | ⟶ | 0 | 0 -⟶ | ↑ | 0 | 2 -⟶ | ↓ | 0 | 1 -⟶ | ⟵ | 0 | 0 -⟶ | ⟶ | 0 | 0 +- Exception: interest too large for bounded temporary buffers/indexing. +- Symptoms like missing fill are usually from invalid input geometry (NaN/Inf) or + ring construction upstream; scanner explicitly skips non-finite edges. +- Performance hotspots are typically in: + - edge walking (`RasterizeLine` family), + - fill-rule conversion (`EmitRowCoverage`), + - downstream blending/compositing callbacks. diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerExtensions.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerExtensions.cs deleted file mode 100644 index 8a123377..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Utilities; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -internal static class RasterizerExtensions -{ - public static bool ScanCurrentPixelLineInto(this ref PolygonScanner scanner, int minX, float xOffset, Span scanline) - { - bool scanlineDirty = false; - while (scanner.MoveToNextSubpixelScanLine()) - { - scanner.ScanCurrentSubpixelLineInto(minX, xOffset, scanline, ref scanlineDirty); - } - - return scanlineDirty; - } - - private static void ScanCurrentSubpixelLineInto(this ref PolygonScanner scanner, int minX, float xOffset, Span scanline, ref bool scanlineDirty) - { - ReadOnlySpan points = scanner.ScanCurrentLine(); - if (points.Length == 0) - { - // nothing on this line, skip - return; - } - - for (int point = 0; point < points.Length - 1; point += 2) - { - // points will be paired up - float scanStart = points[point] - minX; - float scanEnd = points[point + 1] - minX; - int startX = (int)MathF.Floor(scanStart + xOffset); - int endX = (int)MathF.Floor(scanEnd + xOffset); - - if (startX >= 0 && startX < scanline.Length) - { - // Originally, this was implemented by a loop. - // It's possible to emulate the old behavior with MathF.Ceiling, - // but omitting the rounding seems to produce more accurate results. - // float subpixelWidth = MathF.Ceiling((startX + 1 - scanStart) / scanner.SubpixelDistance); - float subpixelWidth = (startX + 1 - scanStart) / scanner.SubpixelDistance; - - scanline[startX] += subpixelWidth * scanner.SubpixelArea; - scanlineDirty |= subpixelWidth > 0; - } - - if (endX >= 0 && endX < scanline.Length) - { - // float subpixelWidth = MathF.Ceiling((scanEnd - endX) / scanner.SubpixelDistance); - float subpixelWidth = (scanEnd - endX) / scanner.SubpixelDistance; - - scanline[endX] += subpixelWidth * scanner.SubpixelArea; - scanlineDirty |= subpixelWidth > 0; - } - - int nextX = startX + 1; - endX = Math.Min(endX, scanline.Length); // reduce to end to the right edge - nextX = Math.Max(nextX, 0); - - if (endX > nextX) - { - scanline.Slice(nextX, endX - nextX).AddToAllElements(scanner.SubpixelDistance); - scanlineDirty = true; - } - } - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs index 20517710..66a8cfbb 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs @@ -3,6 +3,22 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +/// +/// Describes whether rasterizers should emit continuous coverage or binary aliased coverage. +/// +internal enum RasterizationMode +{ + /// + /// Emit continuous coverage in the range [0, 1]. + /// + Antialiased = 0, + + /// + /// Emit binary coverage values (0 or 1). + /// + Aliased = 1 +} + /// /// Describes where sample coverage is aligned relative to destination pixels. /// @@ -28,20 +44,18 @@ internal readonly struct RasterizerOptions /// Initializes a new instance of the struct. /// /// Destination bounds to rasterize into. - /// Subpixel sampling count. /// Polygon intersection rule. + /// Rasterization coverage mode. /// Sampling origin alignment. public RasterizerOptions( Rectangle interest, - int subpixelCount, IntersectionRule intersectionRule, + RasterizationMode rasterizationMode = RasterizationMode.Antialiased, RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary) { - Guard.MustBeGreaterThan(subpixelCount, 0, nameof(subpixelCount)); - this.Interest = interest; - this.SubpixelCount = subpixelCount; this.IntersectionRule = intersectionRule; + this.RasterizationMode = rasterizationMode; this.SamplingOrigin = samplingOrigin; } @@ -51,14 +65,14 @@ public RasterizerOptions( public Rectangle Interest { get; } /// - /// Gets the subpixel sampling count. + /// Gets the polygon intersection rule. /// - public int SubpixelCount { get; } + public IntersectionRule IntersectionRule { get; } /// - /// Gets the polygon intersection rule. + /// Gets the rasterization coverage mode. /// - public IntersectionRule IntersectionRule { get; } + public RasterizationMode RasterizationMode { get; } /// /// Gets the sampling origin alignment. @@ -71,5 +85,5 @@ public RasterizerOptions( /// The replacement interest rectangle. /// A new value. public RasterizerOptions WithInterest(Rectangle interest) - => new(interest, this.SubpixelCount, this.IntersectionRule, this.SamplingOrigin); + => new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin); } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdge.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdge.cs deleted file mode 100644 index 2be9d176..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdge.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// Holds coordinates, and coefficients for a polygon edge to be horizontally scanned. -/// The edge's segment is defined with the reciprocal slope form: -/// x = p * y + q -/// -internal readonly struct ScanEdge -{ - public readonly float Y0; - public readonly float Y1; - private readonly float p; - private readonly float q; - - // Store 3 small values in a single Int32, to make EdgeData more compact: - // EdgeUp, Emit0, Emit1 - private readonly int flags; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal ScanEdge(PointF p0, PointF p1, int flags) - { - this.Y0 = p0.Y; - this.Y1 = p1.Y; - this.flags = flags; - float dy = p1.Y - p0.Y; - - // To improve accuracy, center the edge around zero before calculating the coefficients: - float cx = (p0.X + p1.X) * 0.5f; - float cy = (p0.Y + p1.Y) * 0.5f; - p0.X -= cx; - p0.Y -= cy; - p1.X -= cx; - p1.Y -= cy; - - this.p = (p1.X - p0.X) / dy; - this.q = ((p0.X * p1.Y) - (p1.X * p0.Y)) / dy; - - // After centering, the equation would be: - // x = p * (y-cy) + q + cx - // Adjust the coefficients, so we no longer need (cx,cy): - this.q += cx - (this.p * cy); - } - - // True when non-horizontal edge is oriented upwards in screen coords - public bool EdgeUp => (this.flags & 1) == 1; - - // How many times to include the intersection result - // When the scanline intersects the endpoint at Y0. - public int EmitV0 => (this.flags & 0b00110) >> 1; - - // How many times to include the intersection result - // When the scanline intersects the endpoint at Y1. - public int EmitV1 => (this.flags & 0b11000) >> 3; - - public float GetX(float y) => (this.p * y) + this.q; - - public override string ToString() - => $"(Y0={this.Y0} Y1={this.Y1} E0={this.EmitV0} E1={this.EmitV1} {(this.EdgeUp ? "Up" : "Down")} p={this.p} q={this.q})"; -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs deleted file mode 100644 index 3c6da448..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.Build.cs +++ /dev/null @@ -1,434 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using System.Runtime.Intrinsics.X86; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -internal partial class ScanEdgeCollection -{ - private enum EdgeCategory - { - Up = 0, // Non-horizontal - Down, // Non-horizontal - Left, // Horizontal - Right, // Horizontal - } - - // A pair of EdgeCategories at a given vertex, defined as (fromEdge.EdgeCategory, toEdge.EdgeCategory) - private enum VertexCategory - { - UpUp = 0, - UpDown, - UpLeft, - UpRight, - - DownUp, - DownDown, - DownLeft, - DownRight, - - LeftUp, - LeftDown, - LeftLeft, - LeftRight, - - RightUp, - RightDown, - RightLeft, - RightRight, - } - - internal static ScanEdgeCollection Create(TessellatedMultipolygon multiPolygon, MemoryAllocator allocator, int subsampling) - { - // We allocate more than we need, since we don't know how many horizontal edges do we have: - IMemoryOwner buffer = allocator.Allocate(multiPolygon.TotalVertexCount); - - RingWalker walker = new(buffer.Memory.Span); - - using IMemoryOwner roundedYBuffer = allocator.Allocate(multiPolygon.Max(r => r.Vertices.Length)); - Span roundedY = roundedYBuffer.Memory.Span; - - foreach (TessellatedMultipolygon.Ring ring in multiPolygon) - { - if (ring.VertexCount < 3) - { - continue; - } - - ReadOnlySpan vertices = ring.Vertices; - RoundY(vertices, roundedY, subsampling); - - walker.PreviousEdge = new EdgeData(vertices, roundedY, vertices.Length - 2); // Last edge - walker.CurrentEdge = new EdgeData(vertices, roundedY, 0); // First edge - walker.NextEdge = new EdgeData(vertices, roundedY, 1); // Second edge - walker.Move(false); - - for (int i = 1; i < vertices.Length - 2; i++) - { - walker.NextEdge = new EdgeData(vertices, roundedY, i + 1); - walker.Move(true); - } - - walker.NextEdge = new EdgeData(vertices, roundedY, 0); // First edge - walker.Move(true); // Emit edge before last edge - - walker.NextEdge = new EdgeData(vertices, roundedY, 1); // Second edge - walker.Move(true); // Emit last edge - } - - return new ScanEdgeCollection(buffer, walker.EdgeCounter); - } - - private static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio) - { - 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) - { - 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); - } - } - } - 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) - { - 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); - } - } - } - 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) - { - 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); - } - } - } - - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 AdvSimdShuffle(Vector128 a, Vector128 b, byte control) - { - // TODO: Review the codegen here. Might be better just looping. -#pragma warning disable CA1857 // A constant is expected for the parameter - Vector128 result = Vector128.Create(AdvSimd.Extract(a, (byte)(control & 0x3))); - result = AdvSimd.Insert(result, 1, AdvSimd.Extract(a, (byte)((control >> 2) & 0x3))); - result = AdvSimd.Insert(result, 2, AdvSimd.Extract(b, (byte)((control >> 4) & 0x3))); - result = AdvSimd.Insert(result, 3, AdvSimd.Extract(b, (byte)((control >> 6) & 0x3))); -#pragma warning restore CA1857 // A constant is expected for the parameter - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static VertexCategory CreateVertexCategory(EdgeCategory previousCategory, EdgeCategory currentCategory) - { - VertexCategory value = (VertexCategory)(((int)previousCategory << 2) | (int)currentCategory); - VerifyVertexCategory(value); - return value; - } - - [Conditional("DEBUG")] - private static void VerifyVertexCategory(VertexCategory vertexCategory) - { - int value = (int)vertexCategory; - if (value is < 0 or >= 16) - { - throw new ArgumentOutOfRangeException(nameof(vertexCategory), "EdgeCategoryPair value shall be: 0 <= value < 16"); - } - } - - private struct EdgeData - { - public EdgeCategory EdgeCategory; - - private PointF start; - private PointF end; - private int emitStart; - private int emitEnd; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public EdgeData(ReadOnlySpan vertices, ReadOnlySpan roundedY, int idx) - : this( - vertices[idx].X, - vertices[idx + 1].X, - roundedY[idx], - roundedY[idx + 1]) - { - } - - public EdgeData(float startX, float endX, float startYRounded, float endYRounded) - { - this.start = new PointF(startX, startYRounded); - this.end = new PointF(endX, endYRounded); - - if (this.start.Y == this.end.Y) - { - this.EdgeCategory = this.start.X < this.end.X ? EdgeCategory.Right : EdgeCategory.Left; - } - else - { - this.EdgeCategory = this.start.Y < this.end.Y ? EdgeCategory.Down : EdgeCategory.Up; - } - - this.emitStart = 0; - this.emitEnd = 0; - } - - public void EmitScanEdge(Span edges, ref int edgeCounter) - { - if (this.EdgeCategory is EdgeCategory.Left or EdgeCategory.Right) - { - return; - } - - edges[edgeCounter++] = this.ToScanEdge(); - } - - public static void ApplyVertexCategory( - VertexCategory vertexCategory, - ref EdgeData fromEdge, - ref EdgeData toEdge) - { - // On PolygonScanner needs to handle intersections at edge connections (vertices) in a special way: - // - We need to make sure we do not report ("emit") an intersection point more times than necessary because we detected the intersection at both edges. - // - We need to make sure we we emit proper intersection points when scanning through a horizontal line - // In practice this means that vertex intersections have to emitted: 0-2 times in total: - // - Do not emit on vertex of collinear edges - // - Emit 2 times if: - // - One of the edges is horizontal - // - The corner is concave - // (The reason for tis rule is that we do not scan horizontal edges) - // - Emit once otherwise - // Since PolygonScanner does not process vertices, only edges, we need to define arbitrary rules - // about WHERE (on which edge) do we emit the vertex intersections. - // For visualization of the rules see: - // PoygonScanning.MD - // For an example, see: - // ImageSharp.Drawing.Tests/Shapes/Scan/SimplePolygon_AllEmitCases.png - switch (vertexCategory) - { - case VertexCategory.UpUp: - // 0, 1 - toEdge.emitStart = 1; - break; - case VertexCategory.UpDown: - // 1, 1 - toEdge.emitStart = 1; - fromEdge.emitEnd = 1; - break; - case VertexCategory.UpLeft: - // 2, 0 - fromEdge.emitEnd = 2; - break; - case VertexCategory.UpRight: - // 1, 0 - fromEdge.emitEnd = 1; - break; - case VertexCategory.DownUp: - // 1, 1 - toEdge.emitStart = 1; - fromEdge.emitEnd = 1; - break; - case VertexCategory.DownDown: - // 0, 1 - toEdge.emitStart = 1; - break; - case VertexCategory.DownLeft: - // 1, 0 - fromEdge.emitEnd = 1; - break; - case VertexCategory.DownRight: - // 2, 0 - fromEdge.emitEnd = 2; - break; - case VertexCategory.LeftUp: - // 0, 1 - toEdge.emitStart = 1; - break; - case VertexCategory.LeftDown: - // 0, 2 - toEdge.emitStart = 2; - break; - case VertexCategory.LeftLeft: - // 0, 0 - collinear - break; - case VertexCategory.LeftRight: - // 0, 0 - collinear - break; - case VertexCategory.RightUp: - // 0, 2 - toEdge.emitStart = 2; - break; - case VertexCategory.RightDown: - // 0, 1 - toEdge.emitStart = 1; - break; - case VertexCategory.RightLeft: - // 0, 0 - collinear - break; - case VertexCategory.RightRight: - // 0, 0 - collinear - break; - } - } - - private ScanEdge ToScanEdge() - { - int up = this.EdgeCategory == EdgeCategory.Up ? 1 : 0; - if (up == 1) - { - Swap(ref this.start, ref this.end); - Swap(ref this.emitStart, ref this.emitEnd); - } - - int flags = up | (this.emitStart << 1) | (this.emitEnd << 3); - return new ScanEdge(this.start, this.end, flags); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Swap(ref T left, ref T right) - { - T tmp = left; - left = right; - right = tmp; - } - } - - private ref struct RingWalker - { - private readonly Span output; - public int EdgeCounter; - - public EdgeData PreviousEdge; - public EdgeData CurrentEdge; - public EdgeData NextEdge; - - public RingWalker(Span output) - { - this.output = output; - this.EdgeCounter = 0; - this.PreviousEdge = default; - this.CurrentEdge = default; - this.NextEdge = default; - } - - public void Move(bool emitPreviousEdge) - { - VertexCategory startVertexCategory = - CreateVertexCategory(this.PreviousEdge.EdgeCategory, this.CurrentEdge.EdgeCategory); - VertexCategory endVertexCategory = - CreateVertexCategory(this.CurrentEdge.EdgeCategory, this.NextEdge.EdgeCategory); - - EdgeData.ApplyVertexCategory(startVertexCategory, ref this.PreviousEdge, ref this.CurrentEdge); - EdgeData.ApplyVertexCategory(endVertexCategory, ref this.CurrentEdge, ref this.NextEdge); - - if (emitPreviousEdge) - { - this.PreviousEdge.EmitScanEdge(this.output, ref this.EdgeCounter); - } - - this.PreviousEdge = this.CurrentEdge; - this.CurrentEdge = this.NextEdge; - } - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs deleted file mode 100644 index 7c95e2ca..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanEdgeCollection.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -internal sealed partial class ScanEdgeCollection : IDisposable -{ - private readonly IMemoryOwner buffer; - private Memory memory; - - private ScanEdgeCollection(IMemoryOwner buffer, int count) - { - this.buffer = buffer; - this.memory = buffer.Memory[..count]; - } - - public Span Edges => this.memory.Span; - - public int Count => this.Edges.Length; - - public void Dispose() - { - if (this.buffer == null) - { - return; - } - - this.buffer.Dispose(); - this.memory = default; - } - - public static ScanEdgeCollection Create( - IPath polygon, - MemoryAllocator allocator, - int subsampling) - { - using TessellatedMultipolygon multiPolygon = TessellatedMultipolygon.Create(polygon, allocator); - return Create(multiPolygon, allocator, subsampling); - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs new file mode 100644 index 00000000..6d778120 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +/// +/// Single-pass CPU scanline rasterizer. +/// +/// +/// This implementation directly rasterizes the whole interest rectangle in one pass. +/// It is retained as a compact fallback/reference implementation and as an explicit +/// non-tiled option for profiling and comparison. +/// +internal sealed class ScanlineRasterizer : IRasterizer +{ + /// + /// Gets the singleton scanline rasterizer instance. + /// + public static ScanlineRasterizer Instance { get; } = new(); + + /// + public void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(allocator, nameof(allocator)); + Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); + + Rectangle interest = options.Interest; + if (interest.Equals(Rectangle.Empty)) + { + return; + } + + PolygonScanner.Rasterize(path, options, allocator, ref state, scanlineHandler); + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs deleted file mode 100644 index 8c9f9abe..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/SharpBlazeScanner.cs +++ /dev/null @@ -1,1046 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// SharpBlaze-style fixed-point scanner that converts path segments into per-row coverage runs. -/// -internal static class SharpBlazeScanner -{ - // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows). - // Keeping this bounded prevents pathological full-image allocations on very large interests. - private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L; - - private const int FixedShift = 8; - private const int FixedOne = 1 << FixedShift; - private static readonly int WordBitCount = nint.Size * 8; - private const int AreaToCoverageShift = 9; - private const int CoverageStepCount = 256; - private const int EvenOddMask = (CoverageStepCount * 2) - 1; - private const int EvenOddPeriod = CoverageStepCount * 2; - private const float CoverageScale = 1F / CoverageStepCount; - - public static bool TryRasterize( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - Rectangle interest = options.Interest; - int width = interest.Width; - int height = interest.Height; - if (width <= 0 || height <= 0) - { - return true; - } - - int wordsPerRow = BitVectorsForMaxBitCount(width); - long coverStride = (long)width * 2; - if (coverStride > int.MaxValue || - !TryGetBandHeight(width, height, wordsPerRow, coverStride, out int maxBandRows)) - { - return false; - } - - int coverStrideInt = (int)coverStride; - int bitVectorCapacity = checked(wordsPerRow * maxBandRows); - int coverAreaCapacity = checked(coverStrideInt * maxBandRows); - using IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity); - using IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); - using IMemoryOwner startCoverOwner = allocator.Allocate(maxBandRows); - - // Per-row activity flags avoid scanning the full bit-vector row just to detect "empty row". - using IMemoryOwner rowHasBitsOwner = allocator.Allocate(maxBandRows); - using IMemoryOwner scanlineOwner = allocator.Allocate(width); - - Span bitVectorsBuffer = bitVectorsOwner.Memory.Span; - Span coverAreaBuffer = coverAreaOwner.Memory.Span; - Span startCoverBuffer = startCoverOwner.Memory.Span; - Span rowHasBitsBuffer = rowHasBitsOwner.Memory.Span; - Span scanline = scanlineOwner.Memory.Span; - - float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); - int bandTop = 0; - while (bandTop < height) - { - int bandHeight = Math.Min(maxBandRows, height - bandTop); - int bitVectorCount = wordsPerRow * bandHeight; - int coverCount = coverStrideInt * bandHeight; - - Span bitVectors = bitVectorsBuffer[..bitVectorCount]; - Span coverArea = coverAreaBuffer[..coverCount]; - Span startCover = startCoverBuffer[..bandHeight]; - Span rowHasBits = rowHasBitsBuffer[..bandHeight]; - - bitVectors.Clear(); - coverArea.Clear(); - startCover.Clear(); - rowHasBits.Clear(); - - Context context = new( - bitVectors, - coverArea, - startCover, - rowHasBits, - width, - bandHeight, - wordsPerRow, - coverStrideInt, - options.IntersectionRule); - - context.RasterizeMultipolygon( - multipolygon, - interest.Left, - interest.Top + bandTop, - samplingOffsetX); - - context.EmitScanlines(interest.Top + bandTop, scanline, ref state, scanlineHandler); - bandTop += bandHeight; - } - - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; - - private static bool TryGetBandHeight(int width, int height, int wordsPerRow, long coverStride, out int bandHeight) - { - bandHeight = 0; - if (width <= 0 || height <= 0 || wordsPerRow <= 0 || coverStride <= 0) - { - return false; - } - - long bytesPerRow = - ((long)wordsPerRow * nint.Size) + - (coverStride * sizeof(int)) + - sizeof(int); - - long rowsByBudget = BandMemoryBudgetBytes / bytesPerRow; - if (rowsByBudget < 1) - { - rowsByBudget = 1; - } - - long rowsByBitVectors = int.MaxValue / wordsPerRow; - long rowsByCoverArea = int.MaxValue / coverStride; - long maxRows = Math.Min(rowsByBudget, Math.Min(rowsByBitVectors, rowsByCoverArea)); - if (maxRows < 1) - { - return false; - } - - bandHeight = (int)Math.Min(height, maxRows); - return bandHeight > 0; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); - - private static bool ClipToVerticalBounds(ref float x0, ref float y0, ref float x1, ref float y1, float minY, float maxY) - { - float t0 = 0F; - float t1 = 1F; - float dx = x1 - x0; - float dy = y1 - y0; - - if (!ClipTest(-dy, y0 - minY, ref t0, ref t1)) - { - return false; - } - - if (!ClipTest(dy, maxY - y0, ref t0, ref t1)) - { - return false; - } - - if (t1 < 1F) - { - x1 = x0 + (dx * t1); - y1 = y0 + (dy * t1); - } - - if (t0 > 0F) - { - x0 += dx * t0; - y0 += dy * t0; - } - - return y0 != y1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ClipTest(float p, float q, ref float t0, ref float t1) - { - if (p == 0F) - { - return q >= 0F; - } - - float r = q / p; - if (p < 0F) - { - if (r > t1) - { - return false; - } - - if (r > t0) - { - t0 = r; - } - } - else - { - if (r < t0) - { - return false; - } - - if (r < t1) - { - t1 = r; - } - } - - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FindAdjustment(int value) - { - int lte0 = ~((value - 1) >> 31) & 1; - int divisibleBy256 = (((value & (FixedOne - 1)) - 1) >> 31) & 1; - return lte0 & divisibleBy256; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TrailingZeroCount(nuint value) - => nint.Size == sizeof(ulong) - ? BitOperations.TrailingZeroCount((ulong)value) - : BitOperations.TrailingZeroCount((uint)value); - - private readonly ref struct Context - { - private readonly Span bitVectors; - private readonly Span coverArea; - private readonly Span startCover; - private readonly Span rowHasBits; - private readonly int width; - private readonly int height; - private readonly int wordsPerRow; - private readonly int coverStride; - private readonly IntersectionRule intersectionRule; - - public Context( - Span bitVectors, - Span coverArea, - Span startCover, - Span rowHasBits, - int width, - int height, - int wordsPerRow, - int coverStride, - IntersectionRule intersectionRule) - { - this.bitVectors = bitVectors; - this.coverArea = coverArea; - this.startCover = startCover; - this.rowHasBits = rowHasBits; - this.width = width; - this.height = height; - this.wordsPerRow = wordsPerRow; - this.coverStride = coverStride; - this.intersectionRule = intersectionRule; - } - - public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX, int minY, float samplingOffsetX) - { - foreach (TessellatedMultipolygon.Ring ring in multipolygon) - { - ReadOnlySpan vertices = ring.Vertices; - for (int i = 0; i < ring.VertexCount; i++) - { - PointF p0 = vertices[i]; - PointF p1 = vertices[i + 1]; - - float x0 = (p0.X - minX) + samplingOffsetX; - float y0 = p0.Y - minY; - float x1 = (p1.X - minX) + samplingOffsetX; - float y1 = p1.Y - minY; - - if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) - { - continue; - } - - if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, this.height)) - { - continue; - } - - int fx0 = FloatToFixed24Dot8(x0); - int fy0 = FloatToFixed24Dot8(y0); - int fx1 = FloatToFixed24Dot8(x1); - int fy1 = FloatToFixed24Dot8(y1); - if (fy0 == fy1) - { - continue; - } - - this.RasterizeLine(fx0, fy0, fx1, fy1); - } - } - } - - public void EmitScanlines(int destinationTop, Span scanline, ref TState state, RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - for (int row = 0; row < this.height; row++) - { - int rowCover = this.startCover[row]; - if (rowCover == 0 && this.rowHasBits[row] == 0) - { - continue; - } - - Span rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); - scanline.Clear(); - bool scanlineDirty = this.EmitRowCoverage(rowBitVectors, row, rowCover, scanline); - if (scanlineDirty) - { - scanlineHandler(destinationTop + row, scanline, ref state); - } - } - } - - private bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row, int cover, Span scanline) - { - int rowOffset = row * this.coverStride; - int spanStart = 0; - int spanEnd = 0; - float spanCoverage = 0F; - bool hasCoverage = false; - - for (int wordIndex = 0; wordIndex < rowBitVectors.Length; wordIndex++) - { - nuint bitset = rowBitVectors[wordIndex]; - while (bitset != 0) - { - int localBitIndex = TrailingZeroCount(bitset); - bitset &= bitset - 1; - - int x = (wordIndex * WordBitCount) + localBitIndex; - if ((uint)x >= (uint)this.width) - { - continue; - } - - int tableIndex = rowOffset + (x << 1); - int area = this.coverArea[tableIndex + 1] + (cover << AreaToCoverageShift); - float coverage = this.AreaToCoverage(area); - - if (spanEnd == x) - { - if (coverage <= 0F) - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - spanStart = x + 1; - spanEnd = spanStart; - spanCoverage = 0F; - } - else if (coverage == spanCoverage) - { - spanEnd = x + 1; - } - else - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - } - else - { - if (cover == 0) - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - else - { - float gapCoverage = this.AreaToCoverage(cover << AreaToCoverageShift); - if (spanCoverage == gapCoverage) - { - if (coverage == gapCoverage) - { - spanEnd = x + 1; - } - else - { - hasCoverage |= FlushSpan(scanline, spanStart, x, spanCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - } - else - { - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - hasCoverage |= FlushSpan(scanline, spanEnd, x, gapCoverage); - spanStart = x; - spanEnd = x + 1; - spanCoverage = coverage; - } - } - } - - cover += this.coverArea[tableIndex]; - } - } - - hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); - if (cover != 0 && spanEnd < this.width) - { - hasCoverage |= FlushSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift)); - } - - return hasCoverage; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private float AreaToCoverage(int area) - { - int signedArea = area >> AreaToCoverageShift; - int absoluteArea = signedArea < 0 ? -signedArea : signedArea; - if (this.intersectionRule == IntersectionRule.NonZero) - { - if (absoluteArea >= CoverageStepCount) - { - return 1F; - } - - return absoluteArea * CoverageScale; - } - - int wrapped = absoluteArea & EvenOddMask; - if (wrapped > CoverageStepCount) - { - wrapped = EvenOddPeriod - wrapped; - } - - if (wrapped >= CoverageStepCount) - { - return 1F; - } - - return wrapped * CoverageScale; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool FlushSpan(Span scanline, int start, int end, float coverage) - { - if (coverage <= 0F || end <= start) - { - return false; - } - - scanline[start..end].Fill(coverage); - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ConditionalSetBit(int row, int column) - { - int bitIndex = row * this.wordsPerRow; - int wordIndex = bitIndex + (column / WordBitCount); - nuint mask = (nuint)1 << (column % WordBitCount); - ref nuint word = ref this.bitVectors[wordIndex]; - bool newlySet = (word & mask) == 0; - word |= mask; - this.rowHasBits[row] = 1; - return newlySet; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddCell(int row, int column, int delta, int area) - { - if ((uint)row >= (uint)this.height) - { - return; - } - - if (column < 0) - { - this.startCover[row] += delta; - return; - } - - if ((uint)column >= (uint)this.width) - { - return; - } - - int index = (row * this.coverStride) + (column << 1); - if (this.ConditionalSetBit(row, column)) - { - this.coverArea[index] = delta; - this.coverArea[index + 1] = area; - } - else - { - this.coverArea[index] += delta; - this.coverArea[index + 1] += area; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void CellVertical(int px, int py, int x, int y0, int y1) - { - int delta = y0 - y1; - int area = delta * ((FixedOne * 2) - x - x); - this.AddCell(py, px, delta, area); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Cell(int row, int px, int x0, int y0, int x1, int y1) - { - int delta = y0 - y1; - int area = delta * ((FixedOne * 2) - x0 - x1); - this.AddCell(row, px, delta, area); - } - - private void VerticalDown(int columnIndex, int y0, int y1, int x) - { - int rowIndex0 = y0 >> FixedShift; - int rowIndex1 = (y1 - 1) >> FixedShift; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int fx = x - (columnIndex << FixedShift); - - if (rowIndex0 == rowIndex1) - { - this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); - return; - } - - this.CellVertical(columnIndex, rowIndex0, fx, fy0, FixedOne); - for (int row = rowIndex0 + 1; row < rowIndex1; row++) - { - this.CellVertical(columnIndex, row, fx, 0, FixedOne); - } - - this.CellVertical(columnIndex, rowIndex1, fx, 0, fy1); - } - - private void VerticalUp(int columnIndex, int y0, int y1, int x) - { - int rowIndex0 = (y0 - 1) >> FixedShift; - int rowIndex1 = y1 >> FixedShift; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int fx = x - (columnIndex << FixedShift); - - if (rowIndex0 == rowIndex1) - { - this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); - return; - } - - this.CellVertical(columnIndex, rowIndex0, fx, fy0, 0); - for (int row = rowIndex0 - 1; row > rowIndex1; row--) - { - this.CellVertical(columnIndex, row, fx, FixedOne, 0); - } - - this.CellVertical(columnIndex, rowIndex1, fx, FixedOne, fy1); - } - - private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = p0x >> FixedShift; - int columnIndex1 = (p1x - 1) >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p1x - p0x; - int dy = p1y - p0y; - int pp = (FixedOne - fx0) * dy; - int cy = p0y + (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); - - int idx = columnIndex0 + 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx++) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy + delta; - this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); - } - - private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x < p1x) - { - this.RowDownR(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = p0x >> FixedShift; - int columnIndex1 = (p1x - 1) >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p1x - p0x; - int dy = p0y - p1y; - int pp = (FixedOne - fx0) * dy; - int cy = p0y - (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, FixedOne, cy); - - int idx = columnIndex0 + 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx++) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy - delta; - this.Cell(rowIndex, idx, 0, cy, FixedOne, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); - } - - private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x < p1x) - { - this.RowUpR(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = (p0x - 1) >> FixedShift; - int columnIndex1 = p1x >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p0x - p1x; - int dy = p1y - p0y; - int pp = fx0 * dy; - int cy = p0y + (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); - - int idx = columnIndex0 - 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx--) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy + delta; - this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); - } - - private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x > p1x) - { - this.RowDownL(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - int columnIndex0 = (p0x - 1) >> FixedShift; - int columnIndex1 = p1x >> FixedShift; - int fx0 = p0x - (columnIndex0 << FixedShift); - int fx1 = p1x - (columnIndex1 << FixedShift); - - if (columnIndex0 == columnIndex1) - { - this.Cell(rowIndex, columnIndex0, fx0, p0y, fx1, p1y); - return; - } - - int dx = p0x - p1x; - int dy = p0y - p1y; - int pp = fx0 * dy; - int cy = p0y - (pp / dx); - - this.Cell(rowIndex, columnIndex0, fx0, p0y, 0, cy); - - int idx = columnIndex0 - 1; - if (idx != columnIndex1) - { - int mod = (pp % dx) - dx; - int p = FixedOne * dy; - int lift = p / dx; - int rem = p % dx; - - for (; idx != columnIndex1; idx--) - { - int delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dx; - delta++; - } - - int ny = cy - delta; - this.Cell(rowIndex, idx, FixedOne, cy, 0, ny); - cy = ny; - } - } - - this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); - } - - private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) - { - if (p0x > p1x) - { - this.RowUpL(rowIndex, p0x, p0y, p1x, p1y); - } - else - { - int columnIndex = (p0x - FindAdjustment(p0x)) >> FixedShift; - int x = p0x - (columnIndex << FixedShift); - this.CellVertical(columnIndex, rowIndex, x, p0y, p1y); - } - } - - private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x1 - x0; - int dy = y1 - y0; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int p = (FixedOne - fy0) * dx; - int delta = p / dy; - int cx = x0 + delta; - - this.RowDownR_V(rowIndex0, x0, fy0, cx, FixedOne); - - int row = rowIndex0 + 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row++) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx + delta; - this.RowDownR_V(row, cx, 0, nx, FixedOne); - cx = nx; - } - } - - this.RowDownR_V(rowIndex1, cx, 0, x1, fy1); - } - - private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x1 - x0; - int dy = y0 - y1; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int p = fy0 * dx; - int delta = p / dy; - int cx = x0 + delta; - - this.RowUpR_V(rowIndex0, x0, fy0, cx, 0); - - int row = rowIndex0 - 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row--) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx + delta; - this.RowUpR_V(row, cx, FixedOne, nx, 0); - cx = nx; - } - } - - this.RowUpR_V(rowIndex1, cx, FixedOne, x1, fy1); - } - - private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x0 - x1; - int dy = y1 - y0; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int p = (FixedOne - fy0) * dx; - int delta = p / dy; - int cx = x0 - delta; - - this.RowDownL_V(rowIndex0, x0, fy0, cx, FixedOne); - - int row = rowIndex0 + 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row++) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx - delta; - this.RowDownL_V(row, cx, 0, nx, FixedOne); - cx = nx; - } - } - - this.RowDownL_V(rowIndex1, cx, 0, x1, fy1); - } - - private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) - { - int dx = x0 - x1; - int dy = y0 - y1; - int fy0 = y0 - (rowIndex0 << FixedShift); - int fy1 = y1 - (rowIndex1 << FixedShift); - int p = fy0 * dx; - int delta = p / dy; - int cx = x0 - delta; - - this.RowUpL_V(rowIndex0, x0, fy0, cx, 0); - - int row = rowIndex0 - 1; - if (row != rowIndex1) - { - int mod = (p % dy) - dy; - p = FixedOne * dx; - int lift = p / dy; - int rem = p % dy; - - for (; row != rowIndex1; row--) - { - delta = lift; - mod += rem; - if (mod >= 0) - { - mod -= dy; - delta++; - } - - int nx = cx - delta; - this.RowUpL_V(row, cx, FixedOne, nx, 0); - cx = nx; - } - } - - this.RowUpL_V(rowIndex1, cx, FixedOne, x1, fy1); - } - - private void RasterizeLine(int x0, int y0, int x1, int y1) - { - if (x0 == x1) - { - int columnIndex = (x0 - FindAdjustment(x0)) >> FixedShift; - if (y0 < y1) - { - this.VerticalDown(columnIndex, y0, y1, x0); - } - else - { - this.VerticalUp(columnIndex, y0, y1, x0); - } - - return; - } - - if (y0 < y1) - { - int rowIndex0 = y0 >> FixedShift; - int rowIndex1 = (y1 - 1) >> FixedShift; - if (rowIndex0 == rowIndex1) - { - int rowBase = rowIndex0 << FixedShift; - int localY0 = y0 - rowBase; - int localY1 = y1 - rowBase; - if (x0 < x1) - { - this.RowDownR(rowIndex0, x0, localY0, x1, localY1); - } - else - { - this.RowDownL(rowIndex0, x0, localY0, x1, localY1); - } - } - else if (x0 < x1) - { - this.LineDownR(rowIndex0, rowIndex1, x0, y0, x1, y1); - } - else - { - this.LineDownL(rowIndex0, rowIndex1, x0, y0, x1, y1); - } - - return; - } - - int upRowIndex0 = (y0 - 1) >> FixedShift; - int upRowIndex1 = y1 >> FixedShift; - if (upRowIndex0 == upRowIndex1) - { - int rowBase = upRowIndex0 << FixedShift; - int localY0 = y0 - rowBase; - int localY1 = y1 - rowBase; - if (x0 < x1) - { - this.RowUpR(upRowIndex0, x0, localY0, x1, localY1); - } - else - { - this.RowUpL(upRowIndex0, x0, localY0, x1, localY1); - } - } - else if (x0 < x1) - { - this.LineUpR(upRowIndex0, upRowIndex1, x0, y0, x1, y1); - } - else - { - this.LineUpL(upRowIndex0, upRowIndex1, x0, y0, x1, y1); - } - } - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs deleted file mode 100644 index 9f045565..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/TiledRasterizer.cs +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Threading.Tasks; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// Experimental tiled CPU rasterizer. -/// -/// -/// The implementation splits the Y range into independent bands, rasterizes each band into a -/// temporary coverage buffer, then emits scanlines in deterministic top-to-bottom order. -/// This keeps the external callback contract unchanged while enabling parallel work internally. -/// -internal sealed class TiledRasterizer : IRasterizer -{ - // Keep tiles reasonably tall so small text/glyph workloads stay on the scalar fast path. - private const int MinimumBandHeight = 96; - - // Minimum amount of work assigned to each band. - private const int MinimumPixelsPerBand = 196608; - - // Bounded temporary memory: one float coverage value per destination pixel. - private const int MaximumBufferedPixels = 16777216; // 4096 x 4096 - - private const int MaximumBandCount = 8; - - /// - /// Gets the singleton tiled rasterizer instance. - /// - public static TiledRasterizer Instance { get; } = new(); - - /// - public void Rasterize( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - Guard.NotNull(path, nameof(path)); - Guard.NotNull(allocator, nameof(allocator)); - Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); - - Rectangle interest = options.Interest; - if (interest.Equals(Rectangle.Empty)) - { - return; - } - - if (!TryCreateBandPlan(interest, out Band[]? plannedBands) || plannedBands is null) - { - DefaultRasterizer.Instance.Rasterize(path, options, allocator, ref state, scanlineHandler); - return; - } - - Band[] bands = plannedBands; - RasterizerOptions bandedOptions = options; - - // Most path implementations lazily materialize flattened point buffers. - // Priming this once avoids duplicated cache-building across worker threads. - PrimePathCaches(path); - - try - { - ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = bands.Length }; - Parallel.For( - 0, - bands.Length, - parallelOptions, - i => RasterizeBand(path, bandedOptions, allocator, bands[i])); - - EmitBands(bands, interest.Width, ref state, scanlineHandler); - } - finally - { - foreach (Band band in bands) - { - band.Dispose(); - } - } - } - - private static void PrimePathCaches(IPath path) - { - foreach (ISimplePath simplePath in path.Flatten()) - { - _ = simplePath.Points.Length; - } - } - - private static bool TryCreateBandPlan(Rectangle interest, out Band[]? bands) - { - bands = null; - - int width = interest.Width; - int height = interest.Height; - long totalPixels = (long)width * height; - if (totalPixels > MaximumBufferedPixels) - { - return false; - } - - int processorCount = Environment.ProcessorCount; - if (processorCount < 2 || height < (MinimumBandHeight * 2) || totalPixels < (MinimumPixelsPerBand * 2L)) - { - return false; - } - - int byHeight = height / MinimumBandHeight; - int byPixels = (int)(totalPixels / MinimumPixelsPerBand); - int bandCount = Math.Min(MaximumBandCount, Math.Min(processorCount, Math.Min(byHeight, byPixels))); - if (bandCount < 2) - { - return false; - } - - bands = new Band[bandCount]; - int baseHeight = height / bandCount; - int remainder = height % bandCount; - int y = interest.Top; - - for (int i = 0; i < bandCount; i++) - { - int bandHeight = baseHeight + (i < remainder ? 1 : 0); - bands[i] = new Band(y, bandHeight); - y += bandHeight; - } - - return true; - } - - private static void RasterizeBand( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - Band band) - { - int width = options.Interest.Width; - int coverageLength = checked(width * band.Height); - - IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); - IMemoryOwner dirtyRowsOwner = allocator.Allocate(band.Height, AllocationOptions.Clean); - - try - { - RasterizerOptions bandOptions = options.WithInterest( - new Rectangle(options.Interest.Left, band.Top, width, band.Height)); - - BandCaptureState captureState = new(band.Top, width, coverageOwner.Memory, dirtyRowsOwner.Memory); - DefaultRasterizer.Instance.Rasterize(path, bandOptions, allocator, ref captureState, CaptureBandScanline); - - band.SetBuffers(coverageOwner, dirtyRowsOwner); - } - catch - { - coverageOwner.Dispose(); - dirtyRowsOwner.Dispose(); - throw; - } - } - - private static void EmitBands( - Band[] bands, - int scanlineWidth, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - foreach (Band band in bands) - { - if (band.CoverageOwner is null || band.DirtyRowsOwner is null) - { - continue; - } - - Span coverage = band.CoverageOwner.Memory.Span; - Span dirtyRows = band.DirtyRowsOwner.Memory.Span; - - for (int row = 0; row < band.Height; row++) - { - if (dirtyRows[row] == 0) - { - continue; - } - - Span scanline = coverage.Slice(row * scanlineWidth, scanlineWidth); - scanlineHandler(band.Top + row, scanline, ref state); - } - } - } - - private static void CaptureBandScanline(int y, Span scanline, ref BandCaptureState state) - { - int row = y - state.Top; - Span coverage = state.Coverage.Span; - scanline.CopyTo(coverage.Slice(row * state.Width, state.Width)); - state.DirtyRows.Span[row] = 1; - } - - private struct BandCaptureState - { - public BandCaptureState(int top, int width, Memory coverage, Memory dirtyRows) - { - this.Top = top; - this.Width = width; - this.Coverage = coverage; - this.DirtyRows = dirtyRows; - } - - public int Top { get; } - - public int Width { get; } - - public Memory Coverage { get; } - - public Memory DirtyRows { get; } - } - - private sealed class Band : IDisposable - { - public Band(int top, int height) - { - this.Top = top; - this.Height = height; - } - - public int Top { get; } - - public int Height { get; } - - public IMemoryOwner? CoverageOwner { get; private set; } - - public IMemoryOwner? DirtyRowsOwner { get; private set; } - - public void SetBuffers(IMemoryOwner coverageOwner, IMemoryOwner dirtyRowsOwner) - { - this.CoverageOwner = coverageOwner; - this.DirtyRowsOwner = dirtyRowsOwner; - } - - public void Dispose() - { - this.CoverageOwner?.Dispose(); - this.DirtyRowsOwner?.Dispose(); - this.CoverageOwner = null; - this.DirtyRowsOwner = null; - } - } -} diff --git a/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs b/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs index 114c2537..eade3443 100644 --- a/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs +++ b/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs @@ -58,7 +58,7 @@ public static TessellatedMultipolygon Create(IPath path, MemoryAllocator memoryA } else { - ReadOnlyMemory[] points = path.Flatten().Select(sp => sp.Points).ToArray(); + ReadOnlyMemory[] points = [.. path.Flatten().Select(sp => sp.Points)]; // If we have only one ring, we can change it's orientation without negative side-effects. // Since the algorithm works best with positively-oriented polygons, diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index cb35b1b0..0c7fb810 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -51,6 +51,13 @@ protected virtual PointF[][] GetPoints(FeatureCollection features) => [GlobalSetup] public void Setup() { + // Tiled rasterization benefits from a warmed worker pool. Doing this once in setup + // reduces first-iteration noise without affecting per-method correctness. + ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads); + int desiredWorkerThreads = Math.Max(minWorkerThreads, Environment.ProcessorCount); + ThreadPool.SetMinThreads(desiredWorkerThreads, minCompletionPortThreads); + Parallel.For(0, desiredWorkerThreads, static _ => { }); + string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); @@ -147,31 +154,34 @@ public void Cleanup() public void SystemDrawing() => this.sdGraphics.DrawPath(this.sdPen, this.sdPath); + // Keep explicit legacy path for side-by-side comparison now that tiled is default. [Benchmark] public void ImageSharpCombinedPaths() - => this.image.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); + => this.image.Mutate(c => c.SetRasterizer(ScanlineRasterizer.Instance).Draw(this.isPen, this.imageSharpPath)); [Benchmark] public void ImageSharpSeparatePaths() => this.image.Mutate( c => { + // Keep explicit legacy path for side-by-side comparison now that tiled is default. + c.SetRasterizer(ScanlineRasterizer.Instance); foreach (PointF[] loop in this.points) { c.DrawPolygon(Color.White, this.Thickness, loop); } }); + // Tiled is now the framework default rasterizer path. [Benchmark] public void ImageSharpCombinedPathsTiled() - => this.image.Mutate(c => c.SetRasterizer(TiledRasterizer.Instance).Draw(this.isPen, this.imageSharpPath)); + => this.image.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); [Benchmark] public void ImageSharpSeparatePathsTiled() => this.image.Mutate( c => { - c.SetRasterizer(TiledRasterizer.Instance); foreach (PointF[] loop in this.points) { c.DrawPolygon(Color.White, this.Thickness, loop); @@ -183,16 +193,10 @@ public void SkiaSharp() => this.skSurface.Canvas.DrawPath(this.skPath, this.skPaint); [Benchmark] - public IPath ImageSharpStrokeAndClip() - { - return this.isPen.GeneratePath(this.imageSharpPath); - } + public IPath ImageSharpStrokeAndClip() => this.isPen.GeneratePath(this.imageSharpPath); [Benchmark] - public void FillPolygon() - { - this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); - } + public void FillPolygon() => this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); } public class DrawPolygonAll : DrawPolygon diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index be0430b2..ff01fd28 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -12,6 +12,8 @@ CA1822 + + CA1416 diff --git a/tests/ImageSharp.Drawing.Benchmarks/Program.cs b/tests/ImageSharp.Drawing.Benchmarks/Program.cs index 199bdd2b..9822ba4e 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Program.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Program.cs @@ -22,8 +22,14 @@ public InProcessConfig() this.AddExporter(DefaultExporters.Html, DefaultExporters.Csv); - this.AddJob(Job.MediumRun - .WithToolchain(InProcessEmitToolchain.Instance)); + // Use a long, stable job for rasterization benchmarks where scheduler noise and + // thread-pool startup can otherwise dominate short in-process runs. + this.AddJob( + Job.Default + .WithLaunchCount(3) + .WithWarmupCount(15) + .WithIterationCount(40) + .WithToolchain(InProcessEmitToolchain.Instance)); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index 2405dace..82eb9db5 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -314,7 +314,7 @@ public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider image.Mutate(c => { - c.SetRasterizer(TiledRasterizer.Instance); + c.SetRasterizer(DefaultRasterizer.Instance); c.Draw(Color.White, thickness, path); }); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs index 9f5e6fe1..0d49fa0e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs @@ -238,7 +238,7 @@ public void FillPathProcessor_UsesConfiguredRasterizer() [Theory] [InlineData(true)] [InlineData(false)] - public void FillPathProcessor_UsesFixedSubpixelCount(bool antialias) + public void FillPathProcessor_UsesExpectedRasterizationModeAndPixelBoundarySamplingOrigin(bool antialias) { RecordingRasterizer rasterizer = new(); Configuration configuration = new(); @@ -260,14 +260,18 @@ public void FillPathProcessor_UsesFixedSubpixelCount(bool antialias) using Image image = new(configuration, 20, 20); processor.Execute(configuration, image, image.Bounds); - Assert.Equal(FillPathProcessor.FixedRasterizerSubpixelCount, rasterizer.LastSubpixelCount); + RasterizationMode expectedMode = antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + Assert.Equal(expectedMode, rasterizer.LastRasterizationMode); + Assert.Equal(RasterizerSamplingOrigin.PixelBoundary, rasterizer.LastSamplingOrigin); } private sealed class RecordingRasterizer : IRasterizer { public int CallCount { get; private set; } - public int LastSubpixelCount { get; private set; } + public RasterizationMode LastRasterizationMode { get; private set; } + + public RasterizerSamplingOrigin LastSamplingOrigin { get; private set; } public void Rasterize( IPath path, @@ -278,7 +282,8 @@ public void Rasterize( where TState : struct { this.CallCount++; - this.LastSubpixelCount = options.SubpixelCount; + this.LastRasterizationMode = options.RasterizationMode; + this.LastSamplingOrigin = options.SamplingOrigin; } } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TiledRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs similarity index 84% rename from tests/ImageSharp.Drawing.Tests/Shapes/Scan/TiledRasterizerTests.cs rename to tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs index de270faa..382d6fa8 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TiledRasterizerTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; -public class TiledRasterizerTests +public class DefaultRasterizerTests { [Theory] [InlineData(IntersectionRule.EvenOdd)] @@ -32,10 +32,10 @@ public void MatchesDefaultRasterizer_ForLargeSelfIntersectingPath(IntersectionRu .Transform(Matrix3x2.CreateScale(200F)); Rectangle interest = Rectangle.Ceiling(path.Bounds); - RasterizerOptions options = new(interest, 4, rule); + RasterizerOptions options = new(interest, rule); - float[] expected = Rasterize(DefaultRasterizer.Instance, path, options); - float[] actual = Rasterize(TiledRasterizer.Instance, path, options); + float[] expected = Rasterize(ScanlineRasterizer.Instance, path, options); + float[] actual = Rasterize(DefaultRasterizer.Instance, path, options); AssertCoverageEqual(expected, actual); } @@ -47,12 +47,11 @@ public void MatchesDefaultRasterizer_ForPixelCenterSampling() Rectangle interest = Rectangle.Ceiling(path.Bounds); RasterizerOptions options = new( interest, - 1, IntersectionRule.NonZero, - RasterizerSamplingOrigin.PixelCenter); + samplingOrigin: RasterizerSamplingOrigin.PixelCenter); - float[] expected = Rasterize(DefaultRasterizer.Instance, path, options); - float[] actual = Rasterize(TiledRasterizer.Instance, path, options); + float[] expected = Rasterize(ScanlineRasterizer.Instance, path, options); + float[] actual = Rasterize(DefaultRasterizer.Instance, path, options); AssertCoverageEqual(expected, actual); } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCasePolygons.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCasePolygons.cs deleted file mode 100644 index 900b82dc..00000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCasePolygons.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; - -/// -/// See: NumericCornerCases.jpg -/// -internal class NumericCornerCasePolygons -{ - public static readonly Polygon A = PolygonFactory.CreatePolygon( - (2, 2.5f), (11, 2.5f), (11, 3.25f), (8, 3.1f), (5, 3), (2, 3)); - - public static readonly Polygon B = PolygonFactory.CreatePolygon( - (12, 2.5f), (21, 2.5f), (21, 3.2f), (18, 3.125f), (15, 3), (12, 3)); - - public static readonly Polygon C = PolygonFactory.CreatePolygon( - (2, 3.4f), (8, 3.6f), (8, 4), (5, 3.875f), (2, 4)); - - public static readonly Polygon D = PolygonFactory.CreatePolygon( - (12, 3.3f), (18, 3.6f), (18, 4), (15, 3.87f), (12, 4)); - - public static readonly Polygon E = PolygonFactory.CreatePolygon( - (3, 4.4f), (4, 4.75f), (6, 4.6f), (6, 5), (2, 5)); - - public static readonly Polygon F = PolygonFactory.CreatePolygon( - (13, 4.3f), (14, 4.75f), (16, 4.6f), (16, 5), (12, 5)); - - public static readonly Polygon G = PolygonFactory.CreatePolygon((2, 2.25f), (6, 1.87f), (10, 2.25f)); - - public static readonly Polygon H = PolygonFactory.CreatePolygon( - (14, 1.88f), (16, 1.75f), (16, 2.25f), (14, 2.11f)); - - public static Polygon GetByName(string name) => (Polygon)typeof(NumericCornerCasePolygons).GetField(name).GetValue(null); -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/PolygonScannerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/PolygonScannerTests.cs deleted file mode 100644 index 479901c4..00000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/PolygonScannerTests.cs +++ /dev/null @@ -1,615 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Text; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; -using Xunit.Abstractions; - -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; - -public class PolygonScannerTests -{ - private readonly ITestOutputHelper output; - - private static readonly DebugDraw DebugDraw = new(nameof(PolygonScannerTests)); - - public PolygonScannerTests(ITestOutputHelper output) - => this.output = output; - - private void PrintPoints(ReadOnlySpan points) - { - StringBuilder sb = new(); - - foreach (PointF p in points) - { - sb.Append($"({p.X},{p.Y}), "); - } - - this.output.WriteLine(sb.ToString()); - } - - private void PrintPointsX(PointF[] isc) - { - string s = string.Join(",", isc.Select(p => $"{p.X}")); - this.output.WriteLine(s); - } - - private static void VerifyScanline( - ReadOnlySpan expected, - ReadOnlySpan actual, - string scanlineId) - { - if (expected == null) - { - return; - } - - Assert.True( - expected.Length == actual.Length, - $"Scanline had {actual.Length} intersections instead of {expected.Length}: {scanlineId}"); - - for (int i = 0; i < expected.Length; i++) - { - Assert.True(expected[i].Equals(actual[i]), $"Mismatch at scanline {scanlineId}: {expected[i]} != {actual[i]}"); - } - } - - private void TestScan(IPath path, int min, int max, int subsampling, FuzzyFloat[][] expected) => - this.TestScan(path, min, max, subsampling, expected, IntersectionRule.EvenOdd); - - private void TestScan( - IPath path, - int min, - int max, - int subsampling, - FuzzyFloat[][] expected, - IntersectionRule intersectionRule) - { - PolygonScanner scanner = PolygonScanner.Create( - path, - min, - max, - subsampling, - intersectionRule, - Configuration.Default.MemoryAllocator); - - try - { - int counter = 0; - while (scanner.MoveToNextPixelLine()) - { - while (scanner.MoveToNextSubpixelScanLine()) - { - ReadOnlySpan intersections = scanner.ScanCurrentLine(); - VerifyScanline(expected[counter], intersections, $"Y={scanner.SubPixelY} Cnt={counter}"); - - counter++; - } - } - - Assert.Equal(expected.Length, counter + 1); - } - finally - { - scanner.Dispose(); - } - } - - [Fact] - public void BasicConcave00() - { - IPath poly = PolygonFactory.CreatePolygon((2, 2), (5, 3), (5, 6), (8, 6), (8, 9), (5, 11), (2, 7)); - DebugDraw.Polygon(poly, 1f, 50f); - - FuzzyFloat[][] expected = - [ - [2, 2], - [2, 5], - [2, 5], - [2, 5], - [2, 5, 5, 8], - [2, 8], - [2.75f, 8], - [3.5f, 8], - [4.25f, 6.5f], - [5, 5] - ]; - - this.TestScan(poly, 2, 11, 1, expected); - } - - [Fact] - public void BasicConcave01() - { - IPath poly = PolygonFactory.CreatePolygon((0, 0), (10, 10), (20, 0), (20, 20), (0, 20)); - DebugDraw.Polygon(poly); - - FuzzyFloat[][] expected = - [ - [0f, 0f, 20.000000f, 20.000000f], - [0f, 1.0000000f, 19.000000f, 20.000000f], - [0f, 2.0000000f, 18.000000f, 20.000000f], - [0f, 3.0000000f, 17.000000f, 20.000000f], - [0f, 4.0000000f, 16.000000f, 20.000000f], - [0f, 5.0000000f, 15.000000f, 20.000000f], - [0f, 6.0000000f, 14.000000f, 20.000000f], - [0f, 7.0000000f, 13.000000f, 20.000000f], - [0f, 8.0000000f, 12.000000f, 20.000000f], - [0f, 9.0000000f, 11.000000f, 20.000000f], - [0f, 10.000000f, 10.000000f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f], - [0f, 20.000000f] - ]; - - this.TestScan(poly, 0, 20, 1, expected); - } - - [Fact] - public void BasicConcave02() - { - IPath poly = PolygonFactory.CreatePolygon((0, 3), (3, 3), (3, 0), (1, 2), (1, 1), (0, 0)); - DebugDraw.Polygon(poly, 1f, 100f); - - FuzzyFloat[][] expected = - [ - [0f, 0f, 3.0000000f, 3.0000000f], - [0f, 0.50000000f, 2.5000000f, 3.0000000f], - [0f, 1.0000000f, 2.0000000f, 3.0000000f], - [0f, 1.0000000f, 1.5000000f, 3.0000000f], - [0f, 1.0000000f, 1.0000000f, 3.0000000f], - [0f, 3.0000000f], - [0f, 3.0000000f] - ]; - this.TestScan(poly, 0, 3, 2, expected); - } - - [Fact] - public void BasicConcave03() - { - IPath poly = PolygonFactory.CreatePolygon( - (0, 0), - (2, 0), - (3, 1), - (3, 0), - (6, 0), - (6, 2), - (5, 2), - (5, 1), - (4, 1), - (4, 2), - (2, 2), - (1, 1), - (0, 2)); - - DebugDraw.Polygon(poly, 1f, 100f); - - FuzzyFloat[][] expected = - [ - [0f, 2.0000000f, 3.0000000f, 6.0000000f], - [0f, 2.2000000f, 3.0000000f, 6.0000000f], - [0f, 2.4000000f, 3.0000000f, 6.0000000f], - [0f, 2.6000000f, 3.0000000f, 6.0000000f], - [0f, 2.8000000f, 3.0000000f, 6.0000000f], - [ - 0f, 1.0000000f, 1.0000000f, 3.0000000f, 3.0000000f, 4.0000000f, 4.0000000f, 5.0000000f, 5.0000000f, - 6.0000000f - ], - [0f, 0.80000000f, 1.2000000f, 4.0000000f, 5.0000000f, 6.0000000f], - [0f, 0.60000000f, 1.4000000f, 4.0000000f, 5.0000000f, 6.0000000f], - [0f, 0.40000000f, 1.6000000f, 4.0000000f, 5.0000000f, 6.0000000f], - [0f, 0.20000000f, 1.8000000f, 4.0000000f, 5.0000000f, 6.0000000f], - [0f, 0f, 2.0000000f, 4.0000000f, 5.0000000f, 6.0000000f] - ]; - - this.TestScan(poly, 0, 2, 5, expected); - } - - [Fact] - public void SelfIntersecting01() - { - // TODO: This case is not handled intuitively with the current rules - IPath poly = PolygonFactory.CreatePolygon((0, 0), (10, 0), (0, 10), (10, 10)); - DebugDraw.Polygon(poly, 10f, 10f); - - FuzzyFloat[][] expected = - [ - [0f, 10.000000f], - [0.50000000f, 9.5000000f], - [1.0000000f, 9.0000000f], - [1.5000000f, 8.5000000f], - [2.0000000f, 8.0000000f], - [2.5000000f, 7.5000000f], - [3.0000000f, 7.0000000f], - [3.5000000f, 6.5000000f], - [4.0000000f, 6.0000000f], - [4.5000000f, 5.5000000f], - [5.0000000f, 5.0000000f], - [4.5000000f, 5.5000000f], - [4.0000000f, 6.0000000f], - [3.5000000f, 6.5000000f], - [3.0000000f, 7.0000000f], - [2.5000000f, 7.5000000f], - [2.0000000f, 8.0000000f], - [1.5000000f, 8.5000000f], - [1.0000000f, 9.0000000f], - [0.50000000f, 9.5000000f], - [0f, 10.000000f] - ]; - this.TestScan(poly, 0, 10, 2, expected); - } - - [Fact] - public void SelfIntersecting02() - { - IPath poly = PolygonFactory.CreatePolygon((0, 0), (10, 10), (10, 0), (0, 10)); - DebugDraw.Polygon(poly, 10f, 10f); - - FuzzyFloat[][] expected = - [ - [0f, 0f, 10.000000f, 10.000000f], - [0f, 0.50000000f, 9.5000000f, 10.000000f], - [0f, 1.0000000f, 9.0000000f, 10.000000f], - [0f, 1.5000000f, 8.5000000f, 10.000000f], - [0f, 2.0000000f, 8.0000000f, 10.000000f], - [0f, 2.5000000f, 7.5000000f, 10.000000f], - [0f, 3.0000000f, 7.0000000f, 10.000000f], - [0f, 3.5000000f, 6.5000000f, 10.000000f], - [0f, 4.0000000f, 6.0000000f, 10.000000f], - [0f, 4.5000000f, 5.5000000f, 10.000000f], - [0f, 5.0000000f, 5.0000000f, 10.000000f], - [0f, 4.5000000f, 5.5000000f, 10.000000f], - [0f, 4.0000000f, 6.0000000f, 10.000000f], - [0f, 3.5000000f, 6.5000000f, 10.000000f], - [0f, 3.0000000f, 7.0000000f, 10.000000f], - [0f, 2.5000000f, 7.5000000f, 10.000000f], - [0f, 2.0000000f, 8.0000000f, 10.000000f], - [0f, 1.5000000f, 8.5000000f, 10.000000f], - [0f, 1.0000000f, 9.0000000f, 10.000000f], - [0f, 0.50000000f, 9.5000000f, 10.000000f], - [0f, 0f, 10.000000f, 10.000000f] - ]; - this.TestScan(poly, 0, 10, 2, expected); - } - - [Theory] - [InlineData(IntersectionRule.EvenOdd)] - [InlineData(IntersectionRule.NonZero)] - public void SelfIntersecting03(IntersectionRule rule) - { - IPath poly = PolygonFactory.CreatePolygon( - (1, 3), - (1, 2), - (5, 2), - (5, 5), - (2, 5), - (2, 1), - (3, 1), - (3, 4), - (4, 4), - (4, 3), - (1, 3)); - - DebugDraw.Polygon(poly, 1f, 100f); - - FuzzyFloat[][] expected; - if (rule == IntersectionRule.EvenOdd) - { - expected = - [ - [2.0000000f, 3.0000000f], - [2.0000000f, 3.0000000f], - [1.0000000f, 2.0000000f, 3.0000000f, 5.0000000f], - [1.0000000f, 2.0000000f, 3.0000000f, 5.0000000f], - [1.0000000f, 2.0000000f, 3.0000000f, 4.0000000f, 4.0000000f, 5.0000000f], - [2.0000000f, 3.0000000f, 4.0000000f, 5.0000000f], - [2.0000000f, 3.0000000f, 3.0000000f, 4.0000000f, 4.0000000f, 5.0000000f], - [2.0000000f, 5.0000000f], - [2.0000000f, 5.0000000f] - ]; - } - else - { - expected = - [ - [2.0000000f, 3.0000000f], - [2.0000000f, 3.0000000f], - [1.0000000f, 5.0000000f], - [1.0000000f, 5.0000000f], - [1.0000000f, 4.0000000f, 4.0000000f, 5.0000000f], - [2.0000000f, 3.0000000f, 4.0000000f, 5.0000000f], - [2.0000000f, 3.0000000f, 3.0000000f, 4.0000000f, 4.0000000f, 5.0000000f], - [2.0000000f, 5.0000000f], - [2.0000000f, 5.0000000f] - ]; - } - - this.TestScan(poly, 1, 5, 2, expected, rule); - } - - [Theory] - [InlineData(IntersectionRule.EvenOdd)] - [InlineData(IntersectionRule.NonZero)] - public void SelfIntersecting04(IntersectionRule rule) - { - IPath poly = PolygonFactory.CreatePolygon( - (1, 4), - (1, 3), - (3, 3), - (3, 2), - (2, 2), - (2, 4), - (1, 4), - (1, 1), - (4, 1), - (4, 4), - (3, 4), - (3, 5), - (2, 5), - (2, 4), - (1, 4)); - - DebugDraw.Polygon(poly, 1f, 100f); - - FuzzyFloat[][] expected; - if (rule == IntersectionRule.EvenOdd) - { - expected = - [ - [1, 4], - [1, 4], - [1, 2, 2, 3, 3, 4], - [1, 2, 3, 4], - [1, 1, 2, 3, 3, 4], - [1, 1, 2, 4], - [1, 1, 2, 2, 2, 3, 3, 4], - [2, 3], - [2, 3] - ]; - } - else - { - expected = - [ - [1, 4], - [1, 4], - [1, 2, 2, 3, 3, 4], - [1, 2, 3, 4], - [1, 3, 3, 4], - [1, 4], - [1, 2, 2, 3, 3, 4], - [2, 3], - [2, 3] - ]; - } - - this.TestScan(poly, 1, 5, 2, expected, rule); - } - - [Theory] - [InlineData(IntersectionRule.EvenOdd)] - [InlineData(IntersectionRule.NonZero)] - public void NegativeOrientation01(IntersectionRule intersectionRule) - { - // IPath poly = PolygonFactory.CreatePolygon((0, 0), (0, 2), (2, 2), (2, 0)); - PointF[] interest = PolygonFactory.CreatePointArray((0, 0), (0, 2), (2, 2), (2, 0)); - - // Adding a dummy ring outside the area of interest, so the actual loop is not oriented positively - PointF[] dummy = PolygonFactory.CreatePointArray((0, 10), (10, 10), (0, 11)); - - ComplexPolygon poly = new( - new Polygon(new LinearLineSegment(interest)), - new Polygon(new LinearLineSegment(dummy))); - - FuzzyFloat[][] expected = - [ - [0, 0, 2, 2], - [0, 2], - [0, 2], - [0, 2], - [0, 0, 2, 2] - ]; - - this.TestScan(poly, 0, 2, 2, expected, intersectionRule); - } - - [Fact] - public void OutOfBounds1() - { - IPath poly = PolygonFactory.CreatePolygon((1, -5), (5, -5), (5, -3), (10, -1), (10, 2), (12, 4), (1, 4)); - - FuzzyFloat[][] expected = - [ - [1, 10], - [1, 10], - [1, 10], - [1, 10], - [1, 10], - [1, 10.5], - [1, 11] - ]; - - this.TestScan(poly, 0, 3, 2, expected); - } - - [Fact] - public void OutOfBounds2() - { - IPath poly = PolygonFactory.CreatePolygon((3, -3), (3, 1), (1, 1), (1, -1), (2, -1.5f), (2, 0.5f), (3, -3)); - FuzzyFloat[][] expected = - [ - [1, 2, 2.14285707, 3], - [1, 2, 2, 3], - [1, 3] - ]; - - this.TestScan(poly, 0, 1, 2, expected); - } - - [Fact] - public void AllOutOfBounds() - { - IPath poly = PolygonFactory.CreatePolygon((1, -3), (3, -3), (2, -1)); - FuzzyFloat[][] expected = - [ - [], - [], - [] - ]; - - this.TestScan(poly, 0, 1, 2, expected); - } - - private static (float Y, FuzzyFloat[] X) Empty(float y) => (y, []); - - private static FuzzyFloat F(float x, float eps) => new(x, eps); - - public static readonly TheoryData NumericCornerCasesData = - new() - { - { - "A", [ - Empty(2f), Empty(2.25f), - - (2.5f, [2, 11]), - (2.75f, [2, 11]), - (3f, [2, 8, 8, 11]), - (3.25f, [11, 11]), - - Empty(3.5f), Empty(3.75f), Empty(4f) - ] - }, - { - "B", [ - Empty(2f), Empty(2.25f), - - (2.5f, [12, 21]), - (2.75f, [12, 21]), - (3f, [12, 15, 15, 21]), - (3.25f, [18, 21]), - - Empty(3.5f), Empty(3.75f), Empty(4f) - ] - }, - { - "C", [ - Empty(3f), Empty(3.25f), - - (3.5f, [2, 8]), - (3.75f, [2, 8]), - (4f, [2, 8]) - ] - }, - { - "D", [ - Empty(3f), - - (3.25f, [12, 12]), - (3.5f, [12, 18]), - (3.75f, [12, 15, 15, 18]), - (4f, [12, 12, 18, 18]) - ] - }, - { - "E", [ - Empty(4f), Empty(4.25f), - - (4.5f, [3, 3, 6, 6]), - (4.75f, [F(2.4166667f, 0.5f), 4, 4, 6]), - (5f, [2, 6]) - ] - }, - { - "F", [ - Empty(4f), - - // Eps = 0.01 to address inaccuracies on .NET Framework - (4.25f, [F(13, 0.01f), F(13, 0.01f)]), - (4.5f, [F(12.714286f, 0.5f), F(13.444444f, 0.5f), 16, 16]), - (4.75f, [F(12.357143f, 0.5f), 14, 14, 16]), - (5f, [12, 16]) - ] - }, - { - "G", [ - Empty(1f), Empty(1.25f), Empty(1.5f), - - (1.75f, [6, 6]), - (2f, [F(4.6315789f, 1f), F(7.3684211f, 1f)]), - (2.25f, [2, 10]), - - Empty(2.5f), Empty(1.75f), Empty(3f) - ] - }, - { - "H", [ - Empty(1f), Empty(1.25f), Empty(1.5f), - - (1.75f, [16, 16]), - (2f, [14, 14, 14, 16]), // this emits 2 dummy points, but normally it should not corrupt quality too much - (2.25f, [16, 16]), - - Empty(2.5f), Empty(1.75f), Empty(3f) - ] - } - }; - - [Theory] - [MemberData(nameof(NumericCornerCasesData))] - public void NumericCornerCases(string name, (float Y, FuzzyFloat[] X)[] expectedIntersections) - { - Polygon poly = NumericCornerCasePolygons.GetByName(name); - DebugDraw.Polygon(poly, 0.25f, 100f, $"{nameof(this.NumericCornerCases)}_{name}"); - - int min = (int)expectedIntersections.First().Y; - int max = (int)expectedIntersections.Last().Y; - - this.TestScan(poly, min, max, 4, expectedIntersections.Select(i => i.X).ToArray()); - } - - public static TheoryData NumericCornerCases_Offset_Data() - { - TheoryData result = new(); - - float[] offsets = [1e3f, 1e4f, 1e5f]; - - foreach (float offset in offsets) - { - foreach (object[] data in NumericCornerCasesData) - { - result.Add(offset, (string)data[0], ((float Y, FuzzyFloat[] X)[])data[1]); - } - } - - return result; - } - - [Theory] - [MemberData(nameof(NumericCornerCases_Offset_Data))] - public void NumericCornerCases_Offset(float offset, string name, (float Y, FuzzyFloat[] X)[] expectedIntersections) - { - float dx = offset; - float dy = offset; - - IPath poly = NumericCornerCasePolygons.GetByName(name).Transform(Matrix3x2.CreateTranslation(dx, dy)); - expectedIntersections = TranslateIntersections(expectedIntersections, dx, dy); - - int min = (int)expectedIntersections.First().Y; - int max = (int)expectedIntersections.Last().Y; - - this.TestScan(poly, min, max, 4, expectedIntersections.Select(i => i.X).ToArray()); - } - - private static (float Y, FuzzyFloat[] X)[] TranslateIntersections( - (float Y, FuzzyFloat[] X)[] ex, float dx, float dy) - => ex.Select(e => (e.Y + dy, e.X.Select(xx => xx + dx).ToArray())).ToArray(); -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/RasterizerExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/RasterizerExtensionsTests.cs deleted file mode 100644 index f05412e6..00000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/RasterizerExtensionsTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; - -public class RasterizerExtensionsTests -{ - [Fact] - public void DoesNotOverwriteIsDirtyFlagWhenOnlyFillingSubpixels() - { - PolygonScanner scanner = PolygonScanner.Create(new RectangularPolygon(0.3f, 0.2f, 0.7f, 1.423f), 0, 20, 1, IntersectionRule.EvenOdd, MemoryAllocator.Default); - - float[] buffer = new float[12]; - - scanner.MoveToNextPixelLine(); // offset - - bool isDirty = scanner.ScanCurrentPixelLineInto(0, 0, buffer.AsSpan()); - - Assert.True(isDirty); - } - - [Theory] - [WithSolidFilledImages(400, 75, "White", PixelTypes.Rgba32)] - public void AntialiasingIsAntialiased(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font36 = TestFontUtilities.GetFont(TestFonts.OpenSans, 20); - RichTextOptions textOpt = new(font36) - { - Dpi = 96, - Origin = new PointF(0, 0) - }; - - ImageComparer comparer = ImageComparer.TolerantPercentage(0.001f); - provider.RunValidatingProcessorTest( - x => x - .SetGraphicsOptions(o => o.Antialias = false) - .DrawText(textOpt, "Hello, World!", Color.Black), - comparer: comparer); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs deleted file mode 100644 index 0a1dbd4d..00000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/ScanEdgeCollectionTests.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; - -public class ScanEdgeCollectionTests -{ - private static MemoryAllocator MemoryAllocator => Configuration.Default.MemoryAllocator; - - private static readonly DebugDraw DebugDraw = new(nameof(ScanEdgeCollectionTests)); - - private static void VerifyEdge( - ScanEdgeCollection edges, - float y0, - float y1, - (FuzzyFloat X, FuzzyFloat Y) arbitraryPoint, - int emit0, - int emit1, - bool edgeUp) - { - foreach (ScanEdge e in edges.Edges) - { - if (y0 == e.Y0 && y1 == e.Y1) - { - bool containsPoint = arbitraryPoint.X.Equals(e.GetX(arbitraryPoint.Y)); - if (containsPoint) - { - Assert.Equal(emit0, e.EmitV0); - Assert.Equal(emit1, e.EmitV1); - Assert.Equal(edgeUp, e.EdgeUp); - - // Found the edge - return; - } - } - } - - Assert.True(false, $"Failed to find edge {y0}->{y1} with {arbitraryPoint}"); - } - - [Fact] - [ValidateDisposedMemoryAllocations] - public void SimplePolygon_AllEmitCases() - { - static void RunTest() - { - // see: SimplePolygon_AllEmitCases.png - Polygon polygon = PolygonFactory.CreatePolygon( - (1, 2), - (2, 2), - (3, 1), - (4, 3), - (6, 1), - (7, 2), - (8, 2), - (9, 3), - (9, 4), - (10, 5), - (9, 6), - (8, 6), - (8, 7), - (9, 7), - (9, 8), - (7, 8), - (6, 7), - (5, 8), - (4, 7), - (3, 8), - (2, 8), - (2, 6), - (3, 5), - (2, 5), - (2, 4), - (1, 3)); - - DebugDraw.Polygon(polygon, 1, 100); - - using ScanEdgeCollection edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16); - - Assert.Equal(19, edges.Edges.Length); - - VerifyEdge(edges, 1f, 2f, (2.5f, 1.5f), 1, 2, true); - VerifyEdge(edges, 1f, 3f, (3.5f, 2f), 1, 1, false); - VerifyEdge(edges, 1f, 3f, (5f, 2f), 1, 1, true); - VerifyEdge(edges, 1f, 2f, (6.5f, 1.5f), 1, 2, false); - VerifyEdge(edges, 2f, 3f, (8.5f, 2.5f), 1, 0, false); - VerifyEdge(edges, 3f, 4f, (9f, 3.5f), 1, 0, false); - VerifyEdge(edges, 4f, 5f, (9.5f, 4.5f), 1, 0, false); - VerifyEdge(edges, 5f, 6f, (9.5f, 5.5f), 1, 1, false); - VerifyEdge(edges, 6f, 7f, (8f, 6.5f), 2, 2, false); - VerifyEdge(edges, 7f, 8f, (9f, 7.5f), 1, 1, false); - VerifyEdge(edges, 7f, 8f, (6.5f, 7.5f), 1, 1, true); - VerifyEdge(edges, 7f, 8f, (5.5f, 7.5f), 1, 1, false); - VerifyEdge(edges, 7f, 8f, (4.5f, 7.5f), 1, 1, true); - VerifyEdge(edges, 7f, 8f, (3.5f, 7.5f), 1, 1, false); - VerifyEdge(edges, 6f, 8f, (2f, 7f), 0, 1, true); - VerifyEdge(edges, 5f, 6f, (2.5f, 5.5f), 2, 1, true); - VerifyEdge(edges, 4f, 5f, (2f, 4.5f), 0, 1, true); - VerifyEdge(edges, 3f, 4f, (1.5f, 3.5f), 0, 1, true); - VerifyEdge(edges, 2f, 3f, (1f, 1.5f), 1, 1, true); - } - - FeatureTestRunner.RunWithHwIntrinsicsFeature(RunTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSSE41 | HwIntrinsics.DisableArm64AdvSimd); - } - - [Fact] - public void ComplexPolygon() - { - Polygon contour = PolygonFactory.CreatePolygon( - (1, 1), (4, 1), (4, 2), (5, 2), (5, 5), (2, 5), (2, 4), (1, 4), (1, 1)); - Polygon hole = PolygonFactory.CreatePolygon( - (2, 2), (2, 3), (3, 3), (3, 4), (4, 4), (4, 3), (3, 2)); - - IPath polygon = contour.Clip(hole); - DebugDraw.Polygon(polygon, 1, 100); - - using ScanEdgeCollection edges = ScanEdgeCollection.Create(polygon, MemoryAllocator, 16); - - Assert.Equal(8, edges.Count); - - VerifyEdge(edges, 1, 4, (1, 2), 1, 1, true); - VerifyEdge(edges, 1, 2, (4, 1.5f), 1, 2, false); - VerifyEdge(edges, 4, 5, (2, 4.5f), 2, 1, true); - VerifyEdge(edges, 2, 5, (5, 3f), 1, 1, false); - - VerifyEdge(edges, 2, 3, (2, 2.5f), 2, 2, false); - VerifyEdge(edges, 2, 3, (3.5f, 2.5f), 2, 1, true); - VerifyEdge(edges, 3, 4, (3, 3.5f), 1, 2, false); - VerifyEdge(edges, 3, 4, (4, 3.5f), 0, 2, true); - } - - [Fact] - public void NumericCornerCase_C() - { - using ScanEdgeCollection edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.C, MemoryAllocator, 4); - Assert.Equal(2, edges.Count); - VerifyEdge(edges, 3.5f, 4f, (2f, 3.75f), 1, 1, true); - VerifyEdge(edges, 3.5f, 4f, (8f, 3.75f), 1, 1, false); - } - - [Fact] - public void NumericCornerCase_D() - { - using ScanEdgeCollection edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.D, MemoryAllocator, 4); - Assert.Equal(5, edges.Count); - - VerifyEdge(edges, 3.25f, 4f, (12f, 3.75f), 1, 1, true); - VerifyEdge(edges, 3.25f, 3.5f, (15f, 3.375f), 1, 0, false); - VerifyEdge(edges, 3.5f, 4f, (18f, 3.75f), 1, 1, false); - - // TODO: verify 2 more edges - } - - [Fact] - public void NumericCornerCase_H_ShouldCollapseNearZeroEdge() - { - using ScanEdgeCollection edges = ScanEdgeCollection.Create(NumericCornerCasePolygons.H, MemoryAllocator, 4); - - Assert.Equal(3, edges.Count); - VerifyEdge(edges, 1.75f, 2f, (15f, 1.875f), 1, 1, true); - VerifyEdge(edges, 1.75f, 2.25f, (16f, 2f), 1, 1, false); - - // this places two dummy points: - VerifyEdge(edges, 2f, 2.25f, (15f, 2.125f), 2, 1, true); - } - - private static FuzzyFloat F(float value, float eps) => new(value, eps); -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs new file mode 100644 index 00000000..19842362 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; + +namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; + +public class SharpBlazeRasterizerTests +{ + [Fact] + public void EmitsCoverageForSubpixelThinRectangle() + { + RectangularPolygon path = new(0.3F, 0.2F, 0.7F, 1.423F); + RasterizerOptions options = new(new Rectangle(0, 0, 12, 20), IntersectionRule.EvenOdd); + CaptureState state = new(new float[options.Interest.Width * options.Interest.Height], options.Interest.Width, options.Interest.Top); + + DefaultRasterizer.Instance.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + + Assert.True(state.DirtyRows > 0); + Assert.True(state.MaxCoverage > 0F); + } + + [Fact] + public void RasterizesFractionalRectangleCoverageDeterministically() + { + RectangularPolygon path = new(0.25F, 0.25F, 1F, 1F); + RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero); + + float[] coverage = Rasterize(DefaultRasterizer.Instance, path, options); + float[] expected = + [ + 0.5625F, 0.1875F, + 0.1875F, 0.0625F + ]; + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], coverage[i], 3); + } + } + + [Fact] + public void AliasedMode_EmitsBinaryCoverage() + { + RectangularPolygon path = new(0.25F, 0.25F, 1F, 1F); + RasterizerOptions options = new(new Rectangle(0, 0, 2, 2), IntersectionRule.NonZero, RasterizationMode.Aliased); + + float[] coverage = Rasterize(DefaultRasterizer.Instance, path, options); + float[] expected = + [ + 1F, 0F, + 0F, 0F + ]; + + Assert.Equal(expected, coverage); + } + + [Fact] + public void ThrowsForInterestTooWideForCoverStrideMath() + { + RectangularPolygon path = new(0F, 0F, 1F, 1F); + RasterizerOptions options = new(new Rectangle(0, 0, (int.MaxValue / 2) + 1, 1), IntersectionRule.NonZero); + NoopState state = default; + + void Rasterize() => + DefaultRasterizer.Instance.Rasterize( + path, + options, + Configuration.Default.MemoryAllocator, + ref state, + static (int y, Span scanline, ref NoopState localState) => { }); + + ImageProcessingException exception = Assert.Throws(Rasterize); + Assert.Contains("too large", exception.Message); + } + + private static float[] Rasterize(IRasterizer rasterizer, IPath path, in RasterizerOptions options) + { + int width = options.Interest.Width; + int height = options.Interest.Height; + float[] coverage = new float[width * height]; + CaptureState state = new(coverage, width, options.Interest.Top); + + rasterizer.Rasterize(path, options, Configuration.Default.MemoryAllocator, ref state, CaptureScanline); + return coverage; + } + + private static void CaptureScanline(int y, Span scanline, ref CaptureState state) + { + int row = y - state.Top; + scanline.CopyTo(state.Coverage.AsSpan(row * state.Width, state.Width)); + state.DirtyRows++; + + for (int i = 0; i < scanline.Length; i++) + { + if (scanline[i] > state.MaxCoverage) + { + state.MaxCoverage = scanline[i]; + } + } + } + + private struct CaptureState + { + public CaptureState(float[] coverage, int width, int top) + { + this.Coverage = coverage; + this.Width = width; + this.Top = top; + this.DirtyRows = 0; + this.MaxCoverage = 0F; + } + + public float[] Coverage { get; } + + public int Width { get; } + + public int Top { get; } + + public int DirtyRows { get; set; } + + public float MaxCoverage { get; set; } + } + + private struct NoopState + { + } +} From 4df8f1aa2a74da3cc1dc3d7d6ed2a0aa2b33f65e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 21:31:29 +1000 Subject: [PATCH 25/35] Document --- .../Shapes/Rasterization/DefaultRasterizer.cs | 344 +---- .../Shapes/Rasterization/PolygonScanner.cs | 1117 ++++++++++++++++- .../Shapes/Rasterization/PolygonScanning.MD | 66 +- .../Rasterization/ScanlineRasterizer.cs | 2 +- .../Drawing/DrawPolygon.cs | 8 +- 5 files changed, 1117 insertions(+), 420 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs index f650e798..2544ff5d 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs @@ -1,40 +1,20 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; /// -/// Default CPU rasterizer that processes large paths in parallel vertical bands. +/// Default CPU rasterizer. /// /// -/// The algorithm preserves the public scanline callback contract (top-to-bottom emission) while -/// parallelizing internal work: -/// 1. Partition the interest rectangle into Y-bands. -/// 2. Rasterize each band independently into temporary coverage buffers. -/// 3. Emit bands back in deterministic top-to-bottom order. -/// -/// This design avoids concurrent writes to destination pixels and keeps per-band work isolated. -/// It also lets the implementation fall back to the single-pass scanner when tiling would not pay -/// off (small workloads, huge temporary buffers, or low core counts). +/// This rasterizer delegates to , which performs fixed-point +/// area/cover scanning and chooses an internal execution strategy (parallel row-tiles when +/// profitable, sequential fallback otherwise). /// internal sealed class DefaultRasterizer : IRasterizer { - // Keep bands reasonably tall so the overhead of per-band setup does not dominate tiny draws. - private const int MinimumBandHeight = 96; - - // Require a minimum pixel workload per band so thread scheduling overhead stays amortized. - private const int MinimumPixelsPerBand = 196608; - - // Hard cap on buffered pixels across all bands for a single rasterization invocation. - // One float is buffered per pixel plus a dirty-row byte map per band. - private const int MaximumBufferedPixels = 16777216; // 4096 x 4096 - - // Bounding band count limits task fan-out and keeps allocator pressure predictable. - private const int MaximumBandCount = 8; - /// /// Gets the singleton default rasterizer instance. /// @@ -49,7 +29,6 @@ public void Rasterize( RasterizerScanlineHandler scanlineHandler) where TState : struct { - // Fast argument validation at entry keeps failure behavior consistent with other rasterizers. Guard.NotNull(path, nameof(path)); Guard.NotNull(allocator, nameof(allocator)); Guard.NotNull(scanlineHandler, nameof(scanlineHandler)); @@ -57,322 +36,9 @@ public void Rasterize( Rectangle interest = options.Interest; if (interest.Equals(Rectangle.Empty)) { - // Nothing intersects the destination; skip all work. - return; - } - - if (!TryCreateBandPlan(interest, out Band[]? plannedBands) || plannedBands is null) - { - // For small or extreme workloads, single-pass rasterization is cheaper and avoids - // temporary band buffers. - ScanlineRasterizer.Instance.Rasterize(path, options, allocator, ref state, scanlineHandler); return; } - Band[] bands = plannedBands; - RasterizerOptions bandedOptions = options; - - // Prime lazy path state once on the caller thread to avoid N workers racing to - // materialize the same internal path structures. - PrimePathState(path); - - try - { - // Limit parallelism to planned band count. This keeps work partition deterministic - // and avoids oversubscribing worker threads for this operation. - ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = bands.Length }; - _ = Parallel.For( - 0, - bands.Length, - parallelOptions, - i => RasterizeBand(path, bandedOptions, allocator, bands[i])); - - // Emit in deterministic order so downstream compositing observes stable scanline order. - EmitBands(bands, interest.Width, ref state, scanlineHandler); - } - finally - { - foreach (Band band in bands) - { - band.Dispose(); - } - } - } - - /// - /// Forces lazy path materialization before worker threads start. - /// - /// The source path. - private static void PrimePathState(IPath path) - { - if (path is IInternalPathOwner owner) - { - // Force ring extraction once for paths that expose internal rings. This is the - // hot path for ComplexPolygon and avoids repeated per-band conversion cost. - _ = owner.GetRingsAsInternalPath().Count; - return; - } - - // Fallback for generic paths: force flattening once so lazy point arrays are available - // before worker threads begin. - foreach (ISimplePath simplePath in path.Flatten()) - { - _ = simplePath.Points.Length; - } - } - - /// - /// Computes a band partitioning plan for the destination rectangle. - /// - /// Destination interest rectangle. - /// - /// When this method returns , contains the planned rasterization bands. - /// - /// - /// when banding should be used; otherwise . - /// - private static bool TryCreateBandPlan(Rectangle interest, out Band[]? bands) - { - bands = null; - - int width = interest.Width; - int height = interest.Height; - long totalPixels = (long)width * height; - if (totalPixels > MaximumBufferedPixels) - { - // Refuse banding for extremely large interests to cap temporary memory use. - return false; - } - - int processorCount = Environment.ProcessorCount; - if (processorCount < 2 || height < (MinimumBandHeight * 2) || totalPixels < (MinimumPixelsPerBand * 2L)) - { - // Not enough parallel work: prefer single-pass path. - return false; - } - - // Bound candidate band count by three limits: - // - image height (minimum band height), - // - total pixels (minimum pixels per band), - // - hardware + hard cap. - int byHeight = height / MinimumBandHeight; - int byPixels = (int)(totalPixels / MinimumPixelsPerBand); - int bandCount = Math.Min(MaximumBandCount, Math.Min(processorCount, Math.Min(byHeight, byPixels))); - if (bandCount < 2) - { - return false; - } - - bands = new Band[bandCount]; - int baseHeight = height / bandCount; - int remainder = height % bandCount; - int y = interest.Top; - - for (int i = 0; i < bandCount; i++) - { - // Distribute remainder rows to the earliest bands to keep shapes balanced. - int bandHeight = baseHeight + (i < remainder ? 1 : 0); - bands[i] = new Band(y, bandHeight); - y += bandHeight; - } - - return true; - } - - /// - /// Rasterizes a single band using the fallback scanline rasterizer into temporary buffers. - /// - /// Path to rasterize. - /// Rasterization options. - /// Memory allocator. - /// The destination band to populate. - private static void RasterizeBand( - IPath path, - in RasterizerOptions options, - MemoryAllocator allocator, - Band band) - { - // Band-local buffers keep writes private to the worker and avoid shared state. - // coverageLength is width * bandHeight and is bounded by band planning constraints. - int width = options.Interest.Width; - int coverageLength = checked(width * band.Height); - - IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); - IMemoryOwner dirtyRowsOwner = allocator.Allocate(band.Height, AllocationOptions.Clean); - - try - { - RasterizerOptions bandOptions = options.WithInterest( - new Rectangle(options.Interest.Left, band.Top, width, band.Height)); - - // Capture state collects scanline output from the fallback scanner into local buffers. - BandCaptureState captureState = new(band.Top, width, coverageOwner.Memory, dirtyRowsOwner.Memory); - ScanlineRasterizer.Instance.Rasterize(path, bandOptions, allocator, ref captureState, CaptureBandScanline); - - band.SetBuffers(coverageOwner, dirtyRowsOwner); - } - catch - { - coverageOwner.Dispose(); - dirtyRowsOwner.Dispose(); - throw; - } - } - - /// - /// Emits all buffered bands in top-to-bottom scanline order. - /// - /// The rasterization callback state type. - /// Bands containing buffered coverage. - /// Width of each scanline. - /// Mutable callback state. - /// Scanline callback. - private static void EmitBands( - Band[] bands, - int scanlineWidth, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - // Serialize final emission in band order so callback consumers receive stable rows. - foreach (Band band in bands) - { - if (band.CoverageOwner is null || band.DirtyRowsOwner is null) - { - continue; - } - - Span coverage = band.CoverageOwner.Memory.Span; - Span dirtyRows = band.DirtyRowsOwner.Memory.Span; - - for (int row = 0; row < band.Height; row++) - { - if (dirtyRows[row] == 0) - { - // Sparse rows are skipped to avoid unnecessary callback invocations. - continue; - } - - Span scanline = coverage.Slice(row * scanlineWidth, scanlineWidth); - scanlineHandler(band.Top + row, scanline, ref state); - } - } - } - - /// - /// Captures one scanline from the fallback scanner into band-local storage. - /// - /// Absolute destination Y. - /// Coverage values for the row. - /// Band capture state. - private static void CaptureBandScanline(int y, Span scanline, ref BandCaptureState state) - { - // The fallback scanner writes one row at a time; copy into contiguous band storage. - int row = y - state.Top; - Span coverage = state.Coverage.Span; - scanline.CopyTo(coverage.Slice(row * state.Width, state.Width)); - state.DirtyRows.Span[row] = 1; - } - - /// - /// Mutable capture state used while rasterizing a single band. - /// - private readonly struct BandCaptureState - { - /// - /// Initializes a new instance of the struct. - /// - /// Top-most Y of the target band. - /// Scanline width for the band. - /// Contiguous storage for band coverage rows. - /// Row activity map for sparse emission. - public BandCaptureState(int top, int width, Memory coverage, Memory dirtyRows) - { - this.Top = top; - this.Width = width; - this.Coverage = coverage; - this.DirtyRows = dirtyRows; - } - - /// - /// Gets the top-most destination Y of the band. - /// - public int Top { get; } - - /// - /// Gets the number of pixels in each band row. - /// - public int Width { get; } - - /// - /// Gets contiguous per-row coverage storage for this band. - /// - public Memory Coverage { get; } - - /// - /// Gets the row activity map where non-zero indicates row data is present. - /// - public Memory DirtyRows { get; } - } - - /// - /// Owns temporary buffers and metadata for a single planned band. - /// - private sealed class Band : IDisposable - { - /// - /// Initializes a new instance of the class. - /// - /// Top-most destination Y for the band. - /// Number of rows in the band. - public Band(int top, int height) - { - this.Top = top; - this.Height = height; - } - - /// - /// Gets the top-most destination Y for this band. - /// - public int Top { get; } - - /// - /// Gets the band height in rows. - /// - public int Height { get; } - - /// - /// Gets the owner of the coverage buffer for this band. - /// - public IMemoryOwner? CoverageOwner { get; private set; } - - /// - /// Gets the owner of the dirty-row map buffer for this band. - /// - public IMemoryOwner? DirtyRowsOwner { get; private set; } - - /// - /// Assigns buffer ownership to this band instance. - /// - /// Coverage buffer owner. - /// Dirty-row buffer owner. - public void SetBuffers(IMemoryOwner coverageOwner, IMemoryOwner dirtyRowsOwner) - { - // Ownership is transferred to the band container and released in Dispose(). - this.CoverageOwner = coverageOwner; - this.DirtyRowsOwner = dirtyRowsOwner; - } - - /// - /// Disposes all band-owned buffers. - /// - public void Dispose() - { - // Always release pooled buffers even if rasterization fails in other bands. - this.CoverageOwner?.Dispose(); - this.DirtyRowsOwner?.Dispose(); - this.CoverageOwner = null; - this.DirtyRowsOwner = null; - } + PolygonScanner.Rasterize(path, options, allocator, ref state, scanlineHandler); } } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs index 29c6d2fc..2f821456 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -9,14 +9,30 @@ namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; /// -/// Fixed-point polygon scanner that converts path segments into per-row coverage runs. +/// Fixed-point polygon scanner that converts polygon edges into per-row coverage runs. /// +/// +/// The scanner has two execution modes: +/// 1. Parallel tiled execution (default): build an edge table once, bucket edges by tile rows, +/// rasterize tiles in parallel with worker-local scratch, then emit in deterministic Y order. +/// 2. Sequential execution: reuse the same edge table and process band buckets on one thread. +/// +/// Both modes share the same coverage math and fill-rule handling, ensuring predictable output +/// regardless of scheduling strategy. +/// internal static class PolygonScanner { // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows). // Keeping this bounded prevents pathological full-image allocations on very large interests. private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L; + // Blaze-style tile height used by the parallel row-tiling pipeline. + private const int DefaultTileHeight = 16; + + // Cap for buffered output coverage in the parallel path. We buffer one float per destination + // pixel plus one dirty-row byte per tile row before deterministic ordered emission. + private const long ParallelOutputPixelBudget = 16L * 1024L * 1024L; // 4096 x 4096 + private const int FixedShift = 8; private const int FixedOne = 1 << FixedShift; private static readonly int WordBitCount = nint.Size * 8; @@ -26,6 +42,15 @@ internal static class PolygonScanner private const int EvenOddPeriod = CoverageStepCount * 2; private const float CoverageScale = 1F / CoverageStepCount; + /// + /// Rasterizes the path using the default execution policy. + /// + /// The caller-owned mutable state type. + /// Path to rasterize. + /// Rasterization options. + /// Temporary buffer allocator. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. public static void Rasterize( IPath path, in RasterizerOptions options, @@ -33,6 +58,46 @@ public static void Rasterize( ref TState state, RasterizerScanlineHandler scanlineHandler) where TState : struct + => RasterizeCore(path, options, allocator, ref state, scanlineHandler, allowParallel: true); + + /// + /// Rasterizes the path using the forced sequential policy. + /// + /// The caller-owned mutable state type. + /// Path to rasterize. + /// Rasterization options. + /// Temporary buffer allocator. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. + public static void RasterizeSequential( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + => RasterizeCore(path, options, allocator, ref state, scanlineHandler, allowParallel: false); + + /// + /// Shared entry point used by both public execution policies. + /// + /// The caller-owned mutable state type. + /// Path to rasterize. + /// Rasterization options. + /// Temporary buffer allocator. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. + /// + /// If , the scanner may use parallel tiled execution when profitable. + /// + private static void RasterizeCore( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler, + bool allowParallel) + where TState : struct { Rectangle interest = options.Interest; int width = interest.Width; @@ -43,80 +108,564 @@ public static void Rasterize( } int wordsPerRow = BitVectorsForMaxBitCount(width); + int maxBandRows = 0; long coverStride = (long)width * 2; if (coverStride > int.MaxValue || - !TryGetBandHeight(width, height, wordsPerRow, coverStride, out int maxBandRows)) + !TryGetBandHeight(width, height, wordsPerRow, coverStride, out maxBandRows)) { - throw new ImageProcessingException("The rasterizer interest bounds are too large for PolygonScanner buffers."); + ThrowInterestBoundsTooLarge(); } int coverStrideInt = (int)coverStride; - int bitVectorCapacity = checked(wordsPerRow * maxBandRows); - int coverAreaCapacity = checked(coverStrideInt * maxBandRows); - using IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); - using IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); - using IMemoryOwner startCoverOwner = allocator.Allocate(maxBandRows, AllocationOptions.Clean); - - // Per-row activity flags avoid scanning the full bit-vector row just to detect "empty row". - using IMemoryOwner rowHasBitsOwner = allocator.Allocate(maxBandRows, AllocationOptions.Clean); - using IMemoryOwner rowTouchedOwner = allocator.Allocate(maxBandRows, AllocationOptions.Clean); - using IMemoryOwner touchedRowsOwner = allocator.Allocate(maxBandRows); - using IMemoryOwner scanlineOwner = allocator.Allocate(width); - - Span bitVectorsBuffer = bitVectorsOwner.Memory.Span; - Span coverAreaBuffer = coverAreaOwner.Memory.Span; - Span startCoverBuffer = startCoverOwner.Memory.Span; - Span rowHasBitsBuffer = rowHasBitsOwner.Memory.Span; - Span rowTouchedBuffer = rowTouchedOwner.Memory.Span; - Span touchedRowsBuffer = touchedRowsOwner.Memory.Span; - Span scanline = scanlineOwner.Memory.Span; - float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + // Create tessellated rings once. Both sequential and parallel paths consume this single + // canonical representation so path flattening/orientation work is never repeated. using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); - int bandTop = 0; - while (bandTop < height) - { - int bandHeight = Math.Min(maxBandRows, height - bandTop); - int bitVectorCount = wordsPerRow * bandHeight; - int coverCount = coverStrideInt * bandHeight; - - Span bitVectors = bitVectorsBuffer[..bitVectorCount]; - Span coverArea = coverAreaBuffer[..coverCount]; - Span startCover = startCoverBuffer[..bandHeight]; - Span rowHasBits = rowHasBitsBuffer[..bandHeight]; - Span rowTouched = rowTouchedBuffer[..bandHeight]; - Span touchedRows = touchedRowsBuffer[..bandHeight]; - - Context context = new( - bitVectors, - coverArea, - startCover, - rowHasBits, - rowTouched, - touchedRows, + using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); + Span edgeBuffer = edgeDataOwner.Memory.Span; + int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeBuffer); + if (edgeCount <= 0) + { + return; + } + + if (allowParallel && + TryRasterizeParallel( + edgeDataOwner.Memory, + edgeCount, width, - bandHeight, + height, + interest.Top, wordsPerRow, coverStrideInt, + maxBandRows, options.IntersectionRule, - options.RasterizationMode); + options.RasterizationMode, + allocator, + ref state, + scanlineHandler)) + { + return; + } + + RasterizeSequentialBands( + edgeDataOwner.Memory.Span[..edgeCount], + width, + height, + interest.Top, + wordsPerRow, + coverStrideInt, + maxBandRows, + options.IntersectionRule, + options.RasterizationMode, + allocator, + ref state, + scanlineHandler); + } - context.RasterizeMultipolygon( - multipolygon, - interest.Left, - interest.Top + bandTop, - samplingOffsetX); + /// + /// Sequential implementation using band buckets over the prebuilt edge table. + /// + /// The caller-owned mutable state type. + /// Prebuilt edges in scanner-local coordinates. + /// Destination width in pixels. + /// Destination height in pixels. + /// Absolute top Y of the interest rectangle. + /// Bit-vector words per row. + /// Cover-area stride in ints. + /// Maximum rows per reusable scratch band. + /// Fill rule. + /// Coverage mode (AA or aliased). + /// Temporary buffer allocator. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. + private static void RasterizeSequentialBands( + ReadOnlySpan edges, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStrideInt, + int maxBandRows, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + int bandHeight = maxBandRows; + int bandCount = (height + bandHeight - 1) / bandHeight; + if (bandCount < 1) + { + return; + } - context.EmitScanlines(interest.Top + bandTop, scanline, ref state, scanlineHandler); + using IMemoryOwner bandCountsOwner = allocator.Allocate(bandCount, AllocationOptions.Clean); + Span bandCounts = bandCountsOwner.Memory.Span; + long totalBandEdgeReferences = 0; + for (int i = 0; i < edges.Length; i++) + { + // Each edge can overlap multiple bands. We first count references so we can build + // a compact contiguous index list (CSR-style) without per-band allocations. + int startBand = edges[i].MinRow / bandHeight; + int endBand = edges[i].MaxRow / bandHeight; + totalBandEdgeReferences += (endBand - startBand) + 1; + if (totalBandEdgeReferences > int.MaxValue) + { + ThrowInterestBoundsTooLarge(); + } + + for (int b = startBand; b <= endBand; b++) + { + bandCounts[b]++; + } + } + + int totalReferences = (int)totalBandEdgeReferences; + using IMemoryOwner bandOffsetsOwner = allocator.Allocate(bandCount + 1); + Span bandOffsets = bandOffsetsOwner.Memory.Span; + int offset = 0; + for (int b = 0; b < bandCount; b++) + { + // Prefix sum: bandOffsets[b] is the start index of band b inside bandEdgeReferences. + bandOffsets[b] = offset; + offset += bandCounts[b]; + } + + bandOffsets[bandCount] = offset; + using IMemoryOwner bandWriteCursorOwner = allocator.Allocate(bandCount); + Span bandWriteCursor = bandWriteCursorOwner.Memory.Span; + bandOffsets[..bandCount].CopyTo(bandWriteCursor); + + using IMemoryOwner bandEdgeReferencesOwner = allocator.Allocate(totalReferences); + Span bandEdgeReferences = bandEdgeReferencesOwner.Memory.Span; + for (int edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++) + { + // Scatter each edge index to all bands touched by its row range. + int startBand = edges[edgeIndex].MinRow / bandHeight; + int endBand = edges[edgeIndex].MaxRow / bandHeight; + for (int b = startBand; b <= endBand; b++) + { + bandEdgeReferences[bandWriteCursor[b]++] = edgeIndex; + } + } + + using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStrideInt, width, bandHeight); + for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) + { + int bandTop = bandIndex * bandHeight; + int currentBandHeight = Math.Min(bandHeight, height - bandTop); + int start = bandOffsets[bandIndex]; + int length = bandOffsets[bandIndex + 1] - start; + if (length == 0) + { + // No edge crosses this band, so there is nothing to rasterize or clear. + continue; + } + + Context context = scratch.CreateContext(currentBandHeight, intersectionRule, rasterizationMode); + ReadOnlySpan bandEdges = bandEdgeReferences.Slice(start, length); + context.RasterizeEdgeTable(edges, bandEdges, bandTop); + context.EmitScanlines(interestTop + bandTop, scratch.Scanline, ref state, scanlineHandler); context.ResetTouchedRows(); - bandTop += bandHeight; } } + /// + /// Attempts to execute the tiled parallel scanner. + /// + /// The caller-owned mutable state type. + /// Memory block containing prebuilt edges. + /// Number of valid edges in . + /// Destination width in pixels. + /// Destination height in pixels. + /// Absolute top Y of the interest rectangle. + /// Bit-vector words per row. + /// Cover-area stride in ints. + /// Maximum rows per worker scratch context. + /// Fill rule. + /// Coverage mode (AA or aliased). + /// Temporary buffer allocator. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. + /// + /// when the parallel path executed successfully; + /// when the caller should run sequential fallback. + /// + private static bool TryRasterizeParallel( + Memory edgeMemory, + int edgeCount, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStride, + int maxBandRows, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + if (Environment.ProcessorCount < 2) + { + return false; + } + + long totalPixels = (long)width * height; + if (totalPixels > ParallelOutputPixelBudget) + { + // Parallel mode buffers tile coverage before ordered emission. Skip when the + // buffered output footprint would exceed our safety budget. + return false; + } + + int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); + if (tileHeight < 1) + { + return false; + } + + int tileCount = (height + tileHeight - 1) / tileHeight; + if (tileCount < 2) + { + return false; + } + + using IMemoryOwner tileCountsOwner = allocator.Allocate(tileCount, AllocationOptions.Clean); + Span tileCounts = tileCountsOwner.Memory.Span; + + long totalTileEdgeReferences = 0; + Span edgeBuffer = edgeMemory.Span; + for (int i = 0; i < edgeCount; i++) + { + // Same CSR construction as sequential mode, now keyed by tile instead of band. + int startTile = edgeBuffer[i].MinRow / tileHeight; + int endTile = edgeBuffer[i].MaxRow / tileHeight; + int tileSpan = (endTile - startTile) + 1; + totalTileEdgeReferences += tileSpan; + + if (totalTileEdgeReferences > int.MaxValue) + { + return false; + } + + for (int t = startTile; t <= endTile; t++) + { + tileCounts[t]++; + } + } + + int totalReferences = (int)totalTileEdgeReferences; + using IMemoryOwner tileOffsetsOwner = allocator.Allocate(tileCount + 1); + Memory tileOffsetsMemory = tileOffsetsOwner.Memory; + Span tileOffsets = tileOffsetsMemory.Span; + + int offset = 0; + for (int t = 0; t < tileCount; t++) + { + // Prefix sum over tile counts so each tile gets one contiguous slice. + tileOffsets[t] = offset; + offset += tileCounts[t]; + } + + tileOffsets[tileCount] = offset; + using IMemoryOwner tileWriteCursorOwner = allocator.Allocate(tileCount); + Span tileWriteCursor = tileWriteCursorOwner.Memory.Span; + tileOffsets[..tileCount].CopyTo(tileWriteCursor); + + using IMemoryOwner tileEdgeReferencesOwner = allocator.Allocate(totalReferences); + Memory tileEdgeReferencesMemory = tileEdgeReferencesOwner.Memory; + Span tileEdgeReferences = tileEdgeReferencesMemory.Span; + + for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) + { + int startTile = edgeBuffer[edgeIndex].MinRow / tileHeight; + int endTile = edgeBuffer[edgeIndex].MaxRow / tileHeight; + for (int t = startTile; t <= endTile; t++) + { + // Scatter edge indices into each tile's contiguous bucket. + tileEdgeReferences[tileWriteCursor[t]++] = edgeIndex; + } + } + + TileOutput[] tileOutputs = new TileOutput[tileCount]; + ParallelOptions parallelOptions = new() + { + MaxDegreeOfParallelism = Math.Min(Environment.ProcessorCount, tileCount) + }; + + try + { + _ = Parallel.For( + 0, + tileCount, + parallelOptions, + () => WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, tileHeight), + (tileIndex, _, scratch) => + { + ReadOnlySpan edges = edgeMemory.Span[..edgeCount]; + Span tileOffsets = tileOffsetsMemory.Span; + Span tileEdgeReferences = tileEdgeReferencesMemory.Span; + int bandTop = tileIndex * tileHeight; + int bandHeight = Math.Min(tileHeight, height - bandTop); + int start = tileOffsets[tileIndex]; + int length = tileOffsets[tileIndex + 1] - start; + ReadOnlySpan tileEdges = tileEdgeReferences.Slice(start, length); + + // Each tile rasterizes fully independently into worker-local scratch. + RasterizeTile( + scratch, + edges, + tileEdges, + bandTop, + bandHeight, + width, + intersectionRule, + rasterizationMode, + allocator, + tileOutputs, + tileIndex); + + return scratch; + }, + static scratch => scratch.Dispose()); + + EmitTileOutputs(tileOutputs, width, interestTop, ref state, scanlineHandler); + return true; + } + finally + { + foreach (TileOutput output in tileOutputs) + { + output?.Dispose(); + } + } + } + + /// + /// Rasterizes one tile/band edge subset into temporary coverage buffers. + /// + /// Worker-local scratch buffers. + /// Shared edge table. + /// Indices of edges intersecting this tile. + /// Tile top row in scanner-local coordinates. + /// Tile height in rows. + /// Destination width in pixels. + /// Fill rule. + /// Coverage mode (AA or aliased). + /// Temporary buffer allocator. + /// Output slot array indexed by tile ID. + /// Current tile index. + private static void RasterizeTile( + WorkerScratch scratch, + ReadOnlySpan edges, + ReadOnlySpan tileEdgeIndices, + int bandTop, + int bandHeight, + int width, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + TileOutput[] outputs, + int tileIndex) + { + if (tileEdgeIndices.Length == 0) + { + return; + } + + Context context = scratch.CreateContext(bandHeight, intersectionRule, rasterizationMode); + context.RasterizeEdgeTable(edges, tileEdgeIndices, bandTop); + + int coverageLength = checked(width * bandHeight); + IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); + IMemoryOwner dirtyRowsOwner = allocator.Allocate(bandHeight, AllocationOptions.Clean); + bool committed = false; + + try + { + TileCaptureState captureState = new(width, coverageOwner.Memory, dirtyRowsOwner.Memory); + + // Emit with destinationTop=0 into tile-local storage; global Y is restored later. + context.EmitScanlines(0, scratch.Scanline, ref captureState, CaptureTileScanline); + outputs[tileIndex] = new TileOutput(bandTop, bandHeight, coverageOwner, dirtyRowsOwner); + committed = true; + } + finally + { + context.ResetTouchedRows(); + + if (!committed) + { + coverageOwner.Dispose(); + dirtyRowsOwner.Dispose(); + } + } + } + + /// + /// Emits buffered tile outputs in deterministic top-to-bottom order. + /// + /// The caller-owned mutable state type. + /// Tile outputs captured by workers. + /// Destination width in pixels. + /// Absolute top Y of the interest rectangle. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. + private static void EmitTileOutputs( + TileOutput[] outputs, + int width, + int destinationTop, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + foreach (TileOutput output in outputs) + { + if (output is null) + { + continue; + } + + Span coverage = output.CoverageOwner.Memory.Span; + Span dirtyRows = output.DirtyRowsOwner.Memory.Span; + for (int row = 0; row < output.Height; row++) + { + if (dirtyRows[row] == 0) + { + // Rows are sparse; untouched rows were never emitted by the tile worker. + continue; + } + + // Stable top-to-bottom emission keeps observable callback order deterministic. + Span scanline = coverage.Slice(row * width, width); + scanlineHandler(destinationTop + output.Top + row, scanline, ref state); + } + } + } + + /// + /// Captures one emitted scanline into a tile-local output buffer. + /// + /// Row index relative to tile-local coordinates. + /// Coverage scanline. + /// Tile capture state. + private static void CaptureTileScanline(int y, Span scanline, ref TileCaptureState state) + { + // y is tile-local (destinationTop was 0 in RasterizeTile). + int row = y - state.Top; + scanline.CopyTo(state.Coverage.Span.Slice(row * state.Width, state.Width)); + state.DirtyRows.Span[row] = 1; + } + + /// + /// Builds a compact edge table in scanner-local coordinates. + /// + /// Input tessellated rings. + /// Interest left in absolute coordinates. + /// Interest top in absolute coordinates. + /// Interest height in pixels. + /// Horizontal sampling offset. + /// Destination span for edge records. + /// Number of valid edge records written. + private static int BuildEdgeTable( + TessellatedMultipolygon multipolygon, + int minX, + int minY, + int height, + float samplingOffsetX, + Span destination) + { + int count = 0; + foreach (TessellatedMultipolygon.Ring ring in multipolygon) + { + ReadOnlySpan vertices = ring.Vertices; + for (int i = 0; i < ring.VertexCount; i++) + { + PointF p0 = vertices[i]; + PointF p1 = vertices[i + 1]; + + float x0 = (p0.X - minX) + samplingOffsetX; + float y0 = p0.Y - minY; + float x1 = (p1.X - minX) + samplingOffsetX; + float y1 = p1.Y - minY; + + if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) + { + continue; + } + + if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, 0F, height)) + { + continue; + } + + if (!TryGetEdgeRowRange(y0, y1, height, out int minRow, out int maxRow)) + { + continue; + } + + destination[count++] = new EdgeData(x0, y0, x1, y1, minRow, maxRow); + } + } + + return count; + } + + /// + /// Computes inclusive row bounds touched by the edge in scanner-local coordinates. + /// + /// Edge start Y. + /// Edge end Y. + /// Scanner height. + /// Minimum affected row. + /// Maximum affected row. + /// if the edge intersects scanner rows. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetEdgeRowRange(float y0, float y1, int height, out int minRow, out int maxRow) + { + float minY = MathF.Min(y0, y1); + float maxY = MathF.Max(y0, y1); + minRow = (int)MathF.Floor(minY); + maxRow = (int)MathF.Ceiling(maxY) - 1; + + if (maxRow < 0 || minRow >= height) + { + return false; + } + + if (minRow < 0) + { + minRow = 0; + } + + if (maxRow >= height) + { + maxRow = height - 1; + } + + return minRow <= maxRow; + } + + /// + /// Converts bit count to the number of machine words needed to hold the bitset row. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; + /// + /// Calculates the maximum reusable band height under memory and indexing constraints. + /// + /// Interest width. + /// Interest height. + /// Bitset words per row. + /// Cover-area stride in ints. + /// Resulting maximum safe band height. + /// when a valid band height was produced. private static bool TryGetBandHeight(int width, int height, int wordsPerRow, long coverStride, out int bandHeight) { bandHeight = 0; @@ -148,9 +697,22 @@ private static bool TryGetBandHeight(int width, int height, int wordsPerRow, lon return bandHeight > 0; } + /// + /// Converts a float coordinate to signed 24.8 fixed-point. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + /// + /// Clips a segment against vertical bounds using Liang-Barsky style parametric tests. + /// + /// Segment start X (updated in place). + /// Segment start Y (updated in place). + /// Segment end X (updated in place). + /// Segment end Y (updated in place). + /// Minimum Y bound. + /// Maximum Y bound. + /// when a non-horizontal clipped segment remains. private static bool ClipToVerticalBounds(ref float x0, ref float y0, ref float x1, ref float y1, float minY, float maxY) { float t0 = 0F; @@ -183,6 +745,9 @@ private static bool ClipToVerticalBounds(ref float x0, ref float y0, ref float x return y0 != y1; } + /// + /// One Liang-Barsky clip test step. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool ClipTest(float p, float q, ref float t0, ref float t1) { @@ -220,6 +785,10 @@ private static bool ClipTest(float p, float q, ref float t0, ref float t1) return true; } + /// + /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. + /// This is used to keep edge ownership consistent for vertical lines. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FindAdjustment(int value) { @@ -228,12 +797,29 @@ private static int FindAdjustment(int value) return lte0 & divisibleBy256; } + /// + /// Machine-word trailing zero count used for sparse bitset iteration. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int TrailingZeroCount(nuint value) => nint.Size == sizeof(ulong) ? BitOperations.TrailingZeroCount((ulong)value) : BitOperations.TrailingZeroCount((uint)value); + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInterestBoundsTooLarge() + => throw new ImageProcessingException("The rasterizer interest bounds are too large for PolygonScanner buffers."); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowBandHeightExceedsScratchCapacity() + => throw new ImageProcessingException("Requested band height exceeds worker scratch capacity."); + + /// + /// Band/tile-local scanner context that owns mutable coverage accumulation state. + /// + /// + /// Instances are intentionally stack-bound to keep hot-path data in spans and avoid heap churn. + /// private ref struct Context { private readonly Span bitVectors; @@ -250,6 +836,9 @@ private ref struct Context private readonly RasterizationMode rasterizationMode; private int touchedRowCount; + /// + /// Initializes a new instance of the struct. + /// public Context( Span bitVectors, Span coverArea, @@ -279,6 +868,13 @@ public Context( this.touchedRowCount = 0; } + /// + /// Rasterizes all edges in a tessellated multipolygon directly into this context. + /// + /// Input tessellated rings. + /// Absolute left coordinate of the current scanner window. + /// Absolute top coordinate of the current scanner window. + /// Horizontal sample origin offset. public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX, int minY, float samplingOffsetX) { foreach (TessellatedMultipolygon.Ring ring in multipolygon) @@ -318,6 +914,53 @@ public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX } } + /// + /// Rasterizes a subset of prebuilt edges that intersect this context's vertical range. + /// + /// Shared edge table. + /// Indices into for this band/tile. + /// Top row of this context in global scanner-local coordinates. + public void RasterizeEdgeTable(ReadOnlySpan edges, ReadOnlySpan edgeIndices, int bandTop) + { + float minY = bandTop; + float maxY = bandTop + this.height; + + for (int i = 0; i < edgeIndices.Length; i++) + { + EdgeData edge = edges[edgeIndices[i]]; + float x0 = edge.X0; + float y0 = edge.Y0; + float x1 = edge.X1; + float y1 = edge.Y1; + + if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, minY, maxY)) + { + continue; + } + + // Convert to fixed-point in band-local Y coordinates so downstream walkers can + // index 0..bandHeight-1 directly without extra subtraction in hot loops. + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0 - bandTop); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1 - bandTop); + if (fy0 == fy1) + { + continue; + } + + this.RasterizeLine(fx0, fy0, fx1, fy1); + } + } + + /// + /// Converts accumulated cover/area tables into scanline coverage callbacks. + /// + /// The caller-owned mutable state type. + /// Absolute destination Y corresponding to row zero in this context. + /// Reusable scanline scratch buffer. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. public readonly void EmitScanlines(int destinationTop, Span scanline, ref TState state, RasterizerScanlineHandler scanlineHandler) where TState : struct { @@ -326,6 +969,7 @@ public readonly void EmitScanlines(int destinationTop, Span scanl int rowCover = this.startCover[row]; if (rowCover == 0 && this.rowHasBits[row] == 0) { + // Nothing contributed to this row. continue; } @@ -339,6 +983,12 @@ public readonly void EmitScanlines(int destinationTop, Span scanl } } + /// + /// Clears only rows touched during the previous rasterization pass. + /// + /// + /// This sparse reset strategy avoids clearing full scratch buffers when geometry is sparse. + /// public void ResetTouchedRows() { // Reset only rows that received contributions in this band. This avoids clearing @@ -361,6 +1011,14 @@ public void ResetTouchedRows() this.touchedRowCount = 0; } + /// + /// Emits one row by iterating touched columns and coalescing equal-coverage spans. + /// + /// Bitset words indicating touched columns in this row. + /// Row index inside the context. + /// Initial carry cover value from x less than zero contributions. + /// Destination scanline coverage buffer. + /// when at least one non-zero span was emitted. private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row, int cover, Span scanline) { int rowOffset = row * this.coverStride; @@ -371,6 +1029,7 @@ private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row for (int wordIndex = 0; wordIndex < rowBitVectors.Length; wordIndex++) { + // Iterate touched columns sparsely by scanning set bits only. nuint bitset = rowBitVectors[wordIndex]; while (bitset != 0) { @@ -384,6 +1043,9 @@ private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row } int tableIndex = rowOffset + (x << 1); + + // Area uses current cover before adding this cell's delta. This matches + // scan-conversion math where area integrates the edge state at cell entry. int area = this.coverArea[tableIndex + 1] + (cover << AreaToCoverageShift); float coverage = this.AreaToCoverage(area); @@ -410,6 +1072,8 @@ private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row } else { + // We jumped over untouched columns. If cover != 0 the gap has a constant + // non-zero coverage and must be emitted as its own run. if (cover == 0) { hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); @@ -449,6 +1113,7 @@ private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row } } + // Flush tail run and any remaining constant-cover tail after the last touched cell. hasCoverage |= FlushSpan(scanline, spanStart, spanEnd, spanCoverage); if (cover != 0 && spanEnd < this.width) { @@ -458,6 +1123,9 @@ private readonly bool EmitRowCoverage(ReadOnlySpan rowBitVectors, int row return hasCoverage; } + /// + /// Converts accumulated signed area to normalized coverage under the selected fill rule. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly float AreaToCoverage(int area) { @@ -466,6 +1134,7 @@ private readonly float AreaToCoverage(int area) float coverage; if (this.intersectionRule == IntersectionRule.NonZero) { + // Non-zero winding clamps absolute winding accumulation to [0, 1]. if (absoluteArea >= CoverageStepCount) { coverage = 1F; @@ -477,6 +1146,7 @@ private readonly float AreaToCoverage(int area) } else { + // Even-odd wraps every 2*CoverageStepCount and mirrors second half. int wrapped = absoluteArea & EvenOddMask; if (wrapped > CoverageStepCount) { @@ -488,12 +1158,16 @@ private readonly float AreaToCoverage(int area) if (this.rasterizationMode == RasterizationMode.Aliased) { + // Aliased mode quantizes final coverage to hard 0/1 per pixel. return coverage >= 0.5F ? 1F : 0F; } return coverage; } + /// + /// Writes one coverage span into the scanline buffer. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool FlushSpan(Span scanline, int start, int end, float coverage) { @@ -506,6 +1180,9 @@ private static bool FlushSpan(Span scanline, int start, int end, float co return true; } + /// + /// Sets a row/column bit and reports whether it was newly set. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly bool ConditionalSetBit(int row, int column) { @@ -515,10 +1192,15 @@ private readonly bool ConditionalSetBit(int row, int column) ref nuint word = ref this.bitVectors[wordIndex]; bool newlySet = (word & mask) == 0; word |= mask; + + // Fast row-level early-out for EmitScanlines. this.rowHasBits[row] = 1; return newlySet; } + /// + /// Adds one cell contribution into cover/area accumulators. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddCell(int row, int column, int delta, int area) { @@ -531,6 +1213,7 @@ private void AddCell(int row, int column, int delta, int area) if (column < 0) { + // Contributions left of x=0 accumulate into the row carry. this.startCover[row] += delta; return; } @@ -543,16 +1226,21 @@ private void AddCell(int row, int column, int delta, int area) int index = (row * this.coverStride) + (column << 1); if (this.ConditionalSetBit(row, column)) { + // First write wins initialization path avoids reading old values. this.coverArea[index] = delta; this.coverArea[index + 1] = area; } else { + // Multiple edges can hit the same cell; accumulate signed values. this.coverArea[index] += delta; this.coverArea[index + 1] += area; } } + /// + /// Marks a row as touched once so sparse reset can clear it later. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void MarkRowTouched(int row) { @@ -565,6 +1253,9 @@ private void MarkRowTouched(int row) this.touchedRows[this.touchedRowCount++] = row; } + /// + /// Emits one vertical cell contribution. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CellVertical(int px, int py, int x, int y0, int y1) { @@ -573,6 +1264,9 @@ private void CellVertical(int px, int py, int x, int y0, int y1) this.AddCell(py, px, delta, area); } + /// + /// Emits one general cell contribution. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Cell(int row, int px, int x0, int y0, int x1, int y1) { @@ -581,6 +1275,9 @@ private void Cell(int row, int px, int x0, int y0, int x1, int y1) this.AddCell(row, px, delta, area); } + /// + /// Rasterizes a downward vertical edge segment. + /// private void VerticalDown(int columnIndex, int y0, int y1, int x) { int rowIndex0 = y0 >> FixedShift; @@ -591,10 +1288,12 @@ private void VerticalDown(int columnIndex, int y0, int y1, int x) if (rowIndex0 == rowIndex1) { + // Entire segment stays within one row. this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); return; } + // First partial row, full middle rows, last partial row. this.CellVertical(columnIndex, rowIndex0, fx, fy0, FixedOne); for (int row = rowIndex0 + 1; row < rowIndex1; row++) { @@ -604,6 +1303,9 @@ private void VerticalDown(int columnIndex, int y0, int y1, int x) this.CellVertical(columnIndex, rowIndex1, fx, 0, fy1); } + /// + /// Rasterizes an upward vertical edge segment. + /// private void VerticalUp(int columnIndex, int y0, int y1, int x) { int rowIndex0 = (y0 - 1) >> FixedShift; @@ -614,10 +1316,12 @@ private void VerticalUp(int columnIndex, int y0, int y1, int x) if (rowIndex0 == rowIndex1) { + // Entire segment stays within one row. this.CellVertical(columnIndex, rowIndex0, fx, fy0, fy1); return; } + // First partial row, full middle rows, last partial row (upward direction). this.CellVertical(columnIndex, rowIndex0, fx, fy0, 0); for (int row = rowIndex0 - 1; row > rowIndex1; row--) { @@ -627,6 +1331,12 @@ private void VerticalUp(int columnIndex, int y0, int y1, int x) this.CellVertical(columnIndex, rowIndex1, fx, FixedOne, fy1); } + // The following row/line helpers are directional variants of the same fixed-point edge + // walker. They are intentionally split to minimize branch costs in hot loops. + + /// + /// Rasterizes a downward, left-to-right segment within a single row. + /// private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = p0x >> FixedShift; @@ -674,6 +1384,9 @@ private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); } + /// + /// RowDownR variant that handles perfectly vertical edge ownership consistently. + /// private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x < p1x) @@ -688,6 +1401,9 @@ private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) } } + /// + /// Rasterizes an upward, left-to-right segment within a single row. + /// private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = p0x >> FixedShift; @@ -735,6 +1451,9 @@ private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) this.Cell(rowIndex, columnIndex1, 0, cy, fx1, p1y); } + /// + /// RowUpR variant that handles perfectly vertical edge ownership consistently. + /// private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x < p1x) @@ -749,6 +1468,9 @@ private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) } } + /// + /// Rasterizes a downward, right-to-left segment within a single row. + /// private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = (p0x - 1) >> FixedShift; @@ -796,6 +1518,9 @@ private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); } + /// + /// RowDownL variant that handles perfectly vertical edge ownership consistently. + /// private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x > p1x) @@ -810,6 +1535,9 @@ private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) } } + /// + /// Rasterizes an upward, right-to-left segment within a single row. + /// private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = (p0x - 1) >> FixedShift; @@ -857,6 +1585,9 @@ private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) this.Cell(rowIndex, columnIndex1, FixedOne, cy, fx1, p1y); } + /// + /// RowUpL variant that handles perfectly vertical edge ownership consistently. + /// private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x > p1x) @@ -871,12 +1602,18 @@ private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) } } + /// + /// Rasterizes a downward, left-to-right segment spanning multiple rows. + /// private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x1 - x0; int dy = y1 - y0; int fy0 = y0 - (rowIndex0 << FixedShift); int fy1 = y1 - (rowIndex1 << FixedShift); + + // p/delta/mod/rem implement an integer DDA that advances x at row boundaries + // without per-row floating-point math. int p = (FixedOne - fy0) * dx; int delta = p / dy; int cx = x0 + delta; @@ -910,12 +1647,17 @@ private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int this.RowDownR_V(rowIndex1, cx, 0, x1, fy1); } + /// + /// Rasterizes an upward, left-to-right segment spanning multiple rows. + /// private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x1 - x0; int dy = y0 - y1; int fy0 = y0 - (rowIndex0 << FixedShift); int fy1 = y1 - (rowIndex1 << FixedShift); + + // Upward version of the same integer DDA stepping as LineDownR. int p = fy0 * dx; int delta = p / dy; int cx = x0 + delta; @@ -949,12 +1691,17 @@ private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y this.RowUpR_V(rowIndex1, cx, FixedOne, x1, fy1); } + /// + /// Rasterizes a downward, right-to-left segment spanning multiple rows. + /// private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x0 - x1; int dy = y1 - y0; int fy0 = y0 - (rowIndex0 << FixedShift); int fy1 = y1 - (rowIndex1 << FixedShift); + + // Right-to-left variant of the integer DDA. int p = (FixedOne - fy0) * dx; int delta = p / dy; int cx = x0 - delta; @@ -988,12 +1735,17 @@ private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int this.RowDownL_V(rowIndex1, cx, 0, x1, fy1); } + /// + /// Rasterizes an upward, right-to-left segment spanning multiple rows. + /// private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x0 - x1; int dy = y0 - y1; int fy0 = y0 - (rowIndex0 << FixedShift); int fy1 = y1 - (rowIndex1 << FixedShift); + + // Upward + right-to-left variant of the integer DDA. int p = fy0 * dx; int delta = p / dy; int cx = x0 - delta; @@ -1027,10 +1779,14 @@ private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y this.RowUpL_V(rowIndex1, cx, FixedOne, x1, fy1); } + /// + /// Dispatches a clipped edge to the correct directional fixed-point walker. + /// private void RasterizeLine(int x0, int y0, int x1, int y1) { if (x0 == x1) { + // Vertical edges need ownership adjustment to avoid double counting at cell seams. int columnIndex = (x0 - FindAdjustment(x0)) >> FixedShift; if (y0 < y1) { @@ -1046,6 +1802,7 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) if (y0 < y1) { + // Downward edges use inclusive top/exclusive bottom row mapping. int rowIndex0 = y0 >> FixedShift; int rowIndex1 = (y1 - 1) >> FixedShift; if (rowIndex0 == rowIndex1) @@ -1074,6 +1831,7 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) return; } + // Upward edges mirror the mapping to preserve winding consistency. int upRowIndex0 = (y0 - 1) >> FixedShift; int upRowIndex1 = y1 >> FixedShift; if (upRowIndex0 == upRowIndex1) @@ -1100,4 +1858,257 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) } } } + + /// + /// Immutable scanner-local edge record with precomputed affected-row bounds. + /// + private readonly struct EdgeData + { + /// + /// Initializes a new instance of the struct. + /// + public EdgeData(float x0, float y0, float x1, float y1, int minRow, int maxRow) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + this.MinRow = minRow; + this.MaxRow = maxRow; + } + + /// + /// Gets edge start X in scanner-local coordinates. + /// + public float X0 { get; } + + /// + /// Gets edge start Y in scanner-local coordinates. + /// + public float Y0 { get; } + + /// + /// Gets edge end X in scanner-local coordinates. + /// + public float X1 { get; } + + /// + /// Gets edge end Y in scanner-local coordinates. + /// + public float Y1 { get; } + + /// + /// Gets the first scanner row affected by this edge. + /// + public int MinRow { get; } + + /// + /// Gets the last scanner row affected by this edge. + /// + public int MaxRow { get; } + } + + /// + /// Mutable state used while capturing one tile's emitted scanlines. + /// + private readonly struct TileCaptureState + { + /// + /// Initializes a new instance of the struct. + /// + public TileCaptureState(int width, Memory coverage, Memory dirtyRows) + { + this.Top = 0; + this.Width = width; + this.Coverage = coverage; + this.DirtyRows = dirtyRows; + } + + /// + /// Gets the row origin of this capture buffer. + /// + public int Top { get; } + + /// + /// Gets the scanline width. + /// + public int Width { get; } + + /// + /// Gets contiguous tile coverage storage. + /// + public Memory Coverage { get; } + + /// + /// Gets per-row dirty flags for sparse output emission. + /// + public Memory DirtyRows { get; } + } + + /// + /// Buffered output produced by one rasterized tile. + /// + private sealed class TileOutput : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + public TileOutput(int top, int height, IMemoryOwner coverageOwner, IMemoryOwner dirtyRowsOwner) + { + this.Top = top; + this.Height = height; + this.CoverageOwner = coverageOwner; + this.DirtyRowsOwner = dirtyRowsOwner; + } + + /// + /// Gets the tile top row relative to interest origin. + /// + public int Top { get; } + + /// + /// Gets the number of rows in this tile. + /// + public int Height { get; } + + /// + /// Gets the tile coverage buffer owner. + /// + public IMemoryOwner CoverageOwner { get; private set; } + + /// + /// Gets the tile dirty-row buffer owner. + /// + public IMemoryOwner DirtyRowsOwner { get; private set; } + + /// + /// Releases tile output buffers back to the allocator. + /// + public void Dispose() + { + this.CoverageOwner?.Dispose(); + this.DirtyRowsOwner?.Dispose(); + this.CoverageOwner = null!; + this.DirtyRowsOwner = null!; + } + } + + /// + /// Reusable per-worker scratch buffers used by tiled and sequential band rasterization. + /// + private sealed class WorkerScratch : IDisposable + { + private readonly int wordsPerRow; + private readonly int coverStride; + private readonly int width; + private readonly int tileCapacity; + private readonly IMemoryOwner bitVectorsOwner; + private readonly IMemoryOwner coverAreaOwner; + private readonly IMemoryOwner startCoverOwner; + private readonly IMemoryOwner rowHasBitsOwner; + private readonly IMemoryOwner rowTouchedOwner; + private readonly IMemoryOwner touchedRowsOwner; + private readonly IMemoryOwner scanlineOwner; + + private WorkerScratch( + int wordsPerRow, + int coverStride, + int width, + int tileCapacity, + IMemoryOwner bitVectorsOwner, + IMemoryOwner coverAreaOwner, + IMemoryOwner startCoverOwner, + IMemoryOwner rowHasBitsOwner, + IMemoryOwner rowTouchedOwner, + IMemoryOwner touchedRowsOwner, + IMemoryOwner scanlineOwner) + { + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.width = width; + this.tileCapacity = tileCapacity; + this.bitVectorsOwner = bitVectorsOwner; + this.coverAreaOwner = coverAreaOwner; + this.startCoverOwner = startCoverOwner; + this.rowHasBitsOwner = rowHasBitsOwner; + this.rowTouchedOwner = rowTouchedOwner; + this.touchedRowsOwner = touchedRowsOwner; + this.scanlineOwner = scanlineOwner; + } + + /// + /// Gets reusable scanline scratch for this worker. + /// + public Span Scanline => this.scanlineOwner.Memory.Span; + + /// + /// Allocates worker-local scratch sized for the configured tile/band capacity. + /// + public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, int coverStride, int width, int tileCapacity) + { + int bitVectorCapacity = checked(wordsPerRow * tileCapacity); + int coverAreaCapacity = checked(coverStride * tileCapacity); + IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); + IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); + IMemoryOwner startCoverOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner rowHasBitsOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner rowTouchedOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + IMemoryOwner touchedRowsOwner = allocator.Allocate(tileCapacity); + IMemoryOwner scanlineOwner = allocator.Allocate(width); + + return new WorkerScratch( + wordsPerRow, + coverStride, + width, + tileCapacity, + bitVectorsOwner, + coverAreaOwner, + startCoverOwner, + rowHasBitsOwner, + rowTouchedOwner, + touchedRowsOwner, + scanlineOwner); + } + + /// + /// Creates a context view over this scratch for the requested band height. + /// + public Context CreateContext(int bandHeight, IntersectionRule intersectionRule, RasterizationMode rasterizationMode) + { + if ((uint)bandHeight > (uint)this.tileCapacity) + { + ThrowBandHeightExceedsScratchCapacity(); + } + + int bitVectorCount = checked(this.wordsPerRow * bandHeight); + int coverAreaCount = checked(this.coverStride * bandHeight); + return new Context( + this.bitVectorsOwner.Memory.Span[..bitVectorCount], + this.coverAreaOwner.Memory.Span[..coverAreaCount], + this.startCoverOwner.Memory.Span[..bandHeight], + this.rowHasBitsOwner.Memory.Span[..bandHeight], + this.rowTouchedOwner.Memory.Span[..bandHeight], + this.touchedRowsOwner.Memory.Span[..bandHeight], + this.width, + bandHeight, + this.wordsPerRow, + this.coverStride, + intersectionRule, + rasterizationMode); + } + + /// + /// Releases worker-local scratch buffers back to the allocator. + /// + public void Dispose() + { + this.bitVectorsOwner.Dispose(); + this.coverAreaOwner.Dispose(); + this.startCoverOwner.Dispose(); + this.rowHasBitsOwner.Dispose(); + this.rowTouchedOwner.Dispose(); + this.touchedRowsOwner.Dispose(); + this.scanlineOwner.Dispose(); + } + } } diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD index ff43cf14..e4fb2445 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD @@ -1,4 +1,4 @@ -# Polygon Scanner (Fixed-Point, Banded) +# Polygon Scanner (Fixed-Point, Tiled + Banded Fallback) This document describes the current `PolygonScanner` implementation in `src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs`. @@ -24,22 +24,31 @@ IPath TessellatedMultipolygon.Create(...) | v -Band loop over interest rectangle Y +Choose execution mode: | - +--> Clear band-local buffers + +--> Parallel row-tiles (default rasterizer path) + | | + | +--> Build edge table once (global local-space edges) + | +--> Assign edges to tile rows + | +--> Rasterize each tile in parallel using worker-local scratch + | +--> Emit tile outputs in deterministic top-to-bottom order | - +--> Rasterize all ring edges into cover/area accumulators - | - +--> Convert accumulators to coverage scanlines - | - +--> Invoke rasterizer callback per dirty row + +--> Sequential band loop (scanline baseline + fallback) + | + +--> Build edge table once (shared with parallel path) + +--> Assign edges to sequential bands + +--> Reuse worker scratch across bands + +--> Rasterize band-local edge subsets into cover/area accumulators + +--> Convert accumulators to coverage scanlines + +--> Invoke rasterizer callback per dirty row ``` ## Coordinate System and Precision - Geometry is transformed to scanner-local coordinates: - `xLocal = (x - interest.Left) + samplingOffsetX` - - `yLocal = y - (interest.Top + bandTop)` + - `yLocal = y - interest.Top` (global local-space edge table) + - Per tile/band pass uses `yLocal - currentBandTop` - Scanner math uses signed 24.8 fixed point: - `FixedShift = 8` - `FixedOne = 256` @@ -49,9 +58,9 @@ Band loop over interest rectangle Y This means 1 fixed unit in Y equals 1/256 pixel row resolution. -## Memory Model and Banding +## Memory Model and Banded Scratch -The scanner bounds temporary memory with a per-band budget: +The scanner bounds scratch memory with a per-band budget: - `BandMemoryBudgetBytes = 64 MB` - Rows per band are computed from per-row byte cost. @@ -64,7 +73,7 @@ coverArea : (width * 2) * sizeof(int) startCover: 1 * sizeof(int) ``` -Band buffers are reused for each band: +Scratch buffers are reused per band/tile worker: ``` bitVectors : [bandHeight][wordsPerRow] // bitset marks touched columns @@ -74,19 +83,28 @@ rowHasBits : [bandHeight] // fast "row touched" flag scanline : [width] float // output coverage row ``` -If width/height are too large for safe indexing math, `PolygonScanner.Rasterize` -throws `InvalidOperationException`. +If width/height are too large for safe indexing math, rasterization throws +`ImageProcessingException`. + +Parallel mode additionally buffers per-tile output coverage before ordered emit. +This path is capped by `ParallelOutputPixelBudget` to avoid pathological output +buffer growth. ## Edge Rasterization Stage -For each tessellated ring edge `(p0 -> p1)`: +For each tessellated ring edge `(p0 -> p1)` during edge-table build: -1. Translate to local band coordinates. +1. Translate to local coordinates. 2. Reject non-finite coordinates. -3. Clip vertically to `[0, bandHeight]` (`ClipToVerticalBounds`). -4. Convert endpoints to 24.8 fixed. -5. Skip horizontal edges (`fy0 == fy1`). -6. Route to directional line walkers (`LineDownR`, `LineUpL`, etc.). +3. Clip vertically to scanner bounds. +4. Record edge row range for tile assignment. + +During tile/band rasterization: + +1. Clip edge to current tile/band vertical bounds. +2. Convert endpoints to 24.8 fixed. +3. Skip horizontal edges (`fy0 == fy1`). +4. Route to directional line walkers (`LineDownR`, `LineUpL`, etc.). The walkers decompose edges into affected cells and call: @@ -153,8 +171,10 @@ rasterization time. ## Fast Paths and Practical Optimizations -- One tessellation build per rasterization call (`TessellatedMultipolygon` reused across bands). -- Band-buffer reuse avoids per-band allocations. +- One tessellation build per rasterization call. +- Parallel path builds a single edge table and reuses it across tiles. +- Worker-local scratch reuse avoids per-tile scratch allocations. +- Sequential path reuses band buffers across the full Y range. - `rowHasBits` avoids scanning all words in empty rows. - Bitset iteration visits only touched columns. - Span coalescing reduces per-pixel operations before blending. @@ -195,7 +215,7 @@ bitVectors[row] -> touched x list -> accumulate cover/area -> coverage spans ## Failure Modes and Diagnostics -- Exception: interest too large for bounded temporary buffers/indexing. +- Exception: interest too large for bounded scratch/output buffers or indexing. - Symptoms like missing fill are usually from invalid input geometry (NaN/Inf) or ring construction upstream; scanner explicitly skips non-finite edges. - Performance hotspots are typically in: diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs index 6d778120..6a2183c0 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs @@ -39,6 +39,6 @@ public void Rasterize( return; } - PolygonScanner.Rasterize(path, options, allocator, ref state, scanlineHandler); + PolygonScanner.RasterizeSequential(path, options, allocator, ref state, scanlineHandler); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 0c7fb810..c3080014 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -154,17 +154,17 @@ public void Cleanup() public void SystemDrawing() => this.sdGraphics.DrawPath(this.sdPen, this.sdPath); - // Keep explicit legacy path for side-by-side comparison now that tiled is default. + // Keep explicit scanline rasterizer path for side-by-side comparison now that tiled is default. [Benchmark] - public void ImageSharpCombinedPaths() + public void ImageSharpCombinedPathsScanlineRasterizer() => this.image.Mutate(c => c.SetRasterizer(ScanlineRasterizer.Instance).Draw(this.isPen, this.imageSharpPath)); [Benchmark] - public void ImageSharpSeparatePaths() + public void ImageSharpSeparatePathsScanlineRasterizer() => this.image.Mutate( c => { - // Keep explicit legacy path for side-by-side comparison now that tiled is default. + // Keep explicit scanline rasterizer path for side-by-side comparison now that tiled is default. c.SetRasterizer(ScanlineRasterizer.Instance); foreach (PointF[] loop in this.points) { From 1714a6e68a2e84776a87442d820f9cab3481e2bf Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 22:01:32 +1000 Subject: [PATCH 26/35] Precompute fixed values. --- .../Shapes/Rasterization/PolygonScanner.cs | 148 ++++++++++++++---- 1 file changed, 120 insertions(+), 28 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs index 2f821456..74628b81 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -565,6 +565,10 @@ private static void CaptureTileScanline(int y, Span scanline, ref TileCap /// /// Builds a compact edge table in scanner-local coordinates. /// + /// + /// Edges are converted to 24.8 fixed-point once during table construction so the hot + /// band/tile rasterization loop does not pay float-to-fixed conversion costs repeatedly. + /// /// Input tessellated rings. /// Interest left in absolute coordinates. /// Interest top in absolute coordinates. @@ -609,7 +613,16 @@ private static int BuildEdgeTable( continue; } - destination[count++] = new EdgeData(x0, y0, x1, y1, minRow, maxRow); + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1); + if (fy0 == fy1) + { + continue; + } + + destination[count++] = new EdgeData(fx0, fy0, fx1, fy1, minRow, maxRow); } } @@ -703,6 +716,49 @@ private static bool TryGetBandHeight(int width, int height, int wordsPerRow, lon [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + /// + /// Clips a fixed-point segment against vertical bounds. + /// + /// Segment start X in 24.8 fixed-point (updated in place). + /// Segment start Y in 24.8 fixed-point (updated in place). + /// Segment end X in 24.8 fixed-point (updated in place). + /// Segment end Y in 24.8 fixed-point (updated in place). + /// Minimum Y bound in 24.8 fixed-point. + /// Maximum Y bound in 24.8 fixed-point. + /// when a non-horizontal clipped segment remains. + private static bool ClipToVerticalBoundsFixed(ref int x0, ref int y0, ref int x1, ref int y1, int minY, int maxY) + { + double t0 = 0D; + double t1 = 1D; + int originX0 = x0; + int originY0 = y0; + long dx = (long)x1 - originX0; + long dy = (long)y1 - originY0; + if (!ClipTestFixed(-(double)dy, originY0 - (double)minY, ref t0, ref t1)) + { + return false; + } + + if (!ClipTestFixed(dy, maxY - (double)originY0, ref t0, ref t1)) + { + return false; + } + + if (t1 < 1D) + { + x1 = originX0 + (int)Math.Round(dx * t1); + y1 = originY0 + (int)Math.Round(dy * t1); + } + + if (t0 > 0D) + { + x0 = originX0 + (int)Math.Round(dx * t0); + y0 = originY0 + (int)Math.Round(dy * t0); + } + + return y0 != y1; + } + /// /// Clips a segment against vertical bounds using Liang-Barsky style parametric tests. /// @@ -785,6 +841,46 @@ private static bool ClipTest(float p, float q, ref float t0, ref float t1) return true; } + /// + /// One Liang-Barsky clip test step for fixed-point clipping. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTestFixed(double p, double q, ref double t0, ref double t1) + { + if (p == 0D) + { + return q >= 0D; + } + + double r = q / p; + if (p < 0D) + { + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } + } + else + { + if (r < t0) + { + return false; + } + + if (r < t1) + { + t1 = r; + } + } + + return true; + } + /// /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. /// This is used to keep edge ownership consistent for vertical lines. @@ -922,34 +1018,27 @@ public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX /// Top row of this context in global scanner-local coordinates. public void RasterizeEdgeTable(ReadOnlySpan edges, ReadOnlySpan edgeIndices, int bandTop) { - float minY = bandTop; - float maxY = bandTop + this.height; + int bandTopFixed = bandTop * FixedOne; + int bandBottomFixed = bandTopFixed + (this.height * FixedOne); for (int i = 0; i < edgeIndices.Length; i++) { EdgeData edge = edges[edgeIndices[i]]; - float x0 = edge.X0; - float y0 = edge.Y0; - float x1 = edge.X1; - float y1 = edge.Y1; + int x0 = edge.X0; + int y0 = edge.Y0; + int x1 = edge.X1; + int y1 = edge.Y1; - if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, minY, maxY)) + if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) { continue; } - // Convert to fixed-point in band-local Y coordinates so downstream walkers can - // index 0..bandHeight-1 directly without extra subtraction in hot loops. - int fx0 = FloatToFixed24Dot8(x0); - int fy0 = FloatToFixed24Dot8(y0 - bandTop); - int fx1 = FloatToFixed24Dot8(x1); - int fy1 = FloatToFixed24Dot8(y1 - bandTop); - if (fy0 == fy1) - { - continue; - } + // Convert global scanner Y to band-local Y after clipping. + y0 -= bandTopFixed; + y1 -= bandTopFixed; - this.RasterizeLine(fx0, fy0, fx1, fy1); + this.RasterizeLine(x0, y0, x1, y1); } } @@ -1862,12 +1951,15 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) /// /// Immutable scanner-local edge record with precomputed affected-row bounds. /// + /// + /// Coordinates are stored as signed 24.8 fixed-point values. + /// private readonly struct EdgeData { /// /// Initializes a new instance of the struct. /// - public EdgeData(float x0, float y0, float x1, float y1, int minRow, int maxRow) + public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow) { this.X0 = x0; this.Y0 = y0; @@ -1878,24 +1970,24 @@ public EdgeData(float x0, float y0, float x1, float y1, int minRow, int maxRow) } /// - /// Gets edge start X in scanner-local coordinates. + /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). /// - public float X0 { get; } + public int X0 { get; } /// - /// Gets edge start Y in scanner-local coordinates. + /// Gets edge start Y in scanner-local coordinates (24.8 fixed-point). /// - public float Y0 { get; } + public int Y0 { get; } /// - /// Gets edge end X in scanner-local coordinates. + /// Gets edge end X in scanner-local coordinates (24.8 fixed-point). /// - public float X1 { get; } + public int X1 { get; } /// - /// Gets edge end Y in scanner-local coordinates. + /// Gets edge end Y in scanner-local coordinates (24.8 fixed-point). /// - public float Y1 { get; } + public int Y1 { get; } /// /// Gets the first scanner row affected by this edge. From c852e1501d301d64a19329070d5ae25a98dc8e99 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Feb 2026 23:37:58 +1000 Subject: [PATCH 27/35] Add single tile hot path. --- .../Shapes/Rasterization/PolygonScanner.cs | 225 ++++++++++++------ 1 file changed, 147 insertions(+), 78 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs index 74628b81..150b2612 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -123,8 +123,7 @@ private static void RasterizeCore( // canonical representation so path flattening/orientation work is never repeated. using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); - Span edgeBuffer = edgeDataOwner.Memory.Span; - int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeBuffer); + int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeDataOwner.Memory.Span); if (edgeCount <= 0) { return; @@ -291,7 +290,7 @@ private static void RasterizeSequentialBands( /// Caller-owned mutable state. /// Scanline callback invoked in ascending Y order. /// - /// when the parallel path executed successfully; + /// when the tiled path executed successfully; /// when the caller should run sequential fallback. /// private static bool TryRasterizeParallel( @@ -310,28 +309,42 @@ private static bool TryRasterizeParallel( RasterizerScanlineHandler scanlineHandler) where TState : struct { - if (Environment.ProcessorCount < 2) + int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); + if (tileHeight < 1) { return false; } - long totalPixels = (long)width * height; - if (totalPixels > ParallelOutputPixelBudget) + int tileCount = (height + tileHeight - 1) / tileHeight; + if (tileCount == 1) { - // Parallel mode buffers tile coverage before ordered emission. Skip when the - // buffered output footprint would exceed our safety budget. - return false; + // Tiny workload fast path: avoid bucket construction, worker scheduling, and + // tile-output buffering when everything fits in a single tile. + RasterizeSingleTileDirect( + edgeMemory.Span[..edgeCount], + width, + height, + interestTop, + wordsPerRow, + coverStride, + intersectionRule, + rasterizationMode, + allocator, + ref state, + scanlineHandler); + return true; } - int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); - if (tileHeight < 1) + if (Environment.ProcessorCount < 2) { return false; } - int tileCount = (height + tileHeight - 1) / tileHeight; - if (tileCount < 2) + long totalPixels = (long)width * height; + if (totalPixels > ParallelOutputPixelBudget) { + // Parallel mode buffers tile coverage before ordered emission. Skip when the + // buffered output footprint would exceed our safety budget. return false; } @@ -446,6 +459,46 @@ private static bool TryRasterizeParallel( } } + /// + /// Rasterizes a single tile directly into the caller callback. + /// + /// + /// This avoids parallel setup and tile-output buffering for tiny workloads while preserving + /// the same scan-conversion math and callback ordering as the general tiled path. + /// + /// The caller-owned mutable state type. + /// Prebuilt edge table. + /// Destination width in pixels. + /// Destination height in pixels. + /// Absolute top Y of the interest rectangle. + /// Bit-vector words per row. + /// Cover-area stride in ints. + /// Fill rule. + /// Coverage mode (AA or aliased). + /// Temporary buffer allocator. + /// Caller-owned mutable state. + /// Scanline callback invoked in ascending Y order. + private static void RasterizeSingleTileDirect( + ReadOnlySpan edges, + int width, + int height, + int interestTop, + int wordsPerRow, + int coverStride, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); + Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode); + context.RasterizeEdgeTable(edges, bandTop: 0); + context.EmitScanlines(interestTop, scratch.Scanline, ref state, scanlineHandler); + context.ResetTouchedRows(); + } + /// /// Rasterizes one tile/band edge subset into temporary coverage buffers. /// @@ -563,12 +616,8 @@ private static void CaptureTileScanline(int y, Span scanline, ref TileCap } /// - /// Builds a compact edge table in scanner-local coordinates. + /// Builds an edge table in scanner-local coordinates. /// - /// - /// Edges are converted to 24.8 fixed-point once during table construction so the hot - /// band/tile rasterization loop does not pay float-to-fixed conversion costs repeatedly. - /// /// Input tessellated rings. /// Interest left in absolute coordinates. /// Interest top in absolute coordinates. @@ -608,11 +657,6 @@ private static int BuildEdgeTable( continue; } - if (!TryGetEdgeRowRange(y0, y1, height, out int minRow, out int maxRow)) - { - continue; - } - int fx0 = FloatToFixed24Dot8(x0); int fy0 = FloatToFixed24Dot8(y0); int fx1 = FloatToFixed24Dot8(x1); @@ -622,6 +666,7 @@ private static int BuildEdgeTable( continue; } + ComputeEdgeRowBounds(fy0, fy1, out int minRow, out int maxRow); destination[count++] = new EdgeData(fx0, fy0, fx1, fy1, minRow, maxRow); } } @@ -629,41 +674,6 @@ private static int BuildEdgeTable( return count; } - /// - /// Computes inclusive row bounds touched by the edge in scanner-local coordinates. - /// - /// Edge start Y. - /// Edge end Y. - /// Scanner height. - /// Minimum affected row. - /// Maximum affected row. - /// if the edge intersects scanner rows. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryGetEdgeRowRange(float y0, float y1, int height, out int minRow, out int maxRow) - { - float minY = MathF.Min(y0, y1); - float maxY = MathF.Max(y0, y1); - minRow = (int)MathF.Floor(minY); - maxRow = (int)MathF.Ceiling(maxY) - 1; - - if (maxRow < 0 || minRow >= height) - { - return false; - } - - if (minRow < 0) - { - minRow = 0; - } - - if (maxRow >= height) - { - maxRow = height - 1; - } - - return minRow <= maxRow; - } - /// /// Converts bit count to the number of machine words needed to hold the bitset row. /// @@ -716,6 +726,33 @@ private static bool TryGetBandHeight(int width, int height, int wordsPerRow, lon [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + /// + /// Computes the inclusive row range affected by a clipped non-horizontal edge. + /// + /// Edge start Y in 24.8 fixed-point. + /// Edge end Y in 24.8 fixed-point. + /// First affected integer scan row. + /// Last affected integer scan row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ComputeEdgeRowBounds(int y0, int y1, out int minRow, out int maxRow) + { + int y0Row = y0 >> FixedShift; + int y1Row = y1 >> FixedShift; + + // First touched row is floor(min(y0, y1)). + minRow = y0Row < y1Row ? y0Row : y1Row; + + int y0Fraction = y0 & (FixedOne - 1); + int y1Fraction = y1 & (FixedOne - 1); + + // Last touched row is ceil(max(y)) - 1: + // - when fractional part is non-zero, row is unchanged; + // - when exactly on a row boundary, subtract 1 (edge ownership rule). + int y0Candidate = y0Row - (((y0Fraction - 1) >> 31) & 1); + int y1Candidate = y1Row - (((y1Fraction - 1) >> 31) & 1); + maxRow = y0Candidate > y1Candidate ? y0Candidate : y1Candidate; + } + /// /// Clips a fixed-point segment against vertical bounds. /// @@ -1010,6 +1047,37 @@ public void RasterizeMultipolygon(TessellatedMultipolygon multipolygon, int minX } } + /// + /// Rasterizes all prebuilt edges that overlap this context. + /// + /// Shared edge table. + /// Top row of this context in global scanner-local coordinates. + public void RasterizeEdgeTable(ReadOnlySpan edges, int bandTop) + { + int bandTopFixed = bandTop * FixedOne; + int bandBottomFixed = bandTopFixed + (this.height * FixedOne); + + for (int i = 0; i < edges.Length; i++) + { + EdgeData edge = edges[i]; + int x0 = edge.X0; + int y0 = edge.Y0; + int x1 = edge.X1; + int y1 = edge.Y1; + + if (!ClipToVerticalBoundsFixed(ref x0, ref y0, ref x1, ref y1, bandTopFixed, bandBottomFixed)) + { + continue; + } + + // Convert global scanner Y to band-local Y after clipping. + y0 -= bandTopFixed; + y1 -= bandTopFixed; + + this.RasterizeLine(x0, y0, x1, y1); + } + } + /// /// Rasterizes a subset of prebuilt edges that intersect this context's vertical range. /// @@ -1952,52 +2020,53 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) /// Immutable scanner-local edge record with precomputed affected-row bounds. /// /// - /// Coordinates are stored as signed 24.8 fixed-point values. + /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path + /// access without per-read unpacking. /// private readonly struct EdgeData { - /// - /// Initializes a new instance of the struct. - /// - public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow) - { - this.X0 = x0; - this.Y0 = y0; - this.X1 = x1; - this.Y1 = y1; - this.MinRow = minRow; - this.MaxRow = maxRow; - } - /// /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). /// - public int X0 { get; } + public readonly int X0; /// /// Gets edge start Y in scanner-local coordinates (24.8 fixed-point). /// - public int Y0 { get; } + public readonly int Y0; /// /// Gets edge end X in scanner-local coordinates (24.8 fixed-point). /// - public int X1 { get; } + public readonly int X1; /// /// Gets edge end Y in scanner-local coordinates (24.8 fixed-point). /// - public int Y1 { get; } + public readonly int Y1; /// /// Gets the first scanner row affected by this edge. /// - public int MinRow { get; } + public readonly int MinRow; /// /// Gets the last scanner row affected by this edge. /// - public int MaxRow { get; } + public readonly int MaxRow; + + /// + /// Initializes a new instance of the struct. + /// + public EdgeData(int x0, int y0, int x1, int y1, int minRow, int maxRow) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + this.MinRow = minRow; + this.MaxRow = maxRow; + } } /// From 38e9d2d3b33763225e964a5802a39b8f6a133d9e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 00:14:09 +1000 Subject: [PATCH 28/35] Better IDrawingBackend --- .../Processing/Backends/CpuDrawingBackend.cs | 301 +++++++++++++++++- .../Processing/Backends/IDrawingBackend.cs | 48 ++- .../Drawing/FillPathProcessor{TPixel}.cs | 173 +--------- .../Text/DrawTextProcessor{TPixel}.cs | 1 - .../Processors/Text/RichTextGlyphRenderer.cs | 31 +- .../Drawing/DrawingRobustnessTests.cs | 1 - .../RasterizerDefaultsExtensionsTests.cs | 21 +- 7 files changed, 372 insertions(+), 204 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs index 5c742ec7..cd2c22ed 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs @@ -9,14 +9,20 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Default CPU drawing backend. /// +/// +/// This backend keeps all CPU-specific scanline handling internal so higher-level processors +/// can remain backend-agnostic. +/// internal sealed class CpuDrawingBackend : IDrawingBackend { - private readonly IRasterizer primaryRasterizer; - + /// + /// Initializes a new instance of the class. + /// + /// Rasterizer used for CPU coverage generation. private CpuDrawingBackend(IRasterizer primaryRasterizer) { Guard.NotNull(primaryRasterizer, nameof(primaryRasterizer)); - this.primaryRasterizer = primaryRasterizer; + this.PrimaryRasterizer = primaryRasterizer; } /// @@ -27,7 +33,7 @@ private CpuDrawingBackend(IRasterizer primaryRasterizer) /// /// Gets the primary rasterizer used by this backend. /// - public IRasterizer PrimaryRasterizer => this.primaryRasterizer; + public IRasterizer PrimaryRasterizer { get; } /// /// Creates a backend that uses the given rasterizer as the primary implementation. @@ -41,12 +47,287 @@ public static CpuDrawingBackend Create(IRasterizer rasterizer) } /// - public void RasterizePath( + public void FillPath( + Configuration configuration, + ImageFrame source, + IPath path, + Brush brush, + in GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + Rectangle brushBounds, + MemoryAllocator allocator) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(source, nameof(source)); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(allocator, nameof(allocator)); + + Rectangle interest = rasterizerOptions.Interest; + if (interest.Equals(Rectangle.Empty)) + { + return; + } + + // Detect the common "opaque solid without blending" case and bypass brush sampling + // for fully covered runs. + TPixel solidBrushColor = default; + bool isSolidBrushWithoutBlending = false; + if (brush is SolidBrush solidBrush && graphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color)) + { + isSolidBrushWithoutBlending = true; + solidBrushColor = solidBrush.Color.ToPixel(); + } + + int minX = interest.Left; + using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, brushBounds); + FillRasterizationState state = new( + source, + applicator, + minX, + isSolidBrushWithoutBlending, + solidBrushColor); + + this.PrimaryRasterizer.Rasterize(path, rasterizerOptions, allocator, ref state, ProcessRasterizedScanline); + } + + /// + public void RasterizeCoverage( IPath path, - in RasterizerOptions options, + in RasterizerOptions rasterizerOptions, MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - => this.primaryRasterizer.Rasterize(path, options, allocator, ref state, scanlineHandler); + Buffer2D destination) + { + Guard.NotNull(path, nameof(path)); + Guard.NotNull(allocator, nameof(allocator)); + Guard.NotNull(destination, nameof(destination)); + + CoverageRasterizationState state = new(destination); + this.PrimaryRasterizer.Rasterize(path, rasterizerOptions, allocator, ref state, ProcessCoverageScanline); + } + + /// + /// Copies one rasterized coverage row into the destination coverage buffer. + /// + /// Destination row index. + /// Source coverage row. + /// Callback state containing destination storage. + private static void ProcessCoverageScanline(int y, Span scanline, ref CoverageRasterizationState state) + { + Span destination = state.Buffer.DangerousGetRowSpan(y); + scanline.CopyTo(destination); + } + + /// + /// Dispatches rasterized coverage to either the generic brush path or the opaque-solid fast path. + /// + /// The pixel format. + /// Destination row index. + /// Rasterized coverage row. + /// Callback state. + private static void ProcessRasterizedScanline(int y, Span scanline, ref FillRasterizationState state) + where TPixel : unmanaged, IPixel + { + if (state.IsSolidBrushWithoutBlending) + { + ApplyCoverageRunsForOpaqueSolidBrush(state.Source, state.Applicator, scanline, state.MinX, y, state.SolidBrushColor); + } + else + { + ApplyPositiveCoverageRuns(state.Applicator, scanline, state.MinX, y); + } + } + + /// + /// Applies a brush to contiguous positive-coverage runs on a scanline. + /// + /// + /// The rasterizer has already resolved the fill rule (NonZero or EvenOdd) into per-pixel + /// coverage values. This method simply consumes the resulting positive runs. + /// + /// The pixel format. + /// Brush applicator. + /// Coverage values for one row. + /// Absolute X of scanline index 0. + /// Destination row index. + private static void ApplyPositiveCoverageRuns(BrushApplicator applicator, Span scanline, int minX, int y) + where TPixel : unmanaged, IPixel + { + int i = 0; + while (i < scanline.Length) + { + while (i < scanline.Length && scanline[i] <= 0F) + { + i++; + } + + int runStart = i; + while (i < scanline.Length && scanline[i] > 0F) + { + i++; + } + + int runLength = i - runStart; + if (runLength > 0) + { + // Apply only the positive-coverage run. This avoids invoking brush logic + // for fully transparent gaps. + applicator.Apply(scanline.Slice(runStart, runLength), minX + runStart, y); + } + } + } + + /// + /// Applies coverage using a mixed strategy for opaque solid brushes. + /// + /// + /// Semi-transparent edges still go through brush blending, but fully covered interior runs + /// are written directly with . + /// + /// The pixel format. + /// Destination frame. + /// Brush applicator for non-opaque segments. + /// Coverage values for one row. + /// Absolute X of scanline index 0. + /// Destination row index. + /// Pre-converted solid color for direct writes. + private static void ApplyCoverageRunsForOpaqueSolidBrush( + ImageFrame source, + BrushApplicator applicator, + Span scanline, + int minX, + int y, + TPixel solidBrushColor) + where TPixel : unmanaged, IPixel + { + Span destinationRow = source.PixelBuffer.DangerousGetRowSpan(y).Slice(minX, scanline.Length); + int i = 0; + + while (i < scanline.Length) + { + while (i < scanline.Length && scanline[i] <= 0F) + { + i++; + } + + int runStart = i; + while (i < scanline.Length && scanline[i] > 0F) + { + i++; + } + + int runEnd = i; + if (runEnd <= runStart) + { + continue; + } + + // Leading partially-covered segment. + int opaqueStart = runStart; + while (opaqueStart < runEnd && scanline[opaqueStart] < 1F) + { + opaqueStart++; + } + + if (opaqueStart > runStart) + { + int prefixLength = opaqueStart - runStart; + applicator.Apply(scanline.Slice(runStart, prefixLength), minX + runStart, y); + } + + // Trailing partially-covered segment. + int opaqueEnd = runEnd; + while (opaqueEnd > opaqueStart && scanline[opaqueEnd - 1] < 1F) + { + opaqueEnd--; + } + + // Fully covered interior can skip blending entirely. + if (opaqueEnd > opaqueStart) + { + destinationRow[opaqueStart..opaqueEnd].Fill(solidBrushColor); + } + + if (runEnd > opaqueEnd) + { + int suffixLength = runEnd - opaqueEnd; + applicator.Apply(scanline.Slice(opaqueEnd, suffixLength), minX + opaqueEnd, y); + } + } + } + + /// + /// Callback state used while writing coverage maps. + /// + private readonly struct CoverageRasterizationState + { + /// + /// Initializes a new instance of the struct. + /// + /// Destination coverage buffer. + public CoverageRasterizationState(Buffer2D buffer) => this.Buffer = buffer; + + /// + /// Gets the destination coverage buffer. + /// + public Buffer2D Buffer { get; } + } + + /// + /// Callback state used while filling into an image frame. + /// + /// The pixel format. + private readonly struct FillRasterizationState + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the struct. + /// + /// Destination frame. + /// Brush applicator for blended segments. + /// Absolute X corresponding to scanline index 0. + /// + /// Indicates whether opaque solid fast-path writes are allowed. + /// + /// Pre-converted opaque solid color. + public FillRasterizationState( + ImageFrame source, + BrushApplicator applicator, + int minX, + bool isSolidBrushWithoutBlending, + TPixel solidBrushColor) + { + this.Source = source; + this.Applicator = applicator; + this.MinX = minX; + this.IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending; + this.SolidBrushColor = solidBrushColor; + } + + /// + /// Gets the destination frame. + /// + public ImageFrame Source { get; } + + /// + /// Gets the brush applicator used for blended segments. + /// + public BrushApplicator Applicator { get; } + + /// + /// Gets the absolute X origin of the current scanline. + /// + public int MinX { get; } + + /// + /// Gets a value indicating whether opaque interior runs can be direct-filled. + /// + public bool IsSolidBrushWithoutBlending { get; } + + /// + /// Gets the pre-converted solid color used by the opaque fast path. + /// + public TPixel SolidBrushColor { get; } + } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index b82f1ef1..92f6f56e 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -16,19 +16,47 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal interface IDrawingBackend { /// - /// Rasterizes a path into scanline coverage. + /// Fills a path into the destination image using the given brush and drawing options. /// - /// The caller-provided mutable state type. + /// + /// This operation-level API keeps processors independent from scanline rasterization details, + /// allowing alternate backend implementations (for example GPU backends) to consume brush + /// and path data directly. + /// + /// The pixel format. + /// Active processing configuration. + /// Destination image frame. /// The path to rasterize. - /// Rasterizer options. + /// Brush used to shade covered pixels. + /// Graphics blending/composition options. + /// Rasterizer options. + /// Brush bounds used when creating the applicator. /// Allocator for temporary data. - /// Caller-owned mutable state passed to the scanline callback. - /// Scanline callback. - void RasterizePath( + public void FillPath( + Configuration configuration, + ImageFrame source, IPath path, - in RasterizerOptions options, + Brush brush, + in GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + Rectangle brushBounds, + MemoryAllocator allocator) + where TPixel : unmanaged, IPixel; + + /// + /// Rasterizes path coverage into a floating-point destination map. + /// + /// + /// Coverage values are written in local destination coordinates where (0,0) maps to + /// the top-left of . + /// + /// The path to rasterize. + /// Rasterizer options. + /// Allocator for temporary data. + /// Destination coverage map. + public void RasterizeCoverage( + IPath path, + in RasterizerOptions rasterizerOptions, MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct; + Buffer2D destination); } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index 88b1a5a0..0788cb6f 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Memory; @@ -21,6 +20,13 @@ internal class FillPathProcessor : ImageProcessor private readonly IPath path; private readonly Rectangle bounds; + /// + /// Initializes a new instance of the class. + /// + /// The processing configuration. + /// The processor definition. + /// The source image. + /// The source bounds. public FillPathProcessor( Configuration configuration, FillPathProcessor definition, @@ -47,14 +53,6 @@ protected override void OnFrameApply(ImageFrame source) GraphicsOptions graphicsOptions = this.definition.Options.GraphicsOptions; Brush brush = this.definition.Brush; - TPixel solidBrushColor = default; - bool isSolidBrushWithoutBlending = false; - if (IsSolidBrushWithoutBlending(graphicsOptions, brush, out SolidBrush? solidBrush)) - { - isSolidBrushWithoutBlending = true; - solidBrushColor = solidBrush.Color.ToPixel(); - } - // Align start/end positions. Rectangle interest = Rectangle.Intersect(this.bounds, source.Bounds); if (interest.Equals(Rectangle.Empty)) @@ -62,9 +60,6 @@ protected override void OnFrameApply(ImageFrame source) return; // No effect inside image; } - int minX = interest.Left; - - using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, this.bounds); MemoryAllocator allocator = this.Configuration.MemoryAllocator; IDrawingBackend drawingBackend = configuration.GetDrawingBackend(); RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; @@ -74,154 +69,16 @@ protected override void OnFrameApply(ImageFrame source) rasterizationMode, RasterizerSamplingOrigin.PixelBoundary); - RasterizationState state = new( + // The backend owns rasterization/compositing details. Processors only submit + // operation-level data (path, brush, options, bounds). + drawingBackend.FillPath( + configuration, source, - applicator, - minX, - isSolidBrushWithoutBlending, - solidBrushColor); - - drawingBackend.RasterizePath( this.path, + brush, + graphicsOptions, rasterizerOptions, - allocator, - ref state, - ProcessRasterizedScanline); - } - - private static bool IsSolidBrushWithoutBlending(GraphicsOptions options, Brush inputBrush, [NotNullWhen(true)] out SolidBrush? solidBrush) - { - solidBrush = inputBrush as SolidBrush; - - if (solidBrush == null) - { - return false; - } - - return options.IsOpaqueColorWithoutBlending(solidBrush.Color); - } - - private static void ProcessRasterizedScanline(int y, Span scanline, ref RasterizationState state) - { - if (state.IsSolidBrushWithoutBlending) - { - ApplyCoverageRunsForOpaqueSolidBrush(state.Source, state.Applicator, scanline, state.MinX, y, state.SolidBrushColor); - } - else - { - ApplyNonZeroCoverageRuns(state.Applicator, scanline, state.MinX, y); - } - } - - private static void ApplyNonZeroCoverageRuns(BrushApplicator applicator, Span scanline, int minX, int y) - { - int i = 0; - while (i < scanline.Length) - { - while (i < scanline.Length && scanline[i] <= 0F) - { - i++; - } - - int runStart = i; - while (i < scanline.Length && scanline[i] > 0F) - { - i++; - } - - int runLength = i - runStart; - if (runLength > 0) - { - applicator.Apply(scanline.Slice(runStart, runLength), minX + runStart, y); - } - } - } - - private static void ApplyCoverageRunsForOpaqueSolidBrush( - ImageFrame source, - BrushApplicator applicator, - Span scanline, - int minX, - int y, - TPixel solidBrushColor) - { - Span destinationRow = source.PixelBuffer.DangerousGetRowSpan(y).Slice(minX, scanline.Length); - int i = 0; - - while (i < scanline.Length) - { - while (i < scanline.Length && scanline[i] <= 0F) - { - i++; - } - - int runStart = i; - while (i < scanline.Length && scanline[i] > 0F) - { - i++; - } - - int runEnd = i; - if (runEnd <= runStart) - { - continue; - } - - int opaqueStart = runStart; - while (opaqueStart < runEnd && scanline[opaqueStart] < 1F) - { - opaqueStart++; - } - - if (opaqueStart > runStart) - { - int prefixLength = opaqueStart - runStart; - applicator.Apply(scanline.Slice(runStart, prefixLength), minX + runStart, y); - } - - int opaqueEnd = runEnd; - while (opaqueEnd > opaqueStart && scanline[opaqueEnd - 1] < 1F) - { - opaqueEnd--; - } - - if (opaqueEnd > opaqueStart) - { - destinationRow.Slice(opaqueStart, opaqueEnd - opaqueStart).Fill(solidBrushColor); - } - - if (runEnd > opaqueEnd) - { - int suffixLength = runEnd - opaqueEnd; - applicator.Apply(scanline.Slice(opaqueEnd, suffixLength), minX + opaqueEnd, y); - } - } - } - - private readonly struct RasterizationState - { - public RasterizationState( - ImageFrame source, - BrushApplicator applicator, - int minX, - bool isSolidBrushWithoutBlending, - TPixel solidBrushColor) - { - this.Source = source; - this.Applicator = applicator; - this.MinX = minX; - this.IsSolidBrushWithoutBlending = isSolidBrushWithoutBlending; - this.SolidBrushColor = solidBrushColor; - } - - public ImageFrame Source { get; } - - public BrushApplicator Applicator { get; } - - public int MinX { get; } - - public bool IsSolidBrushWithoutBlending { get; } - - public TPixel SolidBrushColor { get; } + this.bounds, + allocator); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs index 86baf90b..a8f60b24 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -3,7 +3,6 @@ using System.Numerics; using SixLabors.Fonts.Rendering; -using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing.Processors; diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs index f2b5e953..6436d67e 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -7,7 +7,6 @@ using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Memory; @@ -531,6 +530,11 @@ private Matrix3x2 ComputeTransform(in FontRectangle bounds) return Matrix3x2.CreateTranslation(translation) * Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point); } + /// + /// Rasterizes a glyph path to a local coverage map. + /// + /// The glyph path in destination coordinates. + /// A coverage buffer used by later text draw operations. private Buffer2D Render(IPath path) { // We need to offset the path now by the difference between the clamped location and the @@ -543,33 +547,29 @@ private Buffer2D Render(IPath path) RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; GraphicsOptions graphicsOptions = this.drawingOptions.GraphicsOptions; - RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; // Take the path inside the path builder, scan thing and generate a Buffer2D representing the glyph. Buffer2D buffer = this.memoryAllocator.Allocate2D(size.Width, size.Height, AllocationOptions.Clean); - TextRasterizationState state = new(buffer); RasterizerOptions rasterizerOptions = new( new Rectangle(0, 0, size.Width, size.Height), TextUtilities.MapFillRule(this.currentFillRule), rasterizationMode, samplingOrigin); - this.drawingBackend.RasterizePath( + // Request coverage generation from the configured backend. CPU backends will produce + // this via scanlines; future GPU backends can supply equivalent coverage by other means. + this.drawingBackend.RasterizeCoverage( offsetPath, rasterizerOptions, this.memoryAllocator, - ref state, - ProcessTextScanline); + buffer); return buffer; } - private static void ProcessTextScanline(int y, Span scanline, ref TextRasterizationState state) - { - Span destination = state.Buffer.DangerousGetRowSpan(y); - scanline.CopyTo(destination); - } - private void Dispose(bool disposing) { if (!this.isDisposed) @@ -613,13 +613,6 @@ public readonly void Dispose() } } - private readonly struct TextRasterizationState - { - public TextRasterizationState(Buffer2D buffer) => this.Buffer = buffer; - - public Buffer2D Buffer { get; } - } - private readonly struct CacheKey : IEquatable { public string Font { get; init; } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index 82eb9db5..3e66d92f 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -10,7 +10,6 @@ using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using SixLabors.PolygonClipper; using SkiaSharp; namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 10281b68..ffea506c 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -109,13 +109,24 @@ public void Rasterize( private sealed class RecordingDrawingBackend : IDrawingBackend { - public void RasterizePath( + public void FillPath( + Configuration configuration, + ImageFrame source, IPath path, - in RasterizerOptions options, + Brush brush, + in GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + Rectangle brushBounds, + MemoryAllocator allocator) + where TPixel : unmanaged, IPixel + { + } + + public void RasterizeCoverage( + IPath path, + in RasterizerOptions rasterizerOptions, MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct + Buffer2D destination) { } } From 48b4236cc38cff7711cb164c5c35316d1cbf36b9 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 00:27:43 +1000 Subject: [PATCH 29/35] Update ClippedShapeGenerator.cs --- .../PolygonGeometry/ClippedShapeGenerator.cs | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs index 286dfb01..d423b57a 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs @@ -54,18 +54,60 @@ public static ComplexPolygon GenerateClippedShapes( int index = 0; for (int i = 0; i < result.Count; i++) { - Contour contour = result[i]; - PointF[] points = new PointF[contour.Count]; + shapes[index++] = new Polygon(CreateContourPoints(result, i)); + } + + return new(shapes); + } - for (int j = 0; j < contour.Count; j++) + /// + /// Converts a PolygonClipper contour to ImageSharp points and normalizes winding for parent/child rings. + /// + /// The polygon containing the contour hierarchy. + /// The contour index to convert. + /// The converted point array. + private static PointF[] CreateContourPoints(PCPolygon polygon, int contourIndex) + { + Contour contour = polygon[contourIndex]; + PointF[] points = new PointF[contour.Count]; + bool reverse = ShouldReverseForNonZeroWinding(polygon, contourIndex); + + if (!reverse) + { + for (int i = 0; i < contour.Count; i++) { - Vertex vertex = contour[j]; - points[j] = new PointF((float)vertex.X, (float)vertex.Y); + Vertex vertex = contour[i]; + points[i] = new PointF((float)vertex.X, (float)vertex.Y); } - shapes[index++] = new Polygon(points); + return points; } - return new(shapes); + for (int sourceIndex = contour.Count - 1, targetIndex = 0; sourceIndex >= 0; sourceIndex--, targetIndex++) + { + Vertex vertex = contour[sourceIndex]; + points[targetIndex] = new PointF((float)vertex.X, (float)vertex.Y); + } + + return points; + } + + /// + /// Ensures child contours (holes/islands) use opposite winding to their direct parent. + /// This keeps clipped output deterministic when consumed with the NonZero fill rule. + /// + /// The polygon containing contour hierarchy information. + /// The contour index to inspect. + /// when the contour should be reversed. + private static bool ShouldReverseForNonZeroWinding(PCPolygon polygon, int contourIndex) + { + Contour contour = polygon[contourIndex]; + if (contour.ParentIndex is not int parentIndex || (uint)parentIndex >= (uint)polygon.Count) + { + return false; + } + + Contour parentContour = polygon[parentIndex]; + return contour.IsCounterClockwise() == parentContour.IsCounterClockwise(); } } From 8c8466648d9f28edf85ab37a5f8e5c21bf0a371d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 08:22:41 +1000 Subject: [PATCH 30/35] Update Issue_330.cs --- .../ImageSharp.Drawing.Tests/Issues/Issue_330.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs index 7d261782..26e151dd 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs @@ -21,22 +21,6 @@ public void OffsetTextOutlines(TestImageProvider provider) provider.RunValidatingProcessorTest(p => { - - //p.DrawText( - // new RichTextOptions(namefont) - // { - // VerticalAlignment = VerticalAlignment.Center, - // HorizontalAlignment = HorizontalAlignment.Center, - // TextAlignment = TextAlignment.Center, - // TextDirection = TextDirection.LeftToRight, - // Origin = new Point(1156, 713), - // }, - // "O", - // Brushes.Solid(Color.White), - // Pens.Solid(Color.Black, 5)); - - //return; - p.DrawText( new RichTextOptions(bibfont) { From 35acddcceb7e17f2c70f26ccb9fee3921ed8979e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 10:04:09 +1000 Subject: [PATCH 31/35] Remove project references --- ImageSharp.Drawing.sln | 16 -------------- .../ImageSharp.Drawing.csproj | 8 +++---- .../Drawing/DrawingRobustnessTests.cs | 22 +++++++++---------- 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 07251836..74e8e154 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -337,10 +337,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PolygonClipper", "..\..\SixLabors\PolygonClipper\src\PolygonClipper\PolygonClipper.csproj", "{5ED54794-99BF-5E50-A861-0BAAAC794E44}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp", "..\..\SixLabors\ImageSharp\src\ImageSharp\ImageSharp.csproj", "{AAF7501C-8537-7F13-5193-B538318BD071}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -363,14 +359,6 @@ 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 - {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5ED54794-99BF-5E50-A861-0BAAAC794E44}.Release|Any CPU.Build.0 = Release|Any CPU - {AAF7501C-8537-7F13-5193-B538318BD071}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AAF7501C-8537-7F13-5193-B538318BD071}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AAF7501C-8537-7F13-5193-B538318BD071}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AAF7501C-8537-7F13-5193-B538318BD071}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -398,17 +386,13 @@ Global {68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} - {5ED54794-99BF-5E50-A861-0BAAAC794E44} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} - {AAF7501C-8537-7F13-5193-B538318BD071} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 - ..\..\SixLabors\PolygonClipper\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{5ed54794-99bf-5e50-a861-0baaac794e44}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 - ..\..\SixLabors\ImageSharp\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{aaf7501c-8537-7f13-5193-b538318bd071}*SharedItemsImports = 5 EndGlobalSection GlobalSection(Performance) = preSolution HasPerformanceSessions = true diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 9eccafbf..c0d392bb 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -44,11 +44,9 @@ - - - - - + + + diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index 3e66d92f..6d230ee2 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#pragma warning disable xUnit1004 // Test methods should not be skipped using System.Numerics; using System.Runtime.InteropServices; using GeoJSON.Net.Feature; @@ -73,10 +74,10 @@ private static void CompareToSkiaResultsImpl(TestImageProvider provider, appendSourceFileOrDescription: false); ImageSimilarityReport result = ImageComparer.Exact.CompareImagesOrFrames(image, skResultImage); - throw new Exception(result.DifferencePercentageString); + throw new ImagesSimilarityException(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) { @@ -107,14 +108,14 @@ public void LargeGeoJson_Lines(TestImageProvider provider, string geoJso [WithSolidFilledImages(7200, 3300, "Black", PixelTypes.Rgba32)] public void LargeGeoJson_States_Fill(TestImageProvider provider) { - using Image image = this.FillGeoJsonPolygons(provider, TestImages.GeoJson.States, 16, new Vector2(60), new Vector2(0, -1000)); + using Image image = FillGeoJsonPolygons(provider, TestImages.GeoJson.States, true, new Vector2(60), new Vector2(0, -1000)); ImageComparer comparer = ImageComparer.TolerantPercentage(0.001f); image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); image.CompareToReferenceOutput(comparer, provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } - private Image FillGeoJsonPolygons(TestImageProvider provider, string geoJsonFile, int aa, Vector2 scale, Vector2 pixelOffset) + private static Image FillGeoJsonPolygons(TestImageProvider provider, string geoJsonFile, bool aa, Vector2 scale, Vector2 pixelOffset) { string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile)); @@ -123,7 +124,7 @@ private Image FillGeoJsonPolygons(TestImageProvider provider, st Image image = provider.GetImage(); DrawingOptions options = new() { - GraphicsOptions = new GraphicsOptions() { Antialias = aa > 0 }, + GraphicsOptions = new GraphicsOptions() { Antialias = aa }, }; Random rnd = new(42); byte[] rgb = new byte[3]; @@ -131,7 +132,7 @@ private Image FillGeoJsonPolygons(TestImageProvider provider, st { rnd.NextBytes(rgb); - Color color = Color.FromPixel(new Rgb24(rgb[0], rgb[1], rgb[2])); + Color color = Color.FromPixel(new Rgb24(rgb[0], rgb[1], rgb[2])); image.Mutate(c => c.FillPolygon(options, color, loop)); } @@ -201,9 +202,7 @@ public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provi image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } -#pragma warning disable xUnit1004 // Test methods should not be skipped [Theory(Skip = "For local experiments only")] -#pragma warning restore xUnit1004 // Test methods should not be skipped [InlineData(0)] [InlineData(5000)] [InlineData(9000)] @@ -259,7 +258,7 @@ public void Missisippi_Skia(int offset) data.SaveTo(fs); } - [Theory] + [Theory(Skip = "For local experiments only")] [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] public void LargeGeoJson_States_Separate_Benchmark(TestImageProvider provider, int thickness) { @@ -286,7 +285,7 @@ public void LargeGeoJson_States_Separate_Benchmark(TestImageProvider pro image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } - [Theory] + [Theory(Skip = "For local experiments only")] [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider, int thickness) { @@ -320,7 +319,7 @@ public void LargeGeoJson_States_All_Benchmark(TestImageProvider provider image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } - [Theory] + [Theory(Skip = "For local experiments only")] [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] public void LargeStar_Benchmark(TestImageProvider provider, int thickness) { @@ -362,3 +361,4 @@ private static List CreateStarPolygon(int vertexCount, float radius) return [[.. contour]]; } } +#pragma warning restore xUnit1004 // Test methods should not be skipped From f1d51f99973d2b8970c22b74aab9816b9220830b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 10:39:26 +1000 Subject: [PATCH 32/35] Update reference images to match new rasterizer --- .../Drawing/FillPolygonTests.cs | 26 +++++++-------- .../MemoryAllocatorValidator.cs | 31 ++++++++++-------- .../ClipTests/Clip_offset_x-20_y-100.png | 4 +-- .../ClipTests/Clip_offset_x-20_y-20.png | 4 +-- .../Drawing/ClipTests/Clip_offset_x0_y0.png | 4 +-- .../Drawing/ClipTests/Clip_offset_x20_y20.png | 4 +-- .../Drawing/ClipTests/Clip_offset_x40_y60.png | 4 +-- .../DrawBeziers_HotPink_A150_T5.png | 4 +-- .../DrawBeziers_HotPink_A255_T5.png | 4 +-- .../DrawBeziers_Red_A255_T3.png | 4 +-- .../DrawBeziers_White_A255_T1.5.png | 4 +-- .../DrawBeziers_White_A255_T15.png | 4 +-- .../DrawComplexPolygon.png | Bin 6176 -> 129 bytes .../DrawComplexPolygon__Dashed.png | 4 +-- .../DrawComplexPolygon__Overlap.png | 4 +-- .../DrawComplexPolygon__Transparent.png | Bin 5640 -> 129 bytes ...Dot_Rgba32_Black_A(1)_T(5)_NoAntialias.png | Bin 1268 -> 128 bytes ...ot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png | Bin 1282 -> 129 bytes ...ash_Rgba32_White_A(1)_T(5)_NoAntialias.png | Bin 1381 -> 129 bytes ...gba32_LightGreen_A(1)_T(5)_NoAntialias.png | Bin 1114 -> 128 bytes ...nes_EndCapButt_Rgba32_Yellow_A(1)_T(5).png | 4 +-- ...es_EndCapRound_Rgba32_Yellow_A(1)_T(5).png | 4 +-- ...s_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png | 4 +-- ...intStyleMiter_Rgba32_Yellow_A(1)_T(10).png | 4 +-- ...intStyleRound_Rgba32_Yellow_A(1)_T(10).png | 4 +-- ...ntStyleSquare_Rgba32_Yellow_A(1)_T(10).png | 4 +-- ...awLines_Simple_Bgr24_Yellow_A(1)_T(10).png | 4 +-- ...Lines_Simple_Rgba32_White_A(0.6)_T(10).png | 4 +-- ...wLines_Simple_Rgba32_White_A(1)_T(2.5).png | 4 +-- ...ple_Rgba32_White_A(1)_T(5)_NoAntialias.png | 4 +-- .../DrawCircleUsingAddArc_359.png | 4 +-- .../DrawCircleUsingAddArc_360.png | 4 +-- .../DrawCircleUsingArcTo_False.png | 4 +-- .../DrawCircleUsingArcTo_True.png | 4 +-- .../DrawPathTests/DrawPathClippedOnTop.png | 4 +-- .../DrawPath_HotPink_A150_T5.png | 4 +-- .../DrawPath_HotPink_A255_T5.png | 4 +-- .../DrawPathTests/DrawPath_Red_A255_T3.png | 4 +-- .../DrawPath_White_A255_T1.5.png | 4 +-- .../DrawPathTests/DrawPath_White_A255_T15.png | 4 +-- ...endingOffEdgeOfImageShouldNotBeCropped.png | 4 +-- .../DrawPolygon_Bgr24_Yellow_A(1)_T(10).png | Bin 4392 -> 129 bytes .../DrawPolygon_Rgba32_White_A(0.6)_T(10).png | Bin 4448 -> 129 bytes .../DrawPolygon_Rgba32_White_A(1)_T(2.5).png | Bin 4491 -> 129 bytes ...gon_Rgba32_White_A(1)_T(5)_NoAntialias.png | Bin 1543 -> 129 bytes ...sformed_Rgba32_BasicTestPattern250x350.png | 4 +-- ...sformed_Rgba32_BasicTestPattern100x100.png | 4 +-- ...Json_Mississippi_LinesScaled_Scale(10).png | 4 +-- ...oJson_Mississippi_LinesScaled_Scale(3).png | 4 +-- ...oJson_Mississippi_LinesScaled_Scale(5).png | 4 +-- ...oJson_Mississippi_Lines_PixelOffset(0).png | 4 +-- ...on_Mississippi_Lines_PixelOffset(5500).png | 4 +-- .../LargeGeoJson_States_Fill.png | 4 +-- .../ComplexPolygon_SolidFill.png | Bin 2334 -> 129 bytes .../ComplexPolygon_SolidFill__Overlap.png | Bin 3226 -> 129 bytes .../ComplexPolygon_SolidFill__Transparent.png | Bin 2299 -> 129 bytes ...CircleOutsideBoundsDrawingArea_(0_-20).png | 4 +-- ...CircleOutsideBoundsDrawingArea_(0_-49).png | Bin 204 -> 128 bytes ...awCircleOutsideBoundsDrawingArea_(0_0).png | 4 +-- ...rcleOutsideBoundsDrawingArea_(110_-20).png | 4 +-- ...CircleOutsideBoundsDrawingArea_(110_0).png | 4 +-- ...wCircleOutsideBoundsDrawingArea_(99_0).png | 4 +-- .../FillPathTests/FillPathArcToAlternates.png | 4 +-- .../FillPathTests/FillPathCanvasArcs.png | 4 +-- .../Drawing/FillPathTests/FillPathSVGArcs.png | 4 +-- ...everse(True)_IntersectionRule(EvenOdd).png | 4 +-- ...everse(True)_IntersectionRule(Nonzero).png | Bin 256 -> 128 bytes ...olygon_ImageBrush_Rect_Rgba32_Car_rect.png | 4 +-- ...ygon_ImageBrush_Rect_Rgba32_ducky_rect.png | 4 +-- .../FillPolygon_ImageBrush_Rgba32_Car.png | Bin 14043 -> 130 bytes .../FillPolygon_ImageBrush_Rgba32_ducky.png | Bin 18032 -> 130 bytes .../FillPolygon_Pattern_Rgba32.png | Bin 1307 -> 129 bytes .../FillPolygon_Solid_Basic_aa0.png | Bin 148 -> 127 bytes .../FillPolygon_Solid_Basic_aa16.png | Bin 160 -> 128 bytes .../FillPolygon_Solid_Basic_aa8.png | Bin 160 -> 128 bytes .../FillPolygon_Solid_Bgr24_Yellow_A1.png | Bin 2504 -> 129 bytes .../FillPolygon_Solid_Rgba32_White_A0.6.png | Bin 2443 -> 129 bytes .../FillPolygon_Solid_Rgba32_White_A1.png | Bin 2532 -> 129 bytes ...ygon_Solid_Rgba32_White_A1_NoAntialias.png | Bin 980 -> 128 bytes ...sformed_Rgba32_BasicTestPattern250x350.png | 4 +-- .../FillPolygon_StarCircle.png | 4 +-- ...on_StarCircle_AllOperations_Difference.png | 4 +-- ..._StarCircle_AllOperations_Intersection.png | 4 +-- ...Polygon_StarCircle_AllOperations_Union.png | 4 +-- ...llPolygon_StarCircle_AllOperations_Xor.png | 2 +- ...verse(False)_IntersectionRule(EvenOdd).png | 4 +-- ...verse(False)_IntersectionRule(Nonzero).png | 4 +-- ...everse(True)_IntersectionRule(EvenOdd).png | 4 +-- ...everse(True)_IntersectionRule(Nonzero).png | 4 +-- ...uration_Rgba32_BasicTestPattern100x100.png | 4 +-- ...sformed_Rgba32_BasicTestPattern100x100.png | 4 +-- .../Fill_RegularPolygon_V(3)_R(50)_Ang(0).png | Bin 1654 -> 129 bytes ...ll_RegularPolygon_V(3)_R(60)_Ang(-180).png | Bin 1879 -> 129 bytes ...Fill_RegularPolygon_V(3)_R(60)_Ang(20).png | Bin 1991 -> 129 bytes .../Fill_RegularPolygon_V(5)_R(70)_Ang(0).png | Bin 2451 -> 129 bytes ...ll_RegularPolygon_V(7)_R(80)_Ang(-180).png | 4 +-- ...ExistingBackground_Rgba32_Blank200x200.png | 4 +-- .../FilledBezier_Rgba32_Blank500x500.png | 4 +-- ...lledPolygonOpacity_Rgba32_Blank500x500.png | 4 +-- ...Ellipse_composition-Clear_blending-Add.png | Bin 682 -> 128 bytes ...ipse_composition-Clear_blending-Darken.png | Bin 682 -> 128 bytes ...e_composition-Clear_blending-HardLight.png | Bin 682 -> 128 bytes ...pse_composition-Clear_blending-Lighten.png | Bin 682 -> 128 bytes ...se_composition-Clear_blending-Multiply.png | Bin 682 -> 128 bytes ...ipse_composition-Clear_blending-Normal.png | Bin 682 -> 128 bytes ...pse_composition-Clear_blending-Overlay.png | Bin 682 -> 128 bytes ...ipse_composition-Clear_blending-Screen.png | Bin 682 -> 128 bytes ...se_composition-Clear_blending-Subtract.png | Bin 682 -> 128 bytes ...ipse_composition-DestAtop_blending-Add.png | Bin 2222 -> 129 bytes ...e_composition-DestAtop_blending-Darken.png | Bin 2117 -> 129 bytes ...omposition-DestAtop_blending-HardLight.png | Bin 2212 -> 129 bytes ..._composition-DestAtop_blending-Lighten.png | Bin 2209 -> 129 bytes ...composition-DestAtop_blending-Multiply.png | Bin 2128 -> 129 bytes ...e_composition-DestAtop_blending-Normal.png | Bin 2190 -> 129 bytes ..._composition-DestAtop_blending-Overlay.png | Bin 2183 -> 129 bytes ...e_composition-DestAtop_blending-Screen.png | Bin 2217 -> 129 bytes ...composition-DestAtop_blending-Subtract.png | Bin 2138 -> 129 bytes ...llipse_composition-DestIn_blending-Add.png | Bin 1207 -> 129 bytes ...pse_composition-DestIn_blending-Darken.png | Bin 1207 -> 129 bytes ..._composition-DestIn_blending-HardLight.png | Bin 1207 -> 129 bytes ...se_composition-DestIn_blending-Lighten.png | Bin 1207 -> 129 bytes ...e_composition-DestIn_blending-Multiply.png | Bin 1207 -> 129 bytes ...pse_composition-DestIn_blending-Normal.png | Bin 1207 -> 129 bytes ...se_composition-DestIn_blending-Overlay.png | Bin 1207 -> 129 bytes ...pse_composition-DestIn_blending-Screen.png | Bin 1207 -> 129 bytes ...e_composition-DestIn_blending-Subtract.png | Bin 1207 -> 129 bytes ...Ellipse_composition-SrcIn_blending-Add.png | Bin 1310 -> 129 bytes ...ipse_composition-SrcIn_blending-Darken.png | Bin 1310 -> 129 bytes ...e_composition-SrcIn_blending-HardLight.png | Bin 1310 -> 129 bytes ...pse_composition-SrcIn_blending-Lighten.png | Bin 1310 -> 129 bytes ...se_composition-SrcIn_blending-Multiply.png | Bin 1310 -> 129 bytes ...ipse_composition-SrcIn_blending-Normal.png | Bin 1310 -> 129 bytes ...pse_composition-SrcIn_blending-Overlay.png | Bin 1310 -> 129 bytes ...ipse_composition-SrcIn_blending-Screen.png | Bin 1310 -> 129 bytes ...se_composition-SrcIn_blending-Subtract.png | Bin 1310 -> 129 bytes ...llipse_composition-SrcOut_blending-Add.png | Bin 1638 -> 129 bytes ...pse_composition-SrcOut_blending-Darken.png | Bin 1638 -> 129 bytes ..._composition-SrcOut_blending-HardLight.png | Bin 1638 -> 129 bytes ...se_composition-SrcOut_blending-Lighten.png | Bin 1638 -> 129 bytes ...e_composition-SrcOut_blending-Multiply.png | Bin 1638 -> 129 bytes ...pse_composition-SrcOut_blending-Normal.png | Bin 1638 -> 129 bytes ...se_composition-SrcOut_blending-Overlay.png | Bin 1638 -> 129 bytes ...pse_composition-SrcOut_blending-Screen.png | Bin 1638 -> 129 bytes ...e_composition-SrcOut_blending-Subtract.png | Bin 1638 -> 129 bytes ...edEllipse_composition-Src_blending-Add.png | Bin 2098 -> 129 bytes ...llipse_composition-Src_blending-Darken.png | Bin 2098 -> 129 bytes ...pse_composition-Src_blending-HardLight.png | Bin 2098 -> 129 bytes ...lipse_composition-Src_blending-Lighten.png | Bin 2098 -> 129 bytes ...ipse_composition-Src_blending-Multiply.png | Bin 2098 -> 129 bytes ...llipse_composition-Src_blending-Normal.png | Bin 2098 -> 129 bytes ...lipse_composition-Src_blending-Overlay.png | Bin 2098 -> 129 bytes ...llipse_composition-Src_blending-Screen.png | Bin 2098 -> 129 bytes ...ipse_composition-Src_blending-Subtract.png | Bin 2098 -> 129 bytes ...Ellipse_composition-Clear_blending-Add.png | Bin 670 -> 128 bytes ...ipse_composition-Clear_blending-Darken.png | Bin 670 -> 128 bytes ...e_composition-Clear_blending-HardLight.png | Bin 670 -> 128 bytes ...pse_composition-Clear_blending-Lighten.png | Bin 670 -> 128 bytes ...se_composition-Clear_blending-Multiply.png | Bin 670 -> 128 bytes ...ipse_composition-Clear_blending-Normal.png | Bin 670 -> 128 bytes ...pse_composition-Clear_blending-Overlay.png | Bin 670 -> 128 bytes ...ipse_composition-Clear_blending-Screen.png | Bin 670 -> 128 bytes ...se_composition-Clear_blending-Subtract.png | Bin 670 -> 128 bytes ...ipse_composition-DestAtop_blending-Add.png | Bin 705 -> 128 bytes ...e_composition-DestAtop_blending-Darken.png | Bin 705 -> 128 bytes ...omposition-DestAtop_blending-HardLight.png | Bin 705 -> 128 bytes ..._composition-DestAtop_blending-Lighten.png | Bin 705 -> 128 bytes ...composition-DestAtop_blending-Multiply.png | Bin 705 -> 128 bytes ...e_composition-DestAtop_blending-Normal.png | Bin 705 -> 128 bytes ..._composition-DestAtop_blending-Overlay.png | Bin 705 -> 128 bytes ...e_composition-DestAtop_blending-Screen.png | Bin 705 -> 128 bytes ...composition-DestAtop_blending-Subtract.png | Bin 705 -> 128 bytes ...llipse_composition-DestIn_blending-Add.png | Bin 670 -> 128 bytes ...pse_composition-DestIn_blending-Darken.png | Bin 670 -> 128 bytes ..._composition-DestIn_blending-HardLight.png | Bin 670 -> 128 bytes ...se_composition-DestIn_blending-Lighten.png | Bin 670 -> 128 bytes ...e_composition-DestIn_blending-Multiply.png | Bin 670 -> 128 bytes ...pse_composition-DestIn_blending-Normal.png | Bin 670 -> 128 bytes ...se_composition-DestIn_blending-Overlay.png | Bin 670 -> 128 bytes ...pse_composition-DestIn_blending-Screen.png | Bin 670 -> 128 bytes ...e_composition-DestIn_blending-Subtract.png | Bin 670 -> 128 bytes ...Ellipse_composition-SrcIn_blending-Add.png | Bin 670 -> 128 bytes ...ipse_composition-SrcIn_blending-Darken.png | Bin 670 -> 128 bytes ...e_composition-SrcIn_blending-HardLight.png | Bin 670 -> 128 bytes ...pse_composition-SrcIn_blending-Lighten.png | Bin 670 -> 128 bytes ...se_composition-SrcIn_blending-Multiply.png | Bin 670 -> 128 bytes ...ipse_composition-SrcIn_blending-Normal.png | Bin 670 -> 128 bytes ...pse_composition-SrcIn_blending-Overlay.png | Bin 670 -> 128 bytes ...ipse_composition-SrcIn_blending-Screen.png | Bin 670 -> 128 bytes ...se_composition-SrcIn_blending-Subtract.png | Bin 670 -> 128 bytes ...llipse_composition-SrcOut_blending-Add.png | Bin 705 -> 128 bytes ...pse_composition-SrcOut_blending-Darken.png | Bin 705 -> 128 bytes ..._composition-SrcOut_blending-HardLight.png | Bin 705 -> 128 bytes ...se_composition-SrcOut_blending-Lighten.png | Bin 705 -> 128 bytes ...e_composition-SrcOut_blending-Multiply.png | Bin 705 -> 128 bytes ...pse_composition-SrcOut_blending-Normal.png | Bin 705 -> 128 bytes ...se_composition-SrcOut_blending-Overlay.png | Bin 705 -> 128 bytes ...pse_composition-SrcOut_blending-Screen.png | Bin 705 -> 128 bytes ...e_composition-SrcOut_blending-Subtract.png | Bin 705 -> 128 bytes ...ntEllipse_composition-Src_blending-Add.png | Bin 705 -> 128 bytes ...llipse_composition-Src_blending-Darken.png | Bin 705 -> 128 bytes ...pse_composition-Src_blending-HardLight.png | Bin 705 -> 128 bytes ...lipse_composition-Src_blending-Lighten.png | Bin 705 -> 128 bytes ...ipse_composition-Src_blending-Multiply.png | Bin 705 -> 128 bytes ...llipse_composition-Src_blending-Normal.png | Bin 705 -> 128 bytes ...lipse_composition-Src_blending-Overlay.png | Bin 705 -> 128 bytes ...llipse_composition-Src_blending-Screen.png | Bin 705 -> 128 bytes ...ipse_composition-Src_blending-Subtract.png | Bin 705 -> 128 bytes ...100_(0,0,0,255)_RichText-Path-(spiral).png | 4 +-- ...0_(0,0,0,255)_RichText-Path-(triangle).png | 4 +-- ...350_(0,0,0,255)_RichText-Path-(circle).png | 4 +-- ...zontal_Rgba32_Blank100x100_type-spiral.png | 4 +-- ...ntal_Rgba32_Blank120x120_type-triangle.png | 4 +-- ...zontal_Rgba32_Blank350x350_type-circle.png | 4 +-- ...ical_Rgba32_Blank250x250_type-triangle.png | 4 +-- ...rtical_Rgba32_Blank350x350_type-circle.png | 4 +-- ...anDrawTextVertical2_Rgba32_Blank48x935.png | 4 +-- ...wTextVerticalMixed2_Rgba32_Blank48x839.png | 4 +-- ...wTextVerticalMixed_Rgba32_Blank500x400.png | 4 +-- ...anDrawTextVertical_Rgba32_Blank500x400.png | 4 +-- ...lTextVerticalMixed_Rgba32_Blank500x400.png | 4 +-- ...anFillTextVertical_Rgba32_Blank500x400.png | 4 +-- .../CanRenderTextOutOfBoundsIssue301.png | 4 +-- ...penSans-Regular.ttf)-S(32)-A(75)-Quic).png | 4 +-- ...penSans-Regular.ttf)-S(40)-A(90)-Quic).png | 4 +-- ...-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png | 4 +-- ...-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png | 4 +-- ...x200_(0,0,0,255)_RichText-Arabic-F(32).png | 4 +-- ...x300_(0,0,0,255)_RichText-Arabic-F(40).png | 4 +-- ...200_(0,0,0,255)_RichText-Rainbow-F(32).png | 4 +-- ...300_(0,0,0,255)_RichText-Rainbow-F(40).png | 4 +-- ...olid500x200_(0,0,0,255)_RichText-F(32).png | 4 +-- ...olid500x300_(0,0,0,255)_RichText-F(40).png | 4 +-- ...5,255,255,255)_ColorFontsEnabled-False.png | 4 +-- ...55,255,255,255)_ColorFontsEnabled-True.png | 4 +-- ..._Rgba32_Solid400x200_(255,255,255,255).png | 4 +-- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 +-- ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 2 +- ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 +-- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 +-- ...n_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 +-- ...pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 +-- ...ntShapesAreRenderedCorrectly_LargeText.png | 4 +-- ..._OpenSans-Regular.ttf-50-Sphi-(150,50).png | 4 +-- ...)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png | 4 +-- ...5,255)_OpenSans-Regular.ttf-50-i-(0,0).png | 4 +-- ...55)_OpenSans-Regular.ttf-20-Sphi-(0,0).png | 4 +-- ...55)_OpenSans-Regular.ttf-50-Sphi-(0,0).png | 4 +-- ...linespacing_1.5_linecount_3_wrap_False.png | 4 +-- ..._linespacing_1.5_linecount_3_wrap_True.png | 4 +-- ...g_linespacing_1_linecount_5_wrap_False.png | 4 +-- ...ng_linespacing_1_linecount_5_wrap_True.png | 4 +-- ...g_linespacing_2_linecount_2_wrap_False.png | 4 +-- ...ng_linespacing_2_linecount_2_wrap_True.png | 4 +-- ...egular.ttf)-S(50)-A(45)-Sphi-(550,550).png | 4 +-- ...pleAB.woff)-S(50)-A(45)-ABAB-(100,100).png | 4 +-- ...egular.ttf)-S(20)-A(45)-Sphi-(200,200).png | 4 +-- ...ans-Regular.ttf)-S(50)-A(45)-i-(25,25).png | 4 +-- ...ular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png | 4 +-- ...eAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png | 4 +-- ...lar.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png | 4 +-- ...-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png | 4 +-- ...gba32_Solid1000x1000_(255,255,255,255).png | 4 +-- ...sitioningIsRobust_OpenSans-Regular.ttf.png | 4 +-- ...d300x300_(255,255,255,255)_scale-0.003.png | 4 +-- ...lid300x300_(255,255,255,255)_scale-0.3.png | 4 +-- ...lid300x300_(255,255,255,255)_scale-0.7.png | 4 +-- ...Solid300x300_(255,255,255,255)_scale-1.png | 4 +-- ...Solid300x300_(255,255,255,255)_scale-3.png | 4 +-- ...Rgba32_Solid2084x2084_(138,43,226,255).png | 4 +-- ...d492x360_(255,255,255,255)_ColrV1-draw.png | 4 +-- ...d492x360_(255,255,255,255)_ColrV1-fill.png | 4 +-- ...olid492x360_(255,255,255,255)_Svg-draw.png | 4 +-- ...olid492x360_(255,255,255,255)_Svg-fill.png | 4 +-- ...vgPath_Rgba32_Blank100x100_type-arrows.png | 4 +-- ...erSvgPath_Rgba32_Blank110x50_type-wave.png | 4 +-- ...derSvgPath_Rgba32_Blank110x70_type-zag.png | 4 +-- ...SvgPath_Rgba32_Blank500x400_type-bumpy.png | 4 +-- ..._Rgba32_Blank500x400_type-chopped_oval.png | 4 +-- ...gPath_Rgba32_Blank500x400_type-pie_big.png | 4 +-- ...ath_Rgba32_Blank500x400_type-pie_small.png | 4 +-- 280 files changed, 311 insertions(+), 306 deletions(-) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index facad777..3fc6f89f 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -182,25 +182,25 @@ public void FillPolygon_StarCircle(TestImageProvider provider) } [Theory] - [WithSolidFilledImages(128, 128, "Black", PixelTypes.Rgba32)] - public void FillPolygon_StarCircle_AllOperations(TestImageProvider provider) + [WithSolidFilledImages(128, 128, "Black", PixelTypes.Rgba32, BooleanOperation.Intersection)] + [WithSolidFilledImages(128, 128, "Black", PixelTypes.Rgba32, BooleanOperation.Union)] + [WithSolidFilledImages(128, 128, "Black", PixelTypes.Rgba32, BooleanOperation.Difference)] + [WithSolidFilledImages(128, 128, "Black", PixelTypes.Rgba32, BooleanOperation.Xor)] + public void FillPolygon_StarCircle_AllOperations(TestImageProvider provider, BooleanOperation operation) { IPath circle = new EllipsePolygon(36, 36, 36).Translate(28, 28); Star star = new(64, 64, 5, 24, 64); // See http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/ClipType.htm for reference. - foreach (BooleanOperation operation in (BooleanOperation[])Enum.GetValues(typeof(BooleanOperation))) - { - ShapeOptions options = new() { BooleanOperation = operation }; - IPath shape = star.Clip(options, circle); + ShapeOptions options = new() { BooleanOperation = operation }; + IPath shape = star.Clip(options, circle); - provider.RunValidatingProcessorTest( - c => c.Fill(Color.DeepPink, circle).Fill(Color.LightGray, star).Fill(Color.ForestGreen, shape), - testOutputDetails: operation.ToString(), - comparer: ImageComparer.TolerantPercentage(0.01F), - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } + provider.RunValidatingProcessorTest( + c => c.Fill(Color.DeepPink, circle).Fill(Color.LightGray, star).Fill(Color.ForestGreen, shape), + testOutputDetails: operation.ToString(), + comparer: ImageComparer.TolerantPercentage(0.01F), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); } [Theory] diff --git a/tests/ImageSharp.Drawing.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Drawing.Tests/MemoryAllocatorValidator.cs index a760d29e..d6e8a0f8 100644 --- a/tests/ImageSharp.Drawing.Tests/MemoryAllocatorValidator.cs +++ b/tests/ImageSharp.Drawing.Tests/MemoryAllocatorValidator.cs @@ -20,20 +20,13 @@ static MemoryAllocatorValidator() private static void MemoryDiagnostics_MemoryReleased() { TestMemoryDiagnostics backing = LocalInstance.Value; - if (backing != null) - { - backing.TotalRemainingAllocated--; - } + backing?.OnReleased(); } private static void MemoryDiagnostics_MemoryAllocated() { TestMemoryDiagnostics backing = LocalInstance.Value; - if (backing != null) - { - backing.TotalAllocated++; - backing.TotalRemainingAllocated++; - } + backing?.OnAllocated(); } public static TestMemoryDiagnostics MonitorAllocations() @@ -48,17 +41,29 @@ public static TestMemoryDiagnostics MonitorAllocations() public static void ValidateAllocations(int expectedAllocationCount = 0) => LocalInstance.Value?.Validate(expectedAllocationCount); - public class TestMemoryDiagnostics : IDisposable + public sealed class TestMemoryDiagnostics : IDisposable { - public int TotalAllocated { get; set; } + private int totalAllocated; + private int totalRemainingAllocated; + + public int TotalAllocated => Volatile.Read(ref this.totalAllocated); + + public int TotalRemainingAllocated => Volatile.Read(ref this.totalRemainingAllocated); + + internal void OnAllocated() + { + Interlocked.Increment(ref this.totalAllocated); + Interlocked.Increment(ref this.totalRemainingAllocated); + } - public int TotalRemainingAllocated { get; set; } + internal void OnReleased() + => Interlocked.Decrement(ref this.totalRemainingAllocated); public void Validate(int expectedAllocationCount) { int count = this.TotalRemainingAllocated; bool pass = expectedAllocationCount == count; - Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}"); + Assert.True(pass, $"Expected {expectedAllocationCount} undisposed buffers but found {count}"); } public void Dispose() diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png index 6195a230..9a791fde 100644 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png +++ b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ec585523a17e1780408ca643f38261159cd331d948fefd120241482731cb0ed -size 5702 +oid sha256:e44f9598e2f6c9a5f3aac6dcd73edb1a818d1e864fd154371b0d54ca075aa05e +size 3694 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png index 1d55c599..d60adda7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png +++ b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cba43d7b20b634630515c4be65b2e3adbfbfed0d824f649e166856abf385761c -size 4140 +oid sha256:ecf41b05a42a6f275524131bcaf89298a059e2a0aabbaf2348ce2ad036197ede +size 5013 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png index cc0d01fc..ee0f3d4f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png +++ b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2521a16b8378a6a9da1a1e2af9506c62a5f149c9a04f8f4fa0efcaa3030ed7e -size 4643 +oid sha256:15622bb81ee71518a2fa56e758f2df5fddee69e0a01f2617e0f67201e930553f +size 5356 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png index 76c34f9d..55715e2a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png +++ b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:141c0538433a547e09a34690915b1bd84fb2c35f8f71e39075cbcc2db8a12336 -size 4585 +oid sha256:3739ab0effb4caf5e84add7c0c1d1cc3bbec0c1fb7e7d7826a818bf0976fbe4f +size 5446 diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png index d4e811f5..3d61682c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png +++ b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a30ca888127436a977aef4672d5cc4af3e6fd636a05844fcc08430ccf7ebdd9 -size 4515 +oid sha256:bd217c38b95baedd42064b696d975805120d91561c8d77248b749d35c1fbcf75 +size 2315 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A150_T5.png index 9a7d7ac0..13f33766 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1efde4790cdd3aa5ff66434881ad8df5e3025218fdea728da0f0ac810467e037 -size 4524 +oid sha256:174c98c137feb54c05fa59823af2a09fdade5d2ceb59e70e37c507dafcf6118f +size 4334 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A255_T5.png index e42f359b..84a84ba7 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7ee0aed826a7509c54542ae642099c3a177118ebcdbc7bb6f284d063d43714e -size 8525 +oid sha256:6e531b54fbfcbcba2df2a3373734314a1644541a2faf8c15420c53a959bb57a7 +size 4613 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_Red_A255_T3.png index de2c975e..6eaaee08 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f87aaba20d7a47a71769f621bdd3e0301e0763e84c75f7fe1769386a7ea0f13 -size 4659 +oid sha256:d1839a05c5eeb8c7b90758a7b9c3d2919a726a78eefd9de2728f5edf37a2018a +size 4613 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T1.5.png index 01dfc2c2..8c376a11 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:856bd9e4cb88d85284b8cbec4fdca18b93462f8b4617b0e1a94590ed0eb3fd30 -size 4659 +oid sha256:1057cf06d0acc8dd05883c14475210953827b0cf8cde751c8dc2bc8eedc6554d +size 4613 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T15.png index 01dfc2c2..8c376a11 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:856bd9e4cb88d85284b8cbec4fdca18b93462f8b4617b0e1a94590ed0eb3fd30 -size 4659 +oid sha256:1057cf06d0acc8dd05883c14475210953827b0cf8cde751c8dc2bc8eedc6554d +size 4613 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png index 2ac2d03af3e7348f4745542f6a5b3dfe10e4960a..0e1070e7bbcc5c33336d9e0d9a9a83d6fc1d0975 100644 GIT binary patch literal 129 zcmWN?K@x)?3;@8pukZsEhy=mkkPs>}ZFL&-(AU?!>?L1q`)AyGKXf(b+4|#Eb-Le9 zns|R3J%r8WhP{>ZV{o%?12Pn_DnW8pqtGlFCnH{INIVWW&lok L(Dswj`w)gd@4qN@ literal 6176 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 npa1{27#Kb?|MYR_V=#E^@dvVgCYJyJ literal 5640 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 XfJ|9Z5#(3C9Af}2E{P(I&HVLWMi#zi diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png index 1bf69b24e3367aab122bc2e5bc17cd0420914044..cb1e62505580bae0ad37b1a9a6f205463a6f68c3 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?Ucm$k8OPW5paOgrdD>%EghfV9-LO0*tB} M8@JiNEIKXF~;cZrqkl!@+g#|lk!k02HV1PpkY{)wmeD;=y-{) zB-6|(bn+91#E@aF)2?5yziQ!-0?)$DjeqryFfJS&|wW6$$|Nq#g^&OL4wD zBl2h_0IYiO$qBtY#R)t~j5&CijsX~eTft|*0B==G;HP2yIOd^(CQf1mK8v?MJoIq3 zX=*h2^ows-7AIpfgtdQfU6yxl#Jx1#j2-M7*wepv=!;Wvl+}It9#9V>wyt8>CJ)o; z9JRO7atE(Vc8Sd~?7cChHOy)!Qk_j}lQ^Y;*7pb9Lr$E^ zHE!GO(a5V&PK%=17MOSVBg4YGs+>Xrv76wCg<++|bM84u1w#thm`a~=38JRl`1EnQ zb@w&R!o%hIavi!(1F4AyY7E7WhKK{w-;^UkOo|On61&Y$wwDbT-N`E^4zT{W4*n@OdC^}}?+gaJ z(ZxbD*pXsPNNRHGo?ExI=4(rnHNPFb281*TGf+D?#jBZ=>zuda#b|ZUks+< zdpxevO{_BX-j6;&DXAi}ku@EHEzzZBONq}dk3Tj3xuu+qAdMpRtNnvPeCDmk4+Oe8 z$#z$wmkj8t!M;p)tEQr}&f*gEbMjS_Go9x_OO~nkkg=(w)$BmVj*?gPX z4}PO}@qgq~$G#kkqSE1kbiHq@B}AhNhaDe%dhi z%FQ=AJH`Vcv-(9Kf0JPRSk{1)9;_ht){zaw2&o7?U3f-CtE3@DXP!!9tbTs^_H<%b&-Qp>x4 z_XY2OE*J7Q&A)0K2V6g^WX#mJsOC#_@FhKs;Uc|=xBPg zeCrL?;P3i0J1LGEB3{jZvg2w{jo03ew{6g-5oa3&ae^72rREAHYn4Qr-SS@K34+En z8!g2m^=oo6h{L<}=jRVX2_OlK*U^bONk@UBy8_ARfwlx=uc6RL(CDnq6*g%y1D+&zYO|35 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png index 94d45fdf0b2d2b167edc880d4fbbe438bc84c1eb..15adb7c3141847d6553f7e8ca1323446823dcc1c 100644 GIT binary patch literal 129 zcmWm4K@!3s3_#I+Pr(H&0f9ho0|bN_m5!lZczWyVzr{bxhx>SoHs{Wpl#f1d&&uo7 zcBD0rm*l-hOhP2nvYCDP0dY4Sp^$~MVj4d?J;WE zn3YKeJropH%`EgnjgxL#APPF7z?Q3WMWU?>7Xy>hAvbf!@Sv9d*=Ac7$`=_d^xr=(ow zK60VKwcV3XOQHv<&Wtt%=X~T)%Oz5I0+G*co+4xgcLI;F?3uBMdO?lW#lMG1dy(a((44{tywMjn5i*nwZoY}(&tC_#U{$gNG!5Inug;23G!!mu z9_(PQm>y9%#$hVUIBc$4g63;TJV`tDl@qKY99lLdQymOS=KQ_bhwt^(o^;d3=~y+o z>=x9jK!a#lm2msdQPM$`8Nhs1{ngY$}Z{vUXxv>Y)&`$v=qAnD(0xm85_n9L5>n#M{T=Dg9nJ z0}kPgRoxJlqW2H%9DFxB_~USbC}B4jNJB=yey(PJBPUn_%i_TWb6keS+fP2C3w{de z+}dx^LgANSA0{@C=E0{B**y!Vm0lgL{Cm@Kf=-y2t}~Qr+h(|C;7!uKjq;QQR6UQN z(8~=W51O&3PD^Mr!edjew0fiRX^(=dPTb7_1cWpioPFabBdeykt)ZQ8Ssf$MnNCsv zhUSOjfL8fr#W2qs`qsrz*0B~z|nXXsKlTxVWUXQaFWp7>?K z>%^bRs|qkueHTMJ|Jb-F+Fwy6o3)Caxkz1gt-5O45*!f+YlhB^aO@3NRQg+3R;zLK zGC90xeDgK5dZxHVhH@dpl&=C9YYZ~0#9LdOh@3=?i_E(kVTU>)W{WforO9S*q!(N> z^-R1geZA|5K6>7N5SjUlv)2$#&!awtE_*5C^zT_dVE!{GsL;(GLl&1pL1bW7^Mt0v zNmSY$)OboIuu~u)e2vczzQpTlj?SENWowIb4+8-gdBR1{M12%EE*o3S$NKag|woy}o--PeH#3E`Ewgw;%1KE4%YkRg$`ggcBdZ4DF2a-5$ z?Lj5vRJ^7pHJA=QChMc0thPxeNHB~*uzu1hO|9^~ zuV(w3F%fsHI23eSbJpVCDxGF_9-LXKZNe69#Wb4V^}a#C`TZZY_MAfx4=~^Z0=~sp zna;TImLtNMWVvj#mY9;kT=_6`ae24=kE#!R2T>b-rkAb_oSrd~`C77nS$xzqNMlUjP6A diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png index e80e216d0be0d3f2f26426aafbb84ec262965e69..863cf7c7eaa6384227d72a340a926f3ead8fecd6 100644 GIT binary patch literal 128 zcmWN?%Mk)03;@tORnUM-0Qqe8{ zqxm#)FqzY;IjWX^b0O}n0daOAKtjhQ;I&X@##R{s&;sQG3hh7JJmqI(~}82wCeECyc8w1ik-XfD8nCEj36atr{BHu$(0 zI4nQ(RpJ#U0QgPsyVp!f-!-i0pGrxO_W&LMvr6C|d=Fz8-xaRx4=YWhUaB0V=bQ(C z12ai+G3n*Y*5`~Ht)IPphY=DsUswX*-@4T%GVX!jT#qPhSsszkzB+DsMNAXUTIxoh zhv+%$j)(x6Z2p|T;uH5zBH5AD=3h+2DQS}gshlbvfHeswM z<@?slJTLo8_@sRkCXDSSpguj#Wr&C%qN^2OD^JOHAHY_7BP_7t)KWQnp*f!@)GHd= zR}`JfN=_zP?^v)Hr80DpDPu3;%++?BKB^xnL&OM+Umcc;mk`#CNjybyDKcEb;y1po zxW7c#)`yMc&+&aeOTVdxC3_O*mJQSkB|d80#$U#)OwY1=!l8w|Xri#op~T4_90+{d zB$U^bR>@KYde`sfV12k_W;+Jc)4_eclS|*G%H2b9TG3;DM*+TuO&QC*NR-rP4K})_sV-k(XD&=ZPP^Y0HOr@6Ma^H|tk86HG1f zs+lk(ZLJX>6^?X~9zg64q}Y2>9JhLHNY$^JC5Y^~Y0 z^mlF;htGd_^K7J(x8tXl6Df$?@+1a|Vg@T9i$0-wvu1MHVwq@CtwB~1`k}qrV6|In zR&Zbq6j-E0E)Xs%EVFm*N(noUc06eBQ*2S{>8nxW-G(~#6E0DGG5AJSOCX6ova1w7 zqSyw)#}}LfhQZSQvK@qsoqlcWM`r7K^gtFK>_9c_6;!KJHEFuxXDkZ7zxU|lz@=jm zVreTV=DPXsz?_yu+8g$eF=m_q diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png index 4b97aca5..a2902c5a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee0914b544d1a72d8753b7df36c2afabca4adec48ff5b08e4e9ad761611edf99 -size 5452 +oid sha256:ed1478e8fc206b1beaa69a7df49cfbb26adcb395c21bbea85657b9e647f9ef14 +size 2874 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png index a044cc56..8ec38047 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbf283cef01981b183b7ef8b8c07bdacbf39b9a24de3d86a087dc3a54a0cb958 -size 4868 +oid sha256:f3d0acf65b85c8e58096e281b309596d51785502e17d8620ca71ee36b5a46943 +size 4215 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png index 24720d4a..78098f2d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e6e0df05d6eaf36236cf563bd49b5f8de2fd5778664d15829cc5a7d41c43810 -size 5905 +oid sha256:c65de578e11a666d95ba9f3f01d19d34f2c376219babcf8a7d032e2c55a43558 +size 3106 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png index 614803d5..f81d2f0a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbec1fd4556be673f1851f19d8b801cebe634ca98608be376364ab0f578e51d3 -size 2939 +oid sha256:246709450f82f00a2008cda56661d313783913a0db9a2f87741abc97dd662eb1 +size 2412 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png index 54607e34..bba63ff5 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bc69286a3fdb525f08f3ef00ef52c62067cc02b9f1c5dc523df714e117ef2e6 -size 2893 +oid sha256:549fec09d5fc231dfc9ac7c72f69cea66be07303216467e335434d412ceca67a +size 2511 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png index 6b1c31e1..a9e1d101 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f32f713d2ef825254c3e2c8fbd5b019ed2f51f8f7512ff8db332fc9ff7e29eec -size 4211 +oid sha256:f2d08e712955e19d82bb5a38247f1a7cbda0bc79e9dfa76514e08e0e90a89ac9 +size 2521 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png index 95d84f2b..28e62bc1 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:516f7f97c363270fc47289d95a9e1defeba97cdd6af1c7d1af07fdc3e37b2b04 -size 3880 +oid sha256:95e68c3f3c108915ccc89ccbb13d4acc089aad3f7d8eff38a263c3016d233511 +size 2445 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png index 92614bdf..6ae3222d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f05741f690a2f3eb52326d5f8d4324d7c2ba16b3c10d36c2f9047efdf2e7350f -size 3088 +oid sha256:575650693f22358528fa2514ce474a1b50b228dff7ec00ed8c695981ade6f12e +size 2300 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png index d2fbc7b6..9d82ad2c 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdbce31f53f92a9babc9a44cd85f15e944e2aa39422aab8654c2f53b6bc76f53 -size 4405 +oid sha256:281d3c8349ea7e15961d1e0be5c5a0c4aad743295381f89bd3f9f7f43a02ac24 +size 2363 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png index 3a1187d2..4a5fecf7 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ec0f8f9c001aa41599ce5eca70e3e27dcec627aebc0b3c059a07463f7d0a99f -size 1339 +oid sha256:2410f4a869bafb8c263dc008e446b441e607f760ff1b60c02f9602339a8625c3 +size 1061 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png index 76e601ac..c6439fdc 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b48a33ee84606f1b74ebb1fe047df7eedd8a36758a17860d04b2b11a2116e55 -size 3927 +oid sha256:4edce89e09ede18430cff57ff74c21bccbac076c5d015d0ca77d039fc586fc62 +size 1747 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_360.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_360.png index b0c7fed2..96098fbf 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_360.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_360.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30d023cfe508a969b3386dfaea3b1b30be935ee932f3e57df705b966c93ef341 -size 3930 +oid sha256:03eb9645a7fb021bd30723dc4a4a8b1bc2900f604fef165a6ac472bd0a25c537 +size 1713 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_False.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_False.png index deff10c0..ceeae75f 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_False.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eadf653708a1d8031e543f17cdea79140792140cce59444979aeccf32a8d983 -size 4148 +oid sha256:da8ba7c8929209a833524ff5cfb59ecded92d7a95b3022bbda80816aff313c31 +size 1559 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_True.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_True.png index aae3b2ca..ceeae75f 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_True.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b752270cb16a191a1868fc91a7a11806f1138259846954ca05ea5c937ece301a -size 3571 +oid sha256:da8ba7c8929209a833524ff5cfb59ecded92d7a95b3022bbda80816aff313c31 +size 1559 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png index 7e4f741f..3d94259f 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:745060dd95dbe2d6f8498c5e0e2ae84e9dc1e252a25d97ed58ebebd07a1ad5fc -size 353 +oid sha256:b74c1eecb18745be829c3effe3f65fd3a965dd624b0098400342360d7d39dfb7 +size 203 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png index 54057f0f..2bd89ce8 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a5a8f9a0b5733529187c97c3d06702266597d9b6f9148da9db0a5eefd0f3c30 -size 8290 +oid sha256:9c79f14ec9d1e1042a9f0c0e09ed1f355889bdd74461050c3529e7c2ac677f26 +size 7725 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png index da36bd50..c5206d91 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc0996b9a33fd74ef317a3f1f8499941c30267d79f1e8e8c0cf54ebc18d42d9e -size 16201 +oid sha256:ffae375183e7df6a7730206ba27dbfe1d94460ee4af4e5774932c72ee88f0bb6 +size 14745 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png index 756107a9..c667647e 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42441cfe220938a81611a18a6a7788a484dd9e6616136b4a1d241f33d63aab03 -size 8483 +oid sha256:87a6e83e4da825413890e9510bf6a3b516f7ca769e9a245583288c76ef6e31a2 +size 14295 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png index 5235b5f2..130ae703 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11703d7379c3c022ee193b63924e753cabaa8d0ed92efed707ad5dce56a3c511 -size 7830 +oid sha256:a0b90ebe9051af282603dd10e07e7743c8ba1ee81c4f56e163c46075a58678bc +size 7159 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png index ad5f2d36..ff7d8b68 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c5d8aee369176c3685f0b5cf0f19dd91c27df467ecf615fc33ff53f121126a9 -size 8260 +oid sha256:4e5d00ab59f163347567cdafc7b1c37c66475dcc4e84de5685214464097ee87a +size 7863 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png index 4fb69e1c..95b8be0e 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a7403ebed6049508ec7c31e7b7d5feeaf2fcb39265ba99580983467205da9d5 -size 5763 +oid sha256:c1fcfd5112a7e5c41d9c9ce257da4fdf5e60f76167f7a81cc6790c554b616e60 +size 5837 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png index 003ac66002f1424bb91a170591a539b332c55c24..141ca9492db7aaf087b4d669d4f2010fc390d088 100644 GIT binary patch literal 129 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 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png index eb87493adc083e938d5dabf38bc2614bcab5af36..2bbf451edb8de5dbe9ae75e0d449591129f20a60 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUMTuzb_a0=vSDN=7gTtJimV7rkd6FWJ^Q=b_ZSuiJy?_P>4d zR>o7!lZv{u7`@8b&^Uk8mT0M*gbdv-ZKMxZ9SHX(W9;**GR#!Wkzdq M5VL$xPfcprJ2m8C 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 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png index b1ed1ea28541a637132260336edad48e2b25ba87..609fc3579eebcfab1ba50842d2488a4ea4b4a321 100644 GIT binary patch literal 129 zcmWN?K@!3s3;@78uiyg~nv?+jO(7u6sB{eW;OliSdzH8J@zQP1Q+8wSecm2bmjCUO zmOP$D&XUcws?k|XJHXNI5$t9U2gKfmH3LJ~pxTNVT`f!%fQZ@qh${q!lq_HhDF&($ My^{Vd8lwgE1CE>~NdN!< literal 4491 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 XuHXYpx4f|Y&NBx1MWRAOK~4KVpVvW3 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png index b1fff021..87e3affc 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56441a7aa111218915395fa26f381027f3823316bf28c5280fa03f6a9995f200 -size 8015 +oid sha256:89d4652a3e12deffc5eafb55d14111134ec8e3047ff43caf96d2ac6483cc0ca3 +size 8874 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png index 1dbb35ff..6f8346d1 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92a027397d64aaf52fe92ae6ad42da490dd8602ea6ea4259a06b82903abcd0b3 -size 1421 +oid sha256:b39e13b16a16caf2bbb8a086fae6eecab8daf01f0b71cec7b6f6939393f554ac +size 601 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 index 4a385c10..652850f5 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a659c9a2a4538dd9adcf3cfbd2894bf20d79a409ae20efd4bbbe315952ce02d -size 77492 +oid sha256:a837b1b94ddc2813b0feaeffabc22c90df4bd4fdaf282c229241b0316e5621b7 +size 77807 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 index 70958358..c622d0bf 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ecc4a0a67422b9be03e9cbcf5936eccd3796790f21ad7c30c4a8f0a39ac9781 -size 17224 +oid sha256:3a282acfa163f23bd7e1a6d97c174ff290afb3edbf6b8a6f65dbcca2b7e0fa8c +size 16748 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 index 8d64a9aa..646002b0 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86c60cdce213c815e6744346374e7cb853ef220d112fe1b934eaaca16527b1dc -size 33193 +oid sha256:68cfa2c39e498a8c147a9fe5ca4dff10d3b53a5a5ce23bfdd3e7b7915fcff8cf +size 32709 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png index a6081c36..a3d1fd99 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:949b0e0af39b177c5214dd2f87355dc3e7a596ac6af05563e9deaa643037e5a1 -size 4377 +oid sha256:cab703fe17ffd19264e0ca155945aa7c1d0bc4c6317bc87e6d64e513368e0f85 +size 4429 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png index adea8da1..4431a489 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64de5c0902dc90f5122ce55bec6b83e520dc8f602c07e66438545e88b0e999a6 -size 40943 +oid sha256:7eaef6cc66cd48c391fda1775da6594728de9f16cf0b9a4718ce312841624f73 +size 40967 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png index 0617cbf3..6ea570a9 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5219b39069d18fad9d964acf32c9e1cd3eeec83d39c9ffd6cd11f927111e739 -size 372252 +oid sha256:85f9dc073233b4703db8ab4df049de3d551912104863bf89756141c61667083a +size 386553 diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill.png b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill.png index 276fc7aa4d1b018ee382f512613300c670caf178..8ad0ef2cfd71e544510fdea8c0ecc3fe96c1248b 100644 GIT binary patch literal 129 zcmWN?K@!3s3;@78uiyg~0-=%qMv@@RsO<>$;OliSd+K}i{bk#n$Jm^^_j!5LvHWkJ zvheWg$zjtrd|J?KH|IVos zn}Y*r=62>33WXNPgSSv9M(*U0nSMuR78REUlEEl(OTapcvJvPZ7shdZ8~rGhveVS@ z9VX=3EP)rENTJXlOb?^_eMQk^a&U7{s6T}=WklXaQ{Nd)8qbVK#kevBeix=I6i9`{Xh2qZez?$9llmu79mv{}1+k z0SkKh4n3Q}_o0ir!xnXiJ9UYj^*dek61R?c_jYt?TjH{}>7FeIJ(~}EH6LI38sogm z=Dz%8mG1Ov-C5tJytU6Stb3|}o?P{BEb?zG3v9T-e^j|aTN9+Y7hL~f^F!^AL__F< zCZzTSQu`+SUTehNx7+R-w%_iKyww|3Ga$Y>xU>4>&g!9}Re`WyXlir+`MrpZz$&1k#5rhD_D>5H{t#Wzc>bnm;uCKAy||ZB50Ucoww4d2+sOT+P=3o9WYUTJiEmuXOF-t@d$k#| zen#CUM}r^HJEf{Kcrd^nlUu?@$68v7B;DIn)lX8;XPWdQcPfWk&T1r`;}s=?11SSL z3y&5is68$y3JcCxspdJ9K)qelMP?@MR^Zhl##0}J-*C^wR|2!m z<}y9rSZm3Y_ByFgIDPgrhX?_{{t48((wX-er~`Y@7Y5XtPrh&;td;u0B^d8}3uacC zikv+`hbjnOPA6sL!g5(xGR`q89pu7&6=%8<@z)ZUhAu{#RfJ(5er~Tv!#hUE0&KeP zpOn28s1Ln>|0meJS2twF!`BkuD;4= zF$E~zSx~ThXrGsr1yhdV?FDT~2pp;gACDv%O!n@>i?SiRfu6Td?Mfjy26&#dDQl@@ zCj{>Yq%wU*r8gW}4-So#f64IihF=h1{jQ}F2!cg`O4gm><29YqjU=_2?2W^VvY^}_ zMNh*r@tU*H^}l*Xsm&Tj$e2_Up`tcFoF?AaHEX;Boi~u}Pf0bgb=2nj)5LqT=Iyr( zpJniisl4WmX@13y?A}PaOIy?VrILOKe$W8UIpHCBJ3}i6dKzuYdqG7{m(1|Oro0%K9RP$^4 zs(zDFDbBbJsNOs=DNUHBPrU`fNBln6vln|{eZxt9#WN&qYnl)~ZO$~+lFi%^J$*Do zPaMCb_!Zl+VGj&%MI_*RQIIqw){=YB64&&2a&o_RFpp*w&$5wF%ngvCNcpe8>* z6Tj0M)8z&4iS}?S1jR1S3(tb$uwx~{Ogwn1H2W4PwoHls2d@jnj?a25w3Hy?uG zHIKaL+9p8(%DbXeWkag{@*P0V@5SSKS+*#~2J7r|t>P?>NAlF*fm@H<=-O667|I(~ zt~d#ON|A2@6!!%_C_WpxNr0}x7)$KW1bG0UAOzMZZimb#z%=10N6cC3odzhhy4NV~ ziY%%JYlNrlG3P|@-GJh;?i`ByA_g^RE;?n6Ns_$70L61155Al3@rp|}gOrXH*m4%uV2iMn_|{8(@S#kt6u zhv00{kTte7NrwR9=YmZrz7d%q!1babOKj_YT>v0{DX>8C9f;PzXi#pc2d{{neu4Jf z;*&xtaxZ$>U#>BLxJ^*UA&wSDqxc-;nx0{!v{Zw>!pkS1{A#`>mWs;RbkyML2k13| z_Z;F(u@c4IkOc-tq_Rd0HVXHjgl^yLwZy6ubs+uY`|F1Q{WC!ehqzpvi{hJ*em&!= zGC~b*7gd~qT5EPQvC;i3M=Uj&Wsg-Sv*u%?$t)X;lfq(QsVSs0s#91@tex+GwfE+7 z=(RehvFe-~$KAMY)j6J+8@H+YWV@NNGoLgRsO;Qh;5WLG_9DX?q4ruflpv_%DmwwP zN>_H4kZCqqZRZDrq}ymV)j5e7ZrpTI@TePi11Xs0#zjc)83!dey|+T7!>Cgn6_JJ| nIK|Ns=@oSPcjW)Oemgov8Mn55b$xl%(P@Q1|6o|TE-K@{s?it3 diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png index 3e24bbaeb7ceccb19541c32be6a6b45bfb53f2bc..6898515718d53c3f3a9457a62864d5feb4948c90 100644 GIT binary patch literal 129 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 VYyWWQ59^))nHgIe6&v8g{{wf64qpHO diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png index ba1b83163ff0e3d9a657c3b69414196131669a59..48f1ff7af90c8251d3b24e0a09e8aee164569e14 100644 GIT binary patch literal 129 zcmWN?K@!3s3;@7;U%>|~DFi6}O$fmM3lmWjcB_x|0iWD>(r#j<7mF1}p(f&X8(y5IKkk9|=$pn4`2cNnT@s+=$jj MNR#%j80S-*exuAL`Tzg` literal 2299 zcmbW2eKb^CAIC>tZiR71-kQefOk81ZlGG7uagIV#WO8+9xJ?vN%&6!-QhG9Tyl1YV z?)6YnVRUXoX%P3O8Lul5x?V~;j6&q4uDD*F!}HJm``K&l{rmjB`?r64t-Zc$?ZiV~ z`?S|m*OEviZFe`sheT3g67$+>RYKV$x$jN{l_;NmE+njlHcTXHC!G&ClStBZ&G}>M zM7}1{%|D7nB0pQ6D)kYCL4??Q$kW%AL|RfIEv-^nQdL=0U$vm2y0BVxVU60nruw{= z`n-@RX`>&1Kf0^lg{ucObNtw3Now8mxX`?q` zt3R>bU}BfixP#HS)B3SJ)G?P0qX=!(jXvT|AMr35_S*R25ct7|@%}Jl$am9_zp3)5 znKHn9;79WT?q>YB1s-D6A8OTqirIhqySI^B`uNsw&fC0>v3VV9+k4Tr=dxXQyj}N| z9o>SRT|(&3B>Pt>yI!U4>b&OAnc>(Wa_q=-YR`1)5IePJ?QW0JSo9{5zA17?oPA?@ z#&S*_rn>@r*KP9fL>Pw7fEuE*yiD4)ZJu7blp)e|kC}N{=1}JF=3+~9xz9a*QQSxn z-H~`GVu-J%^EM7m{b_w|qY|3`qHS>XqX3ATQ`2F*TFTasThgWI8bkyqWWU{wxH=rH zJrx(1l6-`|xKQ`XB)K6(Ig(m3@gmyfSboCKk@ePjxAJdPN0-*-1-{GAcs>)9@T&>^ zSV6pULd7sIzaY0lCpf_{aG)S--%?+heq0b*sGO@Q`2+B7?#^Ti(4%A*buCI~IIEG1 z7M{4{V*n#&)3-DPXy}QNAREMYYtbVGV57KDCcYPsSE>`RZp}iWfnmd>lwM`Qe_Y-(+Z8WL1^Y8B^T{FVAlK$kkVfsi$lNn+}u15 zOq$+dj>3p1trp;kk7SL3xT%!c4$A}F(%yhv+iGSNEax}Gw}IR($;^Bhxd+L|0G{!- zV>}dUHfzQK8+~C(Y)_)T4lMVj@!Q564Cw-N_$@`k*c+;B-w|9xT`;;R7v=H+!o+b)xJUR~_F z;r%;dB$&q6n@-rYXN9Ic7voRvOcS6@(4g<)^u1gGIsyf`&kgLymI;YH7$~!3gyFzD zr|J<2T9^n2&72)mKm8CbOoow}&w?_>?JeYt*X_3dSxgt;;O!k`%Io&t@g%eoDp<%p zQ+;zxoAQX~HtJMuvMc`_T9^v=jEAG`Rh_ z`solfBni%X$!-;_NDSh@$z|!tVNUB20eUwZ9vL70s@c;3w0-3_>@C&osaqzuFHTOj z?wj{_0&VmBhV!+WJ%22dTc^{T*(*D?BHNd4^c?)jSz^NYC6Iih=H9Wc@86^&EWn<>yT<#9zOcEdU%nFR_N3C@KwWy zFTvXj%4?}K!-p*^^myiMkLFP&0LIt{IlNH6ABh4_Xj5~!UYDFlH!w`Cg&$8=-t5$- zh_c}`pN1U`AC`kIQ~U<+a?PXN0C>TEaLB(#^Jpg^HJ=sdf zQlBF1xBQm;l!ef)NUr~*Kx{0`{7~Eh6}b5&)Pcd3mbPSz4ps)wO^U5eft${;0%$%D zBshLe#1R_^^~Q=HKn3A`!3r>;%F>c-(Z_P;xy@otiLjoG=-<{~oCzTb5*HjW#+pz# zlQ19Id!YC-grrL*2LM-WZ#ulag6cr_R)!5iNVbGG00d)uQ{jgdR9kY}`>-bva!b;N z1F2YD66{+^wIH_*hvh&>k;EGZ?qhW*{JE04o9z8DYyv{cB)0~DPD~>WK3hfILG~UG zYlV<12{-^OV1i^=OGagqrTt-rko>A79tSpHfHm6eYGJONT5^(zq?GI!Jy~63#++WJ-FKm;}?2gli8 zP!lvI6;>1s;*b3=DjSL74G){tA$G#Q>iWS0K&Kz#z%Mpu@o60yegVfnfs!!(9f3 ze+&%&85kB8tu_NHGVyeA42d}W_OziOg8~oBhV{&RoG08>r!p`{Z(42?F1<6{dcBBi dAQJi+%lGr4)7d)Fur{E622WQ%mvv4FO#mdxGe7_U diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png index f6b0dc61..39062ac8 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png +++ b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b5901e7823be8e7b893c94820a618cdd7c3c10b0cbc7577658b9eb7bb1bcc44 -size 612 +oid sha256:134d4af8c4ed8c112e5d403eb92e7e215fab0fd881bc0435f8fe164e74c47405 +size 537 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png index 77568a1a..58e64a8a 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png +++ b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ad3c6ae6438196ac4bc7552aa09ef16c2580d469416f4434e4c73ddd08ae5e6 -size 467 +oid sha256:7e3ab90923f0e249b6b2f542311ae57ef9921483f8f934e4d57437654abee240 +size 357 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png index 67cd84f7..cef68360 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png +++ b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bfebd42e4bb1c79bb4866a3414bedc54642fdb2a21b33ff14071259a9abc6c3 -size 573 +oid sha256:15541d241c3e6d1d47388544731bd7c0da4d5533919dc791786193473ce788f1 +size 452 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png index 2e2028ab..b9df1a69 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png +++ b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:730f6a6a54fe58275f3dc7f97e4e66e05b00ee395ff4f06b8c2609b71f954804 -size 608 +oid sha256:a6aeb2dd8e99354cbd41360611642e9b286e646382dc224c5f9ce487aef5beba +size 483 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png index 59cbd3a2..a44d2364 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b66233cbe8fed9cc0c9fea624d0e0b64e3fbc0563aa17fd68cdf658c8c06906b -size 4693 +oid sha256:385a840ce196a34a0c5b85ee77aecd36924e642d7d970ff82a3ac981e32649bd +size 1796 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png index 4defbf48..2180c82a 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c512a34849be87435321ccc0bca8752e2a15703b08acf5ea5f12e2eeda248b91 -size 1923 +oid sha256:d096fcea8556aaf91eea17a31896527f10285c5d17e073dbe2715aadfa3bdcd5 +size 1500 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png index 6b22d53c..8d8db722 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36b7dd7a3a7552826f80263cba1dd504f740ce456b8e6aca11a81dcf40667566 -size 4910 +oid sha256:94d1f65c198c0e57405459392ffa2b6c36d64f7fe4053960216eb487bc4ed0fa +size 2607 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png index 803e4857..d029a873 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ceb0d1c00a5acacbed556f387bca5fd9ac4065475044b397675e1dd447dc5de -size 256 +oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png index 0c0df89cf4b7dede439212094835f909aceaddab..d029a873a97847cb67999b066f30bf7ac6cf8bf1 100644 GIT binary patch literal 128 zcmWN?K@!3s3;@78uiyig5(**x4G<7!R63^i;OliSd)2p$`O^J757~{mkMs6uvi$F# zyp;Jga@1ljtL9)WJ%JCqb1gPZn4=@eIZ@rX5Ujw+95QeqVgk=4Bp0Y4B#&G2(Z;qC LSS90IwMA4vroSfa literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^DIm!lvI6;>1s;*b3=DjSL74G){tA$G=>VS)*Z&Ov|9}6_!0?};f#Lo8|L?!Q2XTO` z|Nr+1f0+eT##9pI7u@hY@1)TgATPnw#W5tp{q5<4ybTT?&t;ucLK6U5lwGU< diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png index b14af1fd..317d4326 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f72faff5b4d42f008e149912e32c5b017417c78057def3d8950fa3fdde0c8b2 -size 48547 +oid sha256:39252d1bc31ac8ffca3e4975f87a8d15197a47e17506f5c4c857a6327db011ca +size 38416 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png index 5e10d9b6..0945fc43 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31bc372733f303b5732dda94f920464394703b7b3e13358ccc2fc9a50e186ae4 -size 30815 +oid sha256:4721c27c827c3f716ad18ae1cafc32132ae47b6557a01d4c16acaa7b7d92400b +size 20601 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_Car.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_Car.png index 61d6f5cc51db012df0b767bc4cd3433e8a8c6599..41994df520d5c7c1c52e3764897fb4f411581ce8 100644 GIT binary patch literal 130 zcmWN?%MrpL5CG6SRnUNeT{a7(8@|GfN=7gTtJimVS9#AqUb3xq&O@ntU$;k{+yC~7 zTN+O_&yv&yV)QI$3%K*ajD@Oo-gEW|Ff)5cpm#EfOVuHeC1Y&Ftc`Z$ks>6kIY1!~ MEN1@}35FQO52@27?EnA( literal 14043 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 _#u&e^|eSo}d#||3_sv;}=KY(F7nG({iN~Ez% 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 Ktrq?s_&)$RS(XU^ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png index 61eac756db78e26cc8da58a05cee5e5e4145ebce..d24bd6b8683b45d86921ef8901a4d5f00342cbef 100644 GIT binary patch literal 130 zcmWN?OA^8$3;@tQr{DsXB&4MDHhhH{m5$UdJiWfnyUKg^@zU))kGUIj@8|7N=kmXO z;*!SG%%dc8p&C8w*#fO@C$7S>5y@Jdt;EO{86i{<51X$PGm#Gw90&#$$_0aUkb<|I MMa}*#8U?&nKbWK@1poj5 literal 18032 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�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 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png index dbe6283dd52a22ccac2a6a16e71dc7b3a22d2d05..ad5b15cb65e6d4852bb50f1a04771c9c53309da7 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>C?7*_LqZT{R63^Z!qe;9yo*0F=3DpU*kv>3F^=1_$@0IS zxX^qV*_q7c)EreyFKbL&MhmS%WX`r=2++L4Vi^-87go-mDpas+i4sCV^d-0CBG$-} MP|5hRb_wq42ju=JSO5S3 literal 1307 zcmb7@drZ?;6vt18m?42J1BU||oIDc2!W4o5l#W+^G8D^02pO+2oo*n=%H$!h!737= zxCKNe*a->@6o!+42xSg~TRIC6cV7qKu+G zDCq~OG_|twgiIFSpn%54$hNlNuC9RIN$Bg_H!$F)(7?!ulTx{3>IJA&?`gGfFT953 zWuuifSY6$u*MnZqLt>5xXb89&&=}AZ&_vV7P8xRhohq^ zV-mMoZ{H=ck<8Du)J~{Pmf1h~qd#Oht0xMoXMLFemaJKK^;||~%vWi;F-=e0r%5*2 zCaE;mHD`P7c-isDBMj48u7BS_t2{Y6JUm+`JlCId=5e#9XVwwj@TJ_8WKL|{BhQrBW-wRKOOsOXZGx5x zs$Uuw26$ao4ph%fPp?du)y_S9eJq&RD$#X(`wwoQV}ersH&c)kdMZ zX0LXx&OY6%;g>NJKl_t7S)pa>`EfUi^}U)_+o_?HkFSw9eL8)|s`Q44&+l2%S%lml zN)9Dqoz5!c4^P@C`{ZoxPxiT-*PJA?JdbzV(`a!V`_TNZIStFSe&#*yv;~{V!b=8; zE40hv(thhGC#pDnBQH6cO(sUiua$*osfl^KBmYPnO^gLvwJZ&hLCbDm)PK1PY1nMc}f-7?K#Yn;_XYBK1j9<0idKHBLOF;&wnm3kkWP3#sEZo|AtlHTI!$&TD? zTHqKdPvq=8$3H9R30HF3r?>f|$~$C2rCwp<^rBPHPA?vMh5SV?x=m_2mtu%J3r1MY zau}EIbWvaafCKBsPcHO7zZdpH|G*HaGB_AG*gRf`mG zQNH^b1vsyt_>}^p*|BdZ;GV8fLp>H~oD|fFz2cR_T?(pqkzbghfbZq|M=8L4J*}1k jl30~L2l&5>bz3#s8FQhRZTvt&{yqo{2=;IBjW75YsK$eS diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa0.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa0.png index 7232168cd1bc9a498f7c6b8f2e74cb8ac3a05109..5a79a3c779930c579a41295f507df2af71602f8a 100644 GIT binary patch literal 127 zcmWN?%MrpL5CG6SRnUMzehZ`yP2j*#4JV6+oJUOAJ4XO90wHUlPzylH LQOy2jnS%HMY*;4o literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^96-#&$P6S?1M=anMpded-Plzj!{{R2~(wp<|0$JjoE{-7_vdIYy mOwtN0jC^dI3f@ddSQxmkFuY}0RqFv%!rFOq->MB^OMy;4PQ6M65Lf*yJ$p9+3G?#FL_foXG`3TBpWR M0hII~YiEP{1I0Qg!~g&Q literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^96-#�(^rK8WxEDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheoCO|{#S9F5he4R}c>anMpde3xPlzj!uBfPJZ*Twq|NpwjiyVM_Sx*FOq->MB^OMy;4PQ6M65Lf*yJ$p9+3G?#FL_foXG`3TBpWR M0hII~YiEP{1I0Qg!~g&Q literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^96-#�(^rK8WxEDaPU;cPEB*=VV?2IV|apzK#qG z8~eHcB(eheoCO|{#S9F5he4R}c>anMpde3xPlzj!E+{DI@9+Qr|Non1TTTM`vYsxE yAso@k2@K5t_!U?fdw6-?h)e8{H!v`DU||Tk$#P_a&yL?fg$$mqelF{r5}E)}0xCWL diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Bgr24_Yellow_A1.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Bgr24_Yellow_A1.png index fb2b51185e447dc5653fdbfc1e8612657deaa654..12ecc129c35652ef37183a83dd24b35f69837e27 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{DsXPx-aCX+k2*sB{c=;pz2l-bHWejS@!Ofzr=qMAf)j0?&ddN0_-(pDU NQ&rNxqH=ux}x>AKyU(ZxJBFvFgOW@hFYUsFwFyVbBzTHtgT<$oB_LY zU~jK*DF!#UL9Y^6v!-X=ui)p`5m*U9L66DghY>em$Bw(Z>mWAvW@2J_QUj!>UOMs+ zFzjr0cJA>O$jf80SeYyloc#mX>?5V62g<}yS(#8Rg_;^_eLdy=00;!3EiXVM+Ss9h z&dznxG3f2}mQ8|O?lSTQl*%v0=U{qz+01*G)55!VrVAfHr(62rBP=fJ1&2P}1R)$C z13-p=i~yMeG6!S<$QqChAUiE z0ZIjQ1P}%&8_;n;d4O1%=qw;Mpi)3(fGPo11F8X359mH10iYH@B0wF0Isr)m^#YOs zk^>q6qy#h$Xd2KApgBPA04)I00r~)FQJ*?AEIz{n7(WdU369D9dvZWt>kzukc&1C@ zkYus)RDwzVCg zL)n$;ty|wO)IRsGcAvP`h8CNeh4o#v{;Hv-ocZB1>Q~5j&%1W63h5kvfj8Gc&;qS{~i+Hax}0 zG20ob8Upq9_y3W zqTys&iwV@K@hh~n0w=TA2A?ZaBsh_5*|DznZxcB7*D4pj$vT#$duUx0rNjoQQ3$UrW$#e(iZJlSJ+|LAqPhZfoWsrNK`z_nSbE4CjNKmVqndlf@ z+ZDQ$ImgcEI`)22X|^OlU^G!^Oze+MSIMfrd=ZpJvE==!y&2EhQbqj-mZaJ2O1@4Y zh6TOl4b<8w!?x_~XUBVr&!sK5>zO!3+RiK?up;g*Qznd!iew(bnfB$*;&W6vvD(%G z8?h|r1qcsv?5mAg_ssH1uWN1b*oa2GY4Sga)NP)}H)XPte65sUSy@>L zLpYZo?%j0F)&z^LjdvEGzQtT55QB>jXv8$i4w1?Nv({)`+XS?uRlGDW4rb`M@g#U`LFZ$x#l5vx9t}hk1?N% z8zU{h8=X}TD2CMwiYayf6XudtIX|grzm?n;Abj{@)*qK|M7=MMKKiSLhSBNm-klpw zIyqJH%v4ho<8$-X+s+&7vT>x7%~{uWQmzPc|(U4|B9wuZJc<2jcyfCd+DM&{suS1(rfggW|(euY`!pzbb3*is^{k_ z)7C52F}pojx0#Nl#6Is1+}Hy>@n><;deWOm%{x7+B@^Y?iqR0}4iEPp;bu~?(~4)s zcFheAHLp@TPixFv+s5fG!Vl6r&D;n}ts+Ly@7`9!v?i4}&2=ni^TX&~!k6v!CEN&K zBK_c3)jfsj+5V(5r@oF3#$5{KrtQ&tL1~_cB62?ThF`J$oaFb*#jyo4`_AY-=DX_e zF;cnHe>%S3<+y36X53`fk6}V8uQ;M8SecyNpYcq=VE%?#gmzsq-cL7@dtY3u{|J%2Azt~6q*-t!ZzZt; zvlIoMs&3U!8*8|sMYW360_v({Nv_w?e{Z6J!a;BK>ID)tEaU7yFM)95|{*xsIJM{mcd}IHO6Jr3g!2sR)hcQ3XA2Frg}?aL3Jxw< z2@aDg7gH1-ae*WHXLM0X_f}xDqifZ{r?d67Dt>;2ylOTUe|Jk@^{bNhz4ok diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A0.6.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A0.6.png index 9e3883873048d551b4db9d890c4672d34f9d5667..d6c24cbf45beab6512ca22614670472ecbbf6b62 100644 GIT binary patch literal 129 zcmWN_!41P83;@7CQ?Nh-7#y5z0~iWYTcSep==9C&q`UHWwEmHG&SUIKJ==V|%2;me z8JE=GY8*MK%ZT1sjv4^h-8kf6lO=GDR)eYNxas1&p-91Rfg-Rkmf2eEAbG@WkTDwY NcSSMUFH7)5;t!wQCZhlV literal 2443 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 Cpx7+{ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1.png index f6d392d73874ac82b49eb576c87f09d9f5addb92..7c67a4ece6863ba41507eb4e6e6da55c353636c6 100644 GIT binary patch literal 129 zcmWN`xe>!45CFiODrmss5syyY0C#7Yv5m$c1+LzTHrR{4qxFxha~@+?>e=Sw#bddx zXI@Hwt8q|Kml3_895q=V_81{)G+Qu+vU8adY6i?0wJU1@GMY1kCD4Hpo!yL%o>%k* NTwoBR{Yct-#UIs=C!hcT literal 2532 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 br<^IWL<`1(OY;2mpFN5gLJDpUI&$W3l{hnB diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png index 128beb7976bd46f2141f4edd216f92a9f31fd3dc..7cd33f3355adcaed990b986502130aa6bb3efad0 100644 GIT binary patch literal 128 zcmWN_OAdn|5CG7FL@V#rS*@hV;r(+{cPj$+GW12 zXP!%cYdN^+%TB$Kl8SB(8=4pm5ETl{nT@vqDN}HqYzlWEV*$%y_Z$f=6kHgckBj#% MnmkGSv4Xfxf5QSN#Q*>R literal 980 zcmeAS@N?(olHy`uVBq!ia0vp^zZe)8<5-x1tT+pKCm_XG9OUlAu+P+x8*eD^xF&jaq{TFPcf>L@-LCh&`Aqdad-8>wFU&ri zy~vawvo8F1?Y<~a+pkxG`L9^7>b`2d>ij3~FVRU~`(73QF^XMtFR->Peg*d>ecw;s zK(VCKjaBZY8+S4My>{pA^w%(M#NOktra${(yDMVv;$I5;y~S6ThsZzr{<7=x`<*{} ze=+P228sNicz<>GRnbN9@h@es9(@I}17yJcb@zimDuT4`iU3*ox43R<-QOi3x6~hF z2P^ykc_C2#{iXMkb`fBcPPBvVzP})TR)=~C#7!Ur!d`!8>4%&CYQ?&#|3mGrTd!IT z(!cPR^}nrpVficmwf&FYFIxLsw95HwWnHIUnD}b*kn7cdgG~1OWz6Ag#`IAgz~wElsG~3pS(S{=)97(yRDCSC{y{EHC(@Ry*PT(r%!?c!9WTRlN7t zHn8`c!3Nk~e1GG|Td+yxH4PvG)&>4`y1o9w7Y?A|o37iXXc8!NkErP zz9PM9{*rt1UWKRp3PVyW>wRw#%v!T-AAwaFuU%1EovVuytUeR7P zJ#^R6rTUk+SAwNMCcA#!6k>nqlK2adw0IEMIKAt-I)9hO1^yLZX}wAsh;NJimGD(B zRW5ah;`_^PXIL$TNP-kdulybNSHefV1g5~f^!B>j2~|7tmb_a5#fHBOr^&5C=!u?@ zm;ADD*Th|m;rQsQ;{jDYNP0f*;$8awy6n~7D^Pr~f8v+3aP<>+74Pa@k`I;C1*Ua> hP=d!w)b9MpZY^Zn{g;`c5SX(VJYD@<);T3K0RW)u_>TYp diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png index 49a33828..5be0df23 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e505d6e0246a1850a269cbb8efc010d87d610b97ed79d6ad917480b46e727d87 -size 5416 +oid sha256:ac469abbc75f28cfb40ff8dc84879c6a5a92b0259ae87cd475e3c2df48b2cbbd +size 5421 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png index 1175a735..52e6c32c 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f8a1ca85aef95cba5fa7c6d1240badccf8b7e68e9460a81cab80bf31b38c8d1 -size 1062 +oid sha256:fa159948e580ca2b8894f05458142eeed7fbfb585eafd6f88f0f1fb035d9cbc1 +size 926 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Difference.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Difference.png index b1b25c50..417ff62a 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Difference.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Difference.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c93ee0806f7ff34be7fe2cf68dd016004ae9516a3d3e4ee5f19885cd77a4914e -size 2842 +oid sha256:f6064686c9b97d19add654ec6807da554f308d34e7d3d22a24b68d7f4590500c +size 2840 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Intersection.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Intersection.png index d553e3e6..32da0db0 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Intersection.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Intersection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4781be479b3feb7d0cfbe6ac39efc9f3b4524142f77aa901ea2182a35220de9c -size 2796 +oid sha256:d5a7a0a64ff4d9c0d68044155aab68f4cb008b094d95633e55332ef9f0ca40ae +size 2955 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png index 034b5bb2..cb8bd9b8 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28abe11e7039f25ac15fe61fd257df9c2985c3e5bf716a062021451f4824379e -size 1593 +oid sha256:dc15adf797a7c22b101eb70d0d31fc8a34767db3941c6aace5cf66b3064fb15e +size 1539 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Xor.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Xor.png index 3b52e584..bab0d07b 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Xor.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Xor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:775af05bb313df0639dc431d86c83ef693e059111bd7d7d5ef335bbaed90fe7e +oid sha256:8f5dd907efe582b038f0a1e74cb006d2e803c0feb2063ee49056625098e6430d size 2832 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png index 91eb354b..85bc516d 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a6709fb7890158006517a3b46cf40077f5a96145252b2a5d8c022a467aa1cdb -size 2437 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png index 91eb354b..85bc516d 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a6709fb7890158006517a3b46cf40077f5a96145252b2a5d8c022a467aa1cdb -size 2437 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png index c279180c..85bc516d 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2973cdddb8623bc94097b42c279311df7d7f20f2b1e0a4c6eeb9c891fae0979d -size 2439 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png index c279180c..85bc516d 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2973cdddb8623bc94097b42c279311df7d7f20f2b1e0a4c6eeb9c891fae0979d -size 2439 +oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png index 7faf0733..7a434207 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac009c65fec2d076e997491a1f8f2158e63959dd9fb2ecf575b88baba2829eb5 -size 606 +oid sha256:ee5d9ccd781eaf64f3dc61cf34cd110acbf21f6fed92c00b52b492f3ea97995e +size 419 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png index 7faf0733..7a434207 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac009c65fec2d076e997491a1f8f2158e63959dd9fb2ecf575b88baba2829eb5 -size 606 +oid sha256:ee5d9ccd781eaf64f3dc61cf34cd110acbf21f6fed92c00b52b492f3ea97995e +size 419 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png index 9e43acadc1ab9f6128c67bfc0b5616a5f96ada66..15431f30a81edf56146fde39894bf0e00a3ee27f 100644 GIT binary patch literal 129 zcmWN_OAf*y5CG6Ur{Dq>7zS{98@@uK(m2?Kr?;z@yt96I%(v`o9lR^`80+?Iyj}Kh z+VXrUK5AB{5pxvDJ%aPQhC7*@N(Ml%6%G36A{J}tyMbcVpFO?TQuY9V2DC$n`aaq zw1f^xbPP5f7BMP2L((8e&M`CJ9e=_;`#j(G{XFll-{*akw|Z{UBbkr@K#$3A^HH&O zS~JvDo|6!CR|UdRpG|ZqdSmuY6*NL=TWA2qsXFptqAF`2W&|7sAU~fLLHJw29ssq= zTRm7C5D*DUR8yeF6cS~qJ%*%F)Eh?f5bF1#ffP-<&|HGH?P%AEi<)rhdvvZt7a`J~ zqFV)e+(*CL=%0_fuHxPt3{S%&7w|+9#&D5y5);{&dKlBgFgpZucOyRt1%Y_m4-0*; z)E_+&jkrD4r#d_l$fpYgR5HZH-YMcB3gKiOc16?U587ZdC;!f%GyI|ut`;{XK* zXW|cC9MQosZ5*G06IwW_i83NiX`oyk6>6v?03gHyfuJPBfts2^U0tqz1~fFLh(wtt z2eh;%W^jQ-8r9Jm(Mox=g+zbp9 z1l@z(yK_TAviFxlcz9YwL`vjiIDR~t%}$J|gxJ{A9L~vv=Wy;^WJ*c|uNE>h4`gM9 zUcI{O`t==x_fSxddUC_|r`1b8j?|Y!R z+2O;7g>8M%-fsKplTF6}NF?T6U8Y}#p{K`4DmCaGh40^I4Gd5QCtzrZJUpy7G6|!j zq_HvW@hO;`)Rf7HG6l%x>T)G06lzK(D3t_My^;W^4yXw@15g)m7N8NJDWDCYJ>W7x z7eG3o2OtYD2yj1OBw!3+0w51i09Xk45U>ic4)8r-8(;_E7rLOjU0gkv^Wjv9WP+ac4LjZhS&w(%G};FQoD=W?ag-a_tYnjr@YUMaB0YJgj)~ ztoo&}{`H%8P0ekh&yvpWuhQQB?}I}lBV*$eld>tfLRoJv3IR}`!*rvu*rNVcPI{m> zMf0DOu=et>L!q4)ra65%iOl)83LC;hj(2~KDantK4y>4vUavU)I2W322xozyL0R|z zP5VS`;oGOUbyM^LdGacSB`@2ssHJSY+ng$DXV072u>O@Os_)(wYRh(6+4oaUy)M++ zmhq{Y#e34q)*Sh9ktwVWA2X5YU*zpKq)s)T=r;SMz17UyB8zjrgnF>j@r(c7sC`d< zG@tKeoo@eo;HVRCvPC#yYPrQ_>{xkK>MCbK_TrtzkB3_-cKf<(n@SXxLFPt@fB7HV z5gotSsN{Z!QqmCdBsF7DoV}vfY`J~?=*%Fq3TkaK$091ugO_ozGGf4gnZxyp@lS=F za>{y&v#G3`b&$ya|vIyd~YO9wFE!zXzp2yP_quV|xE0s9v@xtqvbqFo^fM@SE<0$p_y`Ru3nwjW9u|irORzwAF`NV#Std;<(b#GpQhEP zpJLn-tl1&Z4F++*njPLGEk@Iu?e;ZlHJ!sg>~&fHv7BxuU3TQw(DUyq=3P>44wxG} K-HPZt&-@EA91JD^ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png index 722c61f071776a0d15d6bd2e2f797b556577bcba..4e29cc25b2b5d1f0762169f3112962e335699c0f 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUL|md`@E39tw=Dj64ZuzG!$chQ^2e96An$-7ebv2KsX+yDN_ zTbWPAClz%WF-Mi$5Ab1c$!D@Q72OK9WQ|WDYHbhz5vw7}wdNLx4rfZNsd{iZBKqh6 MgB3i!Bte4{KfX04>;M1& literal 1879 zcmb7FeN@u-8vpW=FhQc1nfQ`ex}|0zLnBM~(?Bs_D8y9UOf*d@+YGDO%VI;XSXi1y z-E>x@1-{-oVWqje`3oyYgr+uBI)dd3dYvn^rPkPXwsp?VJ@@X9=leXL=X{>;^ZfPs z=)9cSTP3&&RqQ5neZRh&eeLW@1{8B!=h{pQIRV!?d**>++*Cd?*7mi%t#GT~h z=6i>W9$j3>RQy12tE8KrllKW2~l ztF-ZGJc%6Xxn`Cge)F;&6)Js9*_QyG3K-GJLzf$rQgXjX>rHz2 z+E$W3bVo%qq)oV*5M>Y$LHTl^gS-qCW_N*6cz~B9ZiI@JCy#Mh?czYNp_=_HK@-re{-_edE;frJXNh+3S&dseFBXYB~^Yo;0&RdNsABuPV6cvud=YEnYPe zp*H9U-_a4oC2KO%l)g0hqnF2@pO`dfo5oRa#n{u2iwgJG3zfGKa85+ote1%pjq@D^ zl4D2nC(Svy<)nRzhQdQ(CWouQc+z&|lV-(+K6UrERubGucOA#mV<9P1Q5B)+bcyK# zaDUC#w|>cln4G91Xo=Xt+ZZ!BlT6xk!x~0s>ica#Mp{lz6e;6|XIAZEGGUimijuD3 zq*Sj0lA^NZYM+scFfFy0wf93M=U~@16zt*^cU7hfZWyaOgd$>SiOmsBN8Y+yISkty z9bW!q{6rLMka^WnyVCX^x)Udg?H$r>rx7K5g>Jp}@bHThAc-HabrZYvi^9U-#x`xS zay6FNvoXvwv9!K;t}8(l%MA*uk)5ibgvU4Z43>Scr5Z@wQc}OwtVrLf9UVs`1-iSZ z@Zw9F$>~+fokhpJ9-vsoo-0_%()um5DI~O_JZmnF_E&*?M{ob9AWIkiJwg6m_Wzs2 u|KSV25&rOn-)?%>IQ}2P@6H1I%-IMEJT9OwpZUaMh9Hy~$*2uV{Q74(zvT@8 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png index e1e5e69db59cd790977256b2615a5abee7e653c8..3fe215ed77352f05e2e1e43d3c4aa6953382c639 100644 GIT binary patch literal 129 zcmWN^Q4+%t5CG8soWcbfENjVa2+B-bnXcrcr*FRAysLbl+-FdA(#VWnDqB2-Y4rVN?1w;u5x MEZl!ZfDK{!1BRt0EC2ui literal 1991 zcmb7E`8yPd8-K?&W*nh1iq?#VqN1d%Y#UL|Y>rerZLLU~Xh@eNcO_y_ZCZ^&TXJ+F zS9Vv7awVyqnJq_Cj@%kE%<=B*fAIZ2&*ykP&-4EDKA-nZ+3w|{MP5t>04+CHXCH+( z|3`IYgsz^|E4%KE+U4&#IYEGl>7|tI-{UJ0MKtll<_af~Bn(}aE7q0HW zwXJB|jP}oQ(;w(mi!M)b`+f8)#$9*tr+f@xV{i_JTtOxaPo2m3vv@8FFCNE?aLf+H zYX|YxUM%v*QXhQih2`#8wH0ffu;Dv=xenW`v12v9UWMHj_-;A!O!1$k*lUdaOHg2l zpBLdE1&8$U%K{wL!*Ly))W#_-oYusz8Ym>=3<*W*IID(pswh!GsWQrxP)-0qhywya zPKX1gcu-cBswM)FC?+NWNr7Y_lZ6@@U$wNR^in`ye@H(SD3n1;DlA_7$=JBp%#6D% z8_X|*`4zCZ0xMT`t-1=<)@?R6E$i03T)+N>on4c|O>n#k&dxO+9#1@OgRgIiZxQS) zhFvAVU=#)fu=nr35g3?zq!O5w5dIV*BGMzO;PmO_({&IR7n_)P=3E1uZ-BJ4$V*L- zkrB>fF|S`gaJ>z3+u=q#u-O6Z4k#$tQ_u;;#eSuwK4oRzj~{zJ;XyeMYHOY9>NY>? zfyTxSjs4KlV$<>oS{3*Vot>6kt{IPK${PhfpWf3$?HPx@3Fw~yfxvKZkTNu+KRmo} zLJX6W+LL0K5`$1k7S6%U3`rzXm&iaWRhG&@CR38jK`tktVw(g&B|vpR5+E6HA)r1W z9nchTIiL;TCO`*3CqQpNA3%S=oqz#=VSq;f!vSLflK_(e(*b`2%muswcnh!u@G)Qw z;0wT)fGvPrz+S*XzzM*wfI`4IKnb8!QD3-W)HS0hxz{ltFWld9%N^9XH}kR_dgvXD+|x=ghmJXX+t- z<;lHA^N2RrwFsi@Cexw~3$ku`>K0UU&9eS9SYwvcEXiv=+7!`N7g^sj5M4hm%6&J@ z8E0m!9f%pL9h1HOo&IJONtddqrTYITKGfLJ9{&y0`n444>|ygbY11A1s34L4D61rP zHZ+-dGW2cCh)dPoSnbd*%@-dvE=F6Dsdh#?awEOr#pl!&t^CZ}I(mHu3+*Xdc_SRV zVAbfQFR8Oecd}jNf9I33S|$}EIk(jwo%a~|r|Oru&;e&zoozm0YPu*%PtN?n*!XEY zGjODkd1s!NuC`sPwnyH#t~No&_ROK0Lq};fe{ai)zmdvp_CK+^Db1n1(U$&2rN;6? z-8r2YeF+uQPz&f~=Z9>dyJL{e&&RyV|6jKrEw`tM7oNq)c z`dVijqDgUr$H`~%QdM0o`Q|aNr~5X~JSe{!y@{g18Vz03F|eO7)nZ=pW7A5jf&CxP zvjanGc8qm2stHyTUrlw&vb4FJc){47P=^x{+YflUxtRqW{+VT^W+t@iKftgH>FaWn zM^#e|IK&_5l9Mw|j;qg}v6{8g6sYv>k_4+WzYpS{EIG3#ZRfWpZ*|c@(stvroPV7fYQC#-NXTu9j3Poy+bp8tO zYdX_8rY_kY%l9-Jtw`f6)=k!2#B7*!-AG$?k*ux??nNxaTA3Lk9onroFXBPzh8_pHan>nDdCgTn2feUjVwuSjmI{Bk%%V`mXR0E^ zpZ&zao#+44;`v4Kmmie!-Ywh067%mJ5A#t?sVp5Ua<8UFRCGTf7i#K7GGwcpSnFL& z!peg$bQ0ZU&#nbFYHXzyiYtEnn&wTaI>m?)7j}|TKKL6pov?MMdGkZwzsl##Q7K1? z@9(ufO$!!Aak36x3T|GuC7oM$DWv&LWLlotG23d|c<%8MV_lgaP0+nI{rk~TE-Hhh9GqtX%V!qe;9yi4Ar^_Q-5p5rj)-sbI5#`3>@ z;*$E)j8l@iK=qdOr~%0xd3T7L3nGb~CE~y$EVKdxI=}8w0D>n6VT&0`0WRKpYneDE MjB2#6XkjIw7GQ;8W-V<(a&CCj)k zqGU)$mXIZiG2$Ki|*i`99}7=Q-y?v9dV1Q&>tE z0DzsQCb%>F-10rP!}x2d9W9%mAc1F29tUWhd#CxuHZQ|dh5+!#J&_GpL4LjCiiv$7 z0Ejkz4@mRnLU%rK;Sq4*$&ypFMeSEBK*MqLV)l7zXVap{r_biqnK4pNiKA5MJ>M!6$0aIs}AfN zi!U#RT&$HsR#oFKLCM^2wHH4+b!bI;W(c$#fe$i|EoqaGPlzxMXU0H~tP%SGH^fI@ z7$vQ7fbK{iR7IIXI2`@8?5^B;QH>30>SgPF@*TN#iwl zcS3QTJgo$oXsJSC)_vSK6vyg8dFPm^LCG~Knq8nRQ4)=pq)|R1FXdF|YVCLjB@+uY zEv)~pDo7@#sGD0W>p~IFgnMx%@Utl;A*WskK=Lf^APexSL<#iFxpuU5@pBn8{C5Q`s}ksEbb zgo+!`(^!Cm(WmXw&zAJ-z}%|l5<)1IlLGE!Nmac!cI^%}4aEQ|Z*&wMeX+mj%nU@+ zL{4tAryoPfO@Fp^t<-UTO$~PT~X z&G_-m2S9pb(Hu&5FdlJ zY-Q_P(!74-^`-7rhPCYR@SJx+@cKf^u)9Y&Ik&5qiz!HCrwYJk5IE+k;3H16h^bQ8G!?hJJNMj zp|WlBV6`+PCYN;8??71CJJZO>?I(6J2>JS@#`}nY@i!ZksdeWfR8 zwTs5~%q7svv6DIWF6ol|i<>`;u(!xcZ=Q-_8r1hGuhLg<4qV}PCOs1~*Y&lNa{Z)J zd7s4RTaL<$o`rr{+%sNwK0_e-MsR#}YtbT z+E=_C(oU-D!VWx}IF&z)0W_l%6Q#yJ-K+QNWE$f=z zf{<)6W{ChrWAWcMq`V0aiWZn}%A8eP*8r})zrT%=nq{x--QJ3KX2*9|?K R#N!(&z|_bBM?3Cv^Pk!QTMYmJ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png index ebbb4aa5..c7cb0018 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png +++ b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52db1c3be6431ea2698325cc96e80a5d10a2e4f863923e29fd41cd9fabc8e43a -size 2847 +oid sha256:20457c79f2f5a782088bc4f9a333d0092ba9c5f5307835cdfc3ca7bf527420e4 +size 3247 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png index 747422fc..36d6f18b 100644 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png +++ b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0dd9159f91297b0bbaefef564b21b3b4242dc6d7d21c0f8b81c50fdd4170a72 -size 1523 +oid sha256:573d4a102d9bda9c6f6e89e4c1fc41dd14e08d2314091c41dea0395b4cd680e8 +size 1131 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png index 5dd5b156..2ec013d5 100644 --- a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png +++ b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f44cc6345363402356c67b19eda8f8279d0481975e5f88e0ce86749df907e72d -size 5585 +oid sha256:2e8d67dbbd4fc8a7f17ed6fe300033e44a050ef2044a3fb6cfd9272c6d55816f +size 3188 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png index 571db971..266a6d6b 100644 --- a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png +++ b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d1013caea84502d7ac4290320bce5a305016b4f2d92428160ab629507cfd4f1 -size 2997 +oid sha256:989c843ed10a31190d812545fff20bb9fa0aeea67ca0053af31fcdb06aa6d4de +size 3004 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png index 41031fab3ac84974d01449c02183730027f57c73..5273f1f6d93c6740f0af62b1206b5da4f5305da8 100644 GIT binary patch literal 128 zcmWN?%MrpL5CG77s-OV_EdO)^yCBS{WL(U_>h)dTRo^`3OZ2r)KBT;lb$c}4|MySX zaXuxV73HP3IY@BN;QV29C?=MmbkxRVBHI$i>HvaQRJ;R6m_h>^s{tSnC5A+Z**Xl> LZaltKfJ)mBdbB2} literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1|}a*7srr_TW{|kH{=$F(L^$N}%pVHy6XpI3r(SF?$FXSInR7aVvm{ Ojlt8^&t;ucLK6VG#L*l8 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png index 55220f49095a3458c572ab11d160fd6252552883..562fb674c74a7d01cb7d110104122634a2921391 100644 GIT binary patch literal 129 zcmWN^OA^8$3;@u5Pr(H&s{=iVwhrp*mlH2*^1OC1Ku30)=Y`C_;(j LqW#Av_z;&r^4TbG literal 2222 zcmb`JYg7{07RT*%BONU!Q_NTCn0FeH6w45wXr`+?hLT-Mj9W`(dBG_W$g&Kb*DC{+%m_ z{151AZ`Ib)($e+u_6k_`b1Me;+wyvOEXZZqt&Q~X^U%_&q3bL}tXqyZobx`8)6&v= zv0|&*;_sf+(gGav@$xuAcq<>|Wre;sefkdfq0X<)?sgA#KkZ0BHSO5u;40%XEB%A( z@8GS_?s#astpatj>%9&B-G0a8Yqx{{j^H2lI_vGT4dL$1&jaMO?Ad_rkk6hM<~i&U zofMbigt*SBr|rj8mbjr=ib9ARFTq9Ld+sOR=zkAbX(;}nT^_^LzrbI`T!UCo)dQGr za@vM>m#)}f#D8u4A^v;z@8X{&`0mpA9j7#xQ$cU?zH2tauo@oT{txw0XHPNq-ZPv@ zR0#aU)*nZzVcc>Mix_jc#+9z=44s>`Z41oCeocp9irYmo+0bbO`SDmq8gg_4d9B45 z4cl*kiO!zK43Kp+YIe#(m%T;<89C>~tpr(HE=-*^Wj1V1di$e!&g3EC~KkO|qms(oPGdj9U_xsIljYz%5P1enjVz+nVI;R4-+rGZ;AcVGXiTmT#BXqM}5& zg=sIn0X6REZMMWZhoF)<6G|^>gMmt*w!+Cc2}Mh*Nfz7(ma5y>l<)}vS^ZS7u6C&W z3_9gOq#I>UM^=Ns3VfCdYV!mxoI;|g6!64OjoOU3-iL_#*LVB#i>07G+lM|Kr0i}*OUHKQs){)CKljnAZH zmL@4An1!d^WZxV468V_YNtxom^!-m9=!{_rOVG}r>YX-x5)wYyt}+avRiK!X*_VR$ z^M=A>aTtoxC$5F^^a12h?W5L!!CG*ys+pPRnqdYHnA5#)S`gKnI12NUw(c*b&{G<$ zf>?t@TObLWTj4nu3r6+#*}i&*Bl-hL|D@_NQ3n6#=AI&?>P5Q3dbkt8)q?n2zH7ZTs1=A)!`r^<} zQ9s8LJAtI=Mq;fXpe|0*QklmO{%-v82&-T2i=#Y<+C&PbL>M`VLiLzBUElXGgT{(sxc4`EnAWb1p&TMc>s+s8ymFmJ{H#kM= zQiYEW7u>?Vc9+uNQJ)1Bk;DezpV{NZ2sxsdADr;qz9>1vd=A@wV%&%#m7)1W-!Asl zH61J>e$tuzWhUKb-$&PfK>45^GikK?{jR<* zoR$3%u2oM41sR)(_kpBHA?qt~(Zx(N_$xQjxt>}F@tIN#Rrojw6H?=FILT_cB_L=N z(s8Hr$*A)2L-HNpA|M^>uj%X;D36yOriS(0FLmW(Zz*&w1mB7$+~sS05Do58_m>?n7|gq(ri^BJb39G50ku`PeGerwx4cI!gmY%-&GOOpMa z_0a2X1fyrZ~l?+H+s5W7LMw z_yWV@CW*eMsm%|EKV`qj0l_gbHluC-2qzRZr;d9!?UEQD*)*r0j-->Lv*qejizQ$U zz);aXrKG4T0Rx6+7D|KXLnfKAu%+d3@A}?t1f4DD?M7{3WLNWw&QpbC6$|S<8GqI8 z<^gsd$CZBFcqw1$QQ%9w^UbJ4vDa91a7o`Bu(;zH?Sob9!lhwIczN9aAA6sv>G4-` zRu{k3zhL@1SN|(M)Bh)dx8Xmd`HvC+(?5CrSq2;sB-|If& GMB1-?J3<)% diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png index a16169eaff752d4b34241b8442e010022c5907d6..ef30b2c385b50527025f91487b1432813bb172eb 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@u5Pr(H&kWxOq4GBS*QRx`$!qe;9yo=w`$4j+2PdT){_j!9%S^u{W zt~8!nP9}ZXW%Qz?W!U}UB5;0MI`Id Ml|}lO75L3AKhzl~@&Et; literal 2117 zcmbuBe>@ZTAIIlPt^6*>ooghAb1b2eHe@NKFDezY7<0l=D|4-Fv?HR+ICU6{S;~)N zB-c2sDVEBz={mobi8AwZ*jQuc%hy%k?>!#(*Y}V2=ks{KKCk!Z^Y}a-pU>y{xpvgY zQ){!)W&i-7b@-6S@s)mIHGmsd+O&y4mz8dPguAyp08o>up$t`D$v2%pguns-+AXW0 z)`ltj4glCVa@fQD#HGH;eqIG=_{-)Q=ec~NVvFqjklT9Z>VinfjQrp+`1NDo-la0# zohB{XcPgz<$Bm+Nt?Ge#RsqRH6^F`9&EW=WrtU|M|NQm$I~?a^_&pfAz#Ffr#4{on z2bvD-7Eld4Aipf(<~hi3-7SgzgN5;>>g6@5hVCRfBk!-@9=A{YBVUca4!$v28wk?f zyBhx^ei-;5UiokLn&|#%3a;QzR4_GgL;VeZ%ZyL*$C+irzp65dsc1(|OnUE8Q5WSw zc+Sp6B8qeRr{4oaQc7)jN9(Pfq*CZxY9XR2iopNF$W=bC|96Bs7;&c3<)Wn*b( z?3fN5#nGF4MgiG5ow{^pCZ1bGF|$j3UhWD^P)~FV^2v*411N4k8fW5dM3LOZBu}w_ zuHn-{aHlflk!n)0Zgg-~|LuA1qmqS+c_533C#bH^#>Zz3>y%k~O_Quk=XNddr4Oc1 zoYzkGs;x;OEPZJR7x`jh4j<5|7xs;P5^G($GFF~lJ>x#mkUp7Kh-#Ul6qxnanmkcj zh=dfjV=Z(xgijvgr2aNVw_e)9N9MWz{9Nn-v2=@BFIOs`({ z=bGgAE{%rOJ*?yeDYpC>3F*A8!aLq2%zv$X}vKOr(&=O zXql@_sZ|AHjy6nw6P?Dd$Yy&!6c{l2rp|;*NU3~iHk&cLd2x(k6@86gA&um^FeA=Q zSw;WEuegA9AWC+NqWuxbT=56 z^B$6yEjuKyw8+S?6yZ!KZ1T6y3k1${Ck~52&v70XY%j`VU(^L`pyP%km~^M$VciD4 z=bO@;ec~XP+wuj6#ird0HKAiZ&UD-xOJ)&HV3WufF#j}=br`2N-@w?@k)L_W9BH%> zXm(YtIt%GEr`-t0bXv5dw1}p%uKGTt4sp<#5PkVOMMbD>ve2lS*4+{eWI>5MIFIF#Hkz<1JOWdTq!YRTsZ(~P{42P11 zgHl-(EI!hDuRIh%Ocv&u*$wn$bj160ZKLA|CwSJ9wiwTd4DY5sQ(57*`=GA;#8hKX zitksp^0>iEi0EV?RUUHP&jja~%Rt$5 z5hJhh%2v#0R|ZAUwY#=GN z^tl=ezld8|3^Fr)oI9U286?!iHXn&h-yz#VB-n^@z1b$}{V7f2oVpDr7acTQvi-I-BS)_Kw!#ElpYhb~zC0;T<~EIt1THl`8ZgpnL>mWA z+nNsqjt60k68Y7}%7U5ce{q*I=6!j%)&w50$yAoDPU8t^>G+~@c}6n~%+tBQ=N&jt zZ(%Hn4K}{mI+2b*J#({mo8IL{no9FM>MoXQxw7XOD^Hgk#$Yqvk{&B!35C_EG+AOQ zkVTz#;Tw}q&6hhPi5YwIL MEQ`^8Sz3c2{=ge2E&u=k literal 2212 zcmb7`c~lbE9>;0a8PZal7O`APG~cvnWT}W)l1pW3Wm6f78cmBW!Ty+3~Up5OQW&i5|o-t)P)5Bhm8 z*I%m-005Tn+v{;y+bcf>@JDTXOL_vX?R3w$`?>=F<(USmFgQ;Xqhqg)Uhi37R`rNiIUZ3^nO&Cl zx9lqp9J^N4@*;xWnLbwfr3&C0fUiV(%`nzZEhSL3*6M`D+qvk_w z_|S_v^3e6zHY?WaMA5=SnOF#orEL&S4U9#D!@}!@k)5snEq5 zK4tL&k-tK1#pU8&BdZ!-y#I(yYtv+sFK+-@9lx(@_(jc+$JlX60KQOP>7wB zh-KS@O(hIy7a>eWzTiG!6|=Y zL2=`b-$V(@>(Q4J(jZuHH5)uXN+|=Xy*~Wv=BhcgGs*NqZjt3dqoEI)pL_3?K7HHt zY){DEr!g9976Xo?%XLQS5Q|$MG@x?(xRHtuxL1f(Ca^?d;g$3}o}C#Xzecyy=`FA? zWM8&s33hi%H$##v%5e_WxD3WZ1swtxX~${`7bACPi7s4Hb$P?!>BA9{(uwee7$QRl z#bk(~pt0E_v{b6SV0B2+YV>`Zpu8Io2pz$+WQ&x>-`jlixkDuaXAI6Uzoxk|wXxn- z<4L@jGagW#KE$Hr6&ohbkX5ExxG-tE^O-aI_k=L&7b}}*Vk}v^#=>0#)+k3t`-d;T zPpO&dy=NqTe;i-jU9ig(omtfL3?4WBhn(j<>{M$QNb`kd4;fQU*2O8|)8L%5oQ(cD z4@rwBrLj=d8JUtes+}(K)J8mtJke6io<8sz9_P0L3yGH@OW(!QAz`U9ftB1!^{hT3 zKYHVtcnT6PR0vgV;R)+b=T04@pz*?DcIouQTNmz#D;vmo&WOX}W|#Q9snro0*vswU z;TT~LN_~jG?S1S0}KyN3%-J5GG1MLhmXdBV8tY%0At> zJ^Hj`=S$cCURa9>Ol})<(nDZ++CAK#WCe>%S zk6%1ucz%PNRojC!Jl|)=fP^@xTb`{!V2FRVbHcXbg`oW9N552n&p{uj@YX$VmYX!6 z05a}t96SH*cP2fpOx4q9>p&U#MRT9FsV#_#$2p<{~ z)QON#f)L&kQtYcQE)Wx69OSWQd(0I`nT4ets!BJn;RGz&1lw$PJh?4Uhw3!`!bU>g zX^a)OX?OKWNTN{eeh4mKmREPU!Pr)wL#|pjNLfrxVr_pAue8M0gQB|+SB^&xAjmwV ztAEICE-B~lF_^(R?Xf~l#b{1PZl;V3mLHuP8E#!Tr%m})$6$D2*2GL|6{gc}hH`@A&EZnaJGR zFjT|RLY{#PJCNLA%^(e}qMB44^T$Y>k1BMt_U(p{`m>T13n#IGk`$1pB4{*ca3rONlYPKP z0-cS9eKbL^r#J+dU^CQtW(}LC@-a~F#f;+6O?%qQndYQ|lN!N4GrjsT zBz>BT+-w1xADY#bj>=(jvv}+i=iomU0(KG!(Hup&#f%GdbfVDvXo&)bmOTf> z)8-CsDVs?huHj|GO}N}JgtI9bhb=OuJct+JOW|X6{0RwjyoP72w7nrRB{f4V$kROj zlPXjUABbkmg$^e?C4iJWKZ1JnLaX^Sp zOxCam4-)G@4f=}eH$Pd>+Vf$7#|UojdQ|^y^YX5_7U$oMMll!#vi6QqJAbrq2z=S{ z>Mb4;h+9QcLl+y+0+&AA$8e&-yZM5&cF!&ot_xzqP1W61I3LS?zy$}t5@oSpYt;AT zUODIE3OD!r#08~AK$rw0{NlqVF4OhVo*wMfFS`;R)|v(YzCr2}=ax`!{FQUNC0_ve zm2<`}->CRi{4e-@kILPWmnXsl0JHx;LjUUWzpZYW0JlVSX$$L-Jd&pUW&rki`gxQg HLX-amCM;J~ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png index 357c07c9729c9d902fe60fdb661dedcb23ec7e57..6cf010a45981568975fab9900e52b4ef810da2e0 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@u5Pr(H&Z6JJl8wf<0QRx`$!qe;9ysN&YkC$q5o^oh?@ALLxS^u|B zUTHkFoR##oFQXSFEdjbCWS2Q)NIM#fzA&0%kbrH7v=xjtC-9+S^ckJ497_s>v{7VZ N$S=~rRT;3b`~b(ICwl+@ literal 2209 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 O;Mfsw&r*2ct$zWY3n~2o diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png index 51af88274e16969952e305ed651ffe88f3832d00..c84ad38192c55bffd5b34e2157946d30987cb827 100644 GIT binary patch literal 129 zcmWN@NfN>!5J1sAr{DsHbR$FOHjF}*O3KD8JiXLkynmGs_wkl(&XW(N9(~@PmA7U4 z#x0GP;228pmmwku(>|QZvDptIt%XW=bgNM4CA&woT<8Vwp>kVaXzq4sPj)W-f(G zl;Dgudd}7J?G1LekXlD%2Qu= zoh|?X(D&NsesCdc76bV0!kRV`fLKV&BX;}l1^{Ykdb8+n7WOO7?(@e100vEqv4n^H zB^&?%+IhL}K8)`f6}4sr4_Z8)#JwxDYW!E;_2UOO!e{X-_b2hoh{VXs%BZ_B>-tWHRE_@map2c|l|O1+vi6z}@b8-jVq%^R zjG59E>hagi`LRkA!t-jd6%n<#)v zM}xP{&U4aiBQQnjvCdC8r`kY7m)LUl)Fyjy8)?i+MCaX_mY_(QL-r_lr3|qKq=_3q zrHb4#Ch>Z))#1_LcB$MgZSsISrCGR&QH_|m3ne>qOHAubRISWD${Gwdp_CU!+L<)} zM7WwE3FvE=?vZv-2JN%};tuZWNiu{s+K}Wa->OrxJzE>L%6F>RY(^?HEaO>~o8J0> zA(hQms(@_KlH@~)b*KNwH+I&Yuj>eck{EbA(lqf9~!>6Cq zA}JR$?Oq5x-F~*!34^~Fm?EB1nDS_AQsU&UKhV&jnbV24U&Ui)w}U?zVrIH_T@6+2 zWXVX%14Qq4kZ+Brr=-?2GF(T{CE$%m&0sPumW6OEa$%;Yrw+4H8MKQ=p&^%RUaiWK z^)cbrf&u|g#+rFwza_M^ZoD10tQM@nS5M8Dnr{<9V+vb-Mz}awXw-0}M-C-=#MU_? zV(%VEsQ<;^`!yAY1EFUfOU(4hv(5zPp!ojNQ%}OaiByC2?($}jlEyO0%X7(Ot&@-RT(}iAq^OjK7btvM zea0WyKUGyiDOXFA(n36!Mcv?K$78>35F{ERaT)G zf8or32j>CVwQK^BqB`T<@jxWjvsG?KV6ftz5UrWe?vcm%YsK5974yae_p!76iA_xb zInBvVb9oe=8nhTou>0 zZsp@5>uUnvL>}UgDr_{`XoW zHnXdpkG7$TO-uEU7<_bWiiF)x7MRyQQ<^jd0NDlePA{J<{rG0DviR{_n`?7n$MhIb z>E$8YlzT=GtqTMoO%FLzlYzD&R`D}pO?)W!3j+}K)GR^Q*rSgwBBpJzEu&<3BrFI4 z3wji@!a2VFf_%F&rTE_R;@WSK*w<#>0$8-Al3RTLlQ0WM1TTLWZsv^p^}+^}+y-{S ztOcPp)RvvF`=E4F#@WknGEa$`pC|8P)Zt4S*V7GcJceCca5O<6ZX4H;|L65+!osty zBu&K)YN>;#TGM+fEs*0+5Ae}dLj-wE>hx{JIX`pMVEtZsF@blb5JLnLMDYB+C-io_`;7)~|@@`fITl0FR%E|)N5-NNYR zp{1hG3Up9e;B(s4jjm_@=dH;^JHC#A#Dp&4cDvRZhQ*?pZoZu43r|U1X0O586gEk> zuQN))w7|5QPsQbgJnd8XKhj--7`Vv}16GuDD{j1*QRAq-g9Z1N;O$R4fD(TCi zzwGHzC-$Y<9)d>b{!O#5;qxEFEsCEDe>?x1drKHUeFEYa=RYX9C?4}Vt@{}KH9e^Y sU(@sdPg%t2{~-0tnV+tlnmA+F_#M*f8fn|zg%1V5%LC!5CFh?Ucm

%dHY!zxs%q#Vq_*K1$;s&5|iCHh(?A5z}Ox;+~2|NAHH zc|IkdHOotHa}>cn8t{ikYUH5mWJ5N_C}^ahMDM{l)J{1p7Ne&Y5-Ng#!DdP-H>%1K MY&^bIm;ieF0e*ia)c^nh literal 2183 zcmb`JYgiIk8po+g(=;#LbW9~}8z(Q3Viy&0#CvA6nR&}d$e4sg!n{D}SYDI#Y09Wj zh_=+MN!e0bSFDD_Oc7E`OOQ2FaFs|QL=YUErrqY#e%TNI^FHVQ{?B_po#%H-_wNf^ zy~=JC003AW5gvSCv1*rOV!XJPOeOd)mKEs0s6YUKRbe)tY_zyHO%IPJ000);B`xc~ z)g1!>OtwY@2Oi9RD;G;9*QYo>nZCwP;O{9raH#A1!*x^CLy6*bg%6Lh){vqj#Qhhj zq?d@7AymClsIbu1dR=~hrBg~#z76a~|INU<%57sibvajEw5($iAo@5RtRz0=W+%`2 z3zTHWH88rTYJJSebun(otAhRpXngE;l-u5ACJt7Xo9DhX zzLT4w@!(MGtFEZdfTPi3f0ohQGhE7FkJQUw{xSUPxLH%9TX|87`<*-Wm8^Im zDv8pRbNE|SjAH;lNlRKN#XIrlAZ?|1-->9@eXr$`t#NN+!;uxxyNv_SjuR+Ro@pg` zzOzADnL}z1F)ScD#)B!Np5-ALbhj@|;qu`jyUm~Hks}b_#>_l&TNpY4&-3P8Y|(>F z$VK>C?gu}Vs4qxl^I%&=@hKIsUGp%_QXeRbG>6EefF-OQyxWEP?5&y(6t;q)+>f>F zmouRzWb6EZ%a+$77oH~*munL#x_45j{gCB~3L(fb+c!`he*QZiV|=Ou4iBF=r^(`t z?zSD$k1?9UWP0NxV_aoNz50^|5((cwEB%+`f}HtQ;T~&CL}yGSY*B)0KjS+kSC z;j#m+5#(4S#s97m_n6APf4;rW(&&T6puk&D0+ z2JGj?%w?rkqfgUO1<=7^ND_n7{&PlJ8D^vqdTXVePa3)#@S|M~TefA2-$z|*aN}*c za?57n!t@?@1H0x|M-DuztTmQ;aNC9TL;*>8jCNZlZstX9Va|Rpwr+14)JGE9UV58w zKyD#%{$+3DL!e_OD^G>i86GS+Yj~^hMrAOmhD3FKiF^eI?#zZ=UD9LZKgFSY^Xa zFcE{OLTha=8MTxSO?!R`UmNv%K!3{Afe8|C?L(MjoK!tk-um{&4V!ljc0OwHKMX;e z8fV?llx0A9xtWD_b$M4XJUXfRZYMDD{4lvn>d2fO-%kO0gyC|#3FShP_--v& z5#}C-EAJncTWNo=h_fK@p9fX=cT+3bByGC4U z)A9qyR+4!Anhq-PfPs(=^t6MIB)#1t!`rA$g-5Phy17t}Pjo?QO*nNNV#^H_Zfs-l zaVP%B@LQt@7|2`QfK--mNodZRIqa-LI@~++YpM^li%=r$ieb~pmHxd4Zutzw(pHWi zt)L^}I5T2q6jK_|+O5X{iy+f?>C8=p+{yF5vu z{hUqB*?*Y(cNlW8!&W*)s3DZTS##1)S$n6RhE!rHksc!r76TzY*)RbM*Uf66YwU8R z>D4cb=qSWST9W5|noN6}EmZHG=G6rWScna@YR~4qSb1;zc3o>B?=OqTTxQ!fq#aYS zfwNB48Ie?DV1}pprB!;Hr?o+vS|rwXf_NLwl%JOSUb67-pbvYs2Jkk=km3T2Cq6>R zp$kC_Sdi?oh2g~@wgNv;6>vEN^UJKrdpQ#U(L8=OK4Q!6ep5mJP@-!iwK}QVjTlGR z0al~xdXd}mzT_OObJ;g(ofIL}WI-&nK+V;_kIlI=N9mMubORPp`zFk-=MOM|lZE%HdSew^-Z?kHI z3#Sg$Xa-{BjJ|O6Ozhb7kQt%3&|RtwM?Z&4@vgiG>K(6J?;4yjnU7yl-j|}11+5Lx ziB9T7;U5J1ic6od_Gh#$^6oS9zMyRhg8!R$U(!z+Q=KfrArfR5F5jSiSCNulk;SykuMJoQG2PzHSem+yC~- z8;z%$XC-xk7`@2Zl0kn|A41N>dGf_!KpP1NfWhg?Hkjb{Y7K?Nh=7qvW8&Spq;)jN MyO{l3r8NlR2e6qYr~m)} literal 2217 zcmai$d0Y}$9>A$))Ur@)Q&PY*kF|7I%*hC^P}dp{X2)8u%!AY^4MRM^v=mF%gQ*6| zP|VC|9MjCa2hB9F$&k&oQp6@p5O2kUduZCGX?H)L`Q!I~@BPmA&+mO--q(I!TQ(VQ z0ssJ84juIPMx$l#L`O@r=1vDYYt&~k2Ye3z0QD4IMa1Wt^TyK$g9rcsu=|~?zKS#dNZzQ+UI)UMYk9y4 ze0S9*JyK~l>?e`sg&oI@j*_o2bHDAq%vU zrTgAc#l^|h8H}J-%n<)jeY*0cbLB}lacfCi7J_d7`Hn<4(OKTQiVbLOGEfJqzyFXT%(3SL=>JmD(e8RzA0-z#kEL@_01U)L~y!O1&k!F-Mtdv&O{6 znH;H`p^SI54NDuD*67Z*`{jM`5^puNTVlkkM8NW1>GdpZ=-MfHxB+9-tGaYOZ-Ks1 zzAzAWQ17OHfsU$j-)!Nl{09Yobxw1aRgg?qTn|xnj<>jGt=2>ZVQ#p1 zG6Yis(D+N3N5&6>Qp8N|E2i_Q?QtTIREfV*xWIwc1rXyJKyoFcZL_$F^ce%zy#fx= zz8l?{zmVRO5d%?ggTECz7fvo0!7ef>H#(fI^}?yJrN3E`X7ZENbA_DON3-Adr5kd(i8rirm$Ce28@^7;w{PFOulW?jrVTe z+gXr;1zAOVWm>Vdypsm^zG=rR2%D7Bs&KE4+hvIsGkMDE0NdUcE%+PvYY3W`pcmhrH0}&DJ(j@$g=o4&Q2qT1ijNJK8qq)o6 zwVIy^`8?FvllWRgT({32*w2Il9%}jU5mi~Rbx@fQT#?d5XZFX$H(3;pOgGG<=bmm& zUK&bXw)W|sH)Ne1RWRY!3Q|FA#)4kV>gHI_iRj@q|BT!PN}wX%GTbYEAY1L2a5ce~ zK_Cl;ag4aezuX?#pHOWfF|0yeU$ae?KP??QX-pOry9S--rqbL+Xw$PK-*dc?eXeSE zNIESrLaWiVzB6Iqrb{_@V0 z7u$&0Lv&SSpl*dDdr*)Ns4El|N?DWe^NspM>`$4;fU#y0_8RVv#-oXV9(o*MM;2Te zyx9pJLW@|6hI&Io@Vk+|<`9};j}u6Yx`TCCh#22YL`@FQxs_bRqs^HNgl&%fHzD*` z0qTS-UsGWAk!2#eBi-8Pw)Sa`4h7+f(JqjP>C=eeb|p}AX$-oOwo0!HobdVG7Ivr^AFbo2fwo8Xk{1T#O8 z9fL`EJ)c+g?WlFyEQlDp!f6Rd1{2Ksk?8oO08D^DFSuw3Qo|ZClN3!iKqjs-F2GQI zM$~4~@acX1U!v#w`B#5{R9vGM_{ zIC^?z2)Euw@*q91HQd23>31)K}MzK!JA zZ;6Hvc~Qsi1n0S@0YSu8aZWdi3O#{*TFUaLYg>hT)oGqnNfR#w(a~_!yLdu;dwAq~ zpRYv7l|eHbd{F~E6;z7=v*#_ z$)~uFCQoU)T}G~$W!#$hx`;oRB!d!*oLK>-U+|O-#%#|vsUk2Xt-RDSW%e6-@Zu z7sBE`oikBgW8e#Rh$ePmNH2xjG5@zSYQBnyV>{m3Awg)B!{lr*ed)2xBt!-B6Vil} z%K1uA3*5;=WJm6F0n!r`&72loUuI7Um{Xlox68#K!qjm}hb5n9j(_($%m}-ml4zNk z1oNjUttYc>W0npE^kOFP=IjN9jf=i)Tb9W)@8LDwuCR`R(eG^lA8GeL{CbbS|3aV+ z^gSCtAn-R>{s|Yu5sfwg)Tb!@KXv}}DsG}ry8e#PKXQA+>Q0Y)=Y_1rIX%r^18~UG K&!ZL{p7krfIy{H~ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png index 698e589c21f08a8b2252c9a129e7736c99b3f37f..8afcb3f4f53d4b1188abe5430f37232ca985cc11 100644 GIT binary patch literal 129 zcmWm3NfN>!5J1sAr{Dqx=nhkE!_X0`R8lr(;pvU5U-kY;Z|>tI+nfh)O5OXsJ$7EM zwohKlcq%@qsMCnitK^ozy3GZbAk;`0G9s)tU`5heWA(|$QuFGq)7lpRSK;s1&X6!0 NZ)g|Xza-JbD1MN+CSL#m literal 2138 zcmcJRdsNbC8pmlWH63r8c_XEGZE@~??jTTWsm3~b`$oJY*()Rk1k&o+ zv@MU4?wkgJRM)*d-A|>DFT5OTG?_Je@V<)B)<0ri9)ISRu8;cJx);5nsdd4Z`c4`D z)v?XpvHi(?4P~KCI~%2A`7brglPzl9R5QwDFXDl(^0rxDIez_)ck5aI+Ipd8d|25U zpUut~%yfH~d{S*?O7=`P=$s-L42j?G$W`{=HoCU*K!V+BPd~lmi0E&?7RpwHDp5<# zM8{>%g-!c|_}|1A@%PoA#oXUoi1tW%{%;h2L78wXMmixFNxZn=Kb?PHi=xg(oF6eY zoY!E&VguPQJMh4y5sk|J)j%-?kEZsATT0He^9~9zgXLYco2f>kiPWQH;1Sw`O()&6 zll4uuJ%p&wfgM*}b4s1TDdvI;?4v$E0&CZhVpR;i5{{dr9@MT>)*$79b`}G*?jrl2 z0y=UGC1yLy>HWO**YN%qV#i;ZL7+vP?}w~56ukGq+cA}8hovvlO@ zU*+6|#!x>7X-QOpQjBTQbU-h^c6%w1bRgg=tn7JjCU&rMW&NVmtFyOYy0njfEsYzu zfQC|p!;-iG@q2s){AW|!Zl$GZDMRthOsZsZK1xV$ZN_Bf@RpT+s9v{{Qqu|7wsMI+ zP?#URVjDGs`iVE8cp}ZnJ7Mx%k}L~U!hH>0h1`P= zSp$%B)n=OtpSod%Lq1>zL+y}gE{?pkeN?z_CV=<@WbRhPe>CuNTD5_ zL&M84@DiS571fCCC-Rw{Yu}kPz@R`&a2Q>IxwN+L9Q}JM`Y%l;BPR0Ga&WjScKGhX zfrJ8}u>_h^nptWcN`sY^zf@{bWcv80ywM*nXD$4oDQoX*1QMimq&4gidlEIOM+r(f z)V0?%ZQ@4NM8v2*CFqsKGoj2oE-leEp$Jfg-{8IUP3rM=G__7B22__X1S)Gng&|?1 zh7|3zLqlR3ZtVKtLb5dz$d}%(!zUq^Ev$Q153?@U;j^7yB}FGTENRT+tmW^TNxZ)C zY_ly$sT*ubX;yE~IAl|@OAbP|M9^>HI?N;Y?_eSENIg!p%$Ac`6r)2xWk-Qk_z_wZ zjjU~*Njr#BkHnHB$KfF#j3GF}(n9K%Ii&w}V)Z^90*5|+shgXt8@yENE2?;)%NpPa z@%6th)1#8b7K<%bQZ4NL^%pG$h_}Tw)^dwMWF1ZrR?%g=f)LfbSKwjud4MaQ*8K); zOPn&oeS0sTr9+4t4N=68X;5rul#VM8RX*LDzNVCJAqBq(9cQ zZO^5tBHm6Kg?;Cgu}3;svys~r)>8@OZSAMQDk%-AX8t0Wa}2l%Rs@l+(YwcKs}$jr zpNlqDX8^W%O`PJ3rQT3xf)zoQ7?MGps^;5q&LNYiJb4j_?WQTxEhS}9}Z&a-{ zHCZE?U*91+?uSMv#4dV?&95Sw*{&r!kI|&)kU(POixV(gk<-ZPwre4e9iSuad%Q*E z{fZM%=Gmgr`Dgbl)L5a9jJwcn`fn>8JVnd#_1gJh)M?{_snt;+}wv0^-Ki6?y6sRwiudewvzA+1T%RDf}YM8EosCIR^EyD$J400 zU4`fFD@#tm{*Kj#zRkDgYkcC;5z(Kxq~r1#SYNnA)cTELU&KGd%Kd-w?jpLa0<`=2 iA6WSRnrZk;&en3SsZ|N^?)=8*0OWnt-;;`ny7X@Y+#YTK diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png index 8a2c7d7d772b2e24f456be43939d1c037ad12150..dd2b6de4fa25be4db0101ac4f7d9c80fb582264b 100644 GIT binary patch literal 129 zcmWN{K@P$o5J1sAr{Dq>2FifFO`$VFqS83ng{PO~CvWpF`bg_o+f@zl@wz%mu2qLrdLVz|F@50l8RfSKwq_@OPhlTm$4(5CM>P%Rm$X3GY@rVI`$sJ|8%Jz$gCGoE1x!XRmq= zat9H>;^?RKOKN?UdDSy>r7|r%voCub_KceCCgpeOmdT}pN!ON5Y@58rtk-1DlC7Cb z9;&Q`DcvC$yZht*q9x!!Gi zf@*{QMlPNGRx0cIwRIcUm2N3XGXb_I%a(RVBe^VrK75 zyYFSC{PpP>1zz(-x;pxy{;NxrZQ5?V+1jiNx4`Q9yU<+w@Rw{AK3ipTKD^om_s{c) zxb<>}zdvrjRu*V&=?*t3ST{ecKIA3i{~6iVEAP(Hf?KUw7Ju^Yli%69Ek9~k!<8q$ z-KYNj`q$mr)>aRzzrjsEe#3V6yUy*0vv*s4y7>bx+&_2<*|=u+>btTCD;1}0U;TyQ+QX}S{1ah8^FnX;eC6*vzkhC(mHSZq_GQnN>Bh|_ zcRKPm{|>yIzxvC8s~@+zSw=Zk%X5Xwe-CvV?{NdIQaL3JiDq6tIy?%x_P@N1Bd1}CwG@QMA=MVeV8*b~_ S1Z#3Y&hd2hb6Mw<&;$U7$oP){ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png index c431c406b0ca5fa8b4b24679a7513e09620f2b4e..1431969edba9f9777c09979ef06d338adc5389b9 100644 GIT binary patch literal 129 zcmWN?%MrpL5CG6SRnUN87ZwQVX88#-DjC5XtX|*cUFALdc*(ZbIS-}oecc{)ZvWdS zZfQK#JWEm+iqW&2jkR@$FWLnHj$9cyGepEnXi)o{S^M22wjNTX62NL_C{c}xfdc2< M8!`L0NYn!lvI6;>1s;*b z3=DjSL74G){)!Z!nggCLjv*Dd-rn);_AQh-@Ui$wVu!~<8#ghNdB-Yd@m^fgla%Nz zEWLTA_=C_2#(Pa$TU&cK1zmFKY8BNMQ{onT_2aMg|IVOfz3&g_RNK%0RjR}G{onbT z_m7cezy(|KI6(^Ge2@YpMSD5^ z=3RXv9dTA(W~FS**?c4Ozsl_|8DE5)Il1Gme$sE34;-~G4_xL>c^+(Hs2x@I`7-mRjbB8R z-yPq4!!GaN>Teu-_cx0F{PHI$&|W@&^B0CvEB5~y+qjqBk94#BvZ`+VZ?7}^6mR># zWPF`<=HZTh{!_pHR=#-AVW=}vzVGZG0k?m3!rA>*V6yE7tqVmuB+jJvVK9$YE=5zIW>U)t_Eh{$9Js?83VXp95dJoypq$ zjz7~k&tlW%R~J5izA#(qfA$-5+w|pA7d{Wao<4V-{%?~yv-Of&l)wD3tF(&!e(3h6 z+%wBR?u%%-CUa5$c+U3KR@-dX%bHk!S@nIbJq=@+orMRbFYgn zG(Xh(==%31mu-_Pa|M4L=zkX7^(%YE%ifcFj&am3I464c|FUal+MlJAzck98z5Mm& z<1)|ly}QNYUNoCL&A7LH@vkYV5_{+J@mR`NF(4;$s-%2KMncX^;5>qE597jS)Xd}X z0+RKRaugyz!UYMMi;;7QF&8!SV|6cvxk#BErX9n*Ac-v({41B1pO>2Zegm+8WAJqK Kb6Mw<&;$Us@j%)D diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png index d331b707d5299489dd2fb609b64e6e2b16c41b4a..cb70160c09cce14c2f1a0a6c7de3c98dc2ba61b6 100644 GIT binary patch literal 129 zcmWN?NfN>!5CFh?UcmnvCyfRd18XDO5gDF}ugjd6*icL>ZJB5hH4 L8SR@DBx9E!=(Q+j literal 1638 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1~v{)7srr_TW|0BcZa6R9QasVq;pn9+H0YY2G6f}DMxL^jssFr z;*YosZZ4{w9i|)gi!0}HlE58~rX9g5&XS2@P76{p1p1b>`Od53|DLHn$@u%6&tLAG zXP3+4{(S!P?)Y2DS z?R&H$dzSCV4a(QPMcn>$ODelK?*F^kU#0cOqIyfs*GF&6ds1q9ZEM-5Y_5Ca;ZHbg z*^X@f^RnWGY@#M4ihyL%=w72^@;r{>c6+47p7p?IQ{M;4m{^IbFxqa92k5!q< z@BD2t|GM?h^M3BXoou<8=X=YmQSuftiJJ^ zlzNCnmHAGUub(6IB~*dzeXHI&81F0*U7`D>qbA)(=1ZN77O$;Ry{6p0j;}jDI<_u+ zuDtWa!fO{kSAS|&YWH$JpMIUc!k=$y+dO-9fBoxoW81s>U(7#Nb#KLjcNe}(2%kAn z4UN-gM7++JrnK+Eg4s2e52tT5vw5KZM6V6Tcd9m$);NJ7! zpTAtUE*dBuZB>3lw5q@C#=oQ)trw~v=j2~=iQ zW`U~wd0YM-HQ-+OJ#F(_{kBMsy{GJ@Hyb2?5^&?Cp32lat02PW<+=5SXPjQli*a&) zkzIcJoA_ZJE%Rq5cSvvBFOjBL;;*+*`ODVbc@SI9nu1JUaVJCl@vW1mk6ODK?6O`v z{hibMo1)4Rb0sEg`B3)S`W=lL<+HEFweCueSl@GpmHy$A0F z*S`>lP%l$1bOrL{EiF{?d%?WV|NWzfa{_tZ#oViIv70eJ>8^R-!8_-x`!2BCu3B#V z-usy0jHTC>%R0%Q%zV7zdWc+=OyW8-yLXa@itc5%*v$%dOX^=~^Tp!QA1VIly-UrX zf0k4Ja&c})>G_Oh@8a%7x5OO@w7t9H!;*_WcTZmPRcMWyVcaY1cDcm#y{41?s}=L~ zUPR>7G@me8VjGhaJHg~~*WY(rE0X?QGd)|V9JT#%rhabo1@E$J9oL>NvR(4yLiFdz z{t^-UEdHPNG8ZFf-`?1_#cr9)i>tr1uYJ#Z*mL3a)>+E`kA^9HsZ##0sPz9PkhLp* zUAN4_@K@WWzvkGR_fmO_vfm!XFI7vs6Z?K~@GiWrIk&`nI@|nGLmsE~!M`-OxL#kU z{KabN^ye?rznuS}Y0?t6yd-;o4>NJ$){FoKn3u%e6jLXL4E++3d3TUlW)Kope(OGOMj o1+35t))qcUiHlHqxaT+fhMQ-4rg!v711ky!Pgg&ebxsLQ04}KnEdT%j diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png index 250d94c687e20957e4f5b9ed83b108012489ced9..caa7c8ba9b07fff86fa73ca2f34f163876feaa21 100644 GIT binary patch literal 129 zcmWN?OA^8$3;@tQr{Dq>nv`E}LjnjhDjie1@bvmN@2YPZ^QHTF9i_@% literal 2098 zcmai#eK^y5AIIl#GC5_3D@7r7IgaG1c}Q&XL#2{%DvZR6%Gg~*8?{@8JVi&@imcVG zB;&9m52JKj9+ug`SlE=z(=V81%W{8JP)}0DtFm(fY&J3>yv>n<)#+l>g5UoRpw~QLw{7}8vi&D5Ca0Of4v+k zZ_p*DK_K-WPj}bfF29=^VrCl77`*(#se42}n|IL5nsxnmz&6mU?3h)C$urvx9u56# zXg!B+7|P9^CB&xt#lx;Ueg6QzD)I$x-vT?T;ni;c>!`hF-hr$D7o%D8^BLhdXQ=qz zojy%C#JFT(u`_|h!Gw?9LGP%!JUrDB4)E8*p?xp2-Xea0?AaQ-SMbj+6|yRzj$aQp zSdPDmD+ep0>+0&S;wpd4$tS)8A#Fn&34;P||k@sJ3H z8*8$89Cn0Wc-=9w?Psims8^Omyf?~>s-rO6MoHq^OP3jyq6moWr_pnB%rL|8tx zpGACVqkuRAimRBU$#LzsT5Dz%MZ8=?OP8=M>5e@+#ig*rW)f$Z;+PbZ*ecrGgEX0M zB6MSki2~`#IjYUvjme32v_G@Od6T#=e0&k(&oZa`pH@@fw@K)*$89`HyKv_fM7QVSV}rS>i7(14$*?dQTeWd z;98!&gkHY|*eFvQjL^VyQ%JUlNQ8YZ7pI2IOeO5a9$iUvnh69uP(5&s%SqNC#^-7b zXfPix0d5z(0GgCm3R*qJr(jeH#2HZ)l1Y-$% z9`y&dUz1!z*YTVPwZe1n&U|foC`Ti$P}a-8Xm6CP0Xm)WtAbSR=>`5q*9l?6 zvbw1jZbi57U#(|-t3HBIs+l(@#9={2mM5S6NUIn01Ph{^&S+vIwD8=zyz2Y$vwH;; zr_gUg130$RUcwI=0SddEE)LrJC(w;#Y1OaZL6^G3VO;?TRxLGK^8z633S%r(KZp?$ zp6?gjlWyggP6-&jReUhb4>L1IR3?@5dILb%7CI10(ZdNyw%qCQ<}X!jFwH^8(T-%y zyD4n>B~juKHcThC+Ne(!oJAra$Ith+Qnwl?|1#DNiqGWLhpRjFVa7LD-;oY7+;@{`E; zBDChB8UNZ;V72C%w+&uxrvPv1?hh_+5~DlHC&X394c{|{0dFIMYX9wdVO((;>rMVO za%vGg1LHQy)01vKlRb>!%Vb&=Us%7){aX&_??cQ}x@U6M#BbdyndmZ*%DH^GXs)9djFJg;?X{5;nPrRk1GW9CO!p7;KJicYnc?73uA^Mqo(y@7L z00z@=1qbB?9i$l1!6n(QJ}xoOp(IW-_J^bz(-}Q(j5>+6 zWklmUX#R&OE3o;FCCHxt!V-S{S6=;9nfw>TR>WoBq);MNZFs}?yS`ce-|kgVTVdjg j?f0ox4ps=7mcF13D^&sJ{%pr7p%3!(IO1LooVxZOo0A`T diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png index df5f08c40fbea4f666efff9529f4e56d5c453875..9d50579392fa5250b673517919ed1d914b2bdb35 100644 GIT binary patch literal 128 zcmWN_!41P83;@7CQ?NjTAYdTdU@!%#Em2MK==9}v(%tmsK0dO|dGevuv(Lw?^0sUj zZh5>FpOn?D#pp$HOUA*GV$LPnsJ-z}Jmq9PSdT7-1O+MPf{`!*IgcT%u4xCoBdjsH L3hqCW8iM!(waX`u literal 670 zcmeAS@N?(olHy`uVBq!ia0vp^zd)FS4M=vpiLwP!Ea{HEjtmSN`?>!lvI6;>1s;*b z3=DjSL74G){)!X^1||nj7srr_TW_y5axy4zFdS?N%j;@er^@B}PK@Ko@n;OTgsnJa z1*{ZG99}eBU|h)T#NNUO!pMTnFbSBTLWwd1L@7+9;esPVKSU7CP!3sCLy5Hs>Mm2K z`|8hMiXMyiye1cNJ=s6DoUQ ql;+`wh*KmFLj*D61>xlncUkYXvp>_d=3fg;U<{tFelF{r5}E*@am)?? diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Add.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Darken.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-HardLight.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Lighten.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Multiply.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Normal.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Overlay.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Screen.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Subtract.png index 237fd3967440b8a24a39da443b408f5afef291cd..17360edc98165c905aacc06030e42d78e65ded67 100644 GIT binary patch literal 128 zcmWN?OA^8$3;@tQr{DsXPizCdO%gzuQRxWn!qe;9yeof3>u+7>JjQO!qs`m1jOBkl zd6E7y!lvI6;>1s;*b z3=DjSL74G){)!X^2Bu_B7srr_TW{|k+keSy<$1zuKl@{y?pE5!QD)pE1cLDO5^kI zgVIEngvH=MKv?aF2pJ-5$-j9n=4HFf{XGTUz2Hzp7=$Buf!0C79ifu2l>$~U)d#jy k7_?AlVMH0Gvl0u%6~g%+oSiCe4@`p$p00i_>zopr0D;=t_W%F@ diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png index 51a22b24..48458452 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3f7a0b2d52400407d09cabf13f8df283ebccc8c664e701a2802b804784cccbf -size 4164 +oid sha256:636fe0a652af8e42868ba8d7b053fd6d0c277b69af4fd9c7f6ac22c258bb8619 +size 4141 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png index 6c35f6af..a663c5d3 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a1c28a34cf8679e7734471c3c08f9c44894c72560871781a98a2fe528d54639 -size 4272 +oid sha256:86564c0c84d83e61b304909c1c836471af0a474ff7f9815862790ab4e364941e +size 4031 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png index bffbd7da..7e26d695 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2eb0655a628c0c5d59015bf4509d554a33eb14a4dce7a0920dc969a4e3f9192f -size 5331 +oid sha256:87d5d761e12286b52035d3659989c43ac27744228510e2993d649910a20c8d34 +size 5228 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png index 11f89ad2..5b5dc452 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f1a0cc896b8ea923ad094ea27cbb21fe83a1410409d13d107d853a6712a183a -size 5353 +oid sha256:5c50feec7f3eb4a9dde88462398c46af6841aa4f27bff943858be8219d03d31f +size 5299 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png index 87ead1ad..1ab954d5 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30d20da56fdfabdcac96b78b9365c29c5d2e30c5249d7153cd23de8cb5aea7f6 -size 4511 +oid sha256:70beac5ff86d52b20e44dc6426747949d8308fb756397f305fd50de303e0cd1b +size 4387 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png index 565e5d6b..d2d64f8b 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2fa1cc25b8212b1260fe2e86bd9e2ca17341555368cfedd528882f4618002fc1 -size 9437 +oid sha256:abb325c92147f9810d04059a1ea24e6be9e7dd0471613a16df266371e25f6f10 +size 9390 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png index 2a7dc8b2..9e50b2fa 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d01cd317d93f6119434f3eb2794bbbc4531d9565ff6ae6589de10aff56ec1541 -size 5346 +oid sha256:cd551e861f821dd70f9a915957703b8c691ccf30a71159e32ff6d301c4c1a4fe +size 5181 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png index 5045119f..dc611586 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4de98e29b8a63ec12041f55f64d08361d78968d20feb16389d14a5c0ed3ba325 -size 7414 +oid sha256:ffe197264326acae59f95a1021f645c423b755f2e9feccc7d284a90c2e0a275f +size 7395 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png index a179a801..1365aa90 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c17b664f4211b759137770cc448027ff90c0d635725bca75636373193bba0701 -size 5543 +oid sha256:6dec4a6f836b95b35dd6b4bfefed4a139faf399f5ee0429d2af6da0d659ccf6b +size 4985 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png index 88a05ee2..483091b7 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8b1c7415e5803b1527c5f08693de6b32a6a392e9465ad3a3fea5f9261d026ca -size 5475 +oid sha256:9d3593b23fc0f52360731271313e444175efbbe5a3fe9df0e01422bb66cd311d +size 4906 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png index 156bd34b..95806e72 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a88ac7de5e96b68f6ff9e1b417f77125649764acbb2e95f72aa04de7891efe1e -size 14164 +oid sha256:fe68e33222e02c38133a6555ec7aab8775ddac52e43e65ca08b9642587725237 +size 14318 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png index 9ce3cfd8..cb39952c 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99094889eb35d52fd20074391f72efa2137a20df93a61ac09c326891dc820f5c -size 12851 +oid sha256:7957a4f6299912762624320746e36a691f14a40f1282b3333d742e44e041e016 +size 13580 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png index 5fe5778e..ebebcb87 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c13f2c38c2c74ff5345a81df3ad2af039e3b5d86ec02d3863caf39ae915033ba -size 11093 +oid sha256:bfb920a3e19a7b6a86e7c16f26f370d91819100b1e9b38052781bdde9bc90078 +size 10593 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png index 1d691b44..a9d95b2b 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:008c1423db6c22ccf532bc502911a8cd896c0d878d0066f59844ce8edb3ad4c7 -size 4513 +oid sha256:eb8c07ae7263cada6fde58146f84132c4fc725d18c96b699716bd468e3d0ae8a +size 5127 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRenderTextOutOfBoundsIssue301.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRenderTextOutOfBoundsIssue301.png index a992186f..2d7907da 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRenderTextOutOfBoundsIssue301.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRenderTextOutOfBoundsIssue301.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0667e1476a91a4bc4e7ee91ef52d213986ac46dcc942fa2874c3cb60c24229bd -size 1321 +oid sha256:2438c3dc6c663a4e51f6c33370a79c0e1a5a659a0508ff2c3696838a183da19e +size 1133 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png index 7350227e..335809ee 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea80289164c0b4d68d822bb8bfd2909222802ec4212bbbfc5f3d52be04196063 -size 1972 +oid sha256:d906e2161a7e83a02fe103d37f7502f6364666f848963119f16e968ebaccaa59 +size 1960 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png index bd6a2050..2b116b14 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bde19b36b56e7974c47c459183210589ced41b93bd9fcd0ba22b4baabaac2176 -size 1704 +oid sha256:d5209d55719175ad95aa4af0ee7b91404c1f0870b0bbf5633d9b6a5041901a88 +size 1723 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png index a650fcf5..12024df6 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01215cf82da27254b5d9e720eb9db7bc7988d7c305d0c26245a7398a5411738c -size 2636 +oid sha256:9bfb1deffe74cd385e005130793fcfaeade200ad6de77348c7624cb66d742204 +size 2582 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png index 83b223d3..9b8104f7 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9e7f3f9aabf29e053e05eb4e4f7c3114e58d6229e9539aa566e42a19a775b90 -size 2430 +oid sha256:80f7a935cc93f5bbc0fa9b02b2f36c294f71204f9654d224540cf69805f68f05 +size 2501 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png index 93e9b11c..d4e7c41a 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f578c497e4dda636fd917eda65203c733f3f52417ba0229a43aecddda462fad8 -size 3103 +oid sha256:766844bcd409f83dd46ff5c0f2615bd9b31e3fa9719109d3127940508862715c +size 3119 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png index 9c9b34e1..474f9fd6 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7c7020f0d906773ba18f787b8045adb3e6b26bdc9ca72978bde278f8a7d61a6 -size 3889 +oid sha256:aa3c5c1e9033618a4bef1b02427176991eb9b767b6570948b55c1067d70ff771 +size 3921 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png index 4e005679..58256c3f 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4980f1302038def74531e96b23b4eebbd6b296f79da8be32e888f047e931226e -size 8685 +oid sha256:ff56241312753f433b55ac70ec8bc12b3f164ad24da212581b53c637cd1711fc +size 8675 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png index 3a85f9a2..e962adb7 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:329b6d3564b26b33f133236313f51dfd361fc73625f19234df828a3d3c05f655 -size 11533 +oid sha256:99f3b08907243b9afa6ec004da2e013cfd82ded5e287e28b02b940b799aabaa2 +size 11445 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png index 55ac43b6..5120c462 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac3e84d14c7e3e6f83259233c88e191c60d568d360df1ce9ef4598b9eaab65bf -size 9222 +oid sha256:58c86318d4963c1841c18c1bf5b88a661427585ef0eee6fb9825d24fc2e64820 +size 9158 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png index ad6ee286..eb910318 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c89d27ebda45bc3da2c7175d9139bebb8b824c042fee8b279cadb8009f32fbc6 -size 11690 +oid sha256:734ded4b3f5b6a42f5a38ff65efee9d8466e5511f8b7c2492f36250a0d0f615c +size 11792 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png index 9fdc8cf6..f9714e30 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18e7d238c5cf9b28f73c083cfdb18e7b4db83bc3dc9d959906e5e1db2e049df6 -size 9986 +oid sha256:809d47db52fe7c6248704e6c4edf257e06365da15bde62140175a3fee534ccba +size 10040 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png index 3d25eda5..a09ecc74 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba6dbef390dc02fa2684355bda6cf9a38f52f6a8c3468fc4e4fb449cfc0ed88a -size 18214 +oid sha256:1c1ab0671873d0ac224ef2303aacfbbec2acb2d914040ce2d5469e51fb5eea18 +size 18524 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png index 0d9b5dd7..b8b94d90 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fdc4f70e69b94796a45ee83c6d79c7e1d3bca364ca164fa2c9bcd38f40b2664 -size 1783 +oid sha256:927f376922e21e380fbd943ddf9a13f14774d4d3b7110436b82364fa1889671a +size 1794 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index ca6f4af5..8dad5340 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8333f5bf72363f6cbebd69efe351d4234ae7f870b5c088991bd0a18ee855aa30 -size 33277 +oid sha256:6038e34918109e904806da6e70ada04a61db754784625b2572f75752fa521627 +size 17528 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 0a714add..37e3bd5f 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5842438b3650d5e7e1ebff9cd2416073ab5e71147ef073873222fc10ab32542d +oid sha256:a541428859171c4d2e0d23d63fc916aea2c3f911333886d6f61fcc198feb19b0 size 759 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index da4ba2e6..0aa68114 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ffcef97021e6572d9827ac1a128882042695bf2b3bf1e27127358d7cef994f6 -size 31488 +oid sha256:8f6ec4b89aebe34fff668d656ff170ffee6c3a6b07d96eb3e414eb989bf21859 +size 16990 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index 5846aaaa..864ffbf1 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95d22407bc35b2ceb3c671ea26a2941acb43a5f5b182d70a7712db8f21aa4ae9 -size 15131 +oid sha256:d618766c3826b46082f6c248205b51dc18e6f4f7a328f454cd085813ecb78a3c +size 15084 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 2b436caa..12ac94d0 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb99b342371e50b3b20e7ae22ee80a13027df94b129a095c8e16120ae71877bd -size 710 +oid sha256:9e26c9ceae90a42180b573f97da0ce2b12e4ef30b3043bcee014e24d227913be +size 706 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 5521b875..d839ae8e 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb532482912d8f63ff78c193d2e9dcaf1abc66163c973d5ffdad31d86a8ac4d3 -size 14673 +oid sha256:4e91bd745be89a8d9126e5a9c73e0f62f286db3be7080545c80fef3ec19da177 +size 15452 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png index dc054247..9780a776 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:041fe44d2a6a920e7deb699d9f089c7441a27533af9afb2152fbabfa5037f9d4 -size 110984 +oid sha256:2911ef69f673be85d75ca8b70f4039823290fdc3096caa0ef792d541bd531b9f +size 115331 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png index a55a451f..cfd64819 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6688a59280db47c5a41e2c0e3e03383066a9e9f6606e55fe129c96159edbe88 -size 11085 +oid sha256:ae6ee08cb58592e49582e3543f99beb955e0e343a874af053098949cef1e25d8 +size 11040 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index b0a435af..3e68f9b7 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b12a095e6f233bceb135d2ae29a276f311d880691af1158a55226a3af8b866d1 -size 793 +oid sha256:f05a32ebdbdca54454ea2624d085cfd4965cf676bbad36f9be9ad199a3b7faa8 +size 604 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png index 6552efe8..d8dfab13 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82a1611e1bfd506bc056a389da40c200eebfd3114297aec8f316baed361bf44b -size 280 +oid sha256:b9a8cb42bef151d899fe30a4799d4a4734a933ac93d7dbff531686babec342a4 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png index 09e23104..6eed6ea6 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16d0df791297d9a491810f05b0994a266164c6a5e0d180b4645107030283aa8a -size 3589 +oid sha256:18e4f94697b80c8900a9c1d67150d96549acddc43e622de41a4e79eb521bb63d +size 3698 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png index fadf35b4..e243c035 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d27be541dfc5a3a0bafca3b8204118ca3c062ad4024f7e6f3debc7160c5679f7 -size 10527 +oid sha256:803037eed7e876797e3920b3b8b1c7874a90affed7360c7911be63405ab37a08 +size 10630 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png index f37eeeb5..2359d15e 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1705d215b8424ca998b8d23264b6078f68b8145079f3c479a479a9e48bd11f9d -size 2830 +oid sha256:5b0d1409a79d3bd21425ad98a18f62151a70ac33edeb5edc1bbc4f2a27708a8d +size 2786 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png index 8a11f2ea..49d87771 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b32fd4bd997df568c0c8ed437a6b9a7fcfbb48e35fef18fa28e87c0f5b0947e2 -size 24872 +oid sha256:a385a85f14d3e0cfa191da6e301239d9ea50f46261f43f6ab49401628dffad9e +size 24751 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png index f6117045..86c4902e 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e40cc5fae7127ee24807a8f4720a57c3ed6f0eea6787d13da213dd7789c6d4f -size 2844 +oid sha256:7eb7c3ce4878f5fd713a612862c2442d2154bd74c03ec672904f3ff1140248d3 +size 2863 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png index 764a9ae0..e99e4a71 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b78d61038876d453c8a6b0a8ac461ab7abc0ab77618c8612d825648d1f1e523 -size 35374 +oid sha256:7d40b26b9137f48c630a3ed52847c1694e048ba71cb74a49513b173a71e83cc1 +size 35293 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png index 652e6c25..8983ed62 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85614685419789b5a656fd19bc0542d91009164da8a86c4b01d975dc99a51ef2 -size 2816 +oid sha256:a4a3efd887effeaa80ec7370c5f4c166bbf4da9856b6a25374c373795bb64113 +size 2837 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png index 195d48aa..d8029d4c 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b910f7c863fc76ad147ab48a3f9f0f5f8a43d7a2290fbfd8ee8cdc4f29568602 -size 17629 +oid sha256:7dab23ca0715b97911510f67d368164cc311ca21b4e8fcccabde5cad6e79b796 +size 17665 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png index 52547028..cd00ebe1 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:518c6b975a9e0c3dfff97f599742a0f4b4171ed6db4558c25a9531d273a5181b -size 16561 +oid sha256:03f8b2b0340e28882217f09502961d26422905144bd55681627a82b02fcc3f42 +size 15823 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png index 709a82f7..ca42e83e 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0254d83921fefb2958ff197ffc46fff9f7ec035dfb23488fc99875035ef641bb -size 1321 +oid sha256:4fd14861fa01d9dc06a0bd2872ff24547cb366784c2d6af35b687e21783ca5f0 +size 1083 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png index 881c6f69..46ca78bc 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ac46db738f49ff6072fafcbe6dbde47bb39a897c3f2ee85500269095938bd44 -size 5561 +oid sha256:7ae72474cd3fa4ca95f93a82fd7b7f544c06f7307faf293151e1d2ce0433fbc1 +size 5234 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png index 794497a7..a1084dd6 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c8e916390f476c4be19cf9af52b538c0c6d4c35ec0b37b357fa393ff380c674 -size 417 +oid sha256:f15a9a114c67c2329e27417f41f1a2d10a70e38b8d8be95cf2972a51400c01d8 +size 229 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png index 9e38a3ab..6d0f59b1 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:530ba20440e64c4a3cc263c9ec13917a52e664af5dbddd1695587244c5e69074 -size 14725 +oid sha256:dd361bad89a3ad48ca0e54b7493f51cfde973f19c44ff3e8af3179bdfb30a9c2 +size 14692 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png index 92e9ea83..f6c0883a 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ea9d092bfd6177cad1f5456e7c75d46d5fe11be1eff48d4121e28b41d6e44ac -size 1259 +oid sha256:2a3ed8e0c4188a81e77da1d5d769865d8de26076f6a60a358bea96299c00718a +size 1000 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png index 2732e246..59db80f4 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e20f601f9193ad75cdc97f978d2adbac023b9592f8daf3f87be0b1330881f2a1 -size 5076 +oid sha256:6223e0db0e24f739b50afb77cf3cb18c6043fe95cc643a201fd70df1c5ef2da4 +size 5021 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png index 266c5231..44f4e51a 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f24b8ededb55b84bac5215f50464f718857dfa7f51749501e3d5156c09196e1 -size 555 +oid sha256:ab5e2ebd26e2c828c0ecde3e6a711adc1bf595ed08581ba223db6a36433b8dee +size 295 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png index e4292f81..0ee6e710 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1f1731bd9ae88094fc7b953cff00b2e17afb26741967b3d802876fdd3ed1f0c -size 36291 +oid sha256:617e6041a8f312f0890d0b287f1a7191407a83b7aa3f47bdf214ae702ef32ee1 +size 36248 diff --git a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png index a7d2d339..6d70f32c 100644 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png +++ b/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a3de2b73c18576d4340899f0c239d22920bece1835a17cbbb3432f84a599e0c -size 183932 +oid sha256:f839ffbac1b001539912b2759206d2b3de2235f059e487505e5fb6226396c531 +size 184457 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index 924d09bf..d93b91a3 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b02efd026d5ed83d1e0e747c8675d340bda7ba246d5a9385ecc6dabc81861e0 -size 3015 +oid sha256:84eff105c799ed23497870d1a13d2e69986cf7240da2d508794b2974bee1c5b6 +size 254 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png index e6b9aee3..f0cad642 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f02d5a40abd8bbef39f2648f2d40d5da5a28adfbc5b015a56641eef5fb50de05 -size 3050 +oid sha256:3581673cd0c053326d7c6947d232f62a7c0c61f3b86aa881be40ae609278100c +size 1188 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png index 8f585eb2..3e0bec9c 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11b915b55e0005b52bf80e4891f293b6cb6477f3e0d83fead71768ac359fa610 -size 3382 +oid sha256:0ccc8f2a14e5a3c8f7aca9c9411dbff01d8e1a3c7dbd66d515881bd753ebf922 +size 1284 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png index 79f87092..78e2037c 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a48c33c58c36ba7f581fe7d60eaf0bbc96948640b14ccbdd891adeae031a7ad -size 3691 +oid sha256:5befd1a942a8b50b40b1f4e2938699b84239d028bd83e31445271ec5f2043c64 +size 1238 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png index 7b5c2352..a4f0a8bf 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e80954c53985fc65b0846778861e3c023a8c798d466be887545fe2438ab5e3a3 -size 4353 +oid sha256:5e491edd79708d65a78d7eec5e32f4ec108d2b73fdc19ea614f09ea02f8e4183 +size 1373 diff --git a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png index 1baa9c43..47329393 100644 --- a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png +++ b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c2938a1b6001c5c1d1906659b790119313014a52f97391d7edc1b0da9007d45 -size 115477 +oid sha256:69c50b96bfc9c30b3d53ca17503ed5072f0e83a0541cfe0ef5570f3549d5b1e4 +size 116690 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png index 4cf04981..6dd59fe2 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8b46c62c9837bbce721d010c7d2f4f5884e6cd053465d649a6ff36267430976 -size 31985 +oid sha256:742e4bd37428a4402b097eb2e33c0cc2611cb17040a34ee1457508b630705f62 +size 31937 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png index cbde62cd..462ffcfc 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2884047623f1b28a4ea2009a55ee4f4be0960c9f012cba7831c32f4245859eec -size 10862 +oid sha256:919a6c8b5be40aa3894050f033d487f90d6bd2621cfb2f337874bd20904d9603 +size 10646 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png index 28b70b34..8cc405e4 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47e528f5b95689e0b1e381c6a7e9d7a8badd1523a4e132fbbf6b54d109cf07d0 -size 31963 +oid sha256:48b6a904ad0557908dd053ff357b8c10d4e279eeaf6dd9d0df40aee653ecca72 +size 31954 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png index ce377606..f3deebc6 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d065961fc27ea7be274c92d591f5f26f332ce47acc7704a0f0c45fabde6a1b6 -size 10864 +oid sha256:1a0df948f516294d3499aaab857729635b160f6ad15adc93c81fbade0fecfce7 +size 10640 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png index 45f63f96..9993d5d5 100644 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c531c3075c647406f3712bb395c5424d0ba85e15c1bb70104fcd4fa9cdaef538 -size 999 +oid sha256:7f7ff95b1daf10aaa3579fdfab07fb8ec570fe1f1ce4fb5f553d04f29dfda255 +size 407 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png index 91eae570..f61f6ff2 100644 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d00fe95e5f1124c104a6bc1343f3c4f2e862f9905425d22d39645114645c2743 -size 870 +oid sha256:b743c5edc9dc9478bdd8eeeea356b4c15c33942415eb0546cd2693453476eed1 +size 647 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png index e955d615..c1a2333a 100644 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f494915daf93b3fa6f8bc6be08cdb410615a9708a89e5542b64f0f73c172466 -size 922 +oid sha256:c4a58002ef2a2f39aee947a2cac4096e1dbeeb597564d049d2bec9de45585835 +size 470 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png index e8d4e856..7fea71a7 100644 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b93cfe79f4038e87af7c2b3908a73cd9315463000e2a70f235c92ff9c3913e1 -size 10086 +oid sha256:d45851a1743d5ebfda9cf3f6ba3f12627633954dea069a106dc9c01ee5458173 +size 4829 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png index 754e01e9..429f4440 100644 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49ae0db9378f7885d8d7aee40f0efa8bd4a03ca1617641fa173f5d145cd880b4 -size 5682 +oid sha256:6503d5ecc224260ce158fbb8775293183220b9be20acf47bcfec1e4f482682ad +size 2746 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png index b8517f64..00af7f35 100644 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:258ac071e347abe666c1c1d556e567547b861c7689199ff7fea9def6acf8be23 -size 2644 +oid sha256:16a17b87c0c302475c51472d93fa038dc39827317489c67f550a986450e35c98 +size 2428 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png index ce8e38a5..cfbbe58a 100644 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png +++ b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e23125bf0125159d19fcc7ef8c371486fa5cb24d4445d5fbde892a5b888deba -size 5036 +oid sha256:576d8476345f085444183cbcb8c61fd03c082113dfb32cc5d9f4859d86fc5be2 +size 4765 From 7a983d3af251322555b728ee1777207074a3158a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 10:53:39 +1000 Subject: [PATCH 33/35] Update build pipeline --- .github/workflows/build-and-test.yml | 49 +++++++++++-------- .../ImageSharp.Drawing.csproj | 2 +- .../ImageSharp.Drawing.Benchmarks.csproj | 2 +- .../ImageSharp.Drawing.Tests.csproj | 2 +- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index da06bad9..5824fae3 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -11,7 +11,7 @@ on: branches: - main - release/* - types: [ labeled, opened, synchronize, reopened ] + types: [ opened, synchronize, reopened ] jobs: # Prime a single LFS cache and expose the exact key for the matrix @@ -62,30 +62,28 @@ jobs: needs: WarmLFS strategy: matrix: - isARM: - - ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }} options: - os: ubuntu-latest - framework: net9.0 - sdk: 9.0.x + framework: net10.0 + sdk: 10.0.x sdk-preview: true runtime: -x64 codecov: false - - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable - framework: net9.0 - sdk: 9.0.x + - os: macos-26 + framework: net10.0 + sdk: 10.0.x sdk-preview: true runtime: -x64 codecov: false - os: windows-latest - framework: net9.0 - sdk: 9.0.x + framework: net10.0 + sdk: 10.0.x sdk-preview: true runtime: -x64 codecov: false - - os: buildjet-4vcpu-ubuntu-2204-arm - framework: net9.0 - sdk: 9.0.x + - os: ubuntu-22.04-arm + framework: net10.0 + sdk: 10.0.x sdk-preview: true runtime: -x64 codecov: false @@ -94,8 +92,8 @@ jobs: framework: net8.0 sdk: 8.0.x runtime: -x64 - codecov: false - - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable + codecov: true + - os: macos-26 framework: net8.0 sdk: 8.0.x runtime: -x64 @@ -105,15 +103,11 @@ jobs: sdk: 8.0.x runtime: -x64 codecov: false - - os: buildjet-4vcpu-ubuntu-2204-arm + - os: ubuntu-22.04-arm framework: net8.0 sdk: 8.0.x runtime: -x64 codecov: false - exclude: - - isARM: false - options: - os: buildjet-4vcpu-ubuntu-2204-arm runs-on: ${{ matrix.options.os }} @@ -124,6 +118,18 @@ jobs: sudo apt-get update sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev + - name: Install libgdi+, which is required for tests running on macos + if: ${{ contains(matrix.options.os, 'macos-26') }} + run: | + brew update + brew install mono-libgdiplus + # Create symlinks to make libgdiplus discoverable + sudo mkdir -p /usr/local/lib + sudo ln -sf $(brew --prefix)/lib/libgdiplus.dylib /usr/local/lib/libgdiplus.dylib + # Verify installation + ls -la $(brew --prefix)/lib/libgdiplus* || echo "libgdiplus not found in brew prefix" + ls -la /usr/local/lib/libgdiplus* || echo "libgdiplus not found in /usr/local/lib" + - name: Git Config shell: bash run: | @@ -170,7 +176,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: | - 9.0.x + 10.0.x - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} @@ -214,6 +220,7 @@ jobs: if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') with: flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} Publish: needs: [Build] diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index c0d392bb..153c102b 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -29,7 +29,7 @@ - net8.0;net9.0 + net8.0;net10.0 diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index ff01fd28..6602f9ad 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -19,7 +19,7 @@ - net8.0;net9.0 + net8.0;net10.0 diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index 92880a30..a7b7f056 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -13,7 +13,7 @@ - net8.0;net9.0 + net8.0;net10.0 From cc44415f21c9048207b1d36f861aee7deaad3661 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 10:57:12 +1000 Subject: [PATCH 34/35] Update DrawShapesWithImageSharp.csproj --- .../DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj index a8711843..2884c603 100644 --- a/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj +++ b/samples/DrawShapesWithImageSharp/DrawShapesWithImageSharp.csproj @@ -8,7 +8,7 @@ - net9.0;net8.0 + net8.0;net10.0 From 3b457fb8e7e76a21d974d318ea8aeade5b3ddeb0 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Feb 2026 11:03:11 +1000 Subject: [PATCH 35/35] Fix mac build --- tests/Directory.Build.targets | 2 +- .../ImageSharp.Drawing.Benchmarks.csproj | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 05f5f7a6..1f2a992f 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -27,7 +27,7 @@ - + diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 6602f9ad..0a2f32ce 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -33,6 +33,7 @@ +