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/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 68bd3309..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 17 -VisualStudioVersion = 17.14.36623.8 d17.14 +# Visual Studio Version 18 +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 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 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 91da382b..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 @@ -44,8 +44,9 @@ - - + + + - \ No newline at end of file + diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs new file mode 100644 index 00000000..cd2c22ed --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs @@ -0,0 +1,333 @@ +// 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; + +/// +/// 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 +{ + /// + /// 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; + } + + /// + /// Gets the default backend instance. + /// + public static CpuDrawingBackend Instance { get; } = new(DefaultRasterizer.Instance); + + /// + /// Gets the primary rasterizer used by this backend. + /// + public IRasterizer PrimaryRasterizer { get; } + + /// + /// 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 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 rasterizerOptions, + MemoryAllocator allocator, + 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 new file mode 100644 index 00000000..92f6f56e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -0,0 +1,62 @@ +// 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 +{ + /// + /// Fills a path into the destination image using the given brush and drawing options. + /// + /// + /// 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. + /// Brush used to shade covered pixels. + /// Graphics blending/composition options. + /// Rasterizer options. + /// Brush bounds used when creating the applicator. + /// Allocator for temporary data. + 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; + + /// + /// 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, + Buffer2D destination); +} 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/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/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs index 477c3325..5dfadb97 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs @@ -11,11 +11,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; /// public class FillPathProcessor : IImageProcessor { - /// - /// Minimum subpixel count for rasterization, being applied even if antialiasing is off. - /// - internal const int MinimumSubpixelCount = 8; - /// /// 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..0788cb6f 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -1,8 +1,7 @@ // 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; @@ -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,96 +60,25 @@ protected override void OnFrameApply(ImageFrame source) return; // No effect inside image; } - 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); - } - - 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, - subpixelCount, + IDrawingBackend drawingBackend = configuration.GetDrawingBackend(); + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + RasterizerOptions rasterizerOptions = new( + interest, shapeOptions.IntersectionRule, - configuration.MemoryAllocator); - - try - { - using IMemoryOwner bScanline = allocator.Allocate(scanlineWidth); - Span scanline = bScanline.Memory.Span; - - while (scanner.MoveToNextPixelLine()) - { - if (scanlineDirty) - { - scanline.Clear(); - } - - scanlineDirty = scanner.ScanCurrentPixelLineInto(minX, 0F, scanline); - - if (scanlineDirty) - { - 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); - } - } - } - finally - { - scanner.Dispose(); - } - } - - 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); + rasterizationMode, + RasterizerSamplingOrigin.PixelBoundary); + + // The backend owns rasterization/compositing details. Processors only submit + // operation-level data (path, brush, options, bounds). + drawingBackend.FillPath( + configuration, + source, + this.path, + brush, + graphicsOptions, + rasterizerOptions, + 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 e1a6e09a..a8f60b24 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs @@ -35,6 +35,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..6436d67e 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs @@ -6,7 +6,7 @@ using SixLabors.Fonts; using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.Memory; @@ -24,6 +24,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 +56,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 = []; @@ -527,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 @@ -537,54 +545,27 @@ private Buffer2D Render(IPath path) // Pad to prevent edge clipping. size += new Size(2, 2); - int subpixelCount = FillPathProcessor.MinimumSubpixelCount; - float xOffset = .5F; + 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); - } + 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); - - PolygonScanner scanner = PolygonScanner.Create( - offsetPath, - 0, - size.Height, - subpixelCount, + RasterizerOptions rasterizerOptions = new( + new Rectangle(0, 0, size.Width, size.Height), TextUtilities.MapFillRule(this.currentFillRule), - this.memoryAllocator); + rasterizationMode, + samplingOrigin); - try - { - while (scanner.MoveToNextPixelLine()) - { - 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; - } - } - } - } - } - finally - { - // Can't use ref struct as a 'ref' or 'out' value when 'using' so as it is readonly explicitly dispose. - scanner.Dispose(); - } + // 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, + buffer); return buffer; } 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/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs index 11c188d8..bba986c0 100644 --- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs @@ -18,22 +18,22 @@ public ShapeOptions() private ShapeOptions(ShapeOptions source) { this.IntersectionRule = source.IntersectionRule; - this.ClippingOperation = source.ClippingOperation; + this.BooleanOperation = source.BooleanOperation; } /// /// Gets or sets the clipping operation. /// - /// Defaults to . + /// Defaults to . /// - public ClippingOperation ClippingOperation { get; set; } = ClippingOperation.Difference; + public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference; /// /// 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/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..51886f91 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs @@ -0,0 +1,77 @@ +// 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 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; } = 4D; + + /// + /// Gets or sets the inner miter limit used to clamp joins on acute interior angles. + /// + public double InnerMiterLimit { get; set; } = 1.01D; + + /// + /// 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 ArcDetailScale { get; set; } = 1D; + + /// + /// 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.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.ArcDetailScale, + 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 690d2291..b9b3ccde 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; @@ -11,15 +11,16 @@ namespace SixLabors.ImageSharp.Drawing; /// public static class ClipPathExtensions { + private static readonly ShapeOptions DefaultOptions = new(); + /// /// Clips the specified subject path with the provided clipping paths. /// /// 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); + => subjectPath.Clip(DefaultOptions, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. @@ -28,12 +29,11 @@ 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, params IPath[] clipPaths) - => subjectPath.Clip(options, (IEnumerable)clipPaths); + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. @@ -41,9 +41,8 @@ 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); + => subjectPath.Clip(DefaultOptions, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. @@ -52,19 +51,9 @@ 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(); - - clipper.AddPath(subjectPath, ClippingType.Subject); - clipper.AddPaths(clipPaths, ClippingType.Clip); - - IPath[] result = clipper.GenerateClippedShapes(options.ClippingOperation, options.IntersectionRule); - - return new ComplexPolygon(result); - } + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); } 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/ComplexPolygon.cs b/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs index 6cfb4319..dcdda406 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]) { } @@ -118,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) @@ -142,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 . /// @@ -153,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/EndCapStyle.cs b/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs deleted file mode 100644 index 50607e20..00000000 --- a/src/ImageSharp.Drawing/Shapes/EndCapStyle.cs +++ /dev/null @@ -1,42 +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 -} - -internal enum LineCap -{ - Butt, - Square, - Round -} 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/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/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/ISimplePath.cs b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs index 70727c95..cabea969 100644 --- a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs +++ b/src/ImageSharp.Drawing/Shapes/ISimplePath.cs @@ -11,10 +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; } + public ReadOnlyMemory Points { get; } } 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 c1464824..00000000 --- a/src/ImageSharp.Drawing/Shapes/JointStyle.cs +++ /dev/null @@ -1,42 +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 -} - -internal enum LineJoin -{ - MiterJoin = 0, - MiterJoinRevert = 1, - RoundJoin = 2, - BevelJoin = 3, - MiterJoinRound = 4 -} - -internal enum InnerJoin -{ - InnerBevel, - InnerMiter, - InnerJag, - InnerRound -} diff --git a/src/ImageSharp.Drawing/Shapes/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 29213304..78ff4fc1 100644 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs @@ -2,8 +2,8 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; namespace SixLabors.ImageSharp.Drawing; @@ -12,34 +12,7 @@ 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; - - /// - /// 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; - } + private static readonly StrokeOptions DefaultOptions = new(); /// /// Generates an outline of the path. @@ -47,34 +20,24 @@ private static float CalculateScalingMatrix(float width, out Matrix3x2 scaleUpMa /// 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); + => GenerateOutline(path, width, DefaultOptions); /// /// 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. - /// Thrown when an offset cannot be calculated. - 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; } - 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); + return StrokedShapeGenerator.GenerateStrokedShapes(path, width, strokeOptions); } /// @@ -84,7 +47,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); @@ -94,11 +56,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. - /// Thrown when an offset cannot be calculated. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) - => GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle); + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, StrokeOptions strokeOptions) + => GenerateOutline(path, width, pattern, false, strokeOptions); /// /// Generates an outline of the path with alternating on and off segments based on the pattern. @@ -106,12 +67,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. - /// 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); + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff) + => GenerateOutline(path, width, pattern, startOff, DefaultOptions); /// /// Generates an outline of the path with alternating on and off segments based on the pattern. @@ -120,11 +79,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. - /// Thrown when an offset cannot be calculated. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle) + public static IPath GenerateOutline( + this IPath path, + float width, + ReadOnlySpan pattern, + bool startOff, + StrokeOptions strokeOptions) { if (width <= 0) { @@ -133,91 +95,147 @@ 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; + + ReadOnlySpan pts = p.Points.Span; + if (pts.Length < 2) + { + continue; + } + + // number of edges to traverse (no wrap for open paths) + int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1; + float totalLength = 0f; + + // Compute total path length to estimate the number of dash segments to produce. + for (int j = 0; j < edgeCount; j++) + { + int nextIndex = p.IsClosed ? (j + 1) % pts.Length : j + 1; + totalLength += Vector2.Distance(pts[j], pts[nextIndex]); + } - // Create a new list of points representing the new outline - int pCount = points.Length; - if (!p.IsClosed) + if (totalLength > eps) { - pCount--; + // 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 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); + + // Skip degenerate segments. + if (segLen <= eps) + { + current = next; + i++; + continue; + } + + // Accumulate into the current dash span when the segment is shorter than the target. + if (segLen + eps < targetLength) { - // find a point between the 2 - float t = targetLength / distToNext; + buffer.Add(current); + current = next; + i++; + targetLength -= segLen; + continue; + } - Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t); - buffer.Add(currentPoint); - buffer.Add(point); + // Close out a dash span when the segment length matches the target length. + 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 to end the current dash span. + 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. + return StrokedShapeGenerator.GenerateStrokedShapes(outlines, width, strokeOptions); } } 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/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/Clipper.cs deleted file mode 100644 index f035a06c..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/Clipper.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -/// -/// 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(ClippingOperation operation, IntersectionRule rule) - { - PathsF closedPaths = []; - PathsF openPaths = []; - - FillRule fillRule = rule == IntersectionRule.EvenOdd ? FillRule.EvenOdd : FillRule.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/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/ClippingType.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs deleted file mode 100644 index 00aa96a4..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClippingType.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -/// -/// Defines the polygon clipping type. -/// -public 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/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/PolygonClipper.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonClipper.cs deleted file mode 100644 index 042382cd..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.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 == 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.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 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 deleted file mode 100644 index 10c63a6e..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonOffsetter.cs +++ /dev/null @@ -1,700 +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; - -/// -/// 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. - if (solution.Count == 0) - { - foreach (PathF path in this.solution) - { - solution.Add(path); - } - } - } - - 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 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 deleted file mode 100644 index 4061d300..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/PolygonStroker.cs +++ /dev/null @@ -1,735 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -#pragma warning disable SA1201 // Elements should appear in the correct order -internal sealed class PolygonStroker -{ - private ArrayBuilder outVertices = new(1); - private ArrayBuilder srcVertices = new(16); - private int closed; - private int outVertex; - private Status prevStatus; - private int srcVertex; - private Status status; - private double strokeWidth = 0.5; - private double widthAbs = 0.5; - private double widthEps = 0.5 / 1024.0; - private int widthSign = 1; - - public double MiterLimit { get; set; } = 4; - - public double InnerMiterLimit { get; set; } = 1.01; - - public double ApproximationScale { get; set; } = 1.0; - - public LineJoin LineJoin { get; set; } = LineJoin.MiterJoin; - - public LineCap LineCap { get; set; } = LineCap.Butt; - - public InnerJoin InnerJoin { get; set; } = InnerJoin.InnerMiter; - - public double Width - { - get => this.strokeWidth * 2.0; - set - { - this.strokeWidth = value * 0.5; - if (this.strokeWidth < 0) - { - this.widthAbs = -this.strokeWidth; - this.widthSign = -1; - } - else - { - this.widthAbs = this.strokeWidth; - this.widthSign = 1; - } - - this.widthEps = this.strokeWidth / 1024.0; - } - } - - public PathF ProcessPath(ReadOnlySpan linePoints, bool isClosed) - { - this.Reset(); - this.AddLinePath(linePoints); - - if (isClosed) - { - this.ClosePath(); - } - - PathF results = new(linePoints.Length * 3); - this.FinishPath(results); - return results; - } - - public void AddLinePath(ReadOnlySpan linePoints) - { - for (int i = 0; i < linePoints.Length; i++) - { - PointF point = linePoints[i]; - this.AddVertex(point.X, point.Y, PathCommand.LineTo); - } - } - - public void ClosePath() - { - this.AddVertex(0, 0, PathCommand.EndPoly | (PathCommand)PathFlags.Close); - } - - public void FinishPath(List results) - { - PointF currentPoint = new(0, 0); - int startIndex = 0; - PointF? lastPoint = null; - PathCommand command; - - while (!(command = this.Accumulate(ref currentPoint)).Stop()) - { - if (command.EndPoly() && results.Count > 0) - { - PointF initial = results[startIndex]; - results.Add(initial); - startIndex = results.Count; - } - else - { - if (currentPoint != lastPoint) - { - results.Add(currentPoint); - lastPoint = currentPoint; - } - } - } - } - - public void Reset() - { - this.srcVertices.Clear(); - this.outVertices.Clear(); - this.srcVertex = 0; - this.outVertex = 0; - this.closed = 0; - this.status = Status.Initial; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddVertex(double x, double y, PathCommand cmd) - { - this.status = Status.Initial; - if (cmd.MoveTo()) - { - if (this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - - this.AddVertex(x, y); - } - else if (cmd.Vertex()) - { - this.AddVertex(x, y); - } - else - { - this.closed = cmd.GetCloseFlag(); - } - } - - private PathCommand Accumulate(ref PointF point) - { - PathCommand cmd = PathCommand.LineTo; - while (!cmd.Stop()) - { - switch (this.status) - { - case Status.Initial: - this.CloseVertexPath(this.closed != 0); - - if (this.srcVertices.Length < 3) - { - this.closed = 0; - } - - this.status = Status.Ready; - - break; - - case Status.Ready: - if (this.srcVertices.Length < 2 + (this.closed != 0 ? 1 : 0)) - { - cmd = PathCommand.Stop; - - break; - } - - this.status = this.closed != 0 ? Status.Outline1 : Status.Cap1; - cmd = PathCommand.MoveTo; - this.srcVertex = 0; - this.outVertex = 0; - - break; - - case Status.Cap1: - this.CalcCap(ref this.srcVertices[0], ref this.srcVertices[1], this.srcVertices[0].Distance); - this.srcVertex = 1; - this.prevStatus = Status.Outline1; - this.status = Status.OutVertices; - this.outVertex = 0; - - break; - - case Status.Cap2: - this.CalcCap(ref this.srcVertices[^1], ref this.srcVertices[^2], this.srcVertices[^2].Distance); - this.prevStatus = Status.Outline2; - this.status = Status.OutVertices; - this.outVertex = 0; - - break; - - case Status.Outline1: - if (this.closed != 0) - { - if (this.srcVertex >= this.srcVertices.Length) - { - this.prevStatus = Status.CloseFirst; - this.status = Status.EndPoly1; - - break; - } - } - else if (this.srcVertex >= this.srcVertices.Length - 1) - { - this.status = Status.Cap2; - - break; - } - - this.CalcJoin( - ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], - ref this.srcVertices[this.srcVertex], - ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], - this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance, - this.srcVertices[this.srcVertex].Distance); - - ++this.srcVertex; - - this.prevStatus = this.status; - this.status = Status.OutVertices; - this.outVertex = 0; - - break; - - case Status.CloseFirst: - this.status = Status.Outline2; - cmd = PathCommand.MoveTo; - this.status = Status.Outline2; - - break; - - case Status.Outline2: - if (this.srcVertex <= (this.closed == 0 ? 1 : 0)) - { - this.status = Status.EndPoly2; - this.prevStatus = Status.Stop; - - break; - } - - --this.srcVertex; - - this.CalcJoin( - ref this.srcVertices[(this.srcVertex + 1) % this.srcVertices.Length], - ref this.srcVertices[this.srcVertex], - ref this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length], - this.srcVertices[this.srcVertex].Distance, - this.srcVertices[(this.srcVertex + this.srcVertices.Length - 1) % this.srcVertices.Length].Distance); - - this.prevStatus = this.status; - this.status = Status.OutVertices; - this.outVertex = 0; - - break; - - case Status.OutVertices: - if (this.outVertex >= this.outVertices.Length) - { - this.status = this.prevStatus; - } - else - { - PointF c = this.outVertices[this.outVertex++]; - point = c; - - return cmd; - } - - break; - - case Status.EndPoly1: - this.status = this.prevStatus; - - return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Ccw); - - case Status.EndPoly2: - this.status = this.prevStatus; - - return PathCommand.EndPoly | (PathCommand)(PathFlags.Close | PathFlags.Cw); - - case Status.Stop: - cmd = PathCommand.Stop; - - break; - } - } - - return cmd; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddVertex(double x, double y, double distance = 0) - { - if (this.srcVertices.Length > 1) - { - ref VertexDistance vd1 = ref this.srcVertices[^2]; - ref VertexDistance vd2 = ref this.srcVertices[^1]; - bool ret = vd1.Measure(vd2); - if (!ret && this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - } - - this.srcVertices.Add(new VertexDistance(x, y, distance)); - } - - private void CloseVertexPath(bool closed) - { - while (this.srcVertices.Length > 1) - { - ref VertexDistance vd1 = ref this.srcVertices[^2]; - ref VertexDistance vd2 = ref this.srcVertices[^1]; - bool ret = vd1.Measure(vd2); - - if (ret) - { - break; - } - - VertexDistance t = this.srcVertices[^1]; - if (this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - - if (this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - - this.AddVertex(t.X, t.Y, t.Distance); - } - - if (!closed) - { - return; - } - - while (this.srcVertices.Length > 1) - { - ref VertexDistance vd1 = ref this.srcVertices[^1]; - ref VertexDistance vd2 = ref this.srcVertices[0]; - bool ret = vd1.Measure(vd2); - - if (ret) - { - break; - } - - if (this.srcVertices.Length != 0) - { - this.srcVertices.RemoveLast(); - } - } - } - - private void CalcArc(double x, double y, double dx1, double dy1, double dx2, double dy2) - { - double a1 = Math.Atan2(dy1 * this.widthSign, dx1 * this.widthSign); - double a2 = Math.Atan2(dy2 * this.widthSign, dx2 * this.widthSign); - int i, n; - - double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125 / this.ApproximationScale))) * 2; - - this.AddPoint(x + dx1, y + dy1); - if (this.widthSign > 0) - { - if (a1 > a2) - { - a2 += Constants.Misc.PiMul2; - } - - n = (int)((a2 - a1) / da); - da = (a2 - a1) / (n + 1); - a1 += da; - for (i = 0; i < n; i++) - { - this.AddPoint(x + (Math.Cos(a1) * this.strokeWidth), y + (Math.Sin(a1) * this.strokeWidth)); - a1 += da; - } - } - else - { - if (a1 < a2) - { - a2 -= Constants.Misc.PiMul2; - } - - n = (int)((a1 - a2) / da); - da = (a1 - a2) / (n + 1); - a1 -= da; - for (i = 0; i < n; i++) - { - this.AddPoint(x + (Math.Cos(a1) * this.strokeWidth), y + (Math.Sin(a1) * this.strokeWidth)); - a1 -= da; - } - } - - this.AddPoint(x + dx2, y + dy2); - } - - private void CalcMiter( - ref VertexDistance v0, - ref VertexDistance v1, - ref VertexDistance v2, - double dx1, - double dy1, - double dx2, - double dy2, - LineJoin lj, - double mlimit, - double dbevel) - { - double xi = v1.X; - double yi = v1.Y; - double di = 1.0; - double lim = this.widthAbs * mlimit; - bool miterLimitExceeded = true; - bool intersectionFailed = true; - - if (UtilityMethods.CalcIntersection(v0.X + dx1, v0.Y - dy1, v1.X + dx1, v1.Y - dy1, v1.X + dx2, v1.Y - dy2, v2.X + dx2, v2.Y - dy2, ref xi, ref yi)) - { - di = UtilityMethods.CalcDistance(v1.X, v1.Y, xi, yi); - if (di <= lim) - { - this.AddPoint(xi, yi); - miterLimitExceeded = false; - } - - intersectionFailed = false; - } - else - { - double x2 = v1.X + dx1; - double y2 = v1.Y - dy1; - if ((UtilityMethods.CrossProduct(v0.X, v0.Y, v1.X, v1.Y, x2, y2) < 0.0) == (UtilityMethods.CrossProduct(v1.X, v1.Y, v2.X, v2.Y, x2, y2) < 0.0)) - { - this.AddPoint(v1.X + dx1, v1.Y - dy1); - miterLimitExceeded = false; - } - } - - if (!miterLimitExceeded) - { - return; - } - - switch (lj) - { - case LineJoin.MiterJoinRevert: - - this.AddPoint(v1.X + dx1, v1.Y - dy1); - this.AddPoint(v1.X + dx2, v1.Y - dy2); - - break; - - case LineJoin.MiterJoinRound: - this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); - - break; - - default: - if (intersectionFailed) - { - mlimit *= this.widthSign; - this.AddPoint(v1.X + dx1 + (dy1 * mlimit), v1.Y - dy1 + (dx1 * mlimit)); - this.AddPoint(v1.X + dx2 - (dy2 * mlimit), v1.Y - dy2 - (dx2 * mlimit)); - } - else - { - double x1 = v1.X + dx1; - double y1 = v1.Y - dy1; - double x2 = v1.X + dx2; - double y2 = v1.Y - dy2; - di = (lim - dbevel) / (di - dbevel); - this.AddPoint(x1 + ((xi - x1) * di), y1 + ((yi - y1) * di)); - this.AddPoint(x2 + ((xi - x2) * di), y2 + ((yi - y2) * di)); - } - - break; - } - } - - private void CalcCap(ref VertexDistance v0, ref VertexDistance v1, double len) - { - this.outVertices.Clear(); - - double dx1 = (v1.Y - v0.Y) / len; - double dy1 = (v1.X - v0.X) / len; - double dx2 = 0; - double dy2 = 0; - - dx1 *= this.strokeWidth; - dy1 *= this.strokeWidth; - - if (this.LineCap != LineCap.Round) - { - if (this.LineCap == LineCap.Square) - { - dx2 = dy1 * this.widthSign; - dy2 = dx1 * this.widthSign; - } - - this.AddPoint(v0.X - dx1 - dx2, v0.Y + dy1 - dy2); - this.AddPoint(v0.X + dx1 - dx2, v0.Y - dy1 - dy2); - } - else - { - double da = Math.Acos(this.widthAbs / (this.widthAbs + (0.125 / this.ApproximationScale))) * 2; - double a1; - int i; - int n = (int)(Constants.Misc.Pi / da); - - da = Constants.Misc.Pi / (n + 1); - this.AddPoint(v0.X - dx1, v0.Y + dy1); - if (this.widthSign > 0) - { - a1 = Math.Atan2(dy1, -dx1); - a1 += da; - for (i = 0; i < n; i++) - { - this.AddPoint(v0.X + (Math.Cos(a1) * this.strokeWidth), v0.Y + (Math.Sin(a1) * this.strokeWidth)); - a1 += da; - } - } - else - { - a1 = Math.Atan2(-dy1, dx1); - a1 -= da; - for (i = 0; i < n; i++) - { - this.AddPoint(v0.X + (Math.Cos(a1) * this.strokeWidth), v0.Y + (Math.Sin(a1) * this.strokeWidth)); - a1 -= da; - } - } - - this.AddPoint(v0.X + dx1, v0.Y - dy1); - } - } - - private void CalcJoin(ref VertexDistance v0, ref VertexDistance v1, ref VertexDistance v2, double len1, double len2) - { - double dx1 = this.strokeWidth * (v1.Y - v0.Y) / len1; - double dy1 = this.strokeWidth * (v1.X - v0.X) / len1; - double dx2 = this.strokeWidth * (v2.Y - v1.Y) / len2; - double dy2 = this.strokeWidth * (v2.X - v1.X) / len2; - - this.outVertices.Clear(); - - double cp = UtilityMethods.CrossProduct(v0.X, v0.Y, v1.X, v1.Y, v2.X, v2.Y); - if (Math.Abs(cp) > double.Epsilon && (cp > 0) == (this.strokeWidth > 0)) - { - double limit = (len1 < len2 ? len1 : len2) / this.widthAbs; - if (limit < this.InnerMiterLimit) - { - limit = this.InnerMiterLimit; - } - - switch (this.InnerJoin) - { - default: // inner_bevel - this.AddPoint(v1.X + dx1, v1.Y - dy1); - this.AddPoint(v1.X + dx2, v1.Y - dy2); - - break; - - case InnerJoin.InnerMiter: - this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterJoinRevert, limit, 0); - - break; - - case InnerJoin.InnerJag: - case InnerJoin.InnerRound: - cp = ((dx1 - dx2) * (dx1 - dx2)) + ((dy1 - dy2) * (dy1 - dy2)); - if (cp < len1 * len1 && cp < len2 * len2) - { - this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, LineJoin.MiterJoinRevert, limit, 0); - } - else if (this.InnerJoin == InnerJoin.InnerJag) - { - this.AddPoint(v1.X + dx1, v1.Y - dy1); - this.AddPoint(v1.X, v1.Y); - this.AddPoint(v1.X + dx2, v1.Y - dy2); - } - else - { - this.AddPoint(v1.X + dx1, v1.Y - dy1); - this.AddPoint(v1.X, v1.Y); - this.CalcArc(v1.X, v1.Y, dx2, -dy2, dx1, -dy1); - this.AddPoint(v1.X, v1.Y); - this.AddPoint(v1.X + dx2, v1.Y - dy2); - } - - break; - } - } - else - { - double dx = (dx1 + dx2) / 2; - double dy = (dy1 + dy2) / 2; - double dbevel = Math.Sqrt((dx * dx) + (dy * dy)); - - if (this.LineJoin is LineJoin.RoundJoin or LineJoin.BevelJoin && this.ApproximationScale * (this.widthAbs - dbevel) < this.widthEps) - { - if (UtilityMethods.CalcIntersection(v0.X + dx1, v0.Y - dy1, v1.X + dx1, v1.Y - dy1, v1.X + dx2, v1.Y - dy2, v2.X + dx2, v2.Y - dy2, ref dx, ref dy)) - { - this.AddPoint(dx, dy); - } - else - { - this.AddPoint(v1.X + dx1, v1.Y - dy1); - } - - return; - } - - switch (this.LineJoin) - { - case LineJoin.MiterJoin: - case LineJoin.MiterJoinRevert: - case LineJoin.MiterJoinRound: - this.CalcMiter(ref v0, ref v1, ref v2, dx1, dy1, dx2, dy2, this.LineJoin, this.MiterLimit, dbevel); - - break; - - case LineJoin.RoundJoin: - this.CalcArc(v1.X, v1.Y, dx1, -dy1, dx2, -dy2); - - break; - - default: - this.AddPoint(v1.X + dx1, v1.Y - dy1); - this.AddPoint(v1.X + dx2, v1.Y - dy2); - - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddPoint(double x, double y) => this.outVertices.Add(new PointF((float)x, (float)y)); - - private enum Status - { - Initial, - Ready, - Cap1, - Cap2, - Outline1, - CloseFirst, - Outline2, - OutVertices, - EndPoly1, - EndPoly2, - Stop - } -} - -[Flags] -internal enum PathCommand : byte -{ - Stop = 0, - MoveTo = 1, - LineTo = 2, - Curve3 = 3, - Curve4 = 4, - CurveN = 5, - Catrom = 6, - Spline = 7, - EndPoly = 0x0F, - Mask = 0x0F -} - -[Flags] -internal enum PathFlags : byte -{ - None = 0, - Ccw = 0x10, - Cw = 0x20, - Close = 0x40, - Mask = 0xF0 -} - -internal static class PathCommandExtensions -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Vertex(this PathCommand c) => c is >= PathCommand.MoveTo and < PathCommand.EndPoly; - - public static bool Drawing(this PathCommand c) => c is >= PathCommand.LineTo and < PathCommand.EndPoly; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Stop(this PathCommand c) => c == PathCommand.Stop; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool MoveTo(this PathCommand c) => c == PathCommand.MoveTo; - - public static bool LineTo(this PathCommand c) => c == PathCommand.LineTo; - - public static bool Curve(this PathCommand c) => c is PathCommand.Curve3 or PathCommand.Curve4; - - public static bool Curve3(this PathCommand c) => c == PathCommand.Curve3; - - public static bool Curve4(this PathCommand c) => c == PathCommand.Curve4; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EndPoly(this PathCommand c) => (c & PathCommand.Mask) == PathCommand.EndPoly; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Closed(this PathCommand c) => ((int)c & ~((int)PathFlags.Cw | (int)PathFlags.Ccw)) == ((int)PathCommand.EndPoly | (int)PathFlags.Close); - - public static bool NextPoly(this PathCommand c) => Stop(c) || MoveTo(c) || EndPoly(c); - - public static bool Oriented(int c) => (c & (int)(PathFlags.Cw | PathFlags.Ccw)) != 0; - - public static bool Cw(int c) => (c & (int)PathFlags.Cw) != 0; - - public static bool Ccw(int c) => (c & (int)PathFlags.Ccw) != 0; - - public static int CloseFlag(this PathCommand c) => (int)c & (int)PathFlags.Close; - - public static int GetOrientation(this PathCommand c) => (int)c & (int)(PathFlags.Cw | PathFlags.Ccw); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ClearOrientation(this PathCommand c) => (int)c & ~(int)(PathFlags.Cw | PathFlags.Ccw); - - public static int SetOrientation(this PathCommand c, PathFlags o) => ClearOrientation(c) | (int)o; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int GetCloseFlag(this PathCommand c) => (int)c & (int)PathFlags.Close; -} -#pragma warning restore SA1201 // Elements should appear in the correct order diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs b/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs deleted file mode 100644 index 89383756..00000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/VertexDistance.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonClipper; - -internal struct VertexDistance -{ - private const double Dd = 1.0 / Constants.Misc.VertexDistanceEpsilon; - public double X; - public double Y; - public double Distance; - - public VertexDistance(double x, double y) - : this() - { - this.X = x; - this.Y = y; - this.Distance = 0; - } - - public VertexDistance(double x, double y, double distance) - : this() - { - this.X = x; - this.Y = y; - this.Distance = distance; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Measure(VertexDistance vd) - { - bool ret = (this.Distance = UtilityMethods.CalcDistance(this.X, this.Y, vd.X, vd.Y)) > Constants.Misc.VertexDistanceEpsilon; - if (!ret) - { - this.Distance = Dd; - } - - return ret; - } -} - -internal static class Constants -{ - public struct Misc - { - public const double BezierArcAngleEpsilon = 0.01; - public const double AffineEpsilon = 1e-14; - public const double VertexDistanceEpsilon = 1e-14; - public const double IntersectionEpsilon = 1.0e-30; - public const double Pi = 3.14159265358979323846; - public const double PiMul2 = 3.14159265358979323846 * 2; - public const double PiDiv2 = 3.14159265358979323846 * 0.5; - public const double PiDiv180 = 3.14159265358979323846 / 180.0; - public const double CurveDistanceEpsilon = 1e-30; - public const double CurveCollinearityEpsilon = 1e-30; - public const double CurveAngleToleranceEpsilon = 0.01; - public const int CurveRecursionLimit = 32; - public const int PolyMaxCoord = (1 << 30) - 1; - } -} - -internal static unsafe class UtilityMethods -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double CalcDistance(double x1, double y1, double x2, double y2) - { - double dx = x2 - x1; - double dy = y2 - y1; - - return Math.Sqrt((dx * dx) + (dy * dy)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool CalcIntersection(double ax, double ay, double bx, double by, double cx, double cy, double dx, double dy, ref double x, ref double y) - { - double num = ((ay - cy) * (dx - cx)) - ((ax - cx) * (dy - cy)); - double den = ((bx - ax) * (dy - cy)) - ((by - ay) * (dx - cx)); - - if (Math.Abs(den) < Constants.Misc.IntersectionEpsilon) - { - return false; - } - - double r = num / den; - x = ax + (r * (bx - ax)); - y = ay + (r * (by - ay)); - - return true; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double CrossProduct(double x1, double y1, double x2, double y2, double x, double y) => ((x - x2) * (y2 - y1)) - ((y - y2) * (x2 - x1)); -} diff --git a/src/ImageSharp.Drawing/Shapes/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 -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs new file mode 100644 index 00000000..d423b57a --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs @@ -0,0 +1,113 @@ +// 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 . +/// 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 static class ClippedShapeGenerator +{ + /// + /// Generates the final clipped shapes from the previously provided subject and clip paths. + /// + /// + /// The boolean operation to perform, such as , + /// , or . + /// + /// The subject path. + /// The clipping paths. + /// + /// The representing the result of the boolean operation. + /// + public static ComplexPolygon GenerateClippedShapes( + BooleanOperation operation, + IPath subject, + IEnumerable clip) + { + Guard.NotNull(subject); + Guard.NotNull(clip); + + PCPolygon s = PolygonClipperFactory.FromSimpleClosedPaths(subject.Flatten()); + PCPolygon c = PolygonClipperFactory.FromClosedPaths(clip); + + PCPolygon result = operation switch + { + 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[result.Count]; + + int index = 0; + for (int i = 0; i < result.Count; i++) + { + shapes[index++] = new Polygon(CreateContourPoints(result, i)); + } + + return new(shapes); + } + + /// + /// 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[i]; + points[i] = new PointF((float)vertex.X, (float)vertex.Y); + } + + return points; + } + + 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(); + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs similarity index 95% rename from src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs rename to src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs index 39ddcfa0..d22aff79 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonClipper/ClipperException.cs +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.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; /// /// The exception that is thrown when an error occurs clipping a polygon. 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/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs new file mode 100644 index 00000000..a3dc7583 --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs @@ -0,0 +1,177 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +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 static class StrokedShapeGenerator +{ + /// + /// 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. + /// The stroke geometry options. + /// + /// A representing the stroked outline after boolean merge. + /// + public static ComplexPolygon GenerateStrokedShapes(List spans, float width, StrokeOptions options) + { + // 1) Stroke each dashed span as open. + PCPolygon rings = new(spans.Count); + foreach (PointF[] span in spans) + { + if (span == null || span.Length < 2) + { + continue; + } + + 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)); + } + + rings.Add(ring); + } + + int count = rings.Count; + if (count == 0) + { + return new([]); + } + + 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++) + { + 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); + } + + 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 geometry options. + /// + /// A representing the stroked outline after boolean merge. + /// + public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, StrokeOptions options) + { + // 1) Stroke the input path as open or closed. + PCPolygon rings = []; + + foreach (ISimplePath sp in path.Flatten()) + { + ReadOnlySpan span = sp.Points.Span; + + if (span.Length < 2) + { + continue; + } + + 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 new([]); + } + + 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++) + { + 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); + } + + return new(shapes); + } + + private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions options) + { + PolygonClipper.StrokeOptions o = new() + { + ArcDetailScale = options.ArcDetailScale, + MiterLimit = options.MiterLimit, + InnerMiterLimit = options.InnerMiterLimit, + NormalizeOutput = options.NormalizeOutput, + LineJoin = options.LineJoin switch + { + 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 + { + InnerJoin.Round => PolygonClipper.InnerJoin.Round, + InnerJoin.Miter => PolygonClipper.InnerJoin.Miter, + InnerJoin.Jag => PolygonClipper.InnerJoin.Jag, + _ => PolygonClipper.InnerJoin.Bevel, + }, + + LineCap = options.LineCap switch + { + LineCap.Round => PolygonClipper.LineCap.Round, + LineCap.Square => PolygonClipper.LineCap.Square, + _ => PolygonClipper.LineCap.Butt, + } + }; + + return o; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs deleted file mode 100644 index a58cce99..00000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ActiveEdgeList.cs +++ /dev/null @@ -1,281 +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 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 new file mode 100644 index 00000000..2544ff5d --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.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; + +/// +/// Default CPU rasterizer. +/// +/// +/// 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 +{ + /// + /// 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; + } + + PolygonScanner.Rasterize(path, options, allocator, ref state, scanlineHandler); + } +} 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..150b2612 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs @@ -2,231 +2,2274 @@ // 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 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 { - 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; + + // 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; + 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; + + /// + /// 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, + MemoryAllocator allocator, + 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 { - 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); + int maxBandRows = 0; + long coverStride = (long)width * 2; + if (coverStride > int.MaxValue || + !TryGetBandHeight(width, height, wordsPerRow, coverStride, out maxBandRows)) { - dataBufferSize += maxIntersectionCount; + ThrowInterestBoundsTooLarge(); } - this.dataBuffer = allocator.Allocate(dataBufferSize); - Span dataBufferInt32Span = this.dataBuffer.Memory.Span; - Span dataBufferFloatSpan = MemoryMarshal.Cast(dataBufferInt32Span); + int coverStrideInt = (int)coverStride; + float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - 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) + // 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); + using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); + int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeDataOwner.Memory.Span); + if (edgeCount <= 0) { - Span remainder = - dataBufferInt32Span.Slice((edgeCount * 3) + maxIntersectionCount, maxIntersectionCount); - this.intersectionTypes = MemoryMarshal.Cast(remainder); + return; } - else + + if (allowParallel && + TryRasterizeParallel( + edgeDataOwner.Memory, + edgeCount, + width, + height, + interest.Top, + wordsPerRow, + coverStrideInt, + maxBandRows, + options.IntersectionRule, + options.RasterizationMode, + allocator, + ref state, + scanlineHandler)) { - this.intersectionTypes = default; + return; } - this.idx0 = 0; - this.idx1 = 0; - this.PixelLineY = minY - 1; - this.SubPixelY = default; - this.yPlusOne = default; + RasterizeSequentialBands( + edgeDataOwner.Memory.Span[..edgeCount], + width, + height, + interest.Top, + wordsPerRow, + coverStrideInt, + maxBandRows, + options.IntersectionRule, + options.RasterizationMode, + allocator, + ref state, + scanlineHandler); } - public static PolygonScanner Create( - IPath polygon, - int minY, - int maxY, - int subsampling, + /// + /// 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, - MemoryAllocator allocator) + RasterizationMode rasterizationMode, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct { - 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; + int bandHeight = maxBandRows; + int bandCount = (height + bandHeight - 1) / bandHeight; + if (bandCount < 1) + { + return; + } + + 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(); + } } - private void Init() + /// + /// 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 tiled 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 { - // 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); + int tileHeight = Math.Min(DefaultTileHeight, maxBandRows); + if (tileHeight < 1) + { + return false; + } + + int tileCount = (height + tileHeight - 1) / tileHeight; + if (tileCount == 1) + { + // 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; + } + + 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; + } + + 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]++; + } + } - for (int i = 0; i < this.edges.Length; i++) + 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++) { - ref ScanEdge edge = ref this.edges[i]; - keys0[i] = edge.Y0; - keys1[i] = edge.Y1; - this.sorted0[i] = i; - this.sorted1[i] = i; + 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; + } } - keys0.Sort(this.sorted0); - keys1.Sort(this.sorted1); + 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(); + } + } + } - this.SkipEdgesBeforeMinY(); + /// + /// 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(); } - private void SkipEdgesBeforeMinY() + /// + /// 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 (this.edges.Length == 0) + if (tileEdgeIndices.Length == 0) { return; } - this.SubPixelY = this.edges[this.sorted0[0]].Y0; + Context context = scratch.CreateContext(bandHeight, intersectionRule, rasterizationMode); + context.RasterizeEdgeTable(edges, tileEdgeIndices, bandTop); - int i0 = 1; - int i1 = 0; + int coverageLength = checked(width * bandHeight); + IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); + IMemoryOwner dirtyRowsOwner = allocator.Allocate(bandHeight, AllocationOptions.Clean); + bool committed = false; - // 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) + try { - this.EnterEdges(); - this.LeaveEdges(); - this.activeEdges.RemoveLeavingEdges(); + TileCaptureState captureState = new(width, coverageOwner.Memory, dirtyRowsOwner.Memory); - bool hasMore0 = i0 < this.sorted0.Length; - bool hasMore1 = i1 < this.sorted1.Length; + // 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 (!hasMore0 && !hasMore1) + if (!committed) { - // The entire polygon is outside the scan region, we skipped all edges, - // scanning will not find any intersections. - break; + coverageOwner.Dispose(); + dirtyRowsOwner.Dispose(); } + } + } - float y0 = hasMore0 ? this.edges[this.sorted0[i0]].Y0 : float.MaxValue; - float y1 = hasMore1 ? this.edges[this.sorted1[i1]].Y1 : float.MaxValue; + /// + /// 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; + } - if (y0 < y1) + Span coverage = output.CoverageOwner.Memory.Span; + Span dirtyRows = output.DirtyRowsOwner.Memory.Span; + for (int row = 0; row < output.Height; row++) { - this.SubPixelY = y0; - i0++; + 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); } - else + } + } + + /// + /// 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 an 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++) { - this.SubPixelY = y1; - i1++; + 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; + } + + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1); + if (fy0 == fy1) + { + continue; + } + + ComputeEdgeRowBounds(fy0, fy1, out int minRow, out int maxRow); + destination[count++] = new EdgeData(fx0, fy0, fx1, fy1, minRow, maxRow); } } + + return count; + } + + /// + /// 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; + 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; } - public bool MoveToNextPixelLine() + /// + /// Converts a float coordinate to signed 24.8 fixed-point. + /// + [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) { - this.PixelLineY++; - this.yPlusOne = this.PixelLineY + 1; - this.SubPixelY = this.PixelLineY - this.SubpixelDistance; - return this.PixelLineY < this.maxY; + 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; } - public bool MoveToNextSubpixelScanLine() + /// + /// 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) { - this.SubPixelY += this.SubpixelDistance; - this.EnterEdges(); - this.LeaveEdges(); - return this.SubPixelY < this.yPlusOne; + 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; } - 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); + /// + /// 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; + 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; + } - public readonly void Dispose() + /// + /// One Liang-Barsky clip test step. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTest(float p, float q, ref float t0, ref float t1) { - this.edgeCollection.Dispose(); - this.dataBuffer.Dispose(); + 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; } - private void EnterEdges() + /// + /// 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) { - while (this.idx0 < this.sorted0.Length) + if (p == 0D) + { + return q >= 0D; + } + + double r = q / p; + if (p < 0D) + { + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } + } + else { - int edge0 = this.sorted0[this.idx0]; - if (this.edges[edge0].Y0 > this.SubPixelY) + if (r < t0) { - break; + return false; } - this.activeEdges.EnterEdge(edge0); - this.idx0++; + 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. + /// + [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 LeaveEdges() + /// + /// 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 { - while (this.idx1 < this.sorted1.Length) + 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; + + /// + /// Initializes a new instance of the struct. + /// + 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) + { + 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; + } + + /// + /// 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) + { + 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); + } + } + } + + /// + /// 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. + /// + /// 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) + { + int bandTopFixed = bandTop * FixedOne; + int bandBottomFixed = bandTopFixed + (this.height * FixedOne); + + for (int i = 0; i < edgeIndices.Length; i++) + { + EdgeData edge = edges[edgeIndices[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); + } + } + + /// + /// 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 + { + for (int row = 0; row < this.height; row++) + { + int rowCover = this.startCover[row]; + if (rowCover == 0 && this.rowHasBits[row] == 0) + { + // Nothing contributed to this row. + 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); + } + } + } + + /// + /// 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 + // full temporary buffers when geometry is sparse relative to the interest bounds. + for (int i = 0; i < this.touchedRowCount; i++) + { + 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(); + } + + 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 edge1 = this.sorted1[this.idx1]; - if (this.edges[edge1].Y1 > this.SubPixelY) + 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++) + { + // Iterate touched columns sparsely by scanning set bits only. + 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); + + // 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); + + 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 + { + // 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); + 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]; + } + } + + // 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) { - break; + hasCoverage |= FlushSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift)); } - this.activeEdges.LeaveEdge(edge1); - this.idx1++; + return hasCoverage; + } + + /// + /// Converts accumulated signed area to normalized coverage under the selected fill rule. + /// + [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) + { + // Non-zero winding clamps absolute winding accumulation to [0, 1]. + if (absoluteArea >= CoverageStepCount) + { + coverage = 1F; + } + else + { + coverage = absoluteArea * CoverageScale; + } + } + else + { + // Even-odd wraps every 2*CoverageStepCount and mirrors second half. + int wrapped = absoluteArea & EvenOddMask; + if (wrapped > CoverageStepCount) + { + wrapped = EvenOddPeriod - wrapped; + } + + coverage = wrapped >= CoverageStepCount ? 1F : wrapped * CoverageScale; + } + + 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) + { + if (coverage <= 0F || end <= start) + { + return false; + } + + scanline[start..end].Fill(coverage); + 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) + { + 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; + + // 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) + { + if ((uint)row >= (uint)this.height) + { + return; + } + + this.MarkRowTouched(row); + + if (column < 0) + { + // Contributions left of x=0 accumulate into the row carry. + this.startCover[row] += delta; + return; + } + + if ((uint)column >= (uint)this.width) + { + return; + } + + 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) + { + if (this.rowTouched[row] != 0) + { + return; + } + + this.rowTouched[row] = 1; + 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) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x - x); + 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) + { + int delta = y0 - y1; + int area = delta * ((FixedOne * 2) - x0 - x1); + 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; + int rowIndex1 = (y1 - 1) >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + 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++) + { + this.CellVertical(columnIndex, row, fx, 0, FixedOne); + } + + 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; + int rowIndex1 = y1 >> FixedShift; + int fy0 = y0 - (rowIndex0 << FixedShift); + int fy1 = y1 - (rowIndex1 << FixedShift); + int fx = x - (columnIndex << FixedShift); + + 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--) + { + this.CellVertical(columnIndex, row, fx, FixedOne, 0); + } + + 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; + 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); + } + + /// + /// 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) + { + 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); + } + } + + /// + /// 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; + 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); + } + + /// + /// 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) + { + 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); + } + } + + /// + /// 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; + 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); + } + + /// + /// 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) + { + 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); + } + } + + /// + /// 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; + 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); + } + + /// + /// 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) + { + 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); + } + } + + /// + /// 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; + + 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); + } + + /// + /// 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; + + 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); + } + + /// + /// 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; + + 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); + } + + /// + /// 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; + + 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); + } + + /// + /// 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) + { + this.VerticalDown(columnIndex, y0, y1, x0); + } + else + { + this.VerticalUp(columnIndex, y0, y1, x0); + } + + return; + } + + if (y0 < y1) + { + // Downward edges use inclusive top/exclusive bottom row mapping. + 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; + } + + // Upward edges mirror the mapping to preserve winding consistency. + 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); + } + } + } + + /// + /// Immutable scanner-local edge record with precomputed affected-row bounds. + /// + /// + /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path + /// access without per-read unpacking. + /// + private readonly struct EdgeData + { + /// + /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int X0; + + /// + /// Gets edge start Y in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int Y0; + + /// + /// Gets edge end X in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int X1; + + /// + /// Gets edge end Y in scanner-local coordinates (24.8 fixed-point). + /// + public readonly int Y1; + + /// + /// Gets the first scanner row affected by this edge. + /// + public readonly int MinRow; + + /// + /// Gets the last scanner row affected by this edge. + /// + 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; + } + } + + /// + /// 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 4206223f..e4fb2445 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD @@ -1,76 +1,224 @@ -# Polygon Scanning with Active Edge List +# Polygon Scanner (Fixed-Point, Tiled + Banded Fallback) -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 +Choose execution mode: + | + +--> 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 + | + +--> 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` (global local-space edge table) + - Per tile/band pass uses `yLocal - currentBandTop` +- 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 Banded Scratch + +The scanner bounds scratch 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) +``` + +Scratch buffers are reused per band/tile worker: + +``` +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 ``` - + - - - - - - - - - - - - - - - - + - | (1) (1)\ - | \ B - | \ - | (0) \ -SCANLINE 1 >>>> | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> | (1) >>>>>>>>> - | | - | A C | - | | - | (2) X | (1) -SCANLINE 2 >>>> | >>>>>>>>>>>>>> + - - - - - - - - - - + >>>>>>>>> - | | - | | - | D | - | | - | (1) | (1) - + - - - - - - - + +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)` during edge-table build: + +1. Translate to local coordinates. +2. Reject non-finite coordinates. +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: + +- `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 +``` + +### 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. +- 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. + +## 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 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: + - 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 new file mode 100644 index 00000000..66a8cfbb --- /dev/null +++ b/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +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. +/// +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. + /// Polygon intersection rule. + /// Rasterization coverage mode. + /// Sampling origin alignment. + public RasterizerOptions( + Rectangle interest, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode = RasterizationMode.Antialiased, + RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary) + { + this.Interest = interest; + this.IntersectionRule = intersectionRule; + this.RasterizationMode = rasterizationMode; + this.SamplingOrigin = samplingOrigin; + } + + /// + /// Gets destination bounds to rasterize into. + /// + public Rectangle Interest { get; } + + /// + /// Gets the polygon intersection rule. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the rasterization coverage mode. + /// + public RasterizationMode RasterizationMode { 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.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..6a2183c0 --- /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.RasterizeSequential(path, options, allocator, ref state, scanlineHandler); + } +} 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/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs index 8cc45fbe..2966e3f7 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs @@ -220,7 +220,7 @@ void IGlyphRenderer.EndLayer() ShapeOptions options = new() { - ClippingOperation = ClippingOperation.Intersection, + BooleanOperation = 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); 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/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/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index 380ce246..c3080014 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; @@ -23,12 +24,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; } @@ -42,56 +51,134 @@ protected virtual PointF[][] GetPoints(FeatureCollection features) => [GlobalSetup] public void Setup() { - string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); + // 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); 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) { - this.skPath.LineTo(ptArr[i].X, ptArr[i].Y); + continue; } - this.skPath.LineTo(ptArr[0].X, ptArr[1].Y); + // 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(loop[i].X, loop[i].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); - } - } + // Keep explicit scanline rasterizer path for side-by-side comparison now that tiled is default. + [Benchmark] + public void ImageSharpCombinedPathsScanlineRasterizer() + => this.image.Mutate(c => c.SetRasterizer(ScanlineRasterizer.Instance).Draw(this.isPen, this.imageSharpPath)); [Benchmark] - public void ImageSharp() + public void ImageSharpSeparatePathsScanlineRasterizer() + => this.image.Mutate( + c => + { + // 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) + { + c.DrawPolygon(Color.White, this.Thickness, loop); + } + }); + + // Tiled is now the framework default rasterizer path. + [Benchmark] + public void ImageSharpCombinedPathsTiled() + => this.image.Mutate(c => c.Draw(this.isPen, this.imageSharpPath)); + + [Benchmark] + public void ImageSharpSeparatePathsTiled() => this.image.Mutate( c => { @@ -103,17 +190,13 @@ public void ImageSharp() [Benchmark(Baseline = true)] public void SkiaSharp() - { - using SKPaint paint = new() - { - Style = SKPaintStyle.Stroke, - Color = SKColors.White, - StrokeWidth = this.Thickness, - IsAntialias = true, - }; + => this.skSurface.Canvas.DrawPath(this.skPath, this.skPaint); - this.skSurface.Canvas.DrawPath(this.skPath, paint); - } + [Benchmark] + public IPath ImageSharpStrokeAndClip() => this.isPen.GeneratePath(this.imageSharpPath); + + [Benchmark] + public void FillPolygon() => this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); } public class DrawPolygonAll : DrawPolygon @@ -122,7 +205,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 +214,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 +228,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/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index be0430b2..0a2f32ce 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -12,12 +12,14 @@ CA1822 + + CA1416 - net8.0;net9.0 + net8.0;net10.0 @@ -31,6 +33,7 @@ + diff --git a/tests/ImageSharp.Drawing.Benchmarks/Program.cs b/tests/ImageSharp.Drawing.Benchmarks/Program.cs index 97bb7feb..9822ba4e 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Program.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Program.cs @@ -16,14 +16,20 @@ 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)); + // 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/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/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/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs index be892373..6d230ee2 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs @@ -1,11 +1,13 @@ // 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; 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; @@ -72,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) { @@ -86,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) { @@ -106,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)); @@ -122,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 }, }; Random rnd = new(42); byte[] rgb = new byte[3]; @@ -130,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)); } @@ -200,7 +202,7 @@ public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provi image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } - [Theory]//(Skip = "For local experiments only")] + [Theory(Skip = "For local experiments only")] [InlineData(0)] [InlineData(5000)] [InlineData(9000)] @@ -255,4 +257,108 @@ public void Missisippi_Skia(int offset) using FileStream fs = File.Create(fn); data.SaveTo(fs); } + + [Theory(Skip = "For local experiments only")] + [WithSolidFilledImages(1000, 1000, "Black", PixelTypes.Rgba32, 10)] + public void LargeGeoJson_States_Separate_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(Skip = "For local experiments only")] + [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(DefaultRasterizer.Instance); + c.Draw(Color.White, thickness, path); + }); + + image.DebugSave(provider, $"Benchmark_{thickness}", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + } + + [Theory(Skip = "For local experiments only")] + [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]]; + } } +#pragma warning restore xUnit1004 // Test methods should not be skipped diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs index bc4963cd..3fc6f89f 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs @@ -22,14 +22,14 @@ 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) .FillPolygon(Color.White, polygon2), + testOutputDetails: $"aa{antialias}", appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false, - testOutputDetails: $"aa{antialias}"); + appendSourceFileOrDescription: false); } [Theory] @@ -177,30 +177,30 @@ 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] - [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 (ClippingOperation operation in (ClippingOperation[])Enum.GetValues(typeof(ClippingOperation))) - { - ShapeOptions options = new() { ClippingOperation = 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), - comparer: ImageComparer.TolerantPercentage(0.01F), - testOutputDetails: operation.ToString(), - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: 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] @@ -300,8 +300,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 = @@ -336,8 +336,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/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/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/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/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 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/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/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 48d61f96..0d49fa0e 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs @@ -7,6 +7,9 @@ using SixLabors.ImageSharp.Advanced; 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; @@ -163,6 +166,126 @@ 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); + } + + [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_UsesExpectedRasterizationModeAndPixelBoundarySamplingOrigin(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); + + 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 RasterizationMode LastRasterizationMode { get; private set; } + + public RasterizerSamplingOrigin LastSamplingOrigin { get; private set; } + + public void Rasterize( + IPath path, + in RasterizerOptions options, + MemoryAllocator allocator, + ref TState state, + RasterizerScanlineHandler scanlineHandler) + where TState : struct + { + this.CallCount++; + this.LastRasterizationMode = options.RasterizationMode; + this.LastSamplingOrigin = options.SamplingOrigin; + } + } } internal static class ReflectionHelpers @@ -172,4 +295,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/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs new file mode 100644 index 00000000..ffea506c --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -0,0 +1,133 @@ +// 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 FillPath( + Configuration configuration, + ImageFrame source, + IPath path, + 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, + Buffer2D destination) + { + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs index 28a20662..ce7f3058 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ShapeOptionsDefaultsExtensionsTests.cs @@ -27,7 +27,7 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance() { ShapeOptions option = new() { - ClippingOperation = ClippingOperation.Intersection, + BooleanOperation = BooleanOperation.Intersection, IntersectionRule = IntersectionRule.NonZero }; Configuration config = new(); @@ -36,18 +36,18 @@ public void UpdateDefaultOptionsOnProcessingContext_AlwaysNewInstance() context.SetShapeOptions(o => { - Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values + Assert.Equal(BooleanOperation.Intersection, o.BooleanOperation); // has original values Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule); - o.ClippingOperation = ClippingOperation.Xor; + o.BooleanOperation = BooleanOperation.Xor; o.IntersectionRule = IntersectionRule.EvenOdd; }); ShapeOptions returnedOption = context.GetShapeOptions(); - Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation); + Assert.Equal(BooleanOperation.Xor, returnedOption.BooleanOperation); Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule); - Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated + Assert.Equal(BooleanOperation.Intersection, option.BooleanOperation); // hasn't been mutated Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule); } @@ -67,7 +67,7 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance() { ShapeOptions option = new() { - ClippingOperation = ClippingOperation.Intersection, + BooleanOperation = BooleanOperation.Intersection, IntersectionRule = IntersectionRule.NonZero }; Configuration config = new(); @@ -75,16 +75,16 @@ public void UpdateDefaultOptionsOnConfiguration_AlwaysNewInstance() config.SetShapeOptions(o => { - Assert.Equal(ClippingOperation.Intersection, o.ClippingOperation); // has original values + Assert.Equal(BooleanOperation.Intersection, o.BooleanOperation); // has original values Assert.Equal(IntersectionRule.NonZero, o.IntersectionRule); - o.ClippingOperation = ClippingOperation.Xor; + o.BooleanOperation = BooleanOperation.Xor; o.IntersectionRule = IntersectionRule.EvenOdd; }); ShapeOptions returnedOption = config.GetShapeOptions(); - Assert.Equal(ClippingOperation.Xor, returnedOption.ClippingOperation); + Assert.Equal(BooleanOperation.Xor, returnedOption.BooleanOperation); Assert.Equal(IntersectionRule.EvenOdd, returnedOption.IntersectionRule); - Assert.Equal(ClippingOperation.Intersection, option.ClippingOperation); // hasn't been mutated + Assert.Equal(BooleanOperation.Intersection, option.BooleanOperation); // hasn't been mutated Assert.Equal(IntersectionRule.NonZero, option.IntersectionRule); } @@ -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] diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs index 5d85c26a..7943ac2d 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; namespace SixLabors.ImageSharp.Drawing.Tests.PolygonClipper; @@ -25,21 +25,8 @@ public class ClipperTests new Vector2(130, 40), new Vector2(65, 137))); - private IEnumerable Clip(IPath shape, params IPath[] hole) - { - Clipper clipper = new(); - - clipper.AddPath(shape, ClippingType.Subject); - if (hole != null) - { - foreach (IPath s in hole) - { - clipper.AddPath(s, ClippingType.Clip); - } - } - - return clipper.GenerateClippedShapes(ClippingOperation.Difference, IntersectionRule.EvenOdd); - } + private static ComplexPolygon Clip(IPath shape, params IPath[] hole) + => ClippedShapeGenerator.GenerateClippedShapes(BooleanOperation.Difference, shape, hole); [Fact] public void OverlappingTriangleCutRightSide() @@ -55,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)); @@ -77,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); @@ -90,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()); @@ -112,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] @@ -126,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); } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs new file mode 100644 index 00000000..382d6fa8 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs @@ -0,0 +1,101 @@ +// 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 DefaultRasterizerTests +{ + [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, rule); + + float[] expected = Rasterize(ScanlineRasterizer.Instance, path, options); + float[] actual = Rasterize(DefaultRasterizer.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, + IntersectionRule.NonZero, + samplingOrigin: RasterizerSamplingOrigin.PixelCenter); + + float[] expected = Rasterize(ScanlineRasterizer.Instance, path, options); + float[] actual = Rasterize(DefaultRasterizer.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/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 + { + } +} 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; } 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 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 2ac2d03a..0e1070e7 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png and b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png index 844faa76..0b71e15a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a1f23951b7043a22968401cd9d01062c140a3f9bb24e11a90bf1037b08266ec -size 8328 +oid sha256:1efe570c8fa8654a615adb12fe993316233dc7af7d317a4cc334ac86aa3f5a44 +size 8166 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png index 30168925..0e639df5 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad72067bee572c9dc977a5afaa0ad540792bf7b59dcb1197ae02dc660356a016 -size 6630 +oid sha256:e75557d8e59a3aae917228cfd9b6a1251c5c4f771d08d8e16e2de99cb16d53d4 +size 6169 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png index 947d14e3..9a7f7901 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png and b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png index c5a1b53b..8c8f5d48 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png and b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png differ 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 1bf69b24..cb1e6250 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png and b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png differ 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 94d45fdf..15adb7c3 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png and b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png differ 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 e80e216d..863cf7c7 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png and b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png differ 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 003ac660..141ca949 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png and b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png differ 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 eb87493a..2bbf451e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png and b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png differ 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 b1ed1ea2..609fc357 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png and b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png index f117c26a..fb196598 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png and b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png differ 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 276fc7aa..8ad0ef2c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill.png and b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png index 3e24bbae..68985157 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png and b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png index ba1b8316..48f1ff7a 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png and b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png index 6fa17f8a..701871eb 100644 --- a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png +++ b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5635388f431026b7a5d4b2f760ffe74a767d5d9d74fa88b400d4912add661bf0 -size 506 +oid sha256:b61f05dc45c224dd9f24be6bec252239058035d96c33a417bcba96b25da66fcf +size 454 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png index 401bfd8c..943f574b 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png and b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png differ 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 0c0df89c..d029a873 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png differ 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 61d6f5cc..41994df5 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_Car.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_Car.png differ 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 61eac756..d24bd6b8 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png index dbe6283d..ad5b15cb 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png differ 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 7232168c..5a79a3c7 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa0.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa0.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa16.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa16.png index be566ff2..8fdea2ff 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa16.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa16.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa8.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa8.png index 4f1fb7f5..8fdea2ff 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa8.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa8.png differ 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 fb2b5118..12ecc129 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Bgr24_Yellow_A1.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Bgr24_Yellow_A1.png differ 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 9e388387..d6c24cbf 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A0.6.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A0.6.png differ 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 f6d392d7..7c67a4ec 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1.png differ 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 128beb79..7cd33f33 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png differ 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 9e43acad..15431f30 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png differ 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 722c61f0..4e29cc25 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png differ 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 e1e5e69d..3fe215ed 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png index b0191924..8ad422f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png and b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png differ 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 41031fab..5273f1f6 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png differ 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 55220f49..562fb674 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png differ 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 a16169ea..ef30b2c3 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png index a5fa4ea1..62c650d8 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png differ 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 357c07c9..6cf010a4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png index bb15ded9..7aeebeb3 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png differ 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 51af8827..c84ad381 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png index 950ee6cb..8cfbd3fa 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png index 6f0a5960..0d1cf527 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png differ 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 698e589c..8afcb3f4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png differ 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 8a2c7d7d..dd2b6de4 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png differ 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 c431c406..1431969e 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png differ 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 d331b707..cb70160c 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png differ 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 250d94c6..caa7c8ba 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png differ 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 df5f08c4..9d505793 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Add.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Add.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Darken.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Darken.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-HardLight.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-HardLight.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Lighten.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Lighten.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Multiply.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Multiply.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Normal.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Normal.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Overlay.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Overlay.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Screen.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Screen.png differ 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 237fd396..17360edc 100644 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Subtract.png and b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Subtract.png differ 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