diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 579efe8a4..84033e301 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -92,12 +92,12 @@ jobs: framework: net8.0 sdk: 8.0.x runtime: -x64 - codecov: true + codecov: false - os: macos-26 framework: net8.0 sdk: 8.0.x runtime: -x64 - codecov: false + codecov: true - os: windows-latest framework: net8.0 sdk: 8.0.x diff --git a/.gitignore b/.gitignore index dca6a480b..4560e54e3 100644 --- a/.gitignore +++ b/.gitignore @@ -227,4 +227,7 @@ artifacts/ /tests/CodeCoverage/OpenCover.* SixLabors.Shapes.Coverage.xml -/tests/SixLabors.Shapes.Benchmarks/BenchmarkDotNet.Artifacts/results/ \ No newline at end of file +/tests/SixLabors.Shapes.Benchmarks/BenchmarkDotNet.Artifacts/results/ +.dotnet +.codex-* +.claude diff --git a/ImageSharp.Drawing.sln b/ImageSharp.Drawing.sln index 74e8e1549..6be384d40 100644 --- a/ImageSharp.Drawing.sln +++ b/ImageSharp.Drawing.sln @@ -337,28 +337,106 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU", "src\ImageSharp.Drawing.WebGPU\ImageSharp.Drawing.WebGPU.csproj", "{061582C2-658F-40AE-A978-7D74A4EB2C0A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUWindowDemo", "samples\WebGPUWindowDemo\WebGPUWindowDemo.csproj", "{2541FDCD-78AC-40DB-B5E3-6A715DC132BA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU.ShaderGen", "src\ImageSharp.Drawing.WebGPU.ShaderGen\ImageSharp.Drawing.WebGPU.ShaderGen.csproj", "{C7606104-5D58-4670-912C-3F336606B02D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x64.Build.0 = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Debug|x86.Build.0 = Debug|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|Any CPU.Build.0 = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x64.ActiveCfg = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x64.Build.0 = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.ActiveCfg = Release|Any CPU + {2E33181E-6E28-4662-A801-E2E7DC206029}.Release|x86.Build.0 = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x64.Build.0 = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|x86.Build.0 = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.ActiveCfg = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x64.Build.0 = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.ActiveCfg = Release|Any CPU + {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|x86.Build.0 = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|Any CPU.Build.0 = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.ActiveCfg = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x64.Build.0 = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.ActiveCfg = Debug|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Debug|x86.Build.0 = Debug|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.ActiveCfg = Release|Any CPU {59804113-1DD4-4F80-8D06-35FF71652508}.Release|Any CPU.Build.0 = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.ActiveCfg = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x64.Build.0 = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.ActiveCfg = Release|Any CPU + {59804113-1DD4-4F80-8D06-35FF71652508}.Release|x86.Build.0 = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x64.ActiveCfg = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x64.Build.0 = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x86.ActiveCfg = Debug|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|x86.Build.0 = Debug|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x64.ActiveCfg = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x64.Build.0 = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x86.ActiveCfg = Release|Any CPU + {5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|x86.Build.0 = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.ActiveCfg = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x64.Build.0 = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.ActiveCfg = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Debug|x86.Build.0 = Debug|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|Any CPU.Build.0 = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.ActiveCfg = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x64.Build.0 = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.ActiveCfg = Release|Any CPU + {061582C2-658F-40AE-A978-7D74A4EB2C0A}.Release|x86.Build.0 = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x64.ActiveCfg = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x64.Build.0 = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x86.ActiveCfg = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Debug|x86.Build.0 = Debug|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|Any CPU.Build.0 = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x64.ActiveCfg = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x64.Build.0 = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x86.ActiveCfg = Release|Any CPU + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA}.Release|x86.Build.0 = Release|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Debug|x64.Build.0 = Debug|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Debug|x86.Build.0 = Debug|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Release|Any CPU.Build.0 = Release|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Release|x64.ActiveCfg = Release|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Release|x64.Build.0 = Release|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.ActiveCfg = Release|Any CPU + {C7606104-5D58-4670-912C-3F336606B02D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -386,11 +464,15 @@ Global {68A8CC40-6AED-4E96-B524-31B1158FDEEA} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} {5493F024-0A3F-420C-AC2D-05B77A36025B} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} {23859314-5693-4E6C-BE5C-80A433439D2A} = {1799C43E-5C54-4A8F-8D64-B1475241DB0D} + {061582C2-658F-40AE-A978-7D74A4EB2C0A} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} + {2541FDCD-78AC-40DB-B5E3-6A715DC132BA} = {528610AC-7C0C-46E8-9A2D-D46FD92FEE29} + {C7606104-5D58-4670-912C-3F336606B02D} = {815C0625-CD3D-440F-9F80-2D83856AB7AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution + shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{061582c2-658f-40ae-a978-7d74a4eb2c0a}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{2e33181e-6e28-4662-a801-e2e7dc206029}*SharedItemsImports = 5 shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{68a8cc40-6aed-4e96-b524-31b1158fdeea}*SharedItemsImports = 13 EndGlobalSection diff --git a/README.md b/README.md index b5a3413ee..13bd490c0 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,10 @@ SixLabors.ImageSharp.Drawing [![Build Status](https://img.shields.io/github/actions/workflow/status/SixLabors/ImageSharp.Drawing/build-and-test.yml?branch=main)](https://github.com/SixLabors/ImageSharp.Drawing/actions) [![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp.Drawing/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp.Drawing) [![License: Six Labors Split](https://img.shields.io/badge/license-Six%20Labors%20Split-%23e30183)](https://github.com/SixLabors/ImageSharp.Drawing/blob/main/LICENSE) -[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors) -### **ImageSharp.Drawing** provides extensions to ImageSharp containing powerful, cross-platform 2D polygon manipulation and drawing APIs. - -Designed to democratize image processing, ImageSharp.Drawing brings you an incredibly powerful yet beautifully simple API. - -Built against [.NET 6](https://docs.microsoft.com/en-us/dotnet/standard/net-standard), ImageSharp.Drawing can be used in device, cloud, and embedded/IoT scenarios. +**ImageSharp.Drawing** is a cross-platform 2D drawing library built on top of [ImageSharp](https://github.com/SixLabors/ImageSharp). It provides path construction, polygon manipulation, fills, strokes, gradient brushes, pattern brushes, and text rendering. Built against [.NET 8](https://docs.microsoft.com/en-us/dotnet/standard/net-standard). ## License @@ -61,12 +56,12 @@ If you prefer, you can compile ImageSharp.Drawing yourself (please do and help!) - Using [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) - Make sure you have the latest version installed - - Make sure you have [the .NET 7 SDK](https://www.microsoft.com/net/core#windows) installed + - Make sure you have [the .NET 8 SDK](https://www.microsoft.com/net/core#windows) installed Alternatively, you can work from command line and/or with a lightweight editor on **both Linux/Unix and Windows**: - [Visual Studio Code](https://code.visualstudio.com/) with [C# Extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp) -- [the .NET 7 SDK](https://www.microsoft.com/net/core#linuxubuntu) +- [the .NET 8 SDK](https://www.microsoft.com/net/core#linuxubuntu) To clone ImageSharp.Drawing locally, click the "Clone in [YOUR_OS]" button above or run the following git commands: diff --git a/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs b/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs deleted file mode 100644 index fd92011bc..000000000 --- a/samples/DrawShapesWithImageSharp/ImageSharpLogo.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.Shapes.DrawShapesWithImageSharp; - -public static class ImageSharpLogo -{ - public static void SaveLogo(float size, string path) - { - // the point are based on a 1206x1206 shape so size requires scaling from there - float scalingFactor = size / 1206; - - Vector2 center = new(603); - - // segment whose center of rotation should be - Vector2 segmentOffset = new(301.16968f, 301.16974f); - IPath segment = new Polygon( - new LinearLineSegment(new Vector2(230.54f, 361.0261f), new Vector2(5.8641942f, 361.46031f)), - new CubicBezierLineSegment( - new Vector2(5.8641942f, 361.46031f), - new Vector2(-11.715693f, 259.54052f), - new Vector2(24.441609f, 158.17478f), - new Vector2(78.26f, 97.0461f))).Translate(center - segmentOffset); - - // we need to create 6 of theses all rotated about the center point - List segments = []; - for (int i = 0; i < 6; i++) - { - float angle = i * ((float)Math.PI / 3); - IPath s = segment.Transform(Matrix3x2.CreateRotation(angle, center)); - segments.Add(s); - } - - List colors = - [ - Color.ParseHex("35a849"), - Color.ParseHex("fcee21"), - Color.ParseHex("ed7124"), - Color.ParseHex("cb202d"), - Color.ParseHex("5f2c83"), - Color.ParseHex("085ba7") - ]; - - Matrix3x2 scaler = Matrix3x2.CreateScale(scalingFactor, Vector2.Zero); - - int dimensions = (int)Math.Ceiling(size); - using (Image img = new(dimensions, dimensions)) - { - img.Mutate(i => i.Fill(Color.Black)); - img.Mutate(i => i.Fill(Color.ParseHex("e1e1e1ff"), new EllipsePolygon(center, 600f).Transform(scaler))); - img.Mutate(i => i.Fill(Color.White, new EllipsePolygon(center, 600f - 60).Transform(scaler))); - - for (int s = 0; s < 6; s++) - { - img.Mutate(i => i.Fill(colors[s], segments[s].Transform(scaler))); - } - - img.Mutate(i => i.Fill(Color.FromPixel(new Rgba32(0, 0, 0, 170)), new ComplexPolygon(new EllipsePolygon(center, 161f), new EllipsePolygon(center, 61f)).Transform(scaler))); - - string fullPath = System.IO.Path.GetFullPath(System.IO.Path.Combine("Output", path)); - - img.Save(fullPath); - } - } -} diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs index 04497dc93..c602d3cf3 100644 --- a/samples/DrawShapesWithImageSharp/Program.cs +++ b/samples/DrawShapesWithImageSharp/Program.cs @@ -21,8 +21,6 @@ public static void Main(string[] args) { OutputClippedRectangle(); OutputStars(); - - ImageSharpLogo.SaveLogo(300, "ImageSharp.png"); } private static void OutputStars() @@ -113,7 +111,7 @@ private static void DrawSerializedOPenSansLetterShape_a() })]; ComplexPolygon complex = new(polys); - complex.SaveImage("letter", "a.png"); + complex.SaveImage("Letter", "a.png"); } private static void DrawSerializedOPenSansLetterShape_o() @@ -131,7 +129,7 @@ private static void DrawSerializedOPenSansLetterShape_o() })]; ComplexPolygon complex = new(polys); - complex.SaveImage("letter", "o.png"); + complex.SaveImage("Letter", "o.png"); } private static void DrawOval() @@ -159,7 +157,7 @@ private static void OutputDrawnShape() sb.AddLine(new Vector2(25, 30), new Vector2(15, 30)); sb.CloseFigure(); - sb.Build().Translate(0, 10).Scale(10).SaveImage("drawing", $"paths.png"); + sb.Build().Translate(0, 10).Scale(10).SaveImage("Drawing", $"paths.png"); } private static void OutputDrawnShapeHourGlass() @@ -176,7 +174,7 @@ private static void OutputDrawnShapeHourGlass() sb.AddLine(new Vector2(15, 30), new Vector2(25, 30)); sb.CloseFigure(); - sb.Build().Translate(0, 10).Scale(10).SaveImage("drawing", $"HourGlass.png"); + 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, LineJoin jointStyle = LineJoin.Miter) @@ -239,11 +237,12 @@ public static void SaveImage(this IPathCollection collection, params string[] pa int height = (int)(collection.Bounds.Top + collection.Bounds.Bottom); using Image img = new(width, height); - // Fill the canvas background and draw our shape - img.Mutate(i => i.Fill(Color.DarkBlue)); - - // Draw our path collection. - img.Mutate(i => i.Fill(Color.HotPink, collection)); + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + // Fill the canvas background and draw our shape. + canvas.Fill(Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.HotPink), collection); + })); // Ensure directory exists string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path))); @@ -264,11 +263,15 @@ public static void SaveImageWithPath(this IPathCollection collection, IPath shap using Image img = new(width, height); - // Fill the canvas background and draw our shape - img.Mutate(i => i.Fill(Color.DarkBlue).Fill(Color.White.WithAlpha(.25F), shape)); + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + // Fill the canvas background and draw our shape. + canvas.Fill(Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.White.WithAlpha(.25F)), shape); - // Draw our path collection. - img.Mutate(i => i.Fill(Color.HotPink, collection)); + // Draw our path collection. + canvas.Fill(Brushes.Solid(Color.HotPink), collection); + })); // Ensure directory exists string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path))); @@ -282,8 +285,11 @@ public static void SaveImage(this IPath shape, int width, int height, params str public static void SaveImage(this IPathCollection shape, int width, int height, params string[] path) { using Image img = new(width, height); - img.Mutate(i => i.Fill(Color.DarkBlue)); - img.Mutate(i => i.Fill(Color.HotPink, shape)); + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.DarkBlue)); + canvas.Fill(Brushes.Solid(Color.HotPink), shape); + })); // Ensure directory exists string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(path))); diff --git a/samples/DrawShapesWithImageSharp/README.md b/samples/DrawShapesWithImageSharp/README.md new file mode 100644 index 000000000..1d1bd9c82 --- /dev/null +++ b/samples/DrawShapesWithImageSharp/README.md @@ -0,0 +1,21 @@ +# Draw Shapes With ImageSharp + +A sample application that demonstrates the core vector drawing capabilities of ImageSharp.Drawing. Each example renders shapes to an `Image` using the `DrawingCanvas` API and saves the result as a PNG file in the `Output/` directory. + +## What it demonstrates + +- **Stars** — Filled and outlined star polygons with varying point counts, inner/outer radii, line join styles (miter, round, bevel), and dashed outlines with different line caps. +- **Clipping** — Rectangle-on-rectangle clipping using `IPath.Clip()`. +- **Path building** — Constructing complex shapes with `PathBuilder`, including multi-figure paths (a V overlaid with a rectangle) and an hourglass shape. +- **Curves** — Ellipses via `EllipsePolygon` and cubic Bezier arcs via `CubicBezierLineSegment`. +- **Text as paths** — Converting text to vector outlines using `TextBuilder.GeneratePaths()` with system fonts, including text laid out along a curved path. +- **Serialized glyph data** — Rendering OpenSans letter shapes ('a' and 'o') from serialized coordinate data as `ComplexPolygon` instances. +- **Canvas API** — `Fill` for solid backgrounds and shape rendering via `ProcessWithCanvas`. + +## Running + +```bash +dotnet run --project samples/DrawShapesWithImageSharp -c Debug +``` + +Output images are written to the `Output/` directory, organized into subdirectories by category: `Stars/`, `Clipping/`, `Curves/`, `Text/`, `Drawing/`, `Letter/`, and `Issues/`. diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs new file mode 100644 index 000000000..b230d4c5b --- /dev/null +++ b/samples/WebGPUWindowDemo/Program.cs @@ -0,0 +1,465 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using Silk.NET.Core.Native; +using Silk.NET.Maths; +using Silk.NET.WebGPU; +using Silk.NET.Windowing; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.PixelFormats; +using Color = SixLabors.ImageSharp.Color; +using Rectangle = SixLabors.ImageSharp.Rectangle; + +namespace WebGPUWindowDemo; + +/// +/// Demonstrates the ImageSharp.Drawing WebGPU backend rendering directly to a native +/// swap chain surface. Bouncing ellipses and vertically scrolling text are composited +/// each frame using the API backed by a WebGPU +/// compute compositor. +/// +public static unsafe class Program +{ + private const int WindowWidth = 800; + private const int WindowHeight = 600; + private const int BallCount = 500; + + // Silk.NET WebGPU API and windowing handles. + private static WebGPU wgpu; + private static IWindow window; + + // WebGPU device-level handles. + private static Instance* instance; + private static Surface* surface; + private static SurfaceConfiguration surfaceConfiguration; + private static Adapter* adapter; + private static Device* device; + private static Queue* queue; + + // ImageSharp.Drawing backend and configuration. + private static WebGPUDrawingBackend backend; + private static Configuration drawingConfiguration; + + // Bouncing ball simulation state. + private static Ball[] balls; + private static readonly Random Rng = new(42); + + // FPS counter state. + private static int frameCount; + private static double fpsElapsed; + + // Scrolling text state — glyph geometry is built once at startup via TextBuilder + // and translated vertically each frame. Only glyphs whose bounds intersect the + // visible viewport are submitted for rasterization. + private static IPathCollection scrollPaths; + private static float scrollOffset; + private static float scrollTextHeight; + private const string ScrollText = + "ImageSharp.Drawing on WebGPU\n\n" + + "Real-time GPU-accelerated 2D vector graphics " + + "rendered directly to a native swap chain surface.\n\n" + + "The canvas API provides a familiar drawing model: " + + "Fill, Draw, DrawText, Clip, and Transform — " + + "all composited on the GPU via compute shaders.\n\n" + + "Text is shaped once using SixLabors.Fonts and " + + "converted to vector paths via TextBuilder. " + + "Each frame simply translates the cached geometry.\n\n" + + "Shapes are rasterized into coverage masks on the " + + "CPU, uploaded to GPU textures, then composited " + + "using a WebGPU compute pipeline that evaluates " + + "Porter-Duff blending per pixel.\n\n" + + "The drawing backend automatically manages texture " + + "atlases, bind groups, and pipeline state. It falls " + + "back to the CPU backend for unsupported pixel " + + "formats or when no GPU device is available.\n\n" + + "SixLabors ImageSharp.Drawing\n" + + "github.com/SixLabors/ImageSharp.Drawing\n\n" + + "Built with Silk.NET WebGPU bindings.\n" + + "Running on your GPU right now."; + + public static void Main() + { + // Create a window with no built-in graphics API — we manage WebGPU ourselves. + WindowOptions options = WindowOptions.Default; + options.API = GraphicsAPI.None; + options.Size = new Vector2D(WindowWidth, WindowHeight); + options.Title = "ImageSharp.Drawing WebGPU Demo"; + options.ShouldSwapAutomatically = false; + options.IsContextControlDisabled = true; + + window = Window.Create(options); + window.Load += OnLoad; + window.Update += OnUpdate; + window.Render += OnRender; + window.Closing += OnClosing; + window.FramebufferResize += OnFramebufferResize; + + window.Run(); + } + + /// + /// Called once when the window is ready. Bootstraps the WebGPU device, configures + /// the swap chain, initializes the ImageSharp.Drawing backend, pre-builds the + /// scrolling text geometry, and seeds the ball simulation. + /// + private static void OnLoad() + { + // Bootstrap WebGPU: instance → surface → adapter → device → queue. + wgpu = WebGPU.GetApi(); + + InstanceDescriptor instanceDescriptor = default; + instance = wgpu.CreateInstance(&instanceDescriptor); + + surface = window.CreateWebGPUSurface(wgpu, instance); + + // Request an adapter compatible with our window surface. + RequestAdapterOptions adapterOptions = new() + { + CompatibleSurface = surface + }; + + wgpu.InstanceRequestAdapter( + instance, + ref adapterOptions, + new PfnRequestAdapterCallback((_, a, _, _) => adapter = a), + null); + + Console.WriteLine($"Adapter: 0x{(nuint)adapter:X}"); + + // Request a device with Bgra8UnormStorage — required by the compute compositor + // to write storage textures in Bgra8Unorm format (the swap chain format). + FeatureName requiredFeature = FeatureName.Bgra8UnormStorage; + DeviceDescriptor deviceDescriptor = new() + { + DeviceLostCallback = new PfnDeviceLostCallback(DeviceLost), + RequiredFeatureCount = 1, + RequiredFeatures = &requiredFeature, + }; + + wgpu.AdapterRequestDevice( + adapter, + in deviceDescriptor, + new PfnRequestDeviceCallback((_, d, _, _) => device = d), + null); + + wgpu.DeviceSetUncapturedErrorCallback(device, new PfnErrorCallback(UncapturedError), null); + + queue = wgpu.DeviceGetQueue(device); + + Console.WriteLine($"Device: 0x{(nuint)device:X}, Queue: 0x{(nuint)queue:X}"); + + // Configure the swap chain. + ConfigureSwapchain(); + + // Initialize the ImageSharp.Drawing WebGPU backend and attach it to a + // cloned Configuration so it doesn't affect the global default. + backend = new WebGPUDrawingBackend(); + drawingConfiguration = Configuration.Default.Clone(); + drawingConfiguration.SetDrawingBackend(backend); + + // Pre-build scrolling text geometry at the origin. TextBuilder converts the + // shaped text into an IPathCollection of glyph outlines that can be cheaply + // translated each frame without re-shaping or re-building outlines. + Font scrollFont = SystemFonts.CreateFont("Arial", 24); + TextOptions textOptions = new(scrollFont) + { + Origin = new Vector2(WindowWidth / 2f, 0), + WrappingLength = WindowWidth - 80, + HorizontalAlignment = HorizontalAlignment.Center, + LineSpacing = 1.6f, + }; + + scrollPaths = TextBuilder.GeneratePaths(ScrollText, textOptions); + FontRectangle bounds = TextMeasurer.MeasureSize(ScrollText, textOptions); + scrollTextHeight = bounds.Height; + + // Seed the bouncing ball simulation with random positions, velocities, and colors. + balls = new Ball[BallCount]; + for (int i = 0; i < BallCount; i++) + { + balls[i] = Ball.CreateRandom(Rng, WindowWidth, WindowHeight); + } + + Console.WriteLine("WebGPU windowed demo initialized."); + } + + /// + /// Configures (or reconfigures) the swap chain for the current framebuffer size. + /// Uses Bgra8Unorm to match the canvas pixel format. + /// CopyDst is required because the compositor copies from a transient output texture + /// into the swap chain target. TextureBinding is needed for backdrop sampling. + /// + private static void ConfigureSwapchain() + { + surfaceConfiguration = new SurfaceConfiguration + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopyDst | TextureUsage.TextureBinding, + Format = TextureFormat.Bgra8Unorm, + PresentMode = PresentMode.Fifo, + Device = device, + Width = (uint)window.FramebufferSize.X, + Height = (uint)window.FramebufferSize.Y, + }; + + wgpu.SurfaceConfigure(surface, ref surfaceConfiguration); + } + + /// + /// Reconfigures the swap chain when the window is resized. + /// + private static void OnFramebufferResize(Vector2D size) + { + if (size.X > 0 && size.Y > 0) + { + ConfigureSwapchain(); + } + } + + /// + /// Fixed-timestep update: advances ball positions and the scroll offset. + /// + private static void OnUpdate(double deltaTime) + { + int w = window.FramebufferSize.X; + int h = window.FramebufferSize.Y; + float dt = (float)deltaTime; + + // Integrate ball positions and bounce off walls. + for (int i = 0; i < balls.Length; i++) + { + balls[i].Update(dt, w, h); + } + + // Advance scrolling text vertically (pixels per second). + scrollOffset += 200f * dt; + } + + /// + /// Per-frame render callback. Acquires a swap chain texture, wraps it as a + /// , creates a , + /// draws all content, flushes the GPU composition, and presents. + /// + private static void OnRender(double deltaTime) + { + int w = window.FramebufferSize.X; + int h = window.FramebufferSize.Y; + if (w <= 0 || h <= 0) + { + return; + } + + // Acquire the next swap chain texture from the surface. + SurfaceTexture surfaceTexture; + wgpu.SurfaceGetCurrentTexture(surface, &surfaceTexture); + switch (surfaceTexture.Status) + { + case SurfaceGetCurrentTextureStatus.Timeout: + case SurfaceGetCurrentTextureStatus.Outdated: + case SurfaceGetCurrentTextureStatus.Lost: + wgpu.TextureRelease(surfaceTexture.Texture); + ConfigureSwapchain(); + return; + case SurfaceGetCurrentTextureStatus.OutOfMemory: + case SurfaceGetCurrentTextureStatus.DeviceLost: + throw new InvalidOperationException($"Surface texture error: {surfaceTexture.Status}"); + } + + TextureView* textureView = wgpu.TextureCreateView(surfaceTexture.Texture, null); + + try + { + // Wrap the swap chain texture as a NativeSurface so the drawing backend + // can composite directly into it. The format must match the swap chain + // configuration (Bgra8Unorm) and the canvas pixel type (Bgra32). + NativeSurface nativeSurface = WebGPUNativeSurfaceFactory.Create( + (nint)device, + (nint)queue, + (nint)surfaceTexture.Texture, + (nint)textureView, + WebGPUTextureFormatId.Bgra8Unorm, + w, + h); + + // NativeCanvasFrame exposes only the GPU surface (no CPU region), + // so the backend always takes the GPU composition path. + NativeCanvasFrame frame = new(new Rectangle(0, 0, w, h), nativeSurface); + + // Create a drawing canvas targeting the swap chain frame. + using DrawingCanvas canvas = new(drawingConfiguration, frame, new DrawingOptions()); + + // Clear to a dark background. + canvas.Fill(Brushes.Solid(Color.FromPixel(new Bgra32(30, 30, 40, 255)))); + + // Draw vertically scrolling text behind the balls. + DrawScrollingText(canvas, w, h); + + // Draw each ball as a filled ellipse. + for (int i = 0; i < balls.Length; i++) + { + ref Ball ball = ref balls[i]; + EllipsePolygon ellipse = new(ball.X, ball.Y, ball.Radius); + canvas.Fill(Brushes.Solid(ball.Color), ellipse); + } + + // Flush submits all queued draw operations to the GPU compositor and + // copies the composited result into the swap chain texture. + canvas.Flush(); + } + finally + { + // Present the frame and release per-frame WebGPU resources. + wgpu.SurfacePresent(surface); + wgpu.TextureViewRelease(textureView); + wgpu.TextureRelease(surfaceTexture.Texture); + } + + // Update FPS counter in the window title once per second. + frameCount++; + fpsElapsed += deltaTime; + if (fpsElapsed >= 1.0) + { + string flushMode = backend.DiagnosticLastFlushUsedGPU ? "GPU" : "CPU"; + window.Title = $"ImageSharp.Drawing WebGPU Demo - {frameCount / fpsElapsed:F1} FPS | Flush: {flushMode} | Failure: {backend.DiagnosticLastSceneFailure}"; + frameCount = 0; + fpsElapsed = 0; + } + } + + /// + /// Draws the pre-built scrolling text geometry. The full text block scrolls upward + /// and loops when it passes above the window. Each glyph path is bounds-tested + /// against the viewport so only visible glyphs are rasterized. + /// + private static void DrawScrollingText(DrawingCanvas canvas, int w, int h) + { + if (scrollTextHeight <= 0) + { + return; + } + + // Total cycle: text enters from the bottom, scrolls up, exits the top, then loops. + float totalCycle = h + scrollTextHeight; + float wrappedOffset = scrollOffset % totalCycle; + float y = h - wrappedOffset; + + Matrix3x2 translation = Matrix3x2.CreateTranslation(0, y); + RectangleF viewport = new(0, 0, w, h); + Brush textBrush = Brushes.Solid(Color.FromPixel(new Bgra32(70, 70, 100, 255))); + + // Each IPath in scrollPaths is one glyph outline. Skip any whose translated + // bounding box doesn't intersect the visible area. + foreach (IPath path in scrollPaths) + { + RectangleF pathBounds = path.Bounds; + RectangleF translated = new( + pathBounds.X + translation.M31, + pathBounds.Y + translation.M32, + pathBounds.Width, + pathBounds.Height); + + if (!viewport.IntersectsWith(translated)) + { + continue; + } + + canvas.Fill(textBrush, path.Transform(new Matrix4x4(translation))); + } + } + + /// + /// Tears down the drawing backend and releases all WebGPU resources in reverse + /// creation order. + /// + private static void OnClosing() + { + backend.Dispose(); + + wgpu.DeviceRelease(device); + wgpu.AdapterRelease(adapter); + wgpu.SurfaceRelease(surface); + wgpu.InstanceRelease(instance); + wgpu.Dispose(); + } + + /// WebGPU device-lost callback — logs the reason to the console. + private static void DeviceLost(DeviceLostReason reason, byte* message, void* userData) + => Console.WriteLine($"Device lost ({reason}): {SilkMarshal.PtrToString((nint)message)}"); + + /// WebGPU uncaptured error callback — logs validation errors to the console. + private static void UncapturedError(ErrorType type, byte* message, void* userData) + => Console.WriteLine($"WebGPU {type}: {SilkMarshal.PtrToString((nint)message)}"); + + /// + /// A simple bouncing ball with position, velocity, radius, and color. + /// Reflects off the window edges each frame. + /// + private struct Ball + { + public float X; + public float Y; + public float VelocityX; + public float VelocityY; + public float Radius; + public Color Color; + + /// + /// Creates a ball with a random position inside the window bounds, a random + /// velocity between 100-300 px/s in each axis, a random radius between 20-60 px, + /// and a random semi-transparent color. + /// + public static Ball CreateRandom(Random rng, int width, int height) + { + float radius = 20f + (rng.NextSingle() * 40f); + return new Ball + { + X = radius + (rng.NextSingle() * (width - (2 * radius))), + Y = radius + (rng.NextSingle() * (height - (2 * radius))), + VelocityX = (100f + (rng.NextSingle() * 200f)) * (rng.Next(2) == 0 ? -1 : 1), + VelocityY = (100f + (rng.NextSingle() * 200f)) * (rng.Next(2) == 0 ? -1 : 1), + Radius = radius, + Color = Color.FromPixel(new Bgra32( + (byte)(80 + rng.Next(176)), + (byte)(80 + rng.Next(176)), + (byte)(80 + rng.Next(176)), + 200)), + }; + } + + /// + /// Integrates position by velocity and reflects off the window edges. + /// + public void Update(float dt, int width, int height) + { + this.X += this.VelocityX * dt; + this.Y += this.VelocityY * dt; + + if (this.X - this.Radius < 0) + { + this.X = this.Radius; + this.VelocityX = MathF.Abs(this.VelocityX); + } + else if (this.X + this.Radius > width) + { + this.X = width - this.Radius; + this.VelocityX = -MathF.Abs(this.VelocityX); + } + + if (this.Y - this.Radius < 0) + { + this.Y = this.Radius; + this.VelocityY = MathF.Abs(this.VelocityY); + } + else if (this.Y + this.Radius > height) + { + this.Y = height - this.Radius; + this.VelocityY = -MathF.Abs(this.VelocityY); + } + } + } +} diff --git a/samples/WebGPUWindowDemo/README.md b/samples/WebGPUWindowDemo/README.md new file mode 100644 index 000000000..a49b25c68 --- /dev/null +++ b/samples/WebGPUWindowDemo/README.md @@ -0,0 +1,46 @@ +# WebGPU Window Demo + +A real-time sample application that renders directly to a native window swap chain using the ImageSharp.Drawing WebGPU backend. Bouncing ellipses and vertically scrolling text are composited each frame via the `DrawingCanvas` API, with all composition performed by a WebGPU compute pipeline. + +## What it demonstrates + +- **Native surface rendering** — The swap chain texture is wrapped as a `NativeSurface` and passed to the canvas via `ICanvasFrame`. The backend composites directly into the swap chain target without CPU readback. +- **WebGPU bootstrap** — Full Silk.NET WebGPU initialization: instance → surface → adapter → device → queue, with `Bgra8UnormStorage` requested for compute storage writes to `Bgra8Unorm` textures. +- **Pre-built text geometry** — `TextBuilder.GeneratePaths` shapes the text once at startup. Each frame translates the cached `IPathCollection` with a `Matrix3x2` — no re-shaping or re-building of glyph outlines. +- **Viewport culling** — Only glyph paths whose translated bounding boxes intersect the visible window are submitted for rasterization, keeping frame times low even with large text blocks. +- **Canvas API** — `Fill` for solid backgrounds, `Fill(IPath, Brush)` for ellipses and glyph outlines, `Flush` to submit all queued operations to the GPU compositor. + +## Prerequisites + +- .NET 8.0 SDK or later +- A GPU with Vulkan, Metal, or D3D12 support +- The adapter must support the `Bgra8UnormStorage` feature (most desktop GPUs do) + +## Running + +```bash +dotnet run --project samples/WebGPUWindowDemo -c Debug +``` + +An 800×600 window opens showing colored ellipses bouncing off the walls with scrolling descriptive text in the background. The window title displays the current FPS. + +## Architecture + +Each frame follows this sequence: + +1. `SurfaceGetCurrentTexture` — acquire the next swap chain texture +2. `WebGPUNativeSurfaceFactory.Create(...)` — wrap it as a `NativeSurface` +3. `NativeSurfaceOnlyFrame` — wrap as `ICanvasFrame` (GPU path only, no CPU region) +4. `new DrawingCanvas(config, frame, options)` — create the canvas +5. `canvas.Fill(...)` — queue background, text glyphs, and ellipses +6. `canvas.Flush()` — rasterize coverage masks on CPU, upload to GPU, composite via compute shader, copy result to swap chain texture +7. `SurfacePresent()` — present the frame + +## Performance + +| Scenario | FPS (typical) | +|---|---| +| Balls only (no text) | ~120 | +| Balls + scrolling text | 65–120 | + +FPS varies with how much text is currently visible in the viewport. diff --git a/samples/WebGPUWindowDemo/WebGPUWindowDemo.csproj b/samples/WebGPUWindowDemo/WebGPUWindowDemo.csproj new file mode 100644 index 000000000..7869e86da --- /dev/null +++ b/samples/WebGPUWindowDemo/WebGPUWindowDemo.csproj @@ -0,0 +1,36 @@ + + + + portable + Exe + true + + + + + + net8.0;net10.0 + + + + + net8.0 + + + + + + + + + + + + + + + + diff --git a/src/ImageSharp.Drawing.WebGPU.ShaderGen/AnalyzerReleases.Shipped.md b/src/ImageSharp.Drawing.WebGPU.ShaderGen/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..60b59dd99 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU.ShaderGen/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/ImageSharp.Drawing.WebGPU.ShaderGen/AnalyzerReleases.Unshipped.md b/src/ImageSharp.Drawing.WebGPU.ShaderGen/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..cbea0e8fd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU.ShaderGen/AnalyzerReleases.Unshipped.md @@ -0,0 +1,10 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +WGSLGEN001 | WGSL | Error | WgslSourceGenerator +WGSLGEN002 | WGSL | Error | WgslSourceGenerator +WGSLGEN003 | WGSL | Error | WgslSourceGenerator \ No newline at end of file diff --git a/src/ImageSharp.Drawing.WebGPU.ShaderGen/ImageSharp.Drawing.WebGPU.ShaderGen.csproj b/src/ImageSharp.Drawing.WebGPU.ShaderGen/ImageSharp.Drawing.WebGPU.ShaderGen.csproj new file mode 100644 index 000000000..5c19baf09 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU.ShaderGen/ImageSharp.Drawing.WebGPU.ShaderGen.csproj @@ -0,0 +1,22 @@ + + + + Library + netstandard2.0 + enable + latest + disable + false + false + false + false + false + $(NoWarn);RS1036;SA1513;SA1600;SA1633 + false + + + + + + + diff --git a/src/ImageSharp.Drawing.WebGPU.ShaderGen/WgslSourceGenerator.cs b/src/ImageSharp.Drawing.WebGPU.ShaderGen/WgslSourceGenerator.cs new file mode 100644 index 000000000..117d61489 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU.ShaderGen/WgslSourceGenerator.cs @@ -0,0 +1,457 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SixLabors.ImageSharp.Drawing.WebGPU.ShaderGen; + +/// +/// Expands the WGSL shader source tree at compile time and emits a generated C# container +/// with one fully inlined source payload per root shader file. +/// +/// +/// +/// Root shaders are every .wgsl file directly under the Shaders tree except files +/// under Shaders/Shared. Shared files are only imported into roots. +/// +/// +/// The generator intentionally supports a very small preprocessor surface: +/// #import, #ifdef, #ifndef, #else, and #endif. +/// That keeps the generated shader text predictable and close to the source WGSL layout. +/// +/// +[Generator] +public sealed class WgslSourceGenerator : IIncrementalGenerator +{ + private static readonly DiagnosticDescriptor MissingShaderPath = new( + id: "WGSLGEN001", + title: "Missing shader import", + messageFormat: "Could not resolve WGSL import '{0}' referenced from '{1}'", + category: "WGSL", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor CyclicImportPath = new( + id: "WGSLGEN002", + title: "Cyclic shader import", + messageFormat: "Detected a cyclic WGSL import involving '{0}'", + category: "WGSL", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor UnsupportedDirective = new( + id: "WGSLGEN003", + title: "Unsupported WGSL preprocessor directive", + messageFormat: "Unsupported WGSL preprocessor directive '{0}' in '{1}'", + category: "WGSL", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + /// + /// Registers the WGSL inputs and wires them into a single + /// source-emission step. + /// + /// The Roslyn initialization context used to register incremental inputs and outputs. + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValueProvider> shaderFiles = context.AdditionalTextsProvider + .Where(static file => file.Path.EndsWith(".wgsl", StringComparison.OrdinalIgnoreCase)) + .Select((file, cancellationToken) => new ShaderFile( + GetRelativeShaderPath(file.Path), + file.GetText(cancellationToken)?.ToString() ?? string.Empty)) + .Where(static file => file.RelativePath is not null) + .Select(static (file, _) => file) + .Collect(); + + context.RegisterSourceOutput(shaderFiles, static (productionContext, files) => Execute(productionContext, files)); + } + + // Keep exception handling here so the compiler gets a diagnostic instead of failing with + // an opaque analyzer crash. + private static void Execute(SourceProductionContext context, ImmutableArray files) + { + try + { + Dictionary fileMap = files + .Where(static file => file.RelativePath is not null) + .ToDictionary(static file => NormalizePath(file.RelativePath!), static file => file.Text, StringComparer.Ordinal); + + string source = GenerateSource(context, fileMap); + context.AddSource("GeneratedWgslShaderSources.g.cs", SourceText.From(source, Encoding.UTF8)); + } + catch (Exception ex) + { + DiagnosticDescriptor descriptor = new( + id: "WGSLGEN999", + title: "WGSL generation failed", + messageFormat: ex.ToString(), + category: "WGSL", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None)); + } + } + + // Generates the single C# container that the runtime shader wrappers reference. Each + // property exposes both the expanded WGSL text and a null-terminated UTF-8 payload. + private static string GenerateSource(SourceProductionContext context, Dictionary fileMap) + { + IEnumerable rootFiles = fileMap.Keys + .Where(static path => !path.StartsWith("Shared/", StringComparison.Ordinal)) + .OrderBy(static path => path, StringComparer.Ordinal); + + StringBuilder builder = new(); + _ = builder.AppendLine("// "); + _ = builder.AppendLine("using System;"); + _ = builder.AppendLine(); + _ = builder.AppendLine("namespace SixLabors.ImageSharp.Drawing.Processing.Backends;"); + _ = builder.AppendLine(); + _ = builder.AppendLine("internal static class GeneratedWgslShaderSources"); + _ = builder.AppendLine("{"); + + foreach (string rootFile in rootFiles) + { + foreach (ShaderVariant variant in GetVariants(rootFile)) + { + string propertyName = variant.PropertySuffix.Length == 0 + ? ToPropertyName(rootFile) + : ToPropertyName(rootFile) + variant.PropertySuffix; + string resolved = ResolveShader( + context, + fileMap, + rootFile, + new HashSet(StringComparer.Ordinal), + rootFile, + variant.Defines); + + string textDelimiter = GetRawStringDelimiter(resolved); + _ = builder.Append(" public static string ").Append(propertyName).AppendLine("Text =>"); + _ = builder.Append(" ").Append(textDelimiter).AppendLine(); + AppendIndentedRawStringContent(builder, resolved, " "); + _ = builder.Append(" ").Append(textDelimiter).AppendLine(";"); + _ = builder.AppendLine(); + + string byteDelimiter = GetRawStringDelimiter(resolved); + _ = builder.Append(" private static readonly byte[] ").Append(propertyName).AppendLine("CodeBytes ="); + _ = builder.AppendLine(" ["); + _ = builder.Append(" .. ").Append(byteDelimiter).AppendLine(); + AppendIndentedRawStringContent(builder, resolved, " "); + _ = builder.Append(" ").Append(byteDelimiter).AppendLine("u8,"); + _ = builder.AppendLine(" 0,"); + _ = builder.AppendLine(" ];"); + _ = builder.AppendLine(); + _ = builder.Append(" public static ReadOnlySpan ").Append(propertyName).Append("Code => ").Append(propertyName).AppendLine("CodeBytes;"); + _ = builder.AppendLine(); + } + } + + _ = builder.AppendLine("}"); + return builder.ToString(); + } + + // Recursively expands imports while interpreting the tiny conditional subset used by the + // upstream shader tree. Unsupported directives are surfaced as generator diagnostics. + private static string ResolveShader( + SourceProductionContext context, + Dictionary fileMap, + string path, + HashSet includeStack, + string rootPath, + HashSet defines) + { + if (!includeStack.Add(path)) + { + context.ReportDiagnostic(Diagnostic.Create(CyclicImportPath, Location.None, path)); + return string.Empty; + } + + try + { + if (!fileMap.TryGetValue(path, out string? text)) + { + context.ReportDiagnostic(Diagnostic.Create(MissingShaderPath, Location.None, path, rootPath)); + return string.Empty; + } + + StringBuilder builder = new(); + Stack conditionalFrames = new(); + bool isActive = true; + foreach (string line in EnumerateLines(text)) + { + string trimmed = line.Trim(); + if (trimmed.StartsWith("#import ", StringComparison.Ordinal)) + { + if (isActive) + { + string importName = trimmed.Substring("#import ".Length).Trim(); + string importPath = ResolveImportPath(fileMap, path, importName); + if (importPath.Length == 0) + { + context.ReportDiagnostic(Diagnostic.Create(MissingShaderPath, Location.None, importName, path)); + } + else + { + _ = builder.Append(ResolveShader(context, fileMap, importPath, includeStack, rootPath, defines)); + } + } + + continue; + } + + if (trimmed.StartsWith("#ifdef ", StringComparison.Ordinal)) + { + string symbol = trimmed.Substring("#ifdef ".Length).Trim(); + bool condition = defines.Contains(symbol); + conditionalFrames.Push(new ConditionalFrame(isActive, condition)); + isActive = isActive && condition; + continue; + } + + if (trimmed.StartsWith("#ifndef ", StringComparison.Ordinal)) + { + string symbol = trimmed.Substring("#ifndef ".Length).Trim(); + bool condition = !defines.Contains(symbol); + conditionalFrames.Push(new ConditionalFrame(isActive, condition)); + isActive = isActive && condition; + continue; + } + + if (trimmed.Equals("#else", StringComparison.Ordinal)) + { + if (conditionalFrames.Count == 0) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedDirective, Location.None, trimmed, path)); + continue; + } + + ConditionalFrame frame = conditionalFrames.Pop(); + conditionalFrames.Push(frame); + isActive = frame.ParentActive && !frame.Condition; + continue; + } + + if (trimmed.StartsWith("#endif", StringComparison.Ordinal)) + { + if (conditionalFrames.Count == 0) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedDirective, Location.None, trimmed, path)); + continue; + } + + ConditionalFrame frame = conditionalFrames.Pop(); + isActive = frame.ParentActive; + continue; + } + + if (trimmed.StartsWith("#", StringComparison.Ordinal)) + { + context.ReportDiagnostic(Diagnostic.Create(UnsupportedDirective, Location.None, trimmed, path)); + continue; + } + + if (isActive) + { + _ = builder.Append(line); + _ = builder.Append('\n'); + } + } + + return builder.ToString(); + } + finally + { + _ = includeStack.Remove(path); + } + } + + // Imports first probe the current shader directory, then fall back to Shared/ for the common + // include blocks used across stages. + private static string ResolveImportPath(Dictionary fileMap, string currentPath, string importName) + { + string currentDirectory = currentPath.IndexOf('/') >= 0 + ? currentPath.Substring(0, currentPath.LastIndexOf('/')) + : string.Empty; + + string localCandidate = currentDirectory.Length == 0 + ? $"{importName}.wgsl" + : $"{currentDirectory}/{importName}.wgsl"; + if (fileMap.ContainsKey(localCandidate)) + { + return localCandidate; + } + + string sharedCandidate = $"Shared/{importName}.wgsl"; + return fileMap.ContainsKey(sharedCandidate) ? sharedCandidate : string.Empty; + } + + // Generates root variants for the few upstream shaders that are compiled with + // different preprocessor symbols. + private static IEnumerable GetVariants(string rootFile) + { + yield return new ShaderVariant(string.Empty, new HashSet(StringComparer.Ordinal)); + + if (string.Equals(rootFile, "pathtag_scan.wgsl", StringComparison.Ordinal)) + { + yield return new ShaderVariant("Small", new HashSet(StringComparer.Ordinal) { "small" }); + } + } + + // Enumerates lines without allocating a full split array, since generators run in the compiler + // and should avoid unnecessary per-build churn. + private static IEnumerable EnumerateLines(string text) + { + int start = 0; + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '\r') + { + yield return text.Substring(start, i - start); + if ((i + 1) < text.Length && text[i + 1] == '\n') + { + i++; + } + + start = i + 1; + } + else if (text[i] == '\n') + { + yield return text.Substring(start, i - start); + start = i + 1; + } + } + + if (start < text.Length) + { + yield return text.Substring(start); + } + } + + // Raw string literals must preserve the original shader text exactly while also satisfying the + // indentation rules of generated C# source. + private static void AppendIndentedRawStringContent(StringBuilder builder, string content, string indent) + { + foreach (string line in EnumerateLines(content)) + { + _ = builder.Append(indent); + _ = builder.Append(line); + _ = builder.Append('\n'); + } + + if (content.Length == 0 || content[content.Length - 1] != '\n') + { + _ = builder.Append(indent); + _ = builder.Append('\n'); + } + } + + // Choose a delimiter width that is guaranteed not to collide with the shader contents. + private static string GetRawStringDelimiter(string text) + { + int maxQuotes = 0; + int current = 0; + foreach (char ch in text) + { + if (ch == '"') + { + current++; + maxQuotes = Math.Max(maxQuotes, current); + } + else + { + current = 0; + } + } + + return new string('"', Math.Max(3, maxQuotes + 1)); + } + + // Converts file names such as "path_count_setup.wgsl" into C# property names like + // "PathCountSetup". + private static string ToPropertyName(string path) + { + string fileName = path.EndsWith(".wgsl", StringComparison.OrdinalIgnoreCase) + ? path.Substring(0, path.Length - 5) + : path; + + string[] segments = fileName.Split(['/', '_', '-'], StringSplitOptions.RemoveEmptyEntries); + StringBuilder builder = new(); + foreach (string segment in segments) + { + _ = builder.Append(char.ToUpperInvariant(segment[0])); + if (segment.Length > 1) + { + _ = builder.Append(segment.Substring(1)); + } + } + + return builder.ToString(); + } + + // AdditionalFiles come through as full paths. The generated API is keyed by the relative + // path beneath the WebGPU Shaders tree so imports remain stable across machines. + private static string? GetRelativeShaderPath(string path) + { + const string marker = "\\Shaders\\"; + int index = path.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase); + if (index < 0) + { + return null; + } + + return NormalizePath(path.Substring(index + marker.Length)); + } + + private static string NormalizePath(string path) + => path.Replace('\\', '/'); + + // Small immutable transport used while collecting AdditionalFiles into a dictionary keyed by + // shader-relative path. + private readonly struct ShaderFile + { + public ShaderFile(string? relativePath, string text) + { + this.RelativePath = relativePath; + this.Text = text; + } + + public string? RelativePath { get; } + + public string Text { get; } + } + + // Tracks the active state of nested conditional blocks while expanding the supported WGSL + // directive subset. + private readonly struct ConditionalFrame + { + public ConditionalFrame(bool parentActive, bool condition) + { + this.ParentActive = parentActive; + this.Condition = condition; + } + + public bool ParentActive { get; } + + public bool Condition { get; } + } + + // One generated source variant for a root shader and the symbol set that should be treated + // as defined while resolving its conditional blocks. + private readonly struct ShaderVariant + { + public ShaderVariant(string propertySuffix, HashSet defines) + { + this.PropertySuffix = propertySuffix; + this.Defines = defines; + } + + public string PropertySuffix { get; } + + public HashSet Defines { get; } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj new file mode 100644 index 000000000..25aaef8e3 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -0,0 +1,88 @@ + + + + SixLabors.ImageSharp.Drawing.WebGPU + SixLabors.ImageSharp.Drawing.WebGPU + SixLabors.ImageSharp.Drawing.Processing.Backends + SixLabors.ImageSharp.Drawing.WebGPU + sixlabors.imagesharp.drawing.128.png + LICENSE + https://github.com/SixLabors/ImageSharp.Drawing/ + $(RepositoryUrl) + Image Draw Shape Path Font + An extension to ImageSharp that allows the drawing of images, paths, and text. + Debug;Release + + Exe + true + true + + + false + + + $(WarningsNotAsErrors);8002;NU1900 + + + + + 1.0 + + + + + enable + Nullable + + + + + + net8.0;net10.0 + + + + + net8.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/Program.cs b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/Program.cs new file mode 100644 index 000000000..d91a0b881 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Entry point for child processes spawned by . +/// Dispatches to the requested probe method by name. +/// Adapted from Microsoft.DotNet.RemoteExecutor (MIT license). +/// +internal static class Program +{ + private static int Main(string[] args) + { + if (args.Length < 1) + { + Console.Error.WriteLine("Usage: {0} methodName", typeof(Program).Assembly.GetName().Name); + return -1; + } + + string methodName = args[0]; + + return methodName switch + { + nameof(WebGPUDrawingBackend.ProbeComputePipelineSupport) => WebGPUDrawingBackend.ProbeComputePipelineSupport(), + _ => -1 + }; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs new file mode 100644 index 000000000..f8749dee5 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs @@ -0,0 +1,159 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using IOPath = System.IO.Path; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Minimal remote executor that invokes a named method in a child process. +/// The child process entry point () dispatches to +/// the requested method by name — no reflection is used. +/// Adapted from Microsoft.DotNet.RemoteExecutor (MIT license). +/// +internal static class RemoteExecutor +{ + private static readonly string? AssemblyPath; + private static readonly string? HostRunner; + private static readonly string? RuntimeConfigPath; + private static readonly string? DepsJsonPath; + + static RemoteExecutor() + { + if (!IsSupported) + { + return; + } + + string? processFileName = Process.GetCurrentProcess().MainModule?.FileName; + if (processFileName is null) + { + return; + } + + string baseDir = AppContext.BaseDirectory; + string assemblyName = typeof(RemoteExecutor).Assembly.GetName().Name!; + AssemblyPath = IOPath.Combine(baseDir, assemblyName + ".dll"); + if (!File.Exists(AssemblyPath)) + { + return; + } + + HostRunner = processFileName; + string hostName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; + + if (!IOPath.GetFileName(HostRunner).Equals(hostName, StringComparison.OrdinalIgnoreCase)) + { + // Walk up from the runtime directory to find the dotnet host executable. + // The runtime directory is typically: + // /shared/Microsoft.NETCore.App// + // so dotnet.exe is 3–4 levels up depending on trailing separator. + string? directory = RuntimeEnvironment.GetRuntimeDirectory(); + for (int i = 0; i < 4 && directory is not null; i++) + { + directory = IOPath.GetDirectoryName(directory); + if (directory is not null) + { + string dotnetExe = IOPath.Combine(directory, hostName); + if (File.Exists(dotnetExe)) + { + HostRunner = dotnetExe; + break; + } + } + } + } + + string runtimeConfigCandidate = IOPath.Combine(baseDir, assemblyName + ".runtimeconfig.json"); + string depsJsonCandidate = IOPath.Combine(baseDir, assemblyName + ".deps.json"); + + RuntimeConfigPath = File.Exists(runtimeConfigCandidate) ? runtimeConfigCandidate : null; + DepsJsonPath = File.Exists(depsJsonCandidate) ? depsJsonCandidate : null; + } + + /// + /// Gets a value indicating whether this remote executor is supported on the current platform. + /// + internal static bool IsSupported { get; } = + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("WASI")) && + Environment.GetEnvironmentVariable("DOTNET_REMOTEEXECUTOR_SUPPORTED") != "0"; + + /// + /// Invokes the specified static method in a child process and returns its exit code. + /// The method name is dispatched by via a switch statement, + /// so no reflection is needed in the child process. + /// + /// A static method returning (the exit code). + /// Maximum time to wait for the child process. + /// The exit code from the child process, or -1 on failure. + internal static int Invoke(Func method, int timeoutMilliseconds = 30_000) + { + if (!IsSupported || AssemblyPath is null || HostRunner is null) + { + return -1; + } + + string methodName = method.Method.Name; + + string args = "exec"; + if (RuntimeConfigPath is not null) + { + args += $" --runtimeconfig \"{RuntimeConfigPath}\""; + } + + if (DepsJsonPath is not null) + { + args += $" --depsfile \"{DepsJsonPath}\""; + } + + args += $" \"{AssemblyPath}\" \"{methodName}\""; + + ProcessStartInfo psi = new() + { + FileName = HostRunner, + Arguments = args, + UseShellExecute = false, + CreateNoWindow = true + }; + + // Remove profiler environment variables from child process. + psi.Environment.Remove("Cor_Profiler"); + psi.Environment.Remove("Cor_Enable_Profiling"); + psi.Environment.Remove("CoreClr_Profiler"); + psi.Environment.Remove("CoreClr_Enable_Profiling"); + + try + { + using Process? process = Process.Start(psi); + if (process is null) + { + return -1; + } + + if (!process.WaitForExit(timeoutMilliseconds)) + { + try + { + process.Kill(); + } + catch + { + // Ignore cleanup errors. + } + + return -1; + } + + return process.ExitCode; + } + catch + { + return -1; + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs new file mode 100644 index 000000000..0b34204b5 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that prefix-sums dynamically allocated per-path tile backdrops. +/// +internal static unsafe class BackdropComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.BackdropDynCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint heightInTiles) + => heightInTiles; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU backdrop bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs new file mode 100644 index 000000000..ccce5f6af --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class BboxClearComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.BboxClearCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint pathCount) + => (pathCount + 255U) / 256U; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the bbox-clear bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BinningComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BinningComputeShader.cs new file mode 100644 index 000000000..418154e67 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BinningComputeShader.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that bins draw objects into 16x16 tile bins using Vello's bitmap-compaction structure. +/// +internal static unsafe class BinningComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.BinningCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint drawObjectCount) + => (drawObjectCount + 255U) / 256U; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[8]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, BufferBindingType.Storage); + entries[7] = SceneShaderBindingLayoutHelper.CreateStorageEntry(7, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 8, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU scene binning bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ClipLeafComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipLeafComputeShader.cs new file mode 100644 index 000000000..22275997a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipLeafComputeShader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class ClipLeafComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.ClipLeafCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.ReadOnlyStorage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 7, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the clip-leaf bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ClipReduceComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipReduceComputeShader.cs new file mode 100644 index 000000000..76ddc3441 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipReduceComputeShader.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class ClipReduceComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.ClipReduceCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.ReadOnlyStorage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the clip-reduce bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoarseComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoarseComputeShader.cs new file mode 100644 index 000000000..0d192124f --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CoarseComputeShader.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that emits per-tile PTCL from bin-compacted draw-object work, matching Vello's coarse stage shape. +/// +internal static unsafe class CoarseComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.CoarseCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint widthInBins) => widthInBins; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchY(uint heightInBins) => heightInBins; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[9]; + entries[0] = CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage, 0); + entries[2] = CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage, 0); + entries[3] = CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage, 0); + entries[4] = CreateStorageEntry(4, BufferBindingType.ReadOnlyStorage, 0); + entries[5] = CreateStorageEntry(5, BufferBindingType.ReadOnlyStorage, 0); + entries[6] = CreateStorageEntry(6, BufferBindingType.Storage, 0); + entries[7] = CreateStorageEntry(7, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[8] = CreateStorageEntry(8, BufferBindingType.Storage, 0); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 9, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU coarse bind-group layout."; + return false; + } + + error = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BindGroupLayoutEntry CreateStorageEntry(uint binding, BufferBindingType type, nuint minBindingSize) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = type, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs new file mode 100644 index 000000000..31023053e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs @@ -0,0 +1,276 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU compute shader that composites a source layer texture onto a destination texture +/// using configurable blend mode, alpha composition mode, and opacity. +/// +internal static class ComposeLayerComputeShader +{ + private static readonly object CacheSync = new(); + private static readonly Dictionary ShaderCache = []; + + private static readonly string ShaderTemplate = + """ + struct LayerConfig { + source_width: u32, + source_height: u32, + dest_offset_x: i32, + dest_offset_y: i32, + color_blend_mode: u32, + alpha_composition_mode: u32, + blend_percentage: u32, + _padding: u32, + }; + + @group(0) @binding(0) var source_texture: texture_2d<__TEXEL_TYPE__>; + @group(0) @binding(1) var backdrop_texture: texture_2d<__TEXEL_TYPE__>; + @group(0) @binding(2) var output_texture: texture_storage_2d<__OUTPUT_FORMAT__, write>; + @group(0) @binding(3) var config: LayerConfig; + + __DECODE_TEXEL_FUNCTION__ + + __ENCODE_OUTPUT_FUNCTION__ + + __BLEND_AND_COMPOSE__ + + @compute @workgroup_size(16, 16, 1) + fn main(@builtin(global_invocation_id) gid: vec3) { + // Output coordinates are in local output-texture space. + let out_x = i32(gid.x); + let out_y = i32(gid.y); + + // Destination coordinates map into the full backdrop texture. + let dest_x = out_x + config.dest_offset_x; + let dest_y = out_y + config.dest_offset_y; + + let dest_dims = textureDimensions(backdrop_texture); + if (dest_x < 0 || dest_y < 0 || u32(dest_x) >= dest_dims.x || u32(dest_y) >= dest_dims.y) { + return; + } + + let src_x = out_x; + let src_y = out_y; + if (u32(src_x) >= config.source_width || u32(src_y) >= config.source_height) { + // Outside layer bounds — pass through the backdrop. + let backdrop_raw = decode_texel(__LOAD_BACKDROP__); + let backdrop = vec4(backdrop_raw.rgb * backdrop_raw.a, backdrop_raw.a); + let alpha = backdrop.a; + let rgb = unpremultiply(backdrop.rgb, alpha); + __STORE_OUTPUT__ + return; + } + + let backdrop_raw = decode_texel(__LOAD_BACKDROP__); + let backdrop = vec4(backdrop_raw.rgb * backdrop_raw.a, backdrop_raw.a); + let source_raw = decode_texel(__LOAD_SOURCE__); + + // Apply layer opacity. + let opacity = bitcast(config.blend_percentage); + let source = vec4(source_raw.rgb, source_raw.a * opacity); + + let result = compose_pixel(backdrop, source, config.color_blend_mode, config.alpha_composition_mode); + let alpha = result.a; + let rgb = unpremultiply(result.rgb, alpha); + __STORE_OUTPUT__ + } + """; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + /// + /// Gets the null-terminated WGSL source for the layer composite shader variant. + /// + public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out string? error) + { + if (!WebGPUDrawingBackend.TryGetCompositeTextureShaderTraits(textureFormat, out _)) + { + code = []; + error = $"Layer composite shader does not support texture format '{textureFormat}'."; + return false; + } + + lock (CacheSync) + { + if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode)) + { + code = cachedCode; + error = null; + return true; + } + + LayerShaderTraits traits = GetTraits(textureFormat); + string source = ShaderTemplate + .Replace("__TEXEL_TYPE__", traits.TexelType, StringComparison.Ordinal) + .Replace("__OUTPUT_FORMAT__", traits.OutputFormat, StringComparison.Ordinal) + .Replace("__DECODE_TEXEL_FUNCTION__", traits.DecodeTexelFunction, StringComparison.Ordinal) + .Replace("__ENCODE_OUTPUT_FUNCTION__", traits.EncodeOutputFunction, StringComparison.Ordinal) + .Replace("__BLEND_AND_COMPOSE__", CompositionShaderSnippets.BlendAndCompose, StringComparison.Ordinal) + .Replace("__LOAD_BACKDROP__", traits.LoadBackdropExpression, StringComparison.Ordinal) + .Replace("__LOAD_SOURCE__", traits.LoadSourceExpression, StringComparison.Ordinal) + .Replace("__STORE_OUTPUT__", traits.StoreOutputStatement, StringComparison.Ordinal); + + int byteCount = Encoding.UTF8.GetByteCount(source); + code = new byte[byteCount + 1]; + _ = Encoding.UTF8.GetBytes(source, code); + code[^1] = 0; + ShaderCache[textureFormat] = code; + } + + error = null; + return true; + } + + private static LayerShaderTraits GetTraits(TextureFormat textureFormat) + { + if (!WebGPUDrawingBackend.TryGetCompositeTextureShaderTraits(textureFormat, out WebGPUDrawingBackend.CompositeTextureShaderTraits traits)) + { + return CreateFloatTraits("rgba8unorm"); + } + + return traits.EncodingKind switch + { + WebGPUDrawingBackend.CompositeTextureEncodingKind.Float => CreateFloatTraits(traits.OutputFormat), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Snorm => CreateSnormTraits(traits.OutputFormat), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Uint8 => CreateUintTraits(traits.OutputFormat, 255F), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Uint16 => CreateUintTraits(traits.OutputFormat, 65535F), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Sint16 => CreateSintTraits(traits.OutputFormat, -32768F, 32767F), + _ => CreateFloatTraits(traits.OutputFormat), + }; + } + + private static LayerShaderTraits CreateFloatTraits(string outputFormat) + { + const string decodeTexel = + """ + fn decode_texel(texel: vec4) -> vec4 { + return texel; + } + """; + + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + return color; + } + """; + + return new LayerShaderTraits( + outputFormat, + "f32", + decodeTexel, + encodeOutput, + "textureLoad(backdrop_texture, vec2(dest_x, dest_y), 0)", + "textureLoad(source_texture, vec2(src_x, src_y), 0)", + "textureStore(output_texture, vec2(out_x, out_y), encode_output(vec4(rgb, alpha)));"); + } + + private static LayerShaderTraits CreateSnormTraits(string outputFormat) + { + const string decodeTexel = + """ + fn decode_texel(texel: vec4) -> vec4 { + return (texel * 0.5) + vec4(0.5); + } + """; + + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return (clamped * 2.0) - vec4(1.0); + } + """; + + return new LayerShaderTraits( + outputFormat, + "f32", + decodeTexel, + encodeOutput, + "textureLoad(backdrop_texture, vec2(dest_x, dest_y), 0)", + "textureLoad(source_texture, vec2(src_x, src_y), 0)", + "textureStore(output_texture, vec2(out_x, out_y), encode_output(vec4(rgb, alpha)));"); + } + + private static LayerShaderTraits CreateUintTraits(string outputFormat, float maxValue) + { + string maxVector = $"vec4({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})"; + string decodeTexel = $@"const UINT_TEXEL_MAX: vec4 = {maxVector}; +fn decode_texel(texel: vec4) -> vec4 {{ + return vec4(texel) / UINT_TEXEL_MAX; +}}"; + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return vec4(round(clamped * UINT_TEXEL_MAX)); + } + """; + + return new LayerShaderTraits( + outputFormat, + "u32", + decodeTexel, + encodeOutput, + "textureLoad(backdrop_texture, vec2(dest_x, dest_y), 0)", + "textureLoad(source_texture, vec2(src_x, src_y), 0)", + "textureStore(output_texture, vec2(out_x, out_y), encode_output(vec4(rgb, alpha)));"); + } + + private static LayerShaderTraits CreateSintTraits(string outputFormat, float minValue, float maxValue) + { + string minVector = $"vec4({minValue:F1}, {minValue:F1}, {minValue:F1}, {minValue:F1})"; + string maxVector = $"vec4({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})"; + string decodeTexel = $@"const SINT_TEXEL_MIN: vec4 = {minVector}; +const SINT_TEXEL_MAX: vec4 = {maxVector}; +const SINT_TEXEL_RANGE: vec4 = SINT_TEXEL_MAX - SINT_TEXEL_MIN; +fn decode_texel(texel: vec4) -> vec4 {{ + return (vec4(texel) - SINT_TEXEL_MIN) / SINT_TEXEL_RANGE; +}}"; + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return vec4(round((clamped * SINT_TEXEL_RANGE) + SINT_TEXEL_MIN)); + } + """; + + return new LayerShaderTraits( + outputFormat, + "i32", + decodeTexel, + encodeOutput, + "textureLoad(backdrop_texture, vec2(dest_x, dest_y), 0)", + "textureLoad(source_texture, vec2(src_x, src_y), 0)", + "textureStore(output_texture, vec2(out_x, out_y), encode_output(vec4(rgb, alpha)));"); + } + + private readonly struct LayerShaderTraits( + string outputFormat, + string texelType, + string decodeTexelFunction, + string encodeOutputFunction, + string loadBackdropExpression, + string loadSourceExpression, + string storeOutputStatement) + { + public string OutputFormat { get; } = outputFormat; + + public string TexelType { get; } = texelType; + + public string DecodeTexelFunction { get; } = decodeTexelFunction; + + public string EncodeOutputFunction { get; } = encodeOutputFunction; + + public string LoadBackdropExpression { get; } = loadBackdropExpression; + + public string LoadSourceExpression { get; } = loadSourceExpression; + + public string StoreOutputStatement { get; } = storeOutputStatement; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs new file mode 100644 index 000000000..8eb50e032 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Shared WGSL function snippets used by compute shaders that perform pixel blending +/// and alpha composition, including . +/// +internal static class CompositionShaderSnippets +{ + /// + /// WGSL functions for unpremultiplying alpha, blending colors by mode, + /// and compositing pixels using Porter-Duff alpha composition. + /// + internal const string BlendAndCompose = + """ + fn unpremultiply(rgb: vec3, alpha: f32) -> vec3 { + if (alpha <= 0.0) { + return vec3(0.0); + } + + return rgb / alpha; + } + + fn blend_color(backdrop: vec3, source: vec3, mode: u32) -> vec3 { + switch mode { + case 1u: { + return backdrop * source; + } + case 2u: { + return min(vec3(1.0), backdrop + source); + } + case 3u: { + return max(vec3(0.0), backdrop - source); + } + case 4u: { + return 1.0 - ((1.0 - backdrop) * (1.0 - source)); + } + case 5u: { + return min(backdrop, source); + } + case 6u: { + return max(backdrop, source); + } + case 7u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + backdrop >= vec3(0.5)); + } + case 8u: { + return select( + 2.0 * backdrop * source, + 1.0 - (2.0 * (1.0 - backdrop) * (1.0 - source)), + source >= vec3(0.5)); + } + default: { + return source; + } + } + } + + fn compose_pixel(destination_premul: vec4, source: vec4, color_mode: u32, alpha_mode: u32) -> vec4 { + let destination_alpha = destination_premul.a; + let destination_rgb_straight = unpremultiply(destination_premul.rgb, destination_alpha); + let source_alpha = source.a; + let source_rgb = source.rgb; + let source_premul = source_rgb * source_alpha; + let forward_blend = blend_color(destination_rgb_straight, source_rgb, color_mode); + let reverse_blend = blend_color(source_rgb, destination_rgb_straight, color_mode); + let shared_alpha = source_alpha * destination_alpha; + + switch alpha_mode { + case 1u: { + return vec4(source_premul, source_alpha); + } + case 2u: { + let premul = (destination_rgb_straight * (destination_alpha - shared_alpha)) + (forward_blend * shared_alpha); + return vec4(premul, destination_alpha); + } + case 3u: { + let alpha = source_alpha * destination_alpha; + return vec4(source_premul * destination_alpha, alpha); + } + case 4u: { + let alpha = source_alpha * (1.0 - destination_alpha); + return vec4(source_premul * (1.0 - destination_alpha), alpha); + } + case 5u: { + return destination_premul; + } + case 6u: { + let premul = (source_rgb * (source_alpha - shared_alpha)) + (reverse_blend * shared_alpha); + return vec4(premul, source_alpha); + } + case 7u: { + let alpha = destination_alpha + source_alpha - shared_alpha; + let premul = + (source_rgb * (source_alpha - shared_alpha)) + + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (reverse_blend * shared_alpha); + return vec4(premul, alpha); + } + case 8u: { + let alpha = destination_alpha * source_alpha; + return vec4(destination_premul.rgb * source_alpha, alpha); + } + case 9u: { + let alpha = destination_alpha * (1.0 - source_alpha); + return vec4(destination_premul.rgb * (1.0 - source_alpha), alpha); + } + case 10u: { + return vec4(0.0, 0.0, 0.0, 0.0); + } + case 11u: { + let source_term = source_premul * (1.0 - destination_alpha); + let destination_term = destination_premul.rgb * (1.0 - source_alpha); + let alpha = source_alpha * (1.0 - destination_alpha) + destination_alpha * (1.0 - source_alpha); + return vec4(source_term + destination_term, alpha); + } + default: { + let alpha = source_alpha + destination_alpha - shared_alpha; + let premul = + (destination_rgb_straight * (destination_alpha - shared_alpha)) + + (source_rgb * (source_alpha - shared_alpha)) + + (forward_blend * shared_alpha); + return vec4(premul, alpha); + } + } + } + """; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/DrawLeafComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawLeafComputeShader.cs new file mode 100644 index 000000000..a2af5f017 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawLeafComputeShader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class DrawLeafComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.DrawLeafCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 7, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the draw-leaf bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/DrawReduceComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawReduceComputeShader.cs new file mode 100644 index 000000000..42e708257 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawReduceComputeShader.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class DrawReduceComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.DrawReduceCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 3, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the draw-reduce bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs new file mode 100644 index 000000000..1e5c2e10f --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs @@ -0,0 +1,247 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Final staged-scene fine pass driven from the source-of-truth WGSL stage. +/// Only the output storage texture encoding is specialized per target format. +/// +internal static class FineAreaComputeShader +{ + private const string OutputBindingMarker = "var output: texture_storage_2d;"; + private const string OutputStoreMarker = "textureStore(output, vec2(coords), rgba_sep);"; + private const string PremulAlphaMarker = "fn premul_alpha(rgba: vec4) -> vec4 {"; + + private static readonly object CacheSync = new(); + private static readonly Dictionary ShaderCache = []; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryGetCode(TextureFormat textureFormat, out byte[] code, out string? error) + { + if (!TryGetTraits(textureFormat, out ShaderTraits traits)) + { + code = []; + error = $"Scene fine shader does not support texture format '{textureFormat}'."; + return false; + } + + lock (CacheSync) + { + if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode)) + { + code = cachedCode; + error = null; + return true; + } + + string source = GeneratedWgslShaderSources.FineText; + source = source.Replace(OutputBindingMarker, $"var output: texture_storage_2d<{traits.OutputFormat}, write>;", StringComparison.Ordinal); + source = source.Replace(OutputStoreMarker, traits.StoreOutputStatement, StringComparison.Ordinal); + source = source.Replace(PremulAlphaMarker, $"{traits.EncodeOutputFunction}\n\n{PremulAlphaMarker}", StringComparison.Ordinal); + + int byteCount = Encoding.UTF8.GetByteCount(source); + code = new byte[byteCount + 1]; + _ = Encoding.UTF8.GetBytes(source, code); + code[^1] = 0; + ShaderCache[textureFormat] = code; + } + + error = null; + return true; + } + + public static unsafe bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + TextureFormat outputTextureFormat, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[9]; + entries[0] = CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage, 0); + entries[2] = CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage, 0); + entries[3] = CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage, 0); + entries[4] = CreateStorageEntry(4, BufferBindingType.Storage, 0); + entries[5] = CreateOutputTextureEntry(5, outputTextureFormat); + entries[6] = CreateSampledTextureEntry(6); + entries[7] = CreateSampledTextureEntry(7); + entries[8] = CreateSampledTextureEntry(8); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 9, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the staged-scene fine bind-group layout."; + return false; + } + + error = null; + return true; + } + + private static bool TryGetTraits(TextureFormat textureFormat, out ShaderTraits traits) + { + if (!WebGPUDrawingBackend.TryGetCompositeTextureShaderTraits(textureFormat, out WebGPUDrawingBackend.CompositeTextureShaderTraits compositeTraits)) + { + traits = default; + return false; + } + + traits = compositeTraits.EncodingKind switch + { + WebGPUDrawingBackend.CompositeTextureEncodingKind.Float => CreateFloatTraits(compositeTraits.OutputFormat), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Snorm => CreateSnormTraits(compositeTraits.OutputFormat), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Uint8 => CreateUintTraits(compositeTraits.OutputFormat, 255F), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Uint16 => CreateUintTraits(compositeTraits.OutputFormat, 65535F), + WebGPUDrawingBackend.CompositeTextureEncodingKind.Sint16 => CreateSintTraits(compositeTraits.OutputFormat, -32768F, 32767F), + _ => default + }; + + return true; + } + + private static ShaderTraits CreateFloatTraits(string outputFormat) + { + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + return color; + } + """; + + return new ShaderTraits( + outputFormat, + encodeOutput, + "textureStore(output, vec2(coords), encode_output(rgba_sep));"); + } + + private static ShaderTraits CreateSnormTraits(string outputFormat) + { + const string encodeOutput = + """ + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return (clamped * 2.0) - vec4(1.0); + } + """; + + return new ShaderTraits( + outputFormat, + encodeOutput, + "textureStore(output, vec2(coords), encode_output(rgba_sep));"); + } + + private static ShaderTraits CreateUintTraits(string outputFormat, float maxValue) + { + string maxVector = $"vec4({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})"; + const string encodeOutput = + """ + const UINT_TEXEL_MAX: vec4 = __UINT_TEXEL_MAX__; + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return vec4(round(clamped * UINT_TEXEL_MAX)); + } + """; + + return new ShaderTraits( + outputFormat, + encodeOutput.Replace("__UINT_TEXEL_MAX__", maxVector, StringComparison.Ordinal), + "textureStore(output, vec2(coords), encode_output(rgba_sep));"); + } + + private static ShaderTraits CreateSintTraits(string outputFormat, float minValue, float maxValue) + { + string minVector = $"vec4({minValue:F1}, {minValue:F1}, {minValue:F1}, {minValue:F1})"; + string maxVector = $"vec4({maxValue:F1}, {maxValue:F1}, {maxValue:F1}, {maxValue:F1})"; + string encodeOutput = + $$""" + const SINT_TEXEL_MIN: vec4 = {{minVector}}; + const SINT_TEXEL_MAX: vec4 = {{maxVector}}; + const SINT_TEXEL_RANGE: vec4 = SINT_TEXEL_MAX - SINT_TEXEL_MIN; + fn encode_output(color: vec4) -> vec4 { + let clamped = clamp(color, vec4(0.0), vec4(1.0)); + return vec4(round((clamped * SINT_TEXEL_RANGE) + SINT_TEXEL_MIN)); + } + """; + + return new ShaderTraits( + outputFormat, + encodeOutput, + "textureStore(output, vec2(coords), encode_output(rgba_sep));"); + } + + private static BindGroupLayoutEntry CreateStorageEntry(uint binding, BufferBindingType type, nuint minBindingSize) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = type, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; + + private static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; + + private static BindGroupLayoutEntry CreateOutputTextureEntry(uint binding, TextureFormat outputTextureFormat) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + StorageTexture = new StorageTextureBindingLayout + { + Access = StorageTextureAccess.WriteOnly, + Format = outputTextureFormat, + ViewDimension = TextureViewDimension.Dimension2D + } + }; + + private static BindGroupLayoutEntry CreateSampledTextureEntry(uint binding) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = TextureSampleType.Float, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + + private readonly struct ShaderTraits( + string outputFormat, + string encodeOutputFunction, + string storeOutputStatement) + { + public string OutputFormat { get; } = outputFormat; + + public string EncodeOutputFunction { get; } = encodeOutputFunction; + + public string StoreOutputStatement { get; } = storeOutputStatement; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs new file mode 100644 index 000000000..fbb1e7e55 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class FlattenComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.FlattenCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint pathTagCount) + => (pathTagCount + 255U) / 256U; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 6, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the flatten bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs new file mode 100644 index 000000000..1aeee1c32 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that counts traversed tile slices from scene lines and writes segment-count records. +/// +internal static unsafe class PathCountComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathCountCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint lineCount) + => (lineCount + 255U) / 256U; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + entries[0] = CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage, 0); + entries[3] = CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage, 0); + entries[4] = CreateStorageEntry(4, BufferBindingType.Storage, 0); + entries[5] = CreateStorageEntry(5, BufferBindingType.Storage, 0); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 6, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU path-count bind-group layout."; + return false; + } + + error = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BindGroupLayoutEntry CreateStorageEntry(uint binding, BufferBindingType type, nuint minBindingSize) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = type, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs new file mode 100644 index 000000000..a5126c186 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that initializes exact per-tile edge counts before ordered work counting. +/// +internal static unsafe class PathCountSetupComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathCountSetupCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX() => 1; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneIndirectCount)); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU path-count-setup bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs new file mode 100644 index 000000000..b4a401ce3 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that writes final tile-relative segments from counted line slices. +/// +internal static unsafe class PathTilingComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathTilingCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.ReadOnlyStorage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 6, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU path-tiling bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs new file mode 100644 index 000000000..05573a0a0 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that prepares the indirect dispatch size for the staged path-tiling phase. +/// +internal static unsafe class PathTilingSetupComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathTilingSetupCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX() => 1; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneIndirectCount)); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 3, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU path-tiling-setup bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduce2ComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduce2ComputeShader.cs new file mode 100644 index 000000000..466443985 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduce2ComputeShader.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class PathtagReduce2ComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathtagReduce2Code; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.ReadOnlyStorage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 2, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the pathtag-reduce2 bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduceComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduceComputeShader.cs new file mode 100644 index 000000000..16ad2c356 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduceComputeShader.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class PathtagReduceComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathtagReduceCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint pathTagWords) + => (pathTagWords + 255U) / 256U; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 3, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the pathtag-reduce bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScan1ComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScan1ComputeShader.cs new file mode 100644 index 000000000..7f17cd36e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScan1ComputeShader.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class PathtagScan1ComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathtagScan1Code; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.ReadOnlyStorage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 3, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the pathtag-scan1 bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs new file mode 100644 index 000000000..0206e4433 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe class PathtagScanComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathtagScanCode; + + public static ReadOnlySpan SmallShaderCode => GeneratedWgslShaderSources.PathtagScanSmallCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the pathtag-scan bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SceneShaderBindingLayoutHelper.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SceneShaderBindingLayoutHelper.cs new file mode 100644 index 000000000..7ab40402d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/SceneShaderBindingLayoutHelper.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static class SceneShaderBindingLayoutHelper +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BindGroupLayoutEntry CreateStorageEntry( + uint binding, + BufferBindingType type, + nuint minBindingSize = 0) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = type, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + => new() + { + Binding = binding, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = minBindingSize + } + }; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/bbox.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/bbox.wgsl new file mode 100644 index 000000000..c4b026f44 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/bbox.wgsl @@ -0,0 +1,23 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// The annotated bounding box for a path. It has been transformed, +// but contains a link to the active transform, mostly for gradients. +// Coordinates are integer pixels (for the convenience of atomic update) +// but will probably become fixed-point fractions for rectangles. +// +// TODO: This also carries a `draw_flags` field that contains information that gets propagated to +// the draw info stream. This is currently only used for the fill rule. If the other bits remain +// unused we could possibly pack this into some other field, such as the MSB of `trans_ix`. +struct PathBbox { + x0: i32, + y0: i32, + x1: i32, + y1: i32, + draw_flags: u32, + trans_ix: u32, +} + +fn bbox_intersect(a: vec4, b: vec4) -> vec4 { + return vec4(max(a.xy, b.xy), min(a.zw, b.zw)); +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/blend.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/blend.wgsl new file mode 100644 index 000000000..7b6116698 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/blend.wgsl @@ -0,0 +1,327 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Color mixing modes + +const MIX_NORMAL = 0u; +const MIX_MULTIPLY = 1u; +const MIX_SCREEN = 2u; +const MIX_OVERLAY = 3u; +const MIX_DARKEN = 4u; +const MIX_LIGHTEN = 5u; +const MIX_ADD = 16u; +const MIX_SUBTRACT = 17u; +const MIX_COLOR_DODGE = 6u; +const MIX_COLOR_BURN = 7u; +const MIX_HARD_LIGHT = 8u; +const MIX_SOFT_LIGHT = 9u; +const MIX_DIFFERENCE = 10u; +const MIX_EXCLUSION = 11u; +const MIX_HUE = 12u; +const MIX_SATURATION = 13u; +const MIX_COLOR = 14u; +const MIX_LUMINOSITY = 15u; +const MIX_CLIP = 128u; + +fn screen(cb: vec3, cs: vec3) -> vec3 { + return cb + cs - (cb * cs); +} + +fn color_dodge(cb: f32, cs: f32) -> f32 { + if cb == 0.0 { + return 0.0; + } else if cs == 1.0 { + return 1.0; + } else { + return min(1.0, cb / (1.0 - cs)); + } +} + +fn color_burn(cb: f32, cs: f32) -> f32 { + if cb == 1.0 { + return 1.0; + } else if cs == 0.0 { + return 0.0; + } else { + return 1.0 - min(1.0, (1.0 - cb) / cs); + } +} + +fn hard_light(cb: vec3, cs: vec3) -> vec3 { + return select( + screen(cb, 2.0 * cs - 1.0), + cb * 2.0 * cs, + cs <= vec3(0.5) + ); +} + +fn soft_light(cb: vec3, cs: vec3) -> vec3 { + let d = select( + sqrt(cb), + ((16.0 * cb - 12.0) * cb + 4.0) * cb, + cb <= vec3(0.25) + ); + return select( + cb + (2.0 * cs - 1.0) * (d - cb), + cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb), + cs <= vec3(0.5) + ); +} + +fn sat(c: vec3) -> f32 { + return max(c.x, max(c.y, c.z)) - min(c.x, min(c.y, c.z)); +} + +fn lum(c: vec3) -> f32 { + let f = vec3(0.3, 0.59, 0.11); + return dot(c, f); +} + +fn svg_lum(c: vec3) -> f32 { + let f = vec3(0.2125, 0.7154, 0.0721); + return dot(c, f); +} + +fn clip_color(c_in: vec3) -> vec3 { + var c = c_in; + let l = lum(c); + let n = min(c.x, min(c.y, c.z)); + let x = max(c.x, max(c.y, c.z)); + if n < 0.0 { + c = l + (((c - l) * l) / (l - n)); + } + if x > 1.0 { + c = l + (((c - l) * (1.0 - l)) / (x - l)); + } + return c; +} + +fn set_lum(c: vec3, l: f32) -> vec3 { + return clip_color(c + (l - lum(c))); +} + +fn set_sat_inner( + cmin: ptr, + cmid: ptr, + cmax: ptr, + s: f32 +) { + if *cmax > *cmin { + *cmid = ((*cmid - *cmin) * s) / (*cmax - *cmin); + *cmax = s; + } else { + *cmid = 0.0; + *cmax = 0.0; + } + *cmin = 0.0; +} + +fn set_sat(c: vec3, s: f32) -> vec3 { + var r = c.r; + var g = c.g; + var b = c.b; + if r <= g { + if g <= b { + set_sat_inner(&r, &g, &b, s); + } else { + if r <= b { + set_sat_inner(&r, &b, &g, s); + } else { + set_sat_inner(&b, &r, &g, s); + } + } + } else { + if r <= b { + set_sat_inner(&g, &r, &b, s); + } else { + if g <= b { + set_sat_inner(&g, &b, &r, s); + } else { + set_sat_inner(&b, &g, &r, s); + } + } + } + return vec3(r, g, b); +} + +// Blends two RGB colors together. The colors are assumed to be in sRGB +// color space, and this function does not take alpha into account. +fn blend_mix(cb: vec3, cs: vec3, mode: u32) -> vec3 { + var b = vec3(0.0); + switch mode { + case MIX_MULTIPLY: { + b = cb * cs; + } + case MIX_SCREEN: { + b = screen(cb, cs); + } + case MIX_OVERLAY: { + b = hard_light(cs, cb); + } + case MIX_DARKEN: { + b = min(cb, cs); + } + case MIX_LIGHTEN: { + b = max(cb, cs); + } + case MIX_ADD: { + b = min(vec3(1.0), cb + cs); + } + case MIX_SUBTRACT: { + b = max(vec3(0.0), cb - cs); + } + case MIX_COLOR_DODGE: { + b = vec3(color_dodge(cb.x, cs.x), color_dodge(cb.y, cs.y), color_dodge(cb.z, cs.z)); + } + case MIX_COLOR_BURN: { + b = vec3(color_burn(cb.x, cs.x), color_burn(cb.y, cs.y), color_burn(cb.z, cs.z)); + } + case MIX_HARD_LIGHT: { + b = hard_light(cb, cs); + } + case MIX_SOFT_LIGHT: { + b = soft_light(cb, cs); + } + case MIX_DIFFERENCE: { + b = abs(cb - cs); + } + case MIX_EXCLUSION: { + b = cb + cs - 2.0 * cb * cs; + } + case MIX_HUE: { + b = set_lum(set_sat(cs, sat(cb)), lum(cb)); + } + case MIX_SATURATION: { + b = set_lum(set_sat(cb, sat(cs)), lum(cb)); + } + case MIX_COLOR: { + b = set_lum(cs, lum(cb)); + } + case MIX_LUMINOSITY: { + b = set_lum(cb, lum(cs)); + } + default: { + b = cs; + } + } + return b; +} + +// Composition modes + +const COMPOSE_CLEAR = 0u; +const COMPOSE_COPY = 1u; +const COMPOSE_DEST = 2u; +const COMPOSE_SRC_OVER = 3u; +const COMPOSE_DEST_OVER = 4u; +const COMPOSE_SRC_IN = 5u; +const COMPOSE_DEST_IN = 6u; +const COMPOSE_SRC_OUT = 7u; +const COMPOSE_DEST_OUT = 8u; +const COMPOSE_SRC_ATOP = 9u; +const COMPOSE_DEST_ATOP = 10u; +const COMPOSE_XOR = 11u; +const COMPOSE_PLUS = 12u; +const COMPOSE_PLUS_LIGHTER = 13u; + +// Apply general compositing operation. +// Inputs are separated colors and alpha, output is premultiplied. +fn blend_compose( + cb: vec3, + cs: vec3, + ab: f32, + as_: f32, + compose_mode: u32, +) -> vec4 { + var fa = 0.0; + var fb = 0.0; + switch compose_mode { + case COMPOSE_COPY: { + fa = 1.0; + fb = 0.0; + } + case COMPOSE_DEST: { + fa = 0.0; + fb = 1.0; + } + case COMPOSE_SRC_OVER: { + fa = 1.0; + fb = 1.0 - as_; + } + case COMPOSE_DEST_OVER: { + fa = 1.0 - ab; + fb = 1.0; + } + case COMPOSE_SRC_IN: { + fa = ab; + fb = 0.0; + } + case COMPOSE_DEST_IN: { + fa = 0.0; + fb = as_; + } + case COMPOSE_SRC_OUT: { + fa = 1.0 - ab; + fb = 0.0; + } + case COMPOSE_DEST_OUT: { + fa = 0.0; + fb = 1.0 - as_; + } + case COMPOSE_SRC_ATOP: { + fa = ab; + fb = 1.0 - as_; + } + case COMPOSE_DEST_ATOP: { + fa = 1.0 - ab; + fb = as_; + } + case COMPOSE_XOR: { + fa = 1.0 - ab; + fb = 1.0 - as_; + } + case COMPOSE_PLUS: { + fa = 1.0; + fb = 1.0; + } + case COMPOSE_PLUS_LIGHTER: { + return min(vec4(1.0), vec4(as_ * cs + ab * cb, as_ + ab)); + } + default: {} + } + let as_fa = as_ * fa; + let ab_fb = ab * fb; + let co = as_fa * cs + ab_fb * cb; + // Modes like COMPOSE_PLUS can generate alpha > 1.0, so clamp. + return vec4(co, min(as_fa + ab_fb, 1.0)); +} + +fn unpremultiply(color: vec4) -> vec3 { + let EPSILON = 1e-15; + // Max with a small epsilon to avoid NaNs. + let inv_alpha = 1.0 / max(color.a, EPSILON); + return color.rgb * inv_alpha; +} + +// Apply color mixing and composition. Both input and output colors are +// premultiplied RGB. +fn blend_mix_compose(backdrop: vec4, src: vec4, mode: u32) -> vec4 { + let BLEND_DEFAULT = ((MIX_NORMAL << 8u) | COMPOSE_SRC_OVER); + if (mode & 0x7fffu) == BLEND_DEFAULT { + // Both normal+src_over blend and clip case + return backdrop * (1.0 - src.a) + src; + } + // Un-premultiply colors for blending. + var cs = unpremultiply(src); + let cb = unpremultiply(backdrop); + let mix_mode = mode >> 8u; + let mixed = blend_mix(cb, cs, mix_mode); + cs = mix(cs, mixed, backdrop.a); + let compose_mode = mode & 0xffu; + if compose_mode == COMPOSE_SRC_OVER { + let co = mix(backdrop.rgb, cs, src.a); + return vec4(co, src.a + backdrop.a * (1.0 - src.a)); + } else { + return blend_compose(cb, cs, backdrop.a, src.a, compose_mode); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/bump.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/bump.wgsl new file mode 100644 index 000000000..c1cdaa48c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/bump.wgsl @@ -0,0 +1,28 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Bitflags for each stage that can fail allocation. +const STAGE_BINNING: u32 = 0x1u; +const STAGE_TILE_ALLOC: u32 = 0x2u; +const STAGE_FLATTEN: u32 = 0x4u; +const STAGE_PATH_COUNT: u32 = 0x8u; +const STAGE_COARSE: u32 = 0x10u; + +// This must be kept in sync with the struct in config.rs in the encoding crate. +struct BumpAllocators { + // Bitmask of stages that have failed allocation. + failed: atomic, + binning: atomic, + ptcl: atomic, + tile: atomic, + seg_counts: atomic, + segments: atomic, + blend: atomic, + lines: atomic, +} + +struct IndirectCount { + count_x: u32, + count_y: u32, + count_z: u32, +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/clip.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/clip.wgsl new file mode 100644 index 000000000..9c768ae8f --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/clip.wgsl @@ -0,0 +1,26 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +struct Bic { + a: u32, + b: u32, +} + +fn bic_combine(x: Bic, y: Bic) -> Bic { + let m = min(x.b, y.a); + return Bic(x.a + y.a - m, x.b + y.b - m); +} + +struct ClipInp { + // Index of the draw object. + ix: u32, + // This is a packed encoding of an enum with the sign bit as the tag. If positive, + // this entry is a BeginClip and contains the associated path index. If negative, + // it is an EndClip and contains the bitwise-not of the EndClip draw object index. + path_ix: i32, +} + +struct ClipEl { + parent_ix: u32, + bbox: vec4, +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/config.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/config.wgsl new file mode 100644 index 000000000..7e87a285c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/config.wgsl @@ -0,0 +1,72 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// This must be kept in sync with `ConfigUniform` in `vello_encoding/src/config.rs` +struct Config { + width_in_tiles: u32, + height_in_tiles: u32, + + target_width: u32, + target_height: u32, + + // The initial color applied to the pixels in a tile during the fine stage. + // The format is packed RGBA8 in MSB order. + base_color: u32, + + n_drawobj: u32, + n_path: u32, + n_clip: u32, + + // To reduce the number of bindings, info and bin data are combined + // into one buffer. + bin_data_start: u32, + + // offsets within scene buffer (in u32 units) + pathtag_base: u32, + pathdata_base: u32, + + drawtag_base: u32, + drawdata_base: u32, + + transform_base: u32, + style_base: u32, + + // Sizes of bump allocated buffers (in element size units) + lines_size: u32, + binning_size: u32, + tiles_size: u32, + seg_counts_size: u32, + segments_size: u32, + blend_size: u32, + ptcl_size: u32, +} + +// Geometry of tiles and bins + +const TILE_WIDTH = 16u; +const TILE_HEIGHT = 16u; +// Number of tiles per bin +const N_TILE_X = 16u; +const N_TILE_Y = 16u; +const N_TILE = N_TILE_X * N_TILE_Y; + +// Not currently supporting non-square tiles +const TILE_SCALE = 0.0625; + +// The "split" point between using local memory in fine for the blend stack and spilling to the blend_spill buffer. +// A higher value will increase vgpr ("register") pressure in fine, but decrease required dynamic memory allocation. +// If changing, also change in vello_shaders/src/cpu/coarse.rs. +const BLEND_STACK_SPLIT = 4u; + +// The following are computed in draw_leaf from the generic gradient parameters +// encoded in the scene, and stored in the gradient's info struct, for +// consumption during fine rasterization. + +// Radial gradient kinds +const RAD_GRAD_KIND_CIRCULAR = 1u; +const RAD_GRAD_KIND_STRIP = 2u; +const RAD_GRAD_KIND_FOCAL_ON_CIRCLE = 3u; +const RAD_GRAD_KIND_CONE = 4u; + +// Radial gradient flags +const RAD_GRAD_SWAPPED = 1u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/cubic.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/cubic.wgsl new file mode 100644 index 000000000..10a306a76 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/cubic.wgsl @@ -0,0 +1,14 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +struct Cubic { + p0: vec2, + p1: vec2, + p2: vec2, + p3: vec2, + stroke: vec2, + path_ix: u32, + flags: u32, +} + +const CUBIC_IS_STROKE = 1u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/drawtag.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/drawtag.wgsl new file mode 100644 index 000000000..fed4c5ace --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/drawtag.wgsl @@ -0,0 +1,60 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// The DrawMonoid is computed as a prefix sum to aid in decoding +// the variable-length encoding of draw objects. +struct DrawMonoid { + // The number of paths preceding this draw object. + path_ix: u32, + // The number of clip operations preceding this draw object. + clip_ix: u32, + // The offset of the encoded draw object in the scene (u32s). + scene_offset: u32, + // The offset of the associated info. + info_offset: u32, +} + +// Each draw object has a 32-bit draw tag, which is a bit-packed +// version of the draw monoid. +const DRAWTAG_NOP = 0u; +const DRAWTAG_FILL_COLOR = 0x44u; +const DRAWTAG_FILL_RECOLOR = 0x4cu; +const DRAWTAG_FILL_LIN_GRADIENT = 0x114u; +const DRAWTAG_FILL_RAD_GRADIENT = 0x29cu; +const DRAWTAG_FILL_ELLIPTIC_GRADIENT = 0x1d8u; +const DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u; +const DRAWTAG_FILL_IMAGE = 0x294u; +const DRAWTAG_BLURRED_ROUNDED_RECT = 0x2d4u; +const DRAWTAG_BEGIN_CLIP = 0x49u; +const DRAWTAG_END_CLIP = 0x21u; + +/// The first word of each draw info stream entry contains the flags. This is not a part of the +/// draw object stream but get used after the draw objects have been reduced on the GPU. +/// 0 represents a non-zero fill. 1 represents an even-odd fill. +const DRAW_INFO_FLAGS_FILL_RULE_BIT = 1u; +const DRAW_FLAGS_BLEND_MODE_SHIFT = 1u; +const DRAW_FLAGS_BLEND_MODE_MASK = 0x3ffeu; +const DRAW_FLAGS_BLEND_ALPHA_SHIFT = 14u; +const DRAW_FLAGS_BLEND_ALPHA_MASK = 0x3fffc000u; + +fn draw_monoid_identity() -> DrawMonoid { + return DrawMonoid(); +} + +fn combine_draw_monoid(a: DrawMonoid, b: DrawMonoid) -> DrawMonoid { + var c: DrawMonoid; + c.path_ix = a.path_ix + b.path_ix; + c.clip_ix = a.clip_ix + b.clip_ix; + c.scene_offset = a.scene_offset + b.scene_offset; + c.info_offset = a.info_offset + b.info_offset; + return c; +} + +fn map_draw_tag(tag_word: u32) -> DrawMonoid { + var c: DrawMonoid; + c.path_ix = u32(tag_word != DRAWTAG_NOP); + c.clip_ix = tag_word & 1u; + c.scene_offset = (tag_word >> 2u) & 0x07u; + c.info_offset = (tag_word >> 6u) & 0x0fu; + return c; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/pathtag.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/pathtag.wgsl new file mode 100644 index 000000000..cebf0a049 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/pathtag.wgsl @@ -0,0 +1,71 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +struct TagMonoid { + trans_ix: u32, + // TODO: I don't think pathseg_ix is used. + pathseg_ix: u32, + pathseg_offset: u32, + style_ix: u32, + path_ix: u32, +} + +const PATH_TAG_SEG_TYPE = 3u; +const PATH_TAG_LINETO = 1u; +const PATH_TAG_QUADTO = 2u; +const PATH_TAG_CUBICTO = 3u; +const PATH_TAG_F32 = 8u; +const PATH_TAG_TRANSFORM = 0x20u; +const PATH_TAG_PATH = 0x10u; +const PATH_TAG_STYLE = 0x40u; +const PATH_TAG_SUBPATH_END = 4u; + +// Size of the `Style` data structure in words +const STYLE_SIZE_IN_WORDS: u32 = 2u; + +const STYLE_FLAGS_STYLE: u32 = 0x80000000u; +const STYLE_FLAGS_FILL: u32 = 0x40000000u; +const STYLE_MITER_LIMIT_MASK: u32 = 0xFFFFu; + +const STYLE_FLAGS_START_CAP_MASK: u32 = 0x0C000000u; +const STYLE_FLAGS_END_CAP_MASK: u32 = 0x03000000u; + +const STYLE_FLAGS_CAP_BUTT: u32 = 0u; +const STYLE_FLAGS_CAP_SQUARE: u32 = 0x01000000u; +const STYLE_FLAGS_CAP_ROUND: u32 = 0x02000000u; + +const STYLE_FLAGS_JOIN_MASK: u32 = 0x30000000u; +const STYLE_FLAGS_JOIN_BEVEL: u32 = 0u; +const STYLE_FLAGS_JOIN_MITER: u32 = 0x10000000u; +const STYLE_FLAGS_JOIN_ROUND: u32 = 0x20000000u; + +// TODO: Declare the remaining STYLE flags here. + +fn tag_monoid_identity() -> TagMonoid { + return TagMonoid(); +} + +fn combine_tag_monoid(a: TagMonoid, b: TagMonoid) -> TagMonoid { + var c: TagMonoid; + c.trans_ix = a.trans_ix + b.trans_ix; + c.pathseg_ix = a.pathseg_ix + b.pathseg_ix; + c.pathseg_offset = a.pathseg_offset + b.pathseg_offset; + c.style_ix = a.style_ix + b.style_ix; + c.path_ix = a.path_ix + b.path_ix; + return c; +} + +fn reduce_tag(tag_word: u32) -> TagMonoid { + var c: TagMonoid; + let point_count = tag_word & 0x3030303u; + c.pathseg_ix = countOneBits((point_count * 7u) & 0x4040404u); + c.trans_ix = countOneBits(tag_word & (PATH_TAG_TRANSFORM * 0x1010101u)); + let n_points = point_count + ((tag_word >> 2u) & 0x1010101u); + var a = n_points + (n_points & (((tag_word >> 3u) & 0x1010101u) * 15u)); + a += a >> 8u; + a += a >> 16u; + c.pathseg_offset = a & 0xffu; + c.path_ix = countOneBits(tag_word & (PATH_TAG_PATH * 0x1010101u)); + c.style_ix = countOneBits(tag_word & (PATH_TAG_STYLE * 0x1010101u)) * STYLE_SIZE_IN_WORDS; + return c; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/ptcl.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/ptcl.wgsl new file mode 100644 index 000000000..09e621509 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/ptcl.wgsl @@ -0,0 +1,128 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Layout of per-tile command list +// Initial allocation, in u32's. +const PTCL_INITIAL_ALLOC = 64u; +const PTCL_INCREMENT = 256u; + +// Amount of space taken by jump +const PTCL_HEADROOM = 2u; + +// Tags for PTCL commands +const CMD_END = 0u; +const CMD_FILL = 1u; +const CMD_STROKE = 2u; +const CMD_SOLID = 3u; +const CMD_COLOR = 5u; +const CMD_RECOLOR = 14u; +const CMD_LIN_GRAD = 6u; +const CMD_RAD_GRAD = 7u; +const CMD_ELLIPTIC_GRAD = 15u; +const CMD_SWEEP_GRAD = 8u; +const CMD_IMAGE = 9u; +const CMD_BEGIN_CLIP = 10u; +const CMD_END_CLIP = 11u; +const CMD_JUMP = 12u; +const CMD_BLUR_RECT = 13u; + +// The individual PTCL structs are written here, but read/write is by +// hand in the relevant shaders + +struct CmdFill { + size_and_rule: u32, + seg_data: u32, + backdrop: i32, +} + +struct CmdStroke { + tile: u32, + half_width: f32, +} + +struct CmdJump { + new_ix: u32, +} + +struct CmdColor { + rgba_color: u32, + draw_flags: u32, +} + +struct CmdRecolor { + source_color: u32, + target_color: u32, + threshold: f32, + draw_flags: u32, +} + +struct CmdBlurRect { + // Solid fill color. + rgba_color: u32, + + // 2x2 transformation matrix (inverse). + matrx: vec4, + // 2D translation (inverse) + xlat: vec2, + + // Rounded rectangle properties. + width: f32, + height: f32, + radius: f32, + + // Gaussian filter standard deviation + std_dev: f32, +} + +struct CmdLinGrad { + index: u32, + extend_mode: u32, + line_x: f32, + line_y: f32, + line_c: f32, +} + +struct CmdRadGrad { + index: u32, + extend_mode: u32, + matrx: vec4, + xlat: vec2, + focal_x: f32, + radius: f32, + kind: u32, + flags: u32, +} + +struct CmdEllipticGrad { + index: u32, + extend_mode: u32, + matrx: vec4, + xlat: vec2, +} + +struct CmdSweepGrad { + index: u32, + extend_mode: u32, + matrx: vec4, + xlat: vec2, + t0: f32, + t1: f32, +} + +struct CmdImage { + matrx: vec4, + xlat: vec2, + atlas_offset: vec2, + extents: vec2, + format: u32, + x_extend_mode: u32, + y_extend_mode: u32, + quality: u32, + alpha: f32, + alpha_type: u32, +} + +struct CmdEndClip { + blend: u32, + alpha: f32, +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/segment.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/segment.wgsl new file mode 100644 index 000000000..560f18c9e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/segment.wgsl @@ -0,0 +1,35 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Segments laid out for contiguous storage +struct Segment { + // Points are relative to tile origin + point0: vec2, + point1: vec2, + y_edge: f32, +} + +// A line segment produced by flattening and ready for rasterization. +// +// The name is perhaps too playful, but reflects the fact that these +// lines are completely unordered. They will flow through coarse path +// rasterization, then the per-tile segments will be scatter-written into +// segment storage so that each (tile, path) tuple gets a contiguous +// slice of segments. +struct LineSoup { + path_ix: u32, + // Note: this creates an alignment gap. Don't worry about + // this now, but maybe move to scalars. + p0: vec2, + p1: vec2, +} + +// An intermediate data structure for sorting tile segments. +struct SegmentCount { + // Reference to element of LineSoup array + line_ix: u32, + // Two count values packed into a single u32 + // Lower 16 bits: index of segment within line + // Upper 16 bits: index of segment within segment slice + counts: u32, +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/tile.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/tile.wgsl new file mode 100644 index 000000000..f57c67433 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/tile.wgsl @@ -0,0 +1,21 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Common datatypes for path and tile intermediate info. + +struct Path { + // bounding box in tiles + bbox: vec4, + // offset (in u32's) to tile rectangle + tiles: u32, +} + +struct Tile { + backdrop: i32, + // This is used for the count of the number of segments in the + // tile up to coarse rasterization, and the index afterwards. + // In the latter variant, the bits are inverted so that tiling + // can detect whether the tile was allocated; it's best to + // consider this an enum packed into a u32. + segment_count_or_ix: u32, +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/transform.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/transform.wgsl new file mode 100644 index 000000000..c59bf563d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/transform.wgsl @@ -0,0 +1,27 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Helpers for working with transforms. + +struct Transform { + matrx: vec4, + translate: vec2, +} + +fn transform_apply(transform: Transform, p: vec2) -> vec2 { + return transform.matrx.xy * p.x + transform.matrx.zw * p.y + transform.translate; +} + +fn transform_inverse(transform: Transform) -> Transform { + let inv_det = 1.0 / (transform.matrx.x * transform.matrx.w - transform.matrx.y * transform.matrx.z); + let inv_mat = inv_det * vec4(transform.matrx.w, -transform.matrx.y, -transform.matrx.z, transform.matrx.x); + let inv_tr = mat2x2(inv_mat.xy, inv_mat.zw) * -transform.translate; + return Transform(inv_mat, inv_tr); +} + +fn transform_mul(a: Transform, b: Transform) -> Transform { + return Transform( + a.matrx.xyxy * b.matrx.xxzz + a.matrx.zwzw * b.matrx.yyww, + a.matrx.xy * b.translate.x + a.matrx.zw * b.translate.y + a.translate + ); +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/util.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/util.wgsl new file mode 100644 index 000000000..d1ce6315e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/Shared/util.wgsl @@ -0,0 +1,24 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// This file defines utility functions that interact with host-shareable buffer objects. It should +// be imported once following the resource binding declarations in the shader module that access +// them. + +// Reads a draw tag from the scene buffer, defaulting to DRAWTAG_NOP if the given `ix` is beyond the +// range of valid draw objects (e.g this can happen if `ix` is derived from an invocation ID in a +// workgroup that partially spans valid range). +// +// This function depends on the following global declarations: +// * `scene`: array +// * `config`: Config (see config.wgsl) +fn read_draw_tag_from_scene(ix: u32) -> u32 { + var tag_word: u32; + if ix < config.n_drawobj { + let tag_ix = config.drawtag_base + ix; + tag_word = scene[tag_ix]; + } else { + tag_word = DRAWTAG_NOP; + } + return tag_word; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/TileAllocComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/TileAllocComputeShader.cs new file mode 100644 index 000000000..ee8713796 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/TileAllocComputeShader.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that allocates and zeroes per-path tile ranges, matching Vello's tile allocation role. +/// +internal static unsafe class TileAllocComputeShader +{ + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.TileAllocCode; + + public static ReadOnlySpan EntryPoint => "main\0"u8; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint GetDispatchX(uint pathCount) + => (pathCount + 255U) / 256U; + + public static bool TryCreateBindGroupLayout( + WebGPU api, + Device* device, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 6, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU tile-allocation bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/UNLICENSE b/src/ImageSharp.Drawing.WebGPU/Shaders/UNLICENSE new file mode 100644 index 000000000..68a49daad --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/backdrop.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/backdrop.wgsl new file mode 100644 index 000000000..79f4a1033 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/backdrop.wgsl @@ -0,0 +1,48 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Note: this is the non-atomic version +struct Tile { + backdrop: i32, + segments: u32, +} + +#import config + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var tiles: array; + +const WG_SIZE = 64u; + +var sh_backdrop: array; + +// Each workgroup computes the inclusive prefix sum of the backdrops +// in one row of tiles. +@compute @workgroup_size(64) +fn main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + let width_in_tiles = config.width_in_tiles; + let ix = wg_id.x * width_in_tiles + local_id.x; + var backdrop = 0; + if local_id.x < width_in_tiles { + backdrop = tiles[ix].backdrop; + } + sh_backdrop[local_id.x] = backdrop; + // iterate log2(WG_SIZE) times + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x >= (1u << i) { + backdrop += sh_backdrop[local_id.x - (1u << i)]; + } + workgroupBarrier(); + sh_backdrop[local_id.x] = backdrop; + } + if local_id.x < width_in_tiles { + tiles[ix].backdrop = backdrop; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/backdrop_dyn.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/backdrop_dyn.wgsl new file mode 100644 index 000000000..69ecaf7f4 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/backdrop_dyn.wgsl @@ -0,0 +1,86 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Prefix sum for dynamically allocated backdrops + +#import bump +#import config +#import tile + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var bump: BumpAllocators; + +@group(0) @binding(2) +var paths: array; + +@group(0) @binding(3) +var tiles: array; + +const WG_SIZE = 256u; + +var sh_row_width: array; +var sh_row_count: array; +var sh_offset: array; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, +) { + // Abort if any of the prior stages failed. + if local_id.x == 0u { + sh_row_count[0] = atomicLoad(&bump.failed); + } + let failed = workgroupUniformLoad(&sh_row_count[0]); + if failed != 0u { + return; + } + let drawobj_ix = global_id.x; + var row_count = 0u; + if drawobj_ix < config.n_drawobj { + // TODO: when rectangles, path and draw obj are not the same + let path = paths[drawobj_ix]; + sh_row_width[local_id.x] = path.bbox.z - path.bbox.x; + row_count = path.bbox.w - path.bbox.y; + sh_offset[local_id.x] = path.tiles; + } else { + // Explicitly zero the row width, just in case. + sh_row_width[local_id.x] = 0u; + } + sh_row_count[local_id.x] = row_count; + + // Prefix sum of row counts + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x >= (1u << i) { + row_count += sh_row_count[local_id.x - (1u << i)]; + } + workgroupBarrier(); + sh_row_count[local_id.x] = row_count; + } + workgroupBarrier(); + let total_rows = sh_row_count[WG_SIZE - 1u]; + for (var row = local_id.x; row < total_rows; row += WG_SIZE) { + var el_ix = 0u; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + let probe = el_ix + ((WG_SIZE / 2u) >> i); + if row >= sh_row_count[probe - 1u] { + el_ix = probe; + } + } + let width = sh_row_width[el_ix]; + if width > 0u { + var seq_ix = row - select(0u, sh_row_count[el_ix - 1u], el_ix > 0u); + var tile_ix = sh_offset[el_ix] + seq_ix * width; + var sum = tiles[tile_ix].backdrop; + for (var x = 1u; x < width; x += 1u) { + tile_ix += 1u; + sum += tiles[tile_ix].backdrop; + tiles[tile_ix].backdrop = sum; + } + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/bbox_clear.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/bbox_clear.wgsl new file mode 100644 index 000000000..067d7c2dd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/bbox_clear.wgsl @@ -0,0 +1,24 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +#import config +#import bbox + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var path_bboxes: array; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, +) { + let ix = global_id.x; + if ix < config.n_path { + path_bboxes[ix].x0 = 0x7fffffff; + path_bboxes[ix].y0 = 0x7fffffff; + path_bboxes[ix].x1 = -0x80000000; + path_bboxes[ix].y1 = -0x80000000; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/binning.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/binning.wgsl new file mode 100644 index 000000000..2c30ca8f4 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/binning.wgsl @@ -0,0 +1,181 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// The binning stage + +#import config +#import drawtag +#import bbox +#import bump + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var draw_monoids: array; + +@group(0) @binding(2) +var path_bbox_buf: array; + +@group(0) @binding(3) +var clip_bbox_buf: array>; + +@group(0) @binding(4) +var intersected_bbox: array>; + +@group(0) @binding(5) +var bump: BumpAllocators; + +@group(0) @binding(6) +var bin_data: array; + +// TODO: put in common place +struct BinHeader { + element_count: u32, + chunk_offset: u32, +} + +@group(0) @binding(7) +var bin_header: array; + +// conversion factors from coordinates to bin +const SX = 1.0 / f32(N_TILE_X * TILE_WIDTH); +const SY = 1.0 / f32(N_TILE_Y * TILE_HEIGHT); + +const WG_SIZE = 256u; +const N_SLICE = WG_SIZE / 32u; +const N_SUBSLICE = 4u; + +var sh_bitmaps: array, N_TILE>, N_SLICE>; +// store count values packed two u16's to a u32 +var sh_count: array, N_SUBSLICE>; +var sh_chunk_offset: array; +var sh_previous_failed: u32; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + for (var i = 0u; i < N_SLICE; i += 1u) { + atomicStore(&sh_bitmaps[i][local_id.x], 0u); + } + if local_id.x == 0u { + let failed = atomicLoad(&bump.lines) > config.lines_size; + sh_previous_failed = u32(failed); + } + // also functions as barrier to protect zeroing of bitmaps + let failed = workgroupUniformLoad(&sh_previous_failed); + if failed != 0u { + if global_id.x == 0u { + atomicOr(&bump.failed, STAGE_FLATTEN); + } + return; + } + + // Read inputs and determine coverage of bins + let element_ix = global_id.x; + var x0 = 0; + var y0 = 0; + var x1 = 0; + var y1 = 0; + if element_ix < config.n_drawobj { + let draw_monoid = draw_monoids[element_ix]; + var clip_bbox = vec4(-1e9, -1e9, 1e9, 1e9); + if draw_monoid.clip_ix > 0u { + // TODO: `clip_ix` should always be valid as long as the monoids are correct. Leaving + // the bounds check in here for correctness but we should assert this condition instead + // once there is a debug-assertion mechanism. + clip_bbox = clip_bbox_buf[min(draw_monoid.clip_ix - 1u, config.n_clip - 1u)]; + } + // For clip elements, clip_box is the bbox of the clip path, + // intersected with enclosing clips. + // For other elements, it is the bbox of the enclosing clips. + // TODO check this is true + + let path_bbox = path_bbox_buf[draw_monoid.path_ix]; + let pb = vec4(vec4(path_bbox.x0, path_bbox.y0, path_bbox.x1, path_bbox.y1)); + let bbox = bbox_intersect(clip_bbox, pb); + + intersected_bbox[element_ix] = bbox; + + // `bbox_intersect` can result in a zero or negative area intersection if the path bbox lies + // outside the clip bbox. If that is the case, Don't round up the bottom-right corner of the + // and leave the coordinates at 0. This way the path will get clipped out and won't get + // assigned to a bin. + if bbox.x < bbox.z && bbox.y < bbox.w { + x0 = i32(floor(bbox.x * SX)); + y0 = i32(floor(bbox.y * SY)); + x1 = i32(ceil(bbox.z * SX)); + y1 = i32(ceil(bbox.w * SY)); + } + } + let width_in_bins = i32((config.width_in_tiles + N_TILE_X - 1u) / N_TILE_X); + let height_in_bins = i32((config.height_in_tiles + N_TILE_Y - 1u) / N_TILE_Y); + x0 = clamp(x0, 0, width_in_bins); + y0 = clamp(y0, 0, height_in_bins); + x1 = clamp(x1, 0, width_in_bins); + y1 = clamp(y1, 0, height_in_bins); + if x0 == x1 { + y1 = y0; + } + var x = x0; + var y = y0; + let my_slice = local_id.x / 32u; + let my_mask = 1u << (local_id.x & 31u); + while y < y1 { + atomicOr(&sh_bitmaps[my_slice][y * width_in_bins + x], my_mask); + x += 1; + if x == x1 { + x = x0; + y += 1; + } + } + + workgroupBarrier(); + // Allocate output segments + var element_count = 0u; + for (var i = 0u; i < N_SUBSLICE; i += 1u) { + element_count += countOneBits(atomicLoad(&sh_bitmaps[i * 2u][local_id.x])); + let element_count_lo = element_count; + element_count += countOneBits(atomicLoad(&sh_bitmaps[i * 2u + 1u][local_id.x])); + let element_count_hi = element_count; + let element_count_packed = element_count_lo | (element_count_hi << 16u); + sh_count[i][local_id.x] = element_count_packed; + } + // element_count is the number of draw objects covering this thread's bin + var chunk_offset = atomicAdd(&bump.binning, element_count); + if chunk_offset + element_count > config.binning_size { + chunk_offset = 0u; + atomicOr(&bump.failed, STAGE_BINNING); + } + sh_chunk_offset[local_id.x] = chunk_offset; + bin_header[global_id.x].element_count = element_count; + bin_header[global_id.x].chunk_offset = chunk_offset; + workgroupBarrier(); + + // loop over bbox of bins touched by this draw object + x = x0; + y = y0; + while y < y1 { + let bin_ix = y * width_in_bins + x; + let out_mask = atomicLoad(&sh_bitmaps[my_slice][bin_ix]); + // I think this predicate will always be true... + if (out_mask & my_mask) != 0u { + var idx = countOneBits(out_mask & (my_mask - 1u)); + if my_slice > 0u { + let count_ix = my_slice - 1u; + let count_packed = sh_count[count_ix / 2u][bin_ix]; + idx += (count_packed >> (16u * (count_ix & 1u))) & 0xffffu; + } + let offset = config.bin_data_start + sh_chunk_offset[bin_ix]; + bin_data[offset + idx] = element_ix; + } + x += 1; + if x == x1 { + x = x0; + y += 1; + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/clip_leaf.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/clip_leaf.wgsl new file mode 100644 index 000000000..20eda44ff --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/clip_leaf.wgsl @@ -0,0 +1,209 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +#import config +#import bbox +#import clip +#import drawtag + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var clip_inp: array; + +@group(0) @binding(2) +var path_bboxes: array; + +@group(0) @binding(3) +var reduced: array; + +@group(0) @binding(4) +var clip_els: array; + +@group(0) @binding(5) +var draw_monoids: array; + +@group(0) @binding(6) +var clip_bboxes: array>; + +const WG_SIZE = 256u; +var sh_bic: array; +var sh_stack: array; +var sh_stack_bbox: array, WG_SIZE>; +var sh_bbox: array, WG_SIZE>; +var sh_link: array; + +fn search_link(bic: ptr, ix_in: u32) -> i32 { + var ix = ix_in; + var j = 0u; + while j < firstTrailingBit(WG_SIZE) { + let base = 2u * WG_SIZE - (2u << (firstTrailingBit(WG_SIZE) - j)); + if ((ix >> j) & 1u) != 0u { + let test = bic_combine(sh_bic[base + (ix >> j) - 1u], *bic); + if test.b > 0u { + break; + } + *bic = test; + ix -= 1u << j; + } + j += 1u; + } + if ix > 0u { + while j > 0u { + j -= 1u; + let base = 2u * WG_SIZE - (2u << (firstTrailingBit(WG_SIZE) - j)); + let test = bic_combine(sh_bic[base + (ix >> j) - 1u], *bic); + if test.b == 0u { + *bic = test; + ix -= 1u << j; + } + } + } + if ix > 0u { + return i32(ix) - 1; + } else { + return i32(~0u - (*bic).a); + } +} + +fn load_clip_path(ix: u32) -> i32 { + if ix < config.n_clip { + return clip_inp[ix].path_ix; + } else { + return -2147483648; + // literal too large? + // return 0x80000000; + } +} + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + var bic: Bic; + if local_id.x < wg_id.x { + bic = reduced[local_id.x]; + } + sh_bic[local_id.x] = bic; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_bic[local_id.x + (1u << i)]; + bic = bic_combine(bic, other); + } + workgroupBarrier(); + sh_bic[local_id.x] = bic; + } + workgroupBarrier(); + let stack_size = sh_bic[0].b; + // TODO: if stack depth > WG_SIZE desired, scan here + + // binary search in stack + let sp = WG_SIZE - 1u - local_id.x; + var ix = 0u; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + let probe = ix + ((WG_SIZE / 2u) >> i); + if sp < sh_bic[probe].b { + ix = probe; + } + } + let b = sh_bic[ix].b; + var bbox = vec4(-1e9, -1e9, 1e9, 1e9); + if sp < b { + let el = clip_els[ix * WG_SIZE + b - sp - 1u]; + sh_stack[local_id.x] = el.parent_ix; + bbox = el.bbox; + } + // forward scan of bbox values of prefix stack + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + sh_stack_bbox[local_id.x] = bbox; + workgroupBarrier(); + if local_id.x >= (1u << i) { + bbox = bbox_intersect(sh_stack_bbox[local_id.x - (1u << i)], bbox); + } + workgroupBarrier(); + } + sh_stack_bbox[local_id.x] = bbox; + + // Read input and compute Bic binary tree + let inp = load_clip_path(global_id.x); + let is_push = inp >= 0; + bic = Bic(1u - u32(is_push), u32(is_push)); + sh_bic[local_id.x] = bic; + if is_push { + let path_bbox = path_bboxes[inp]; + bbox = vec4(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1), f32(path_bbox.y1)); + } else { + bbox = vec4(-1e9, -1e9, 1e9, 1e9); + } + var inbase = 0u; + for (var i = 0u; i < firstTrailingBit(WG_SIZE) - 1u; i += 1u) { + let outbase = 2u * WG_SIZE - (1u << (firstTrailingBit(WG_SIZE) - i)); + workgroupBarrier(); + if local_id.x < 1u << (firstTrailingBit(WG_SIZE) - 1u - i) { + let in_off = inbase + local_id.x * 2u; + sh_bic[outbase + local_id.x] = bic_combine(sh_bic[in_off], sh_bic[in_off + 1u]); + } + inbase = outbase; + } + workgroupBarrier(); + // search for predecessor node + bic = Bic(); + var link = search_link(&bic, local_id.x); + sh_link[local_id.x] = link; + workgroupBarrier(); + let grandparent = select(link - 1, sh_link[link], link >= 0); + var parent: i32; + if link >= 0 { + parent = i32(wg_id.x * WG_SIZE) + link; + } else if link + i32(stack_size) >= 0 { + parent = i32(sh_stack[i32(WG_SIZE) + link]); + } else { + parent = -1; + } + // bbox scan (intersect) across parent links + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + if i != 0u { + sh_link[local_id.x] = link; + } + sh_bbox[local_id.x] = bbox; + workgroupBarrier(); + if link >= 0 { + bbox = bbox_intersect(sh_bbox[link], bbox); + link = sh_link[link]; + } + workgroupBarrier(); + } + if link + i32(stack_size) >= 0 { + bbox = bbox_intersect(sh_stack_bbox[i32(WG_SIZE) + link], bbox); + } + // At this point, bbox is the intersection of bboxes on the path to the root + sh_bbox[local_id.x] = bbox; + workgroupBarrier(); + + if !is_push && global_id.x < config.n_clip { + // Fix up drawmonoid so path_ix of EndClip matches BeginClip + let parent_clip = clip_inp[parent]; + let path_ix = parent_clip.path_ix; + let parent_ix = parent_clip.ix; + let ix = ~inp; + draw_monoids[ix].path_ix = u32(path_ix); + // Make EndClip point to the same draw data as BeginClip + draw_monoids[ix].scene_offset = draw_monoids[parent_ix].scene_offset; + // Make EndClip point to the same info (draw flags) as BeginClip + draw_monoids[ix].info_offset = draw_monoids[parent_ix].info_offset; + if grandparent >= 0 { + bbox = sh_bbox[grandparent]; + } else if grandparent + i32(stack_size) >= 0 { + bbox = sh_stack_bbox[i32(WG_SIZE) + grandparent]; + } else { + bbox = vec4(-1e9, -1e9, 1e9, 1e9); + } + } + if global_id.x < config.n_clip { + clip_bboxes[global_id.x] = bbox; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/clip_reduce.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/clip_reduce.wgsl new file mode 100644 index 000000000..6b8979a57 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/clip_reduce.wgsl @@ -0,0 +1,67 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +#import bbox +#import clip + +@group(0) @binding(0) +var clip_inp: array; + +@group(0) @binding(1) +var path_bboxes: array; + +@group(0) @binding(2) +var reduced: array; + +@group(0) @binding(3) +var clip_out: array; + +const WG_SIZE = 256u; +var sh_bic: array; +var sh_parent: array; +var sh_path_ix: array; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + let inp = clip_inp[global_id.x].path_ix; + let is_push = inp >= 0; + var bic = Bic(1u - u32(is_push), u32(is_push)); + // reverse scan of bicyclic semigroup + sh_bic[local_id.x] = bic; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_bic[local_id.x + (1u << i)]; + bic = bic_combine(bic, other); + } + workgroupBarrier(); + sh_bic[local_id.x] = bic; + } + if local_id.x == 0u { + reduced[wg_id.x] = bic; + } + workgroupBarrier(); + let size = sh_bic[0].b; + bic = Bic(); + if local_id.x + 1u < WG_SIZE { + bic = sh_bic[local_id.x + 1u]; + } + if is_push && bic.a == 0u { + let local_ix = size - bic.b - 1u; + sh_parent[local_ix] = local_id.x; + sh_path_ix[local_ix] = u32(inp); + } + workgroupBarrier(); + // TODO: possibly do forward scan here if depth can exceed wg size + if local_id.x < size { + let path_ix = sh_path_ix[local_id.x]; + let path_bbox = path_bboxes[path_ix]; + let parent_ix = sh_parent[local_id.x] + wg_id.x * WG_SIZE; + let bbox = vec4(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1), f32(path_bbox.y1)); + clip_out[global_id.x] = ClipEl(parent_ix, bbox); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/coarse.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/coarse.wgsl new file mode 100644 index 000000000..29e6a261b --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/coarse.wgsl @@ -0,0 +1,490 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// The coarse rasterization stage. + +#import config +#import bump +#import drawtag +#import ptcl +#import tile + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var scene: array; + +@group(0) @binding(2) +var draw_monoids: array; + +// TODO: dedup +struct BinHeader { + element_count: u32, + chunk_offset: u32, +} + +@group(0) @binding(3) +var bin_headers: array; + +@group(0) @binding(4) +var info_bin_data: array; + +@group(0) @binding(5) +var paths: array; + +@group(0) @binding(6) +var tiles: array; + +@group(0) @binding(7) +var bump: BumpAllocators; + +@group(0) @binding(8) +var ptcl: array; + + + +// Much of this code assumes WG_SIZE == N_TILE. If these diverge, then +// a fair amount of fixup is needed. +const WG_SIZE = 256u; +const N_SLICE = WG_SIZE / 32u; + +var sh_bitmaps: array, N_TILE>, N_SLICE>; +var sh_part_count: array; +var sh_part_offsets: array; +var sh_drawobj_ix: array; +var sh_tile_stride: array; +var sh_tile_width: array; +var sh_tile_x0y0: array; +var sh_tile_count: array; +var sh_tile_base: array; + +// helper functions for writing ptcl + +var cmd_offset: u32; +var cmd_limit: u32; + +// Make sure there is space for a command of given size, plus a jump if needed +fn alloc_cmd(size: u32) { + if cmd_offset + size >= cmd_limit { + // We might be able to save a little bit of computation here + // by setting the initial value of the bump allocator. + let ptcl_dyn_start = config.width_in_tiles * config.height_in_tiles * PTCL_INITIAL_ALLOC; + var new_cmd = ptcl_dyn_start + atomicAdd(&bump.ptcl, PTCL_INCREMENT); + if new_cmd + PTCL_INCREMENT > config.ptcl_size { + // This sets us up for technical UB, as lots of threads will be writing + // to the same locations. But I think it's fine, and predicating the + // writes would probably slow things down. + new_cmd = 0u; + atomicOr(&bump.failed, STAGE_COARSE); + } + ptcl[cmd_offset] = CMD_JUMP; + ptcl[cmd_offset + 1u] = new_cmd; + cmd_offset = new_cmd; + cmd_limit = cmd_offset + (PTCL_INCREMENT - PTCL_HEADROOM); + } +} + +fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) { + // We overload the "segments" field to store both count (written by + // path_count stage) and segment allocation (used by path_tiling and + // fine). + let n_segs = tile.segment_count_or_ix; + if n_segs != 0u { + var seg_ix = atomicAdd(&bump.segments, n_segs); + tiles[tile_ix].segment_count_or_ix = ~seg_ix; + alloc_cmd(4u); + ptcl[cmd_offset] = CMD_FILL; + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; + let size_and_rule = (n_segs << 1u) | u32(even_odd); + let fill = CmdFill(size_and_rule, seg_ix, tile.backdrop); + ptcl[cmd_offset + 1u] = fill.size_and_rule; + ptcl[cmd_offset + 2u] = fill.seg_data; + ptcl[cmd_offset + 3u] = u32(fill.backdrop); + cmd_offset += 4u; + } else { + alloc_cmd(1u); + ptcl[cmd_offset] = CMD_SOLID; + cmd_offset += 1u; + } +} + +fn write_color(color: CmdColor) { + alloc_cmd(3u); + ptcl[cmd_offset] = CMD_COLOR; + ptcl[cmd_offset + 1u] = color.rgba_color; + ptcl[cmd_offset + 2u] = color.draw_flags; + cmd_offset += 3u; +} + +fn write_recolor(source_color: u32, target_color: u32, threshold: u32, draw_flags: u32) { + alloc_cmd(5u); + ptcl[cmd_offset] = CMD_RECOLOR; + ptcl[cmd_offset + 1u] = source_color; + ptcl[cmd_offset + 2u] = target_color; + ptcl[cmd_offset + 3u] = threshold; + ptcl[cmd_offset + 4u] = draw_flags; + cmd_offset += 5u; +} + +fn write_grad(ty: u32, index: u32, info_offset: u32) { + alloc_cmd(3u); + ptcl[cmd_offset] = ty; + ptcl[cmd_offset + 1u] = index; + ptcl[cmd_offset + 2u] = info_offset; + cmd_offset += 3u; +} + +fn write_image(info_offset: u32) { + alloc_cmd(2u); + ptcl[cmd_offset] = CMD_IMAGE; + ptcl[cmd_offset + 1u] = info_offset; + cmd_offset += 2u; +} + +fn write_begin_clip() { + alloc_cmd(1u); + ptcl[cmd_offset] = CMD_BEGIN_CLIP; + cmd_offset += 1u; +} + +fn write_end_clip(end_clip: CmdEndClip) { + alloc_cmd(3u); + ptcl[cmd_offset] = CMD_END_CLIP; + ptcl[cmd_offset + 1u] = end_clip.blend; + ptcl[cmd_offset + 2u] = bitcast(end_clip.alpha); + cmd_offset += 3u; +} + +fn write_blurred_rounded_rect(color: CmdColor, info_offset: u32) { + alloc_cmd(3u); + ptcl[cmd_offset] = CMD_BLUR_RECT; + ptcl[cmd_offset + 1u] = info_offset; + ptcl[cmd_offset + 2u] = color.rgba_color; + cmd_offset += 3u; +} + +@compute @workgroup_size(256) +fn main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + // Exit early if prior stages failed, as we can't run this stage. + // We need to check only prior stages, as if this stage has failed in another workgroup, + // we still want to know this workgroup's memory requirement. + if local_id.x == 0u { + var failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_TILE_ALLOC | STAGE_FLATTEN); + if atomicLoad(&bump.seg_counts) > config.seg_counts_size { + failed |= STAGE_PATH_COUNT; + } + // Reuse sh_part_count to hold failed flag, shmem is tight + sh_part_count[0] = u32(failed); + } + let failed = workgroupUniformLoad(&sh_part_count[0]); + if failed != 0u { + if wg_id.x == 0u && local_id.x == 0u { + // propagate PATH_COUNT failure to path_tiling_setup so it doesn't need to bind config + atomicOr(&bump.failed, failed); + } + return; + } + let width_in_bins = (config.width_in_tiles + N_TILE_X - 1u) / N_TILE_X; + let bin_ix = width_in_bins * wg_id.y + wg_id.x; + let n_partitions = (config.n_drawobj + N_TILE - 1u) / N_TILE; + + // Coordinates of the top left of this bin, in tiles. + let bin_tile_x = N_TILE_X * wg_id.x; + let bin_tile_y = N_TILE_Y * wg_id.y; + + let tile_x = local_id.x % N_TILE_X; + let tile_y = local_id.x / N_TILE_X; + let this_tile_ix = (bin_tile_y + tile_y) * config.width_in_tiles + bin_tile_x + tile_x; + cmd_offset = this_tile_ix * PTCL_INITIAL_ALLOC; + cmd_limit = cmd_offset + (PTCL_INITIAL_ALLOC - PTCL_HEADROOM); + + // clip state + var clip_zero_depth = 0u; + var clip_depth = 0u; + + var partition_ix = 0u; + var rd_ix = 0u; + var wr_ix = 0u; + var part_start_ix = 0u; + var ready_ix = 0u; + + // blend state + var render_blend_depth = 0u; + var max_blend_depth = 0u; + + let blend_offset = cmd_offset; + cmd_offset += 1u; + + while true { + for (var i = 0u; i < N_SLICE; i += 1u) { + atomicStore(&sh_bitmaps[i][local_id.x], 0u); + } + + while true { + if ready_ix == wr_ix && partition_ix < n_partitions { + part_start_ix = ready_ix; + var count = 0u; + if partition_ix + local_id.x < n_partitions { + let in_ix = (partition_ix + local_id.x) * N_TILE + bin_ix; + let bin_header = bin_headers[in_ix]; + count = bin_header.element_count; + sh_part_offsets[local_id.x] = bin_header.chunk_offset; + } + // prefix sum the element counts + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + sh_part_count[local_id.x] = count; + workgroupBarrier(); + if local_id.x >= (1u << i) { + count += sh_part_count[local_id.x - (1u << i)]; + } + workgroupBarrier(); + } + sh_part_count[local_id.x] = part_start_ix + count; + ready_ix = workgroupUniformLoad(&sh_part_count[WG_SIZE - 1u]); + partition_ix += WG_SIZE; + } + // use binary search to find draw object to read + var ix = rd_ix + local_id.x; + if ix >= wr_ix && ix < ready_ix { + var part_ix = 0u; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + let probe = part_ix + ((N_TILE / 2u) >> i); + if ix >= sh_part_count[probe - 1u] { + part_ix = probe; + } + } + ix -= select(part_start_ix, sh_part_count[part_ix - 1u], part_ix > 0u); + let offset = config.bin_data_start + sh_part_offsets[part_ix]; + sh_drawobj_ix[local_id.x] = info_bin_data[offset + ix]; + } + wr_ix = min(rd_ix + N_TILE, ready_ix); + if wr_ix - rd_ix >= N_TILE || (wr_ix >= ready_ix && partition_ix >= n_partitions) { + break; + } + workgroupBarrier(); + } + // At this point, sh_drawobj_ix[0.. wr_ix - rd_ix] contains merged binning results. + var tag = DRAWTAG_NOP; + var drawobj_ix: u32; + if local_id.x + rd_ix < wr_ix { + drawobj_ix = sh_drawobj_ix[local_id.x]; + tag = scene[config.drawtag_base + drawobj_ix]; + } + + var tile_count = 0u; + // I think this predicate is the same as the last, maybe they can be combined + if tag != DRAWTAG_NOP { + let path_ix = draw_monoids[drawobj_ix].path_ix; + let path = paths[path_ix]; + let stride = path.bbox.z - path.bbox.x; + sh_tile_stride[local_id.x] = stride; + let dx = i32(path.bbox.x) - i32(bin_tile_x); + let dy = i32(path.bbox.y) - i32(bin_tile_y); + let x0 = clamp(dx, 0, i32(N_TILE_X)); + let y0 = clamp(dy, 0, i32(N_TILE_Y)); + let x1 = clamp(i32(path.bbox.z) - i32(bin_tile_x), 0, i32(N_TILE_X)); + let y1 = clamp(i32(path.bbox.w) - i32(bin_tile_y), 0, i32(N_TILE_Y)); + sh_tile_width[local_id.x] = u32(x1 - x0); + sh_tile_x0y0[local_id.x] = u32(x0) | u32(y0 << 16u); + tile_count = u32(x1 - x0) * u32(y1 - y0); + // base relative to bin + let base = path.tiles - u32(dy * i32(stride) + dx); + sh_tile_base[local_id.x] = base; + // TODO: there's a write_tile_alloc here in the source, not sure what it's supposed to do + } + + // Prefix sum of tile counts + sh_tile_count[local_id.x] = tile_count; + for (var i = 0u; i < firstTrailingBit(N_TILE); i += 1u) { + workgroupBarrier(); + if local_id.x >= (1u << i) { + tile_count += sh_tile_count[local_id.x - (1u << i)]; + } + workgroupBarrier(); + sh_tile_count[local_id.x] = tile_count; + } + workgroupBarrier(); + let total_tile_count = sh_tile_count[N_TILE - 1u]; + // Parallel iteration over all tiles + for (var ix = local_id.x; ix < total_tile_count; ix += N_TILE) { + // Binary search to find draw object which contains this tile + var el_ix = 0u; + for (var i = 0u; i < firstTrailingBit(N_TILE); i += 1u) { + let probe = el_ix + ((N_TILE / 2u) >> i); + if ix >= sh_tile_count[probe - 1u] { + el_ix = probe; + } + } + drawobj_ix = sh_drawobj_ix[el_ix]; + tag = scene[config.drawtag_base + drawobj_ix]; + let seq_ix = ix - select(0u, sh_tile_count[el_ix - 1u], el_ix > 0u); + let width = sh_tile_width[el_ix]; + let x0y0 = sh_tile_x0y0[el_ix]; + let x = (x0y0 & 0xffffu) + seq_ix % width; + let y = (x0y0 >> 16u) + seq_ix / width; + let tile_ix = sh_tile_base[el_ix] + sh_tile_stride[el_ix] * y + x; + let tile = tiles[tile_ix]; + let is_clip = (tag & 1u) != 0u; + var is_blend = false; + if is_clip { + let BLEND_CLIP = (128u << 8u) | 3u; + let scene_offset = draw_monoids[drawobj_ix].scene_offset; + let dd = config.drawdata_base + scene_offset; + let blend = scene[dd]; + is_blend = blend != BLEND_CLIP; + } + + let di = draw_monoids[drawobj_ix].info_offset; + let draw_flags = info_bin_data[di]; + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; + let n_segs = tile.segment_count_or_ix; + + // If this draw object represents an even-odd fill and we know that no line segment + // crosses this tile and then this draw object should not contribute to the tile if its + // backdrop (i.e. the winding number of its top-left corner) is even. + let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0; + let include_tile = n_segs != 0u || (backdrop_clear == is_clip) || is_blend; + if include_tile { + let el_slice = el_ix / 32u; + let el_mask = 1u << (el_ix & 31u); + atomicOr(&sh_bitmaps[el_slice][y * N_TILE_X + x], el_mask); + } + } + workgroupBarrier(); + // At this point bit drawobj % 32 is set in sh_bitmaps[drawobj / 32][y * N_TILE_X + x] + // if drawobj touches tile (x, y). + + // Write per-tile command list for this tile + var slice_ix = 0u; + var bitmap = atomicLoad(&sh_bitmaps[0u][local_id.x]); + while true { + if bitmap == 0u { + slice_ix += 1u; + // potential optimization: make iteration limit dynamic + if slice_ix == N_SLICE { + break; + } + bitmap = atomicLoad(&sh_bitmaps[slice_ix][local_id.x]); + if bitmap == 0u { + continue; + } + } + + let el_ix = slice_ix * 32u + firstTrailingBit(bitmap); + drawobj_ix = sh_drawobj_ix[el_ix]; + // clear LSB of bitmap, using bit magic + bitmap &= bitmap - 1u; + let drawtag = scene[config.drawtag_base + drawobj_ix]; + let dm = draw_monoids[drawobj_ix]; + let dd = config.drawdata_base + dm.scene_offset; + let di = dm.info_offset; + let draw_flags = info_bin_data[di]; + if clip_zero_depth == 0u { + let tile_ix = sh_tile_base[el_ix] + sh_tile_stride[el_ix] * tile_y + tile_x; + let tile = tiles[tile_ix]; + switch drawtag { + case DRAWTAG_FILL_COLOR: { + write_path(tile, tile_ix, draw_flags); + let rgba_color = scene[dd]; + write_color(CmdColor(rgba_color, draw_flags)); + } + case DRAWTAG_FILL_RECOLOR: { + write_path(tile, tile_ix, draw_flags); + write_recolor(scene[dd], scene[dd + 1u], scene[dd + 2u], draw_flags); + } + case DRAWTAG_BLURRED_ROUNDED_RECT: { + write_path(tile, tile_ix, draw_flags); + let rgba_color = scene[dd]; + let info_offset = di + 1u; + write_blurred_rounded_rect(CmdColor(rgba_color, draw_flags), info_offset); + } + case DRAWTAG_FILL_LIN_GRADIENT: { + write_path(tile, tile_ix, draw_flags); + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_LIN_GRAD, index, info_offset); + } + case DRAWTAG_FILL_RAD_GRADIENT: { + write_path(tile, tile_ix, draw_flags); + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_RAD_GRAD, index, info_offset); + } + case DRAWTAG_FILL_ELLIPTIC_GRADIENT: { + write_path(tile, tile_ix, draw_flags); + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_ELLIPTIC_GRAD, index, info_offset); + } + case DRAWTAG_FILL_SWEEP_GRADIENT: { + write_path(tile, tile_ix, draw_flags); + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_SWEEP_GRAD, index, info_offset); + } + case DRAWTAG_FILL_IMAGE: { + write_path(tile, tile_ix, draw_flags); + write_image(di + 1u); + } + case DRAWTAG_BEGIN_CLIP: { + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; + let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0; + if tile.segment_count_or_ix == 0u && backdrop_clear { + clip_zero_depth = clip_depth + 1u; + } else { + write_begin_clip(); + render_blend_depth += 1u; + max_blend_depth = max(max_blend_depth, render_blend_depth); + } + clip_depth += 1u; + } + case DRAWTAG_END_CLIP: { + clip_depth -= 1u; + write_path(tile, tile_ix, draw_flags); + let blend = scene[dd]; + let alpha = bitcast(scene[dd + 1u]); + write_end_clip(CmdEndClip(blend, alpha)); + render_blend_depth -= 1u; + } + default: {} + } + } else { + // In "clip zero" state, suppress all drawing + switch drawtag { + case DRAWTAG_BEGIN_CLIP: { + clip_depth += 1u; + } + case DRAWTAG_END_CLIP: { + if clip_depth == clip_zero_depth { + clip_zero_depth = 0u; + } + clip_depth -= 1u; + } + default: {} + } + } + } + + rd_ix += N_TILE; + if rd_ix >= ready_ix && partition_ix >= n_partitions { + break; + } + workgroupBarrier(); + } + if bin_tile_x + tile_x < config.width_in_tiles && bin_tile_y + tile_y < config.height_in_tiles { + ptcl[cmd_offset] = CMD_END; + var blend_ix = 0u; + if max_blend_depth > BLEND_STACK_SPLIT { + let scratch_size = (max_blend_depth - BLEND_STACK_SPLIT) * TILE_WIDTH * TILE_HEIGHT; + blend_ix = atomicAdd(&bump.blend, scratch_size); + if blend_ix + scratch_size > config.blend_size { + atomicOr(&bump.failed, STAGE_COARSE); + } + } + ptcl[blend_offset] = blend_ix; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/draw_leaf.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/draw_leaf.wgsl new file mode 100644 index 000000000..3b20209bc --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/draw_leaf.wgsl @@ -0,0 +1,333 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Finish prefix sum of drawtags, decode draw objects. + +#import config +#import clip +#import drawtag +#import bbox +#import transform + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var scene: array; + +@group(0) @binding(2) +var reduced: array; + +@group(0) @binding(3) +var path_bbox: array; + +@group(0) @binding(4) +var draw_monoid: array; + +@group(0) @binding(5) +var info: array; + +@group(0) @binding(6) +var clip_inp: array; + +#import util + +const WG_SIZE = 256u; + +fn read_transform(transform_base: u32, ix: u32) -> Transform { + let base = transform_base + ix * 6u; + let c0 = bitcast(scene[base]); + let c1 = bitcast(scene[base + 1u]); + let c2 = bitcast(scene[base + 2u]); + let c3 = bitcast(scene[base + 3u]); + let c4 = bitcast(scene[base + 4u]); + let c5 = bitcast(scene[base + 5u]); + let matrx = vec4(c0, c1, c2, c3); + let translate = vec2(c4, c5); + return Transform(matrx, translate); +} + +var sh_scratch: array; + +@compute @workgroup_size(256) +fn main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + // Reduce prefix of workgroups up to this one + var agg = draw_monoid_identity(); + if local_id.x < wg_id.x { + agg = reduced[local_id.x]; + } + sh_scratch[local_id.x] = agg; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_scratch[local_id.x + (1u << i)]; + agg = combine_draw_monoid(agg, other); + } + workgroupBarrier(); + sh_scratch[local_id.x] = agg; + } + // Two barriers can be eliminated if we use separate shared arrays + // for prefix and intra-workgroup prefix sum. + workgroupBarrier(); + var prefix = sh_scratch[0]; + + // This is the same division of work as draw_reduce. + let num_blocks_total = (config.n_drawobj + WG_SIZE - 1u) / WG_SIZE; + let n_blocks_base = num_blocks_total / WG_SIZE; + let remainder = num_blocks_total % WG_SIZE; + let first_block = n_blocks_base * wg_id.x + min(wg_id.x, remainder); + let n_blocks = n_blocks_base + u32(wg_id.x < remainder); + var block_start = first_block * WG_SIZE; + let blocks_end = block_start + n_blocks * WG_SIZE; + while block_start != blocks_end { + let ix = block_start + local_id.x; + let tag_word = read_draw_tag_from_scene(ix); + agg = map_draw_tag(tag_word); + workgroupBarrier(); + sh_scratch[local_id.x] = agg; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x >= 1u << i { + let other = sh_scratch[local_id.x - (1u << i)]; + agg = combine_draw_monoid(agg, other); + } + workgroupBarrier(); + sh_scratch[local_id.x] = agg; + } + var m = prefix; + workgroupBarrier(); + if local_id.x > 0u { + m = combine_draw_monoid(m, sh_scratch[local_id.x - 1u]); + } + // m now contains exclusive prefix sum of draw monoid + if ix < config.n_drawobj { + draw_monoid[ix] = m; + } + let dd = config.drawdata_base + m.scene_offset; + let di = m.info_offset; + if tag_word == DRAWTAG_FILL_COLOR || tag_word == DRAWTAG_FILL_RECOLOR || tag_word == DRAWTAG_FILL_LIN_GRADIENT || + tag_word == DRAWTAG_FILL_RAD_GRADIENT || tag_word == DRAWTAG_FILL_ELLIPTIC_GRADIENT || tag_word == DRAWTAG_FILL_SWEEP_GRADIENT || + tag_word == DRAWTAG_FILL_IMAGE || tag_word == DRAWTAG_BEGIN_CLIP || tag_word == DRAWTAG_BLURRED_ROUNDED_RECT + { + let bbox = path_bbox[m.path_ix]; + // TODO: bbox is mostly yagni here, sort that out. Maybe clips? + // let x0 = f32(bbox.x0); + // let y0 = f32(bbox.y0); + // let x1 = f32(bbox.x1); + // let y1 = f32(bbox.y1); + // let bbox_f = vec4(x0, y0, x1, y1); + var transform = Transform(); + let draw_flags = bbox.draw_flags; + if tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT || + tag_word == DRAWTAG_FILL_ELLIPTIC_GRADIENT || + tag_word == DRAWTAG_FILL_SWEEP_GRADIENT || tag_word == DRAWTAG_FILL_IMAGE || + tag_word == DRAWTAG_BLURRED_ROUNDED_RECT + { + transform = read_transform(config.transform_base, bbox.trans_ix); + } + switch tag_word { + case DRAWTAG_FILL_COLOR: { + info[di] = draw_flags; + } + case DRAWTAG_FILL_RECOLOR: { + info[di] = draw_flags; + } + case DRAWTAG_BEGIN_CLIP: { + info[di] = draw_flags; + } + case DRAWTAG_FILL_LIN_GRADIENT: { + info[di] = draw_flags; + var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); + var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); + p0 = transform_apply(transform, p0); + p1 = transform_apply(transform, p1); + let dxy = p1 - p0; + let scale = 1.0 / dot(dxy, dxy); + let line_xy = dxy * scale; + let line_c = -dot(p0, line_xy); + info[di + 1u] = bitcast(line_xy.x); + info[di + 2u] = bitcast(line_xy.y); + info[di + 3u] = bitcast(line_c); + } + case DRAWTAG_FILL_RAD_GRADIENT: { + // Two-point conical gradient implementation based + // on the algorithm at + // This epsilon matches what Skia uses + let GRADIENT_EPSILON = 1.0 / f32(1u << 12u); + info[di] = draw_flags; + var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); + var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); + var r0 = bitcast(scene[dd + 5u]); + var r1 = bitcast(scene[dd + 6u]); + let user_to_gradient = transform_inverse(transform); + // Output variables + var xform = Transform(); + var focal_x = 0.0; + var radius = 0.0; + var kind = 0u; + var flags = 0u; + if abs(r0 - r1) <= GRADIENT_EPSILON { + // When the radii are the same, emit a strip gradient + kind = RAD_GRAD_KIND_STRIP; + let scaled = r0 / distance(p0, p1); + xform = transform_mul( + two_point_to_unit_line(p0, p1), + user_to_gradient + ); + radius = scaled * scaled; + } else { + // Assume a two point conical gradient unless the centers + // are equal. + kind = RAD_GRAD_KIND_CONE; + if all(p0 == p1) { + kind = RAD_GRAD_KIND_CIRCULAR; + // Nudge p0 a bit to avoid denormals. + p0 += GRADIENT_EPSILON; + } + if r1 == 0.0 { + // If r1 == 0.0, swap the points and radii + flags |= RAD_GRAD_SWAPPED; + let tmp_p = p0; + p0 = p1; + p1 = tmp_p; + let tmp_r = r0; + r0 = r1; + r1 = tmp_r; + } + focal_x = r0 / (r0 - r1); + let cf = (1.0 - focal_x) * p0 + focal_x * p1; + radius = r1 / (distance(cf, p1)); + let user_to_unit_line = transform_mul( + two_point_to_unit_line(cf, p1), + user_to_gradient + ); + var user_to_scaled = user_to_unit_line; + // When r == 1.0, focal point is on circle + if abs(radius - 1.0) <= GRADIENT_EPSILON { + kind = RAD_GRAD_KIND_FOCAL_ON_CIRCLE; + let scale = 0.5 * abs(1.0 - focal_x); + user_to_scaled = transform_mul( + Transform(vec4(scale, 0.0, 0.0, scale), vec2(0.0)), + user_to_unit_line + ); + } else { + let a = radius * radius - 1.0; + let scale_ratio = abs(1.0 - focal_x) / a; + let scale_x = radius * scale_ratio; + let scale_y = sqrt(abs(a)) * scale_ratio; + user_to_scaled = transform_mul( + Transform(vec4(scale_x, 0.0, 0.0, scale_y), vec2(0.0)), + user_to_unit_line + ); + } + xform = user_to_scaled; + } + info[di + 1u] = bitcast(xform.matrx.x); + info[di + 2u] = bitcast(xform.matrx.y); + info[di + 3u] = bitcast(xform.matrx.z); + info[di + 4u] = bitcast(xform.matrx.w); + info[di + 5u] = bitcast(xform.translate.x); + info[di + 6u] = bitcast(xform.translate.y); + info[di + 7u] = bitcast(focal_x); + info[di + 8u] = bitcast(radius); + info[di + 9u] = bitcast((flags << 3u) | kind); + } + case DRAWTAG_FILL_ELLIPTIC_GRADIENT: { + info[di] = draw_flags; + var center = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); + var axis_end = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); + let axis_ratio = bitcast(scene[dd + 5u]); + center = transform_apply(transform, center); + axis_end = transform_apply(transform, axis_end); + let dxy = axis_end - center; + let axis = length(dxy); + let inv_axis = 1.0 / axis; + let inv_second_axis = inv_axis / axis_ratio; + let cos_theta = dxy.x * inv_axis; + let sin_theta = dxy.y * inv_axis; + // Map the ellipse to the unit circle so the fill parameter is length(local_xy). + let m0 = cos_theta * inv_axis; + let m1 = sin_theta * inv_second_axis; + let m2 = -sin_theta * inv_axis; + let m3 = cos_theta * inv_second_axis; + let xlat_x = -(m0 * center.x + m2 * center.y); + let xlat_y = -(m1 * center.x + m3 * center.y); + info[di + 1u] = bitcast(m0); + info[di + 2u] = bitcast(m1); + info[di + 3u] = bitcast(m2); + info[di + 4u] = bitcast(m3); + info[di + 5u] = bitcast(xlat_x); + info[di + 6u] = bitcast(xlat_y); + } + case DRAWTAG_FILL_SWEEP_GRADIENT: { + info[di] = draw_flags; + let p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); + let xform = transform_mul(transform, Transform(vec4(1.0, 0.0, 0.0, 1.0), p0)); + let inv = transform_inverse(xform); + info[di + 1u] = bitcast(inv.matrx.x); + info[di + 2u] = bitcast(inv.matrx.y); + info[di + 3u] = bitcast(inv.matrx.z); + info[di + 4u] = bitcast(inv.matrx.w); + info[di + 5u] = bitcast(inv.translate.x); + info[di + 6u] = bitcast(inv.translate.y); + info[di + 7u] = scene[dd + 3u]; + info[di + 8u] = scene[dd + 4u]; + } + case DRAWTAG_FILL_IMAGE: { + info[di] = draw_flags; + info[di + 1u] = bitcast(1.0); + info[di + 2u] = bitcast(0.0); + info[di + 3u] = bitcast(0.0); + info[di + 4u] = bitcast(1.0); + info[di + 5u] = bitcast(-bitcast(scene[dd + 3u])); + info[di + 6u] = bitcast(-bitcast(scene[dd + 4u])); + info[di + 7u] = scene[dd]; + info[di + 8u] = scene[dd + 1u]; + info[di + 9u] = scene[dd + 2u]; + } + case DRAWTAG_BLURRED_ROUNDED_RECT: { + info[di] = draw_flags; + let inv = transform_inverse(transform); + info[di + 1u] = bitcast(inv.matrx.x); + info[di + 2u] = bitcast(inv.matrx.y); + info[di + 3u] = bitcast(inv.matrx.z); + info[di + 4u] = bitcast(inv.matrx.w); + info[di + 5u] = bitcast(inv.translate.x); + info[di + 6u] = bitcast(inv.translate.y); + info[di + 7u] = scene[dd + 1u]; + info[di + 8u] = scene[dd + 2u]; + info[di + 9u] = scene[dd + 3u]; + info[di + 10u] = scene[dd + 4u]; + } + default: {} + } + } + if tag_word == DRAWTAG_BEGIN_CLIP || tag_word == DRAWTAG_END_CLIP { + var path_ix = ~ix; + if tag_word == DRAWTAG_BEGIN_CLIP { + path_ix = m.path_ix; + } + clip_inp[m.clip_ix] = ClipInp(ix, i32(path_ix)); + } + block_start += WG_SIZE; + // break here on end to save monoid aggregation? + prefix = combine_draw_monoid(prefix, sh_scratch[WG_SIZE - 1u]); + } +} + +fn two_point_to_unit_line(p0: vec2, p1: vec2) -> Transform { + let tmp1 = from_poly2(p0, p1); + let inv = transform_inverse(tmp1); + let tmp2 = from_poly2(vec2(0.0), vec2(1.0, 0.0)); + return transform_mul(tmp2, inv); +} + +fn from_poly2(p0: vec2, p1: vec2) -> Transform { + return Transform( + vec4(p1.y - p0.y, p0.x - p1.x, p1.x - p0.x, p1.y - p0.y), + vec2(p0.x, p0.y) + ); +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/draw_reduce.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/draw_reduce.wgsl new file mode 100644 index 000000000..7a12b8188 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/draw_reduce.wgsl @@ -0,0 +1,55 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +#import config +#import drawtag + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var scene: array; + +@group(0) @binding(2) +var reduced: array; + +const WG_SIZE = 256u; + +var sh_scratch: array; + +#import util + +@compute @workgroup_size(256) +fn main( + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + let num_blocks_total = (config.n_drawobj + (WG_SIZE - 1u)) / WG_SIZE; + // When the number of blocks exceeds the workgroup size, divide + // the work evenly so each workgroup handles n_blocks / wg, with + // the low workgroups doing one more each to handle the remainder. + let n_blocks_base = num_blocks_total / WG_SIZE; + let remainder = num_blocks_total % WG_SIZE; + let first_block = n_blocks_base * wg_id.x + min(wg_id.x, remainder); + let n_blocks = n_blocks_base + u32(wg_id.x < remainder); + var block_index = first_block * WG_SIZE + local_id.x; + var agg = draw_monoid_identity(); + for (var i = 0u; i < n_blocks; i++) { + let tag_word = read_draw_tag_from_scene(block_index); + agg = combine_draw_monoid(agg, map_draw_tag(tag_word)); + block_index += WG_SIZE; + } + sh_scratch[local_id.x] = agg; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_scratch[local_id.x + (1u << i)]; + agg = combine_draw_monoid(agg, other); + } + workgroupBarrier(); + sh_scratch[local_id.x] = agg; + } + if local_id.x == 0u { + reduced[wg_id.x] = agg; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/fine.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/fine.wgsl new file mode 100644 index 000000000..e3a57324b --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/fine.wgsl @@ -0,0 +1,1519 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Fine rasterizer. +// +// To enable multisampled rendering, turn on both the msaa ifdef and one of msaa8 +// or msaa16. + +struct Tile { + backdrop: i32, + segments: u32, +} + +#import segment +#import config +#import drawtag + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var segments: array; + +#import blend +#import ptcl + +const GRADIENT_WIDTH = 512; + +const IMAGE_QUALITY_LOW = 0u; +const IMAGE_QUALITY_MEDIUM = 1u; +const IMAGE_QUALITY_HIGH = 2u; + +const LUMINANCE_MASK_LAYER = 0x10000u; + +@group(0) @binding(2) +var ptcl: array; + +@group(0) @binding(3) +var info: array; + +@group(0) @binding(4) +var blend_spill: array; + +@group(0) @binding(5) +var output: texture_storage_2d; + +@group(0) @binding(6) +var gradients: texture_2d; + +@group(0) @binding(7) +var image_atlas: texture_2d; + +@group(0) @binding(8) +var backdrop_texture: texture_2d; + +// MSAA-only bindings and utilities +#ifdef msaa + +const MASK_LUT_INDEX: u32 = 8; + +#ifdef msaa8 +const MASK_WIDTH = 32u; +const MASK_HEIGHT = 32u; +const SH_SAMPLES_SIZE = 512u; +const SAMPLE_WORDS_PER_PIXEL = 2u; +// This might be better in uniform, but that has 16 byte alignment +@group(0) @binding(MASK_LUT_INDEX) +var mask_lut: array; +#endif + +#ifdef msaa16 +const MASK_WIDTH = 64u; +const MASK_HEIGHT = 64u; +const SH_SAMPLES_SIZE = 1024u; +const SAMPLE_WORDS_PER_PIXEL = 4u; +@group(0) @binding(MASK_LUT_INDEX) +var mask_lut: array; +#endif + +const WG_SIZE = 64u; +var sh_count: array; + +// This array contains the winding number of the top left corner of each +// 16 pixel wide row of pixels, relative to the top left corner of the row +// immediately above. +// +// The values are stored packed, as 4 8-bit subwords in a 32 bit word. +// The values are biased signed integers, with 0x80 representing a winding +// number of 0, so that the range of -128 to 127 (inclusive) can be stored +// without carry. +// +// For the even-odd case, the same storage is repurposed, so that a single +// word contains 16 one-bit winding parity values packed to the word. +var sh_winding_y: array, 4u>; +// This array contains the winding number of the top left corner of each +// 16 pixel wide row of pixels, relative to the top left corner of the tile. +// It is expanded from sh_winding_y by inclusive prefix sum. +var sh_winding_y_prefix: array, 4u>; +// This array contains winding numbers of the top left corner of each +// pixel, relative to the top left corner of the enclosing 16 pixel +// wide row. +// +// During winding number accumulation, it stores a delta (winding number +// relative to the pixel immediately to the left), then expanded using +// prefix sum and reusing the same storage. +// +// The encoding and packing is the same as `sh_winding_y`. For the even-odd +// case, only the first 16 values are used, and each word stores packed +// parity values for one row of pixels. +var sh_winding: array, 64u>; +// This array contains winding numbers of multiple sample points within +// a pixel, relative to the winding number of the top left corner of the +// pixel. The encoding and packing is the same as `sh_winding_y`. +var sh_samples: array, SH_SAMPLES_SIZE>; + +// number of integer cells spanned by interval defined by a, b +fn span(a: f32, b: f32) -> u32 { + return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); +} + +const SEG_SIZE = 5u; + +// See cpu_shaders/util.rs for explanation of these. +const ONE_MINUS_ULP: f32 = 0.99999994; +const ROBUST_EPSILON: f32 = 2e-7; + +// Multisampled path rendering algorithm. +// +// FIXME: This could return an array when https://github.com/gfx-rs/naga/issues/1930 is fixed. +// +// Generally, this algorithm works in an accumulation phase followed by a +// resolving phase, with arrays in workgroup shared memory accumulating +// winding number deltas as the results of edge crossings detected in the +// path segments. Accumulation is in two stages: first a counting stage +// which computes the number of pixels touched by each line segment (with +// each thread processing one line segment), then a stage in which the +// deltas are bumped. Separating these two is a partition-wide prefix sum +// and a binary search to assign the work to threads in a load-balanced +// manner. +// +// The resolving phase is also two stages: prefix sums in both x and y +// directions, then counting nonzero winding numbers for all samples within +// all pixels in the tile. +// +// A great deal of SIMD within a register (SWAR) logic is used, as there +// are a great many winding numbers to be computed. The interested reader +// is invited to study the even-odd case first, as there only one bit is +// needed to represent a winding number parity, thus there is a lot less +// bit shifting, and less shuffling altogether. +fn fill_path_ms(fill: CmdFill, local_id: vec2, result: ptr>) { + let even_odd = (fill.size_and_rule & 1u) != 0u; + // This isn't a divergent branch because the fill parameters are workgroup uniform, + // provably so because the ptcl buffer is bound read-only. + if even_odd { + fill_path_ms_evenodd(fill, local_id, result); + return; + } + let n_segs = fill.size_and_rule >> 1u; + let th_ix = local_id.y * (TILE_WIDTH / PIXELS_PER_THREAD) + local_id.x; + // Initialize winding number arrays to a winding number of 0, which is 0x80 in an + // 8 bit biased signed integer encoding. + if th_ix < 64u { + if th_ix < 4u { + atomicStore(&sh_winding_y[th_ix], 0x80808080u); + } + atomicStore(&sh_winding[th_ix], 0x80808080u); + } + let sample_count = PIXELS_PER_THREAD * SAMPLE_WORDS_PER_PIXEL; + for (var i = 0u; i < sample_count; i++) { + atomicStore(&sh_samples[th_ix * sample_count + i], 0x80808080u); + } + workgroupBarrier(); + let n_batch = (n_segs + (WG_SIZE - 1u)) / WG_SIZE; + for (var batch = 0u; batch < n_batch; batch++) { + let seg_ix = batch * WG_SIZE + th_ix; + let seg_off = fill.seg_data + seg_ix; + var count = 0u; + let slice_size = min(n_segs - batch * WG_SIZE, WG_SIZE); + // TODO: might save a register rewriting this in terms of limit + if th_ix < slice_size { + let segment = segments[seg_off]; + let xy0 = segment.point0; + let xy1 = segment.point1; + var y_edge_f = f32(TILE_HEIGHT); + var delta = select(-1, 1, xy1.x <= xy0.x); + if xy0.x == 0.0 { + y_edge_f = xy0.y; + } else if xy1.x == 0.0 { + y_edge_f = xy1.y; + } + // discard horizontal lines aligned to pixel grid + if !(xy0.y == xy1.y && xy0.y == floor(xy0.y)) { + count = span(xy0.x, xy1.x) + span(xy0.y, xy1.y) - 1u; + } + let y_edge = u32(ceil(y_edge_f)); + if y_edge < TILE_HEIGHT { + atomicAdd(&sh_winding_y[y_edge >> 2u], u32(delta) << ((y_edge & 3u) << 3u)); + } + } + // workgroup prefix sum of counts + sh_count[th_ix] = count; + let lg_n = firstLeadingBit(slice_size * 2u - 1u); + for (var i = 0u; i < lg_n; i++) { + workgroupBarrier(); + if th_ix >= 1u << i { + count += sh_count[th_ix - (1u << i)]; + } + workgroupBarrier(); + sh_count[th_ix] = count; + } + let total = workgroupUniformLoad(&sh_count[slice_size - 1u]); + for (var i = th_ix; i < total; i += WG_SIZE) { + // binary search to find pixel + var lo = 0u; + var hi = slice_size; + let goal = i; + while hi > lo + 1u { + let mid = (lo + hi) >> 1u; + if goal >= sh_count[mid - 1u] { + lo = mid; + } else { + hi = mid; + } + } + let el_ix = lo; + let last_pixel = i + 1u == sh_count[el_ix]; + let sub_ix = i - select(0u, sh_count[el_ix - 1u], el_ix > 0u); + let seg_off = fill.seg_data + batch * WG_SIZE + el_ix; + let segment = segments[seg_off]; + // Coordinates are relative to tile origin + let xy0_in = segment.point0; + let xy1_in = segment.point1; + let is_down = xy1_in.y >= xy0_in.y; + let xy0 = select(xy1_in, xy0_in, is_down); + let xy1 = select(xy0_in, xy1_in, is_down); + + // Set up data for line rasterization + // Note: this is duplicated work if total count exceeds a workgroup. + // One alternative is to compute it in a separate dispatch. + let dx = abs(xy1.x - xy0.x); + let dy = xy1.y - xy0.y; + let idxdy = 1.0 / (dx + dy); + var a = dx * idxdy; + // is_positive_slope is true for \ and | slopes, false for /. For + // horizontal lines, it follows the original data. + let is_positive_slope = xy1.x >= xy0.x; + let x_sign = select(-1.0, 1.0, is_positive_slope); + let xt0 = floor(xy0.x * x_sign); + let c = xy0.x * x_sign - xt0; + let y0i = floor(xy0.y); + let ytop = y0i + 1.0; + let b = min((dy * c + dx * (ytop - xy0.y)) * idxdy, ONE_MINUS_ULP); + let count_x = span(xy0.x, xy1.x) - 1u; + let count = count_x + span(xy0.y, xy1.y); + let robust_err = floor(a * (f32(count) - 1.0) + b) - f32(count_x); + if robust_err != 0.0 { + a -= ROBUST_EPSILON * sign(robust_err); + } + let x0i = i32(xt0 * x_sign + 0.5 * (x_sign - 1.0)); + // Use line equation to plot pixel coordinates + + let zf = a * f32(sub_ix) + b; + let z = floor(zf); + let x = x0i + i32(x_sign * z); + let y = i32(y0i) + i32(sub_ix) - i32(z); + // is_delta captures whether the line crosses the top edge of this + // pixel. If so, then a delta is added to `sh_winding`, followed by + // a prefix sum, so that a winding number delta is applied to all + // pixels to the right of this one. + var is_delta: bool; + // is_bump captures whether x0 crosses the left edge of this pixel. + var is_bump = false; + let zp = floor(a * f32(sub_ix - 1u) + b); + if sub_ix == 0u { + // The first (top-most) pixel in the line. It is considered to be + // a line crossing when it touches the top of the pixel. + // + // Note: horizontal lines aligned to the pixel grid have already + // been discarded. + is_delta = y0i == xy0.y; + // The pixel is counted as a left edge crossing only at the left + // edge of the tile (and when it is not the top left corner, + // using logic analogous to tiling). + is_bump = xy0.x == 0.0 && y0i != xy0.y; + } else { + // Pixels other than the first are a crossing at the top or on + // the side, based on the conservative line rasterization. When + // positive slope, the crossing is on the left. + is_delta = z == zp; + is_bump = is_positive_slope && !is_delta; + } + let pix_ix = u32(y) * TILE_WIDTH + u32(x); + if u32(x) < TILE_WIDTH - 1u && u32(y) < TILE_HEIGHT { + let delta_pix = pix_ix + 1u; + if is_delta { + let delta = select(u32(-1i), 1u, is_down) << ((delta_pix & 3u) << 3u); + atomicAdd(&sh_winding[delta_pix >> 2u], delta); + } + } + // Apply sample mask + let mask_block = u32(is_positive_slope) * (MASK_WIDTH * MASK_HEIGHT / 2u); + let half_height = f32(MASK_HEIGHT / 2u); + let mask_row = floor(min(a * half_height, half_height - 1.0)) * f32(MASK_WIDTH); + let mask_col = floor((zf - z) * f32(MASK_WIDTH)); + let mask_ix = mask_block + u32(mask_row + mask_col); +#ifdef msaa8 + var mask = mask_lut[mask_ix / 4u] >> ((mask_ix % 4u) * 8u); + mask &= 0xffu; + // Intersect with y half-plane masks + if sub_ix == 0u && !is_bump { + let mask_shift = u32(round(8.0 * (xy0.y - f32(y)))); + mask &= 0xffu << mask_shift; + } + if last_pixel && xy1.x != 0.0 { + let mask_shift = u32(round(8.0 * (xy1.y - f32(y)))); + mask &= ~(0xffu << mask_shift); + } + // Expand an 8 bit mask value to 8 1-bit values, packed 4 to a subword, + // so that two words are used to represent the result. An efficient + // technique is carry-less multiplication by 0b10_0000_0100_0000_1000_0001 + // followed by and-masking to extract bit in position 4 * k. + // + // See https://en.wikipedia.org/wiki/Carry-less_product + let mask_a = mask ^ (mask << 7u); + let mask_b = mask_a ^ (mask_a << 14u); + let mask0_exp = mask_b & 0x1010101u; + var mask0_signed = select(mask0_exp, u32(-i32(mask0_exp)), is_down); + let mask1_exp = (mask_b >> 4u) & 0x1010101u; + var mask1_signed = select(mask1_exp, u32(-i32(mask1_exp)), is_down); + if is_bump { + let bump_delta = select(u32(-0x1010101i), 0x1010101u, is_down); + mask0_signed += bump_delta; + mask1_signed += bump_delta; + } + atomicAdd(&sh_samples[pix_ix * 2u], mask0_signed); + atomicAdd(&sh_samples[pix_ix * 2u + 1u], mask1_signed); +#endif +#ifdef msaa16 + var mask = mask_lut[mask_ix / 2u] >> ((mask_ix % 2u) * 16u); + mask &= 0xffffu; + // Intersect with y half-plane masks + if sub_ix == 0u && !is_bump { + let mask_shift = u32(round(16.0 * (xy0.y - f32(y)))); + mask &= 0xffffu << mask_shift; + } + if last_pixel && xy1.x != 0.0 { + let mask_shift = u32(round(16.0 * (xy1.y - f32(y)))); + mask &= ~(0xffffu << mask_shift); + } + // Similar logic as above, only a 16 bit mask is divided into + // two 8 bit halves first, then each is expanded as above. + // Mask is 0bABCD_EFGH_IJKL_MNOP. Expand to 4 32 bit words + // mask0_exp will be 0b0000_000M_0000_000N_0000_000O_0000_000P + // mask3_exp will be 0b0000_000A_0000_000B_0000_000C_0000_000D + let mask0 = mask & 0xffu; + // mask0_a = 0b0IJK_LMNO_*JKL_MNOP + let mask0_a = mask0 ^ (mask0 << 7u); + // mask0_b = 0b000I_JKLM_NO*J_KLMN_O*K_LMNO_*JKL_MNOP + // ^ ^ ^ ^ ^ ^ ^ ^ + let mask0_b = mask0_a ^ (mask0_a << 14u); + let mask0_exp = mask0_b & 0x1010101u; + var mask0_signed = select(mask0_exp, u32(-i32(mask0_exp)), is_down); + let mask1_exp = (mask0_b >> 4u) & 0x1010101u; + var mask1_signed = select(mask1_exp, u32(-i32(mask1_exp)), is_down); + let mask1 = (mask >> 8u) & 0xffu; + let mask1_a = mask1 ^ (mask1 << 7u); + // mask1_a = 0b0ABC_DEFG_*BCD_EFGH + let mask1_b = mask1_a ^ (mask1_a << 14u); + // mask1_b = 0b000A_BCDE_FG*B_CDEF_G*C_DEFG_*BCD_EFGH + let mask2_exp = mask1_b & 0x1010101u; + var mask2_signed = select(mask2_exp, u32(-i32(mask2_exp)), is_down); + let mask3_exp = (mask1_b >> 4u) & 0x1010101u; + var mask3_signed = select(mask3_exp, u32(-i32(mask3_exp)), is_down); + if is_bump { + let bump_delta = select(u32(-0x1010101i), 0x1010101u, is_down); + mask0_signed += bump_delta; + mask1_signed += bump_delta; + mask2_signed += bump_delta; + mask3_signed += bump_delta; + } + atomicAdd(&sh_samples[pix_ix * 4u], mask0_signed); + atomicAdd(&sh_samples[pix_ix * 4u + 1u], mask1_signed); + atomicAdd(&sh_samples[pix_ix * 4u + 2u], mask2_signed); + atomicAdd(&sh_samples[pix_ix * 4u + 3u], mask3_signed); +#endif + } + workgroupBarrier(); + } + var area: array; + let major = (th_ix * PIXELS_PER_THREAD) >> 2u; + var packed_w = atomicLoad(&sh_winding[major]); + // Compute prefix sums of both `sh_winding` and `sh_winding_y`. Both + // use the same technique. First, a per-word prefix sum is computed + // of the 4 subwords within each word. The last subword is the sum + // (reduction) of that group of 4 values, and is stored to shared + // memory for broadcast to other threads. Then each thread computes + // the prefix by adding the preceding reduced values. + // + // Addition of 2 biased signed values is accomplished by adding the + // values, then subtracting the bias. + packed_w += (packed_w - 0x808080u) << 8u; + packed_w += (packed_w - 0x8080u) << 16u; + var packed_y = atomicLoad(&sh_winding_y[local_id.y >> 2u]); + packed_y += (packed_y - 0x808080u) << 8u; + packed_y += (packed_y - 0x8080u) << 16u; + var wind_y = (packed_y >> ((local_id.y & 3u) << 3u)) - 0x80u; + if (local_id.y & 3u) == 3u && local_id.x == 0u { + let prefix_y = wind_y; + atomicStore(&sh_winding_y_prefix[local_id.y >> 2u], prefix_y); + } + let prefix_x = ((packed_w >> 24u) - 0x80u) * 0x1010101u; + // reuse sh_winding to store prefix as well + atomicStore(&sh_winding[major], prefix_x); + workgroupBarrier(); + for (var i = (major & ~3u); i < major; i++) { + packed_w += atomicLoad(&sh_winding[i]); + } + // packed_w now contains the winding numbers for a slice of 4 pixels, + // each relative to the top left of the row. + for (var i = 0u; i < (local_id.y >> 2u); i++) { + wind_y += atomicLoad(&sh_winding_y_prefix[i]); + } + // wind_y now contains the winding number of the top left of the row of + // pixels relative to the top left of the tile. Note that this is actually + // a signed quantity stored without bias. + + // The winding number of a sample point is the sum of four levels of + // hierarchy: + // * The winding number of the top left of the tile (backdrop) + // * The winding number of the pixel row relative to tile (wind_y) + // * The winding number of the pixel relative to row (packed_w) + // * The winding number of the sample relative to pixel (sh_samples) + // + // Conceptually, we want to compute each of these total winding numbers + // for each sample within a pixel, then count the number that are non-zero. + // However, we apply a shortcut, partly to make the computation more + // efficient, and partly to avoid overflow of intermediate results. + // + // Here's the technique that's used. The `expected_zero` value contains + // the *negation* of the sum of the first three levels of the hierarchy. + // Thus, `sample - expected` is zero when the sum of all levels in the + // hierarchy is zero, and this is true when `sample = expected`. We + // compute this using SWAR techniques as follows: we compute the xor of + // all bits of `expected` (repeated to all subwords) against the packed + // samples, then the or-reduction of the bits within each subword. This + // value is 1 when the values are unequal, thus the sum is nonzero, and + // 0 when the sum is zero. These bits are then masked and counted. + + for (var i = 0u; i < PIXELS_PER_THREAD; i++) { + let pix_ix = th_ix * PIXELS_PER_THREAD + i; + let minor = i; // assumes PIXELS_PER_THREAD == 4 + let expected_zero = (((packed_w >> (minor * 8u)) + wind_y) & 0xffu) - u32(fill.backdrop); + // When the expected_zero value exceeds the range of what can be stored + // in a (biased) signed integer, then there is no sample value that can + // be equal to the expected value, thus all resulting bits are 1. + if expected_zero >= 256u { + area[i] = 1.0; + } else { +#ifdef msaa8 + let samples0 = atomicLoad(&sh_samples[pix_ix * 2u]); + let samples1 = atomicLoad(&sh_samples[pix_ix * 2u + 1u]); + let xored0 = (expected_zero * 0x1010101u) ^ samples0; + let xored0_2 = xored0 | (xored0 * 2u); + let xored1 = (expected_zero * 0x1010101u) ^ samples1; + let xored1_2 = xored1 | (xored1 >> 1u); + // xored2 contains 2-reductions from each word, interleaved + let xored2 = (xored0_2 & 0xAAAAAAAAu) | (xored1_2 & 0x55555555u); + // bits 4 * k + 2 and 4 * k + 3 contain 4-reductions + let xored4 = xored2 | (xored2 * 4u); + // bits 8 * k + 6 and 8 * k + 7 contain 8-reductions + let xored8 = xored4 | (xored4 * 16u); + area[i] = f32(countOneBits(xored8 & 0xC0C0C0C0u)) * 0.125; +#endif +#ifdef msaa16 + let samples0 = atomicLoad(&sh_samples[pix_ix * 4u]); + let samples1 = atomicLoad(&sh_samples[pix_ix * 4u + 1u]); + let samples2 = atomicLoad(&sh_samples[pix_ix * 4u + 2u]); + let samples3 = atomicLoad(&sh_samples[pix_ix * 4u + 3u]); + let xored0 = (expected_zero * 0x1010101u) ^ samples0; + let xored0_2 = xored0 | (xored0 * 2u); + let xored1 = (expected_zero * 0x1010101u) ^ samples1; + let xored1_2 = xored1 | (xored1 >> 1u); + // xored01 contains 2-reductions from words 0 and 1, interleaved + let xored01 = (xored0_2 & 0xAAAAAAAAu) | (xored1_2 & 0x55555555u); + // bits 4 * k + 2 and 4 * k + 3 contain 4-reductions + let xored01_4 = xored01 | (xored01 * 4u); + let xored2 = (expected_zero * 0x1010101u) ^ samples2; + let xored2_2 = xored2 | (xored2 * 2u); + let xored3 = (expected_zero * 0x1010101u) ^ samples3; + let xored3_2 = xored3 | (xored3 >> 1u); + // xored23 contains 2-reductions from words 2 and 3, interleaved + let xored23 = (xored2_2 & 0xAAAAAAAAu) | (xored3_2 & 0x55555555u); + // bits 4 * k and 4 * k + 1 contain 4-reductions + let xored23_4 = xored23 | (xored23 >> 2u); + // each bit is a 4-reduction, with values from all 4 words + let xored4 = (xored01_4 & 0xCCCCCCCCu) | (xored23_4 & 0x33333333u); + // bits 8 * k + {4, 5, 6, 7} contain 8-reductions + let xored8 = xored4 | (xored4 * 16u); + area[i] = f32(countOneBits(xored8 & 0xF0F0F0F0u)) * 0.0625; +#endif + } + } + *result = area; +} + +// Path rendering specialized to the even-odd rule. +// +// This proceeds very much the same as `fill_path_ms`, but is simpler because +// all winding numbers can be represented in one bit. Formally, addition is +// modulo 2, or, equivalently, winding numbers are elements of GF(2). One +// simplification is that we don't need to track the direction of crossings, +// as both have the same effect on winding number. +// +// TODO: factor some logic out to reduce code duplication. +fn fill_path_ms_evenodd(fill: CmdFill, local_id: vec2, result: ptr>) { + let n_segs = fill.size_and_rule >> 1u; + let th_ix = local_id.y * (TILE_WIDTH / PIXELS_PER_THREAD) + local_id.x; + if th_ix < TILE_HEIGHT { + if th_ix == 0u { + atomicStore(&sh_winding_y[th_ix], 0u); + } + atomicStore(&sh_winding[th_ix], 0u); + } + let sample_count = PIXELS_PER_THREAD; + for (var i = 0u; i < sample_count; i++) { + atomicStore(&sh_samples[th_ix * sample_count + i], 0u); + } + workgroupBarrier(); + let n_batch = (n_segs + (WG_SIZE - 1u)) / WG_SIZE; + for (var batch = 0u; batch < n_batch; batch++) { + let seg_ix = batch * WG_SIZE + th_ix; + let seg_off = fill.seg_data + seg_ix; + var count = 0u; + let slice_size = min(n_segs - batch * WG_SIZE, WG_SIZE); + // TODO: might save a register rewriting this in terms of limit + if th_ix < slice_size { + let segment = segments[seg_off]; + // Coordinates are relative to tile origin + let xy0 = segment.point0; + let xy1 = segment.point1; + var y_edge_f = f32(TILE_HEIGHT); + if xy0.x == 0.0 { + y_edge_f = xy0.y; + } else if xy1.x == 0.0 { + y_edge_f = xy1.y; + } + // discard horizontal lines aligned to pixel grid + if !(xy0.y == xy1.y && xy0.y == floor(xy0.y)) { + count = span(xy0.x, xy1.x) + span(xy0.y, xy1.y) - 1u; + } + let y_edge = u32(ceil(y_edge_f)); + if y_edge < TILE_HEIGHT { + atomicXor(&sh_winding_y[0], 1u << y_edge); + } + } + // workgroup prefix sum of counts + sh_count[th_ix] = count; + let lg_n = firstLeadingBit(slice_size * 2u - 1u); + for (var i = 0u; i < lg_n; i++) { + workgroupBarrier(); + if th_ix >= 1u << i { + count += sh_count[th_ix - (1u << i)]; + } + workgroupBarrier(); + sh_count[th_ix] = count; + } + let total = workgroupUniformLoad(&sh_count[slice_size - 1u]); + for (var i = th_ix; i < total; i += WG_SIZE) { + // binary search to find pixel + var lo = 0u; + var hi = slice_size; + let goal = i; + while hi > lo + 1u { + let mid = (lo + hi) >> 1u; + if goal >= sh_count[mid - 1u] { + lo = mid; + } else { + hi = mid; + } + } + let el_ix = lo; + let last_pixel = i + 1u == sh_count[el_ix]; + let sub_ix = i - select(0u, sh_count[el_ix - 1u], el_ix > 0u); + let seg_off = fill.seg_data + batch * WG_SIZE + el_ix; + let segment = segments[seg_off]; + let xy0_in = segment.point0; + let xy1_in = segment.point1; + let is_down = xy1_in.y >= xy0_in.y; + let xy0 = select(xy1_in, xy0_in, is_down); + let xy1 = select(xy0_in, xy1_in, is_down); + + // Set up data for line rasterization + // Note: this is duplicated work if total count exceeds a workgroup. + // One alternative is to compute it in a separate dispatch. + let dx = abs(xy1.x - xy0.x); + let dy = xy1.y - xy0.y; + let idxdy = 1.0 / (dx + dy); + var a = dx * idxdy; + let is_positive_slope = xy1.x >= xy0.x; + let x_sign = select(-1.0, 1.0, is_positive_slope); + let xt0 = floor(xy0.x * x_sign); + let c = xy0.x * x_sign - xt0; + let y0i = floor(xy0.y); + let ytop = y0i + 1.0; + let b = min((dy * c + dx * (ytop - xy0.y)) * idxdy, ONE_MINUS_ULP); + let count_x = span(xy0.x, xy1.x) - 1u; + let count = count_x + span(xy0.y, xy1.y); + let robust_err = floor(a * (f32(count) - 1.0) + b) - f32(count_x); + if robust_err != 0.0 { + a -= ROBUST_EPSILON * sign(robust_err); + } + let x0i = i32(xt0 * x_sign + 0.5 * (x_sign - 1.0)); + // Use line equation to plot pixel coordinates + + let zf = a * f32(sub_ix) + b; + let z = floor(zf); + let x = x0i + i32(x_sign * z); + let y = i32(y0i) + i32(sub_ix) - i32(z); + var is_delta: bool; + // See comments in nonzero case. + var is_bump = false; + let zp = floor(a * f32(sub_ix - 1u) + b); + if sub_ix == 0u { + is_delta = y0i == xy0.y; + is_bump = xy0.x == 0.0; + } else { + is_delta = z == zp; + is_bump = is_positive_slope && !is_delta; + } + if u32(x) < TILE_WIDTH - 1u && u32(y) < TILE_HEIGHT { + if is_delta { + atomicXor(&sh_winding[y], 2u << u32(x)); + } + } + // Apply sample mask + let mask_block = u32(is_positive_slope) * (MASK_WIDTH * MASK_HEIGHT / 2u); + let half_height = f32(MASK_HEIGHT / 2u); + let mask_row = floor(min(a * half_height, half_height - 1.0)) * f32(MASK_WIDTH); + let mask_col = floor((zf - z) * f32(MASK_WIDTH)); + let mask_ix = mask_block + u32(mask_row + mask_col); + let pix_ix = u32(y) * TILE_WIDTH + u32(x); +#ifdef msaa8 + var mask = mask_lut[mask_ix / 4u] >> ((mask_ix % 4u) * 8u); + mask &= 0xffu; + // Intersect with y half-plane masks + if sub_ix == 0u && !is_bump { + let mask_shift = u32(round(8.0 * (xy0.y - f32(y)))); + mask &= 0xffu << mask_shift; + } + if last_pixel && xy1.x != 0.0 { + let mask_shift = u32(round(8.0 * (xy1.y - f32(y)))); + mask &= ~(0xffu << mask_shift); + } + if is_bump { + mask ^= 0xffu; + } + atomicXor(&sh_samples[pix_ix], mask); +#endif +#ifdef msaa16 + var mask = mask_lut[mask_ix / 2u] >> ((mask_ix % 2u) * 16u); + mask &= 0xffffu; + // Intersect with y half-plane masks + if sub_ix == 0u && !is_bump { + let mask_shift = u32(round(16.0 * (xy0.y - f32(y)))); + mask &= 0xffffu << mask_shift; + } + if last_pixel && xy1.x != 0.0 { + let mask_shift = u32(round(16.0 * (xy1.y - f32(y)))); + mask &= ~(0xffffu << mask_shift); + } + if is_bump { + mask ^= 0xffffu; + } + atomicXor(&sh_samples[pix_ix], mask); +#endif + } + workgroupBarrier(); + } + var area: array; + var scan_x = atomicLoad(&sh_winding[local_id.y]); + // prefix sum over GF(2) is equivalent to carry-less multiplication + // by 0xFFFF + scan_x ^= scan_x << 1u; + scan_x ^= scan_x << 2u; + scan_x ^= scan_x << 4u; + scan_x ^= scan_x << 8u; + // scan_x contains the winding number parity for all pixels in the row + var scan_y = atomicLoad(&sh_winding_y[0]); + scan_y ^= scan_y << 1u; + scan_y ^= scan_y << 2u; + scan_y ^= scan_y << 4u; + scan_y ^= scan_y << 8u; + // winding number parity for the row of pixels is in the LSB of row_parity + let row_parity = (scan_y >> local_id.y) ^ u32(fill.backdrop); + + for (var i = 0u; i < PIXELS_PER_THREAD; i++) { + let pix_ix = th_ix * PIXELS_PER_THREAD + i; + let samples = atomicLoad(&sh_samples[pix_ix]); + let pix_parity = row_parity ^ (scan_x >> (pix_ix % TILE_WIDTH)); + // The LSB of pix_parity contains the sum of the first three levels + // of the hierarchy, thus the absolute winding number of the top left + // of the pixel. + let pix_mask = u32(-i32(pix_parity & 1u)); + // pix_mask is pix_party broadcast to all bits in the word. +#ifdef msaa8 + area[i] = f32(countOneBits((samples ^ pix_mask) & 0xffu)) * 0.125; +#endif +#ifdef msaa16 + area[i] = f32(countOneBits((samples ^ pix_mask) & 0xffffu)) * 0.0625; +#endif + } + *result = area; +} +#endif // msaa + +// Error function approximation. +// +// https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html +fn erf7(x: f32) -> f32 { + // Clamp to prevent overflow. + // Intermediate steps calculate pow(x, 14). + let y = clamp(x * 1.1283791671, -100.0, 100.0); + let yy = y * y; + let z = y + (0.24295 + (0.03395 + 0.0104 * yy) * yy) * (y * yy); + return z / sqrt(1.0 + z * z); +} + +fn hypot(a: f32, b: f32) -> f32 { + return sqrt(a * a + b * b); +} + +fn read_fill(cmd_ix: u32) -> CmdFill { + let size_and_rule = ptcl[cmd_ix + 1u]; + let seg_data = ptcl[cmd_ix + 2u]; + let backdrop = i32(ptcl[cmd_ix + 3u]); + return CmdFill(size_and_rule, seg_data, backdrop); +} + +fn read_color(cmd_ix: u32) -> CmdColor { + let rgba_color = ptcl[cmd_ix + 1u]; + let draw_flags = ptcl[cmd_ix + 2u]; + return CmdColor(rgba_color, draw_flags); +} + +fn read_recolor(cmd_ix: u32) -> CmdRecolor { + let source_color = ptcl[cmd_ix + 1u]; + let target_color = ptcl[cmd_ix + 2u]; + let threshold = bitcast(ptcl[cmd_ix + 3u]); + let draw_flags = ptcl[cmd_ix + 4u]; + return CmdRecolor(source_color, target_color, threshold, draw_flags); +} + +fn read_blur_rect(cmd_ix: u32) -> CmdBlurRect { + let info_offset = ptcl[cmd_ix + 1u]; + let rgba_color = ptcl[cmd_ix + 2u]; + + let m0 = bitcast(info[info_offset]); + let m1 = bitcast(info[info_offset + 1u]); + let m2 = bitcast(info[info_offset + 2u]); + let m3 = bitcast(info[info_offset + 3u]); + let matrx = vec4(m0, m1, m2, m3); + let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); + let width = bitcast(info[info_offset + 6u]); + let height = bitcast(info[info_offset + 7u]); + let radius = bitcast(info[info_offset + 8u]); + let std_dev = bitcast(info[info_offset + 9u]); + + return CmdBlurRect(rgba_color, matrx, xlat, width, height, radius, std_dev); +} + +fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad { + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; + let info_offset = ptcl[cmd_ix + 2u]; + let line_x = bitcast(info[info_offset]); + let line_y = bitcast(info[info_offset + 1u]); + let line_c = bitcast(info[info_offset + 2u]); + return CmdLinGrad(index, extend_mode, line_x, line_y, line_c); +} + +fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; + let info_offset = ptcl[cmd_ix + 2u]; + let m0 = bitcast(info[info_offset]); + let m1 = bitcast(info[info_offset + 1u]); + let m2 = bitcast(info[info_offset + 2u]); + let m3 = bitcast(info[info_offset + 3u]); + let matrx = vec4(m0, m1, m2, m3); + let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); + let focal_x = bitcast(info[info_offset + 6u]); + let radius = bitcast(info[info_offset + 7u]); + let flags_kind = info[info_offset + 8u]; + let flags = flags_kind >> 3u; + let kind = flags_kind & 0x7u; + return CmdRadGrad(index, extend_mode, matrx, xlat, focal_x, radius, kind, flags); +} + +fn read_elliptic_grad(cmd_ix: u32) -> CmdEllipticGrad { + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; + let info_offset = ptcl[cmd_ix + 2u]; + let m0 = bitcast(info[info_offset]); + let m1 = bitcast(info[info_offset + 1u]); + let m2 = bitcast(info[info_offset + 2u]); + let m3 = bitcast(info[info_offset + 3u]); + let matrx = vec4(m0, m1, m2, m3); + let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); + return CmdEllipticGrad(index, extend_mode, matrx, xlat); +} + +fn read_sweep_grad(cmd_ix: u32) -> CmdSweepGrad { + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; + let info_offset = ptcl[cmd_ix + 2u]; + let m0 = bitcast(info[info_offset]); + let m1 = bitcast(info[info_offset + 1u]); + let m2 = bitcast(info[info_offset + 2u]); + let m3 = bitcast(info[info_offset + 3u]); + let matrx = vec4(m0, m1, m2, m3); + let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); + let t0 = bitcast(info[info_offset + 6u]); + let t1 = bitcast(info[info_offset + 7u]); + return CmdSweepGrad(index, extend_mode, matrx, xlat, t0, t1); +} + +fn read_image(cmd_ix: u32) -> CmdImage { + let info_offset = ptcl[cmd_ix + 1u]; + let m0 = bitcast(info[info_offset]); + let m1 = bitcast(info[info_offset + 1u]); + let m2 = bitcast(info[info_offset + 2u]); + let m3 = bitcast(info[info_offset + 3u]); + let matrx = vec4(m0, m1, m2, m3); + let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); + let xy = info[info_offset + 6u]; + let width_height = info[info_offset + 7u]; + let sample_alpha = info[info_offset + 8u]; + let alpha = f32(sample_alpha & 0xFFu) / 255.0; + let format = sample_alpha >> 15u; + let alpha_type = (sample_alpha >> 14u) & 0x1u; + let quality = (sample_alpha >> 12u) & 0x3u; + let x_extend = (sample_alpha >> 10u) & 0x3u; + let y_extend = (sample_alpha >> 8u) & 0x3u; + // The following are not intended to be bitcasts + let x = f32(xy >> 16u); + let y = f32(xy & 0xffffu); + let width = f32(width_height >> 16u); + let height = f32(width_height & 0xffffu); + return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), format, x_extend, y_extend, quality, alpha, alpha_type); +} + +fn read_end_clip(cmd_ix: u32) -> CmdEndClip { + let blend = ptcl[cmd_ix + 1u]; + let alpha = bitcast(ptcl[cmd_ix + 2u]); + return CmdEndClip(blend, alpha); +} + +fn read_draw_blend_mode(draw_flags: u32) -> u32 { + return (draw_flags & DRAW_FLAGS_BLEND_MODE_MASK) >> DRAW_FLAGS_BLEND_MODE_SHIFT; +} + +fn read_draw_mix_mode(draw_flags: u32) -> u32 { + return read_draw_blend_mode(draw_flags) >> 8u; +} + +fn read_draw_compose_mode(draw_flags: u32) -> u32 { + return read_draw_blend_mode(draw_flags) & 0xffu; +} + +fn read_draw_blend_alpha(draw_flags: u32) -> f32 { + let packed = (draw_flags & DRAW_FLAGS_BLEND_ALPHA_MASK) >> DRAW_FLAGS_BLEND_ALPHA_SHIFT; + return f32(packed) / 65535.0; +} + +fn is_default_draw_blend(draw_flags: u32) -> bool { + return read_draw_blend_mode(draw_flags) == ((MIX_NORMAL << 8u) | COMPOSE_SRC_OVER) + && (draw_flags & DRAW_FLAGS_BLEND_ALPHA_MASK) == DRAW_FLAGS_BLEND_ALPHA_MASK; +} + +fn compose_draw(backdrop: vec4, source: vec4, draw_flags: u32) -> vec4 { + let effective_alpha = source.a * read_draw_blend_alpha(draw_flags); + if effective_alpha <= (0.5 / 255.0) { + return backdrop; + } + + if is_default_draw_blend(draw_flags) { + return backdrop * (1.0 - source.a) + source; + } + + let cb = unpremultiply(backdrop); + let cs = unpremultiply(source); + let ab = backdrop.a; + let as_ = effective_alpha; + let mix_mode = read_draw_mix_mode(draw_flags); + let compose_mode = read_draw_compose_mode(draw_flags); + let shared_alpha = as_ * ab; + + switch compose_mode { + case COMPOSE_CLEAR: { + return vec4(0.0); + } + case COMPOSE_COPY: { + return vec4(cs * as_, as_); + } + case COMPOSE_DEST: { + return backdrop; + } + case COMPOSE_SRC_OVER: { + let blend = blend_mix(cb, cs, mix_mode); + let dst_weight = ab - shared_alpha; + let src_weight = as_ - shared_alpha; + let alpha = dst_weight + as_; + let premul = (cb * dst_weight) + (cs * src_weight) + (blend * shared_alpha); + return vec4(premul, alpha); + } + case COMPOSE_DEST_OVER: { + let blend = blend_mix(cs, cb, mix_mode); + let dst_weight = as_ - shared_alpha; + let src_weight = ab - shared_alpha; + let alpha = dst_weight + ab; + let premul = (cs * dst_weight) + (cb * src_weight) + (blend * shared_alpha); + return vec4(premul, alpha); + } + case COMPOSE_SRC_IN: { + return vec4(cs * shared_alpha, shared_alpha); + } + case COMPOSE_DEST_IN: { + return vec4(cb * shared_alpha, shared_alpha); + } + case COMPOSE_SRC_OUT: { + let alpha = as_ * (1.0 - ab); + return vec4(cs * alpha, alpha); + } + case COMPOSE_DEST_OUT: { + let alpha = ab * (1.0 - as_); + return vec4(cb * alpha, alpha); + } + case COMPOSE_SRC_ATOP: { + let blend = blend_mix(cb, cs, mix_mode); + let dst_weight = ab - shared_alpha; + let premul = (cb * dst_weight) + (blend * shared_alpha); + return vec4(premul, ab); + } + case COMPOSE_DEST_ATOP: { + let blend = blend_mix(cs, cb, mix_mode); + let dst_weight = as_ - shared_alpha; + let premul = (cs * dst_weight) + (blend * shared_alpha); + return vec4(premul, as_); + } + case COMPOSE_XOR: { + let src_weight = as_ * (1.0 - ab); + let dst_weight = ab * (1.0 - as_); + return vec4((cs * src_weight) + (cb * dst_weight), src_weight + dst_weight); + } + default: { + return blend_mix_compose(backdrop, source * read_draw_blend_alpha(draw_flags), read_draw_blend_mode(draw_flags)); + } + } +} + +const PIXEL_FORMAT_RGBA: u32 = 0u; +const PIXEL_FORMAT_BGRA: u32 = 1u; +// Normalises subpixel order loaded from an image, based on the image's format. +fn pixel_format(pixel: vec4f, format: u32) -> vec4f { + switch format { + case PIXEL_FORMAT_BGRA: { + // The conversion from RGBA to BGRA is its own inverse. + return pixel.bgra; + } + case PIXEL_FORMAT_RGBA, default: { + return pixel; + } + } +} + +const ALPHA: u32 = 0u; +const PREMULTIPLIED_ALPHA: u32 = 1u; +// Premultiplies alpha if not already +fn maybe_premul_alpha(pixel: vec4f, alpha_type: u32) -> vec4f { + switch alpha_type { + case PREMULTIPLIED_ALPHA: { + return pixel; + } + case ALPHA, default: { + return premul_alpha(pixel); + } + } +} + +const EXTEND_PAD: u32 = 0u; +const EXTEND_REPEAT: u32 = 1u; +const EXTEND_REFLECT: u32 = 2u; +fn extend_mode_normalized(t: f32, mode: u32) -> f32 { + switch mode { + case EXTEND_PAD: { + return clamp(t, 0.0, 1.0); + } + case EXTEND_REPEAT: { + // The CPU gradient brushes do not wrap values before the first stop. + // They hold the first stop for t < 0 and only repeat for t >= 0. + return select(fract(t), 0.0, t < 0.0); + } + case EXTEND_REFLECT, default: { + // Likewise, CPU reflection clamps negative values to the first stop + // and only reflects once the parameter moves forward beyond 0. + let clamped = max(t, 0.0); + return abs(clamped - 2.0 * round(0.5 * clamped)); + } + } +} + +fn extend_mode(t: f32, mode: u32, max: f32) -> f32 { + switch mode { + case EXTEND_PAD: { + return clamp(t, 0.0, max); + } + case EXTEND_REPEAT: { + return extend_mode_normalized(t / max, mode) * max; + } + case EXTEND_REFLECT, default: { + return extend_mode_normalized(t / max, mode) * max; + } + } +} + +fn image_extend_mode_normalized(t: f32, mode: u32) -> f32 { + switch mode { + case EXTEND_PAD: { + return clamp(t, 0.0, 1.0); + } + case EXTEND_REPEAT: { + return fract(t); + } + case EXTEND_REFLECT, default: { + let reflected = fract(0.5 * t) * 2.0; + return 1.0 - abs(reflected - 1.0); + } + } +} + +fn image_extend_mode(t: f32, mode: u32, max: f32) -> f32 { + switch mode { + case EXTEND_PAD: { + return clamp(t, 0.0, max); + } + case EXTEND_REPEAT: { + return image_extend_mode_normalized(t / max, mode) * max; + } + case EXTEND_REFLECT, default: { + return image_extend_mode_normalized(t / max, mode) * max; + } + } +} + +fn image_extend_mode_i32(t: f32, mode: u32, max: i32) -> i32 { + switch mode { + case EXTEND_PAD: { + return clamp(i32(t), 0, max - 1); + } + case EXTEND_REPEAT: { + let wrapped = i32(t) % max; + return select(wrapped + max, wrapped, wrapped >= 0); + } + case EXTEND_REFLECT, default: { + let reflected = clamp(image_extend_mode(t, mode, f32(max)), 0.0, f32(max - 1)); + return i32(reflected); + } + } +} + +const PIXELS_PER_THREAD = 4u; + +#ifndef msaa + +// Analytic area anti-aliasing. +// +// This is currently dead code if msaa is enabled, but it would be fairly straightforward +// to wire this so it's a dynamic choice (even per-path). +// +// FIXME: This should return an array when https://github.com/gfx-rs/naga/issues/1930 is fixed. +fn fill_path(fill: CmdFill, xy: vec2, result: ptr>) { + let n_segs = fill.size_and_rule >> 1u; + let even_odd = (fill.size_and_rule & 1u) != 0u; + var area: array; + let backdrop_f = f32(fill.backdrop); + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + area[i] = backdrop_f; + } + for (var i = 0u; i < n_segs; i++) { + let seg_off = fill.seg_data + i; + let segment = segments[seg_off]; + let y = segment.point0.y - xy.y; + let delta = segment.point1 - segment.point0; + let y0 = clamp(y, 0.0, 1.0); + let y1 = clamp(y + delta.y, 0.0, 1.0); + let dy = y0 - y1; + if dy != 0.0 { + let vec_y_recip = 1.0 / delta.y; + let t0 = (y0 - y) * vec_y_recip; + let t1 = (y1 - y) * vec_y_recip; + let startx = segment.point0.x - xy.x; + let x0 = startx + t0 * delta.x; + let x1 = startx + t1 * delta.x; + let xmin0 = min(x0, x1); + let xmax0 = max(x0, x1); + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let i_f = f32(i); + let xmin = min(xmin0 - i_f, 1.0) - 1.0e-6; + let xmax = xmax0 - i_f; + let b = min(xmax, 1.0); + let c = max(b, 0.0); + let d = max(xmin, 0.0); + let a = (b + 0.5 * (d * d - c * c) - xmin) / (xmax - xmin); + area[i] += a * dy; + } + } + let y_edge = sign(delta.x) * clamp(xy.y - segment.y_edge + 1.0, 0.0, 1.0); + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + area[i] += y_edge; + } + } + if even_odd { + // even-odd winding rule + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let a = area[i]; + area[i] = abs(a - 2.0 * round(0.5 * a)); + } + } else { + // non-zero winding rule + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + area[i] = min(abs(area[i]), 1.0); + } + } + *result = area; +} + +#endif + +// The X size should be 16 / PIXELS_PER_THREAD +@compute @workgroup_size(4, 16) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + if ptcl[0] == ~0u { + // An earlier stage has failed, don't try to render. + // We use ptcl[0] for this so we don't use up a binding for bump. + return; + } + let tile_ix = wg_id.y * config.width_in_tiles + wg_id.x; + let xy = vec2(f32(global_id.x * PIXELS_PER_THREAD), f32(global_id.y)); + let xy_uint = vec2(xy); + let local_xy = vec2(f32(local_id.x * PIXELS_PER_THREAD), f32(local_id.y)); + var rgba: array, PIXELS_PER_THREAD>; + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let coords = vec2(xy_uint + vec2(i, 0u)); + let backdrop_raw = textureLoad(backdrop_texture, coords, 0); + rgba[i] = vec4(backdrop_raw.rgb * backdrop_raw.a, backdrop_raw.a); + } + var blend_stack: array, BLEND_STACK_SPLIT>; + var clip_depth = 0u; + var area: array; + var cmd_ix = tile_ix * PTCL_INITIAL_ALLOC; + let blend_offset = ptcl[cmd_ix]; + cmd_ix += 1u; + // main interpretation loop + while true { + let tag = ptcl[cmd_ix]; + if tag == CMD_END { + break; + } + switch tag { + case CMD_FILL: { + let fill = read_fill(cmd_ix); +#ifdef msaa + fill_path_ms(fill, local_id.xy, &area); +#else + fill_path(fill, local_xy, &area); +#endif + cmd_ix += 4u; + } + case CMD_SOLID: { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + area[i] = 1.0; + } + cmd_ix += 1u; + } + case CMD_COLOR: { + let color = read_color(cmd_ix); + let fg = unpack4x8unorm(color.rgba_color); + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + if area[i] != 0.0 { + let fg_i = fg * area[i]; + rgba[i] = compose_draw(rgba[i], fg_i, color.draw_flags); + } + } + cmd_ix += 3u; + } + case CMD_RECOLOR: { + let recolor = read_recolor(cmd_ix); + let source = unpack4x8unorm(recolor.source_color); + let target_color = unpack4x8unorm(recolor.target_color); + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + if area[i] != 0.0 { + let bg = rgba[i]; + let bg_sep = vec4(bg.rgb / max(bg.a, 1e-6), bg.a); + let delta = bg_sep - source; + let distance = dot(delta, delta); + if distance <= recolor.threshold { + let t = (recolor.threshold - distance) / recolor.threshold; + let target_premul = premul_alpha(target_color); + let recolored = target_premul * t + bg * (1.0 - target_color.a * t); + let fg = (recolored - bg) * area[i] + bg * area[i]; + rgba[i] = compose_draw(bg, fg, recolor.draw_flags); + } + } + } + cmd_ix += 5u; + } + case CMD_BEGIN_CLIP: { + if clip_depth < BLEND_STACK_SPLIT { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + blend_stack[clip_depth][i] = pack4x8unorm(rgba[i]); + rgba[i] = vec4(0.0); + } + } else { + let blend_in_scratch = clip_depth - BLEND_STACK_SPLIT; + let local_tile_ix = local_id.x * PIXELS_PER_THREAD + local_id.y * TILE_WIDTH; + let local_blend_start = blend_offset + blend_in_scratch * TILE_WIDTH * TILE_HEIGHT + local_tile_ix; + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + blend_spill[local_blend_start + i] = pack4x8unorm(rgba[i]); + rgba[i] = vec4(0.0); + } + } + clip_depth += 1u; + cmd_ix += 1u; + } + case CMD_END_CLIP: { + let end_clip = read_end_clip(cmd_ix); + clip_depth -= 1u; + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + var bg_rgba: u32; + if clip_depth < BLEND_STACK_SPLIT { + bg_rgba = blend_stack[clip_depth][i]; + } else { + let blend_in_scratch = clip_depth - BLEND_STACK_SPLIT; + let local_tile_ix = local_id.x * PIXELS_PER_THREAD + local_id.y * TILE_WIDTH; + let local_blend_start = blend_offset + blend_in_scratch * TILE_WIDTH * TILE_HEIGHT + local_tile_ix; + bg_rgba = blend_spill[local_blend_start + i]; + } + let bg = unpack4x8unorm(bg_rgba); + let fg = rgba[i] * area[i] * end_clip.alpha; + if end_clip.blend == LUMINANCE_MASK_LAYER { + // TODO: Does this case apply more generally? + // See https://github.com/linebender/vello/issues/1061 + // TODO: How do we handle anti-aliased edges here? + // This is really an imaging model question + if area[i] == 0f { + rgba[i] = bg; + continue; + } + let luminance = clamp(svg_lum(unpremultiply(fg)) * fg.a, 0.0, 1.0); + rgba[i] = bg * luminance; + } else { + rgba[i] = blend_mix_compose(bg, fg, end_clip.blend); + } + } + cmd_ix += 3u; + } + case CMD_JUMP: { + cmd_ix = ptcl[cmd_ix + 1u]; + } + case CMD_BLUR_RECT: { + /// Approximation for the convolution of a gaussian filter with a rounded rectangle. + /// + /// See https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html + + let blur = read_blur_rect(cmd_ix); + + // Avoid division by 0 + let std_dev = max(blur.std_dev, 1e-5); + let inv_std_dev = 1.0 / std_dev; + + let min_edge = min(blur.width, blur.height); + let radius_max = 0.5 * min_edge; + let r0 = min(hypot(blur.radius, std_dev * 1.15), radius_max); + let r1 = min(hypot(blur.radius, std_dev * 2.0), radius_max); + + let exponent = 2.0 * r1 / r0; + let inv_exponent = 1.0 / exponent; + + // Pull in long end (make less eccentric). + let delta = 1.25 * std_dev * (exp(-pow(0.5 * inv_std_dev * blur.width, 2.0)) - exp(-pow(0.5 * inv_std_dev * blur.height, 2.0))); + let width = blur.width + min(delta, 0.0); + let height = blur.height - max(delta, 0.0); + + let scale = 0.5 * erf7(inv_std_dev * 0.5 * (max(width, height) - 0.5 * blur.radius)); + + let blur_rgba = unpack4x8unorm(blur.rgba_color); + + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + // Transform fragment location to local 'uv' space of the rounded rectangle. + let my_xy = vec2(xy.x + f32(i), xy.y); + let local_xy = blur.matrx.xy * my_xy.x + blur.matrx.zw * my_xy.y + blur.xlat; + let x = local_xy.x; + let y = local_xy.y; + + let y0 = abs(y) - (height * 0.5 - r1); + let y1 = max(y0, 0.0); + + let x0 = abs(x) - (width * 0.5 - r1); + let x1 = max(x0, 0.0); + + let d_pos = pow(pow(x1, exponent) + pow(y1, exponent), inv_exponent); + let d_neg = min(max(x0, y0), 0.0); + let d = d_pos + d_neg - r1; + let alpha = scale * (erf7(inv_std_dev * (min_edge + d)) - erf7(inv_std_dev * d)); + + let fg_rgba = blur_rgba * alpha; + if area[i] != 0.0 { + let draw_flags = info[ptcl[cmd_ix + 1u] - 1u]; + let fg_i = fg_rgba * area[i]; + rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + } + } + cmd_ix += 3u; + } + case CMD_LIN_GRAD: { + let lin = read_lin_grad(cmd_ix); + let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; + let d = lin.line_x * (xy.x + 0.5) + lin.line_y * (xy.y + 0.5) + lin.line_c; + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + if area[i] != 0.0 { + let my_d = d + lin.line_x * f32(i); + let x = i32(round(extend_mode_normalized(my_d, lin.extend_mode) * f32(GRADIENT_WIDTH - 1))); + let fg_rgba = textureLoad(gradients, vec2(x, i32(lin.index)), 0); + let fg_i = fg_rgba * area[i]; + rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + } + } + cmd_ix += 3u; + } + case CMD_RAD_GRAD: { + let rad = read_rad_grad(cmd_ix); + let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; + let focal_x = rad.focal_x; + let radius = rad.radius; + let is_strip = rad.kind == RAD_GRAD_KIND_STRIP; + let is_circular = rad.kind == RAD_GRAD_KIND_CIRCULAR; + let is_focal_on_circle = rad.kind == RAD_GRAD_KIND_FOCAL_ON_CIRCLE; + let is_swapped = (rad.flags & RAD_GRAD_SWAPPED) != 0u; + let r1_recip = select(1.0 / radius, 0.0, is_circular); + let less_scale = select(1.0, -1.0, is_swapped || (1.0 - focal_x) < 0.0); + let t_sign = sign(1.0 - focal_x); + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); + let local_xy = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat; + let x = local_xy.x; + let y = local_xy.y; + let xx = x * x; + let yy = y * y; + var t = 0.0; + var is_valid = true; + if is_strip { + let a = radius - yy; + t = sqrt(a) + x; + is_valid = a >= 0.0; + } else if is_focal_on_circle { + t = (xx + yy) / x; + is_valid = t >= 0.0 && x != 0.0; + } else if radius > 1.0 { + t = sqrt(xx + yy) - x * r1_recip; + } else { // radius < 1.0 + let a = xx - yy; + t = less_scale * sqrt(a) - x * r1_recip; + is_valid = a >= 0.0 && t >= 0.0; + } + if is_valid { + t = extend_mode_normalized(focal_x + t_sign * t, rad.extend_mode); + t = select(t, 1.0 - t, is_swapped); + let x = i32(round(t * f32(GRADIENT_WIDTH - 1))); + let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); + let fg_i = fg_rgba * area[i]; + rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + } + } + cmd_ix += 3u; + } + case CMD_ELLIPTIC_GRAD: { + let elliptic = read_elliptic_grad(cmd_ix); + let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + if area[i] != 0.0 { + let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); + let local_xy = elliptic.matrx.xy * my_xy.x + elliptic.matrx.zw * my_xy.y + elliptic.xlat; + let radius = length(local_xy); + if radius == radius { + let t = extend_mode_normalized(radius, elliptic.extend_mode); + let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1))); + let fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(elliptic.index)), 0); + let fg_i = fg_rgba * area[i]; + rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + } + } + } + cmd_ix += 3u; + } + case CMD_SWEEP_GRAD: { + let sweep = read_sweep_grad(cmd_ix); + let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; + let scale = 1.0 / (sweep.t1 - sweep.t0); + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + if area[i] != 0.0 { + let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); + let local_xy = sweep.matrx.xy * my_xy.x + sweep.matrx.zw * my_xy.y + sweep.xlat; + let x = local_xy.x; + let y = local_xy.y; + // xy_to_unit_angle from Skia: + // See + let xabs = abs(x); + let yabs = abs(y); + let slope = min(xabs, yabs) / max(xabs, yabs); + let s = slope * slope; + // again, from Skia: + // Use a 7th degree polynomial to approximate atan. + // This was generated using sollya.gforge.inria.fr. + // A float optimized polynomial was generated using the following command. + // P1 = fpminimax((1/(2*Pi))*atan(x),[|1,3,5,7|],[|24...|],[2^(-40),1],relative); + var phi = slope * (0.15912117063999176025390625f + s * (-5.185396969318389892578125e-2f + s * (2.476101927459239959716796875e-2f + s * (-7.0547382347285747528076171875e-3f)))); + phi = select(phi, 1.0 / 4.0 - phi, xabs < yabs); + phi = select(phi, 1.0 / 2.0 - phi, x < 0.0); + phi = select(phi, 1.0 - phi, y < 0.0); + phi = select(phi, 0.0, phi != phi); // check for NaN + phi = fract(1.0 - phi); + phi = (phi - sweep.t0) * scale; + let t = extend_mode_normalized(phi, sweep.extend_mode); + let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1))); + let fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(sweep.index)), 0); + let fg_i = fg_rgba * area[i]; + rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + } + } + cmd_ix += 3u; + } + case CMD_IMAGE: { + let image = read_image(cmd_ix); + let draw_flags = info[ptcl[cmd_ix + 1u] - 1u]; + let atlas_max = image.atlas_offset + image.extents - vec2(1.0); + switch image.quality { + case IMAGE_QUALITY_LOW: { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + // We only need to load from the textures if the value will be used. + if area[i] != 0.0 { + let my_xy = vec2(xy.x + f32(i), xy.y); + var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat; + let atlas_ix = image_extend_mode_i32(atlas_uv.x, image.x_extend_mode, i32(image.extents.x)); + let atlas_iy = image_extend_mode_i32(atlas_uv.y, image.y_extend_mode, i32(image.extents.y)); + let atlas_uv_clamped = vec2(i32(image.atlas_offset.x) + atlas_ix, i32(image.atlas_offset.y) + atlas_iy); + // Nearest neighbor sampling + let fg_rgba = maybe_premul_alpha(textureLoad(image_atlas, atlas_uv_clamped, 0), image.alpha_type); + let fg_i = pixel_format(fg_rgba * area[i] * image.alpha, image.format); + rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + } + } + } + case IMAGE_QUALITY_MEDIUM, default: { + // We don't have an implementation for `IMAGE_QUALITY_HIGH` yet, just use the same as medium + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + // We only need to load from the textures if the value will be used. + if area[i] != 0.0 { + let my_xy = vec2(xy.x + f32(i), xy.y); + var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat; + atlas_uv.x = image_extend_mode(atlas_uv.x, image.x_extend_mode, image.extents.x); + atlas_uv.y = image_extend_mode(atlas_uv.y, image.y_extend_mode, image.extents.y); + atlas_uv = atlas_uv + image.atlas_offset - vec2(0.5); + // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust + let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); + // We know that the floor and ceil are within the atlas area because atlas_max and + // atlas_offset are integers + let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); + let uv_frac = fract(atlas_uv); + let a = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), 0), image.alpha_type); + let b = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), 0), image.alpha_type); + let c = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), 0), image.alpha_type); + let d = maybe_premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), 0), image.alpha_type); + // Bilinear sampling + let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); + let fg_i = pixel_format(fg_rgba * area[i] * image.alpha, image.format); + rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + } + } + } + } + cmd_ix += 2u; + } + default: {} + } + } + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let coords = xy_uint + vec2(i, 0u); + if coords.x < config.target_width && coords.y < config.target_height { + let fg = rgba[i]; + // let fg = base_color * (1.0 - foreground.a) + foreground; + // Max with a small epsilon to avoid NaNs + let a_inv = 1.0 / max(fg.a, 1e-6); + let rgba_sep = vec4(fg.rgb * a_inv, fg.a); + textureStore(output, vec2(coords), rgba_sep); + } + } +} + +fn premul_alpha(rgba: vec4) -> vec4 { + return vec4(rgba.rgb * rgba.a, rgba.a); +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/flatten.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/flatten.wgsl new file mode 100644 index 000000000..c66f98fe0 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/flatten.wgsl @@ -0,0 +1,925 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Flatten curves to lines + +#import config +#import drawtag +#import pathtag +#import segment +#import cubic +#import bump + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var scene: array; + +@group(0) @binding(2) +var tag_monoids: array; + +struct AtomicPathBbox { + x0: atomic, + y0: atomic, + x1: atomic, + y1: atomic, + draw_flags: u32, + trans_ix: u32, +} + +@group(0) @binding(3) +var path_bboxes: array; + +@group(0) @binding(4) +var bump: BumpAllocators; + +@group(0) @binding(5) +var lines: array; + +struct SubdivResult { + val: f32, + a0: f32, + a2: f32, +} + +const D = 0.67; +fn approx_parabola_integral(x: f32) -> f32 { + return x * inverseSqrt(sqrt(1.0 - D + (D * D * D * D + 0.25 * x * x))); +} + +const B = 0.39; +fn approx_parabola_inv_integral(x: f32) -> f32 { + return x * sqrt(1.0 - B + (B * B + 0.5 * x * x)); +} + +// Functions for Euler spirals + +struct CubicParams { + th0: f32, + th1: f32, + chord_len: f32, + err: f32, +} + +struct EulerParams { + th0: f32, + // th1 need not be explicitly stored, as it can be derived from k0 - th0 + k0: f32, + k1: f32, + ch: f32, +} + +struct EulerSeg { + p0: vec2f, + p1: vec2f, + params: EulerParams, +} + +// Threshold below which a derivative is considered too small. +const DERIV_THRESH: f32 = 1e-6; +const DERIV_THRESH_SQUARED: f32 = DERIV_THRESH * DERIV_THRESH; +// Amount to nudge t when derivative is near-zero. +const DERIV_EPS: f32 = 1e-6; +// Limit for subdivision of cubic Béziers. +const SUBDIV_LIMIT: f32 = 1.0 / 65536.0; +// Robust ESPC computation: below this value, treat curve as circular arc +const K1_THRESH: f32 = 1e-3; +// Robust ESPC: below this value, evaluate ES rather than parallel curve +const DIST_THRESH: f32 = 1e-3; +// Threshold for tangents to be considered near zero length +const TANGENT_THRESH: f32 = 1e-6; + +/// Compute cubic parameters from endpoints and derivatives. +fn cubic_from_points_derivs(p0: vec2f, p1: vec2f, q0: vec2f, q1: vec2f, dt: f32) -> CubicParams { + let chord = p1 - p0; + let chord_squared = dot(chord, chord); + let chord_len = sqrt(chord_squared); + if chord_squared < DERIV_THRESH_SQUARED { + let chord_err = sqrt((9. / 32.0) * (dot(q0, q0) + dot(q1, q1))) * dt; + return CubicParams(0.0, 0.0, DERIV_THRESH, chord_err); + } + let scale = dt / chord_squared; + let h0 = vec2(q0.x * chord.x + q0.y * chord.y, q0.y * chord.x - q0.x * chord.y); + let th0 = atan2(h0.y, h0.x); + let d0 = length(h0) * scale; + let h1 = vec2(q1.x * chord.x + q1.y * chord.y, q1.x * chord.y - q1.y * chord.x); + let th1 = atan2(h1.y, h1.x); + let d1 = length(h1) * scale; + + // Estimate error of geometric Hermite interpolation to Euler spiral. + let cth0 = cos(th0); + let cth1 = cos(th1); + var err = 2.0; + if cth0 * cth1 >= 0.0 { + let e0 = (2. / 3.) / max(1.0 + cth0, 1e-9); + let e1 = (2. / 3.) / max(1.0 + cth1, 1e-9); + let s0 = sin(th0); + let s1 = sin(th1); + let s01 = cth0 * s1 + cth1 * s0; + let amin = 0.15 * (2. * e0 * s0 + 2. * e1 * s1 - e0 * e1 * s01); + let a = 0.15 * (2. * d0 * s0 + 2. * d1 * s1 - d0 * d1 * s01); + let aerr = abs(a - amin); + let symm = abs(th0 + th1); + let asymm = abs(th0 - th1); + let dist = length(vec2(d0 - e0, d1 - e1)); + let symm2 = symm * symm; + let ctr = (4.625e-6 * symm * symm2 + 7.5e-3 * asymm) * symm2; + let halo = (5e-3 * symm + 7e-2 * asymm) * dist; + err = ctr + 1.55 * aerr + halo; + } + err *= chord_len; + return CubicParams(th0, th1, chord_len, err); +} + +fn es_params_from_angles(th0: f32, th1: f32) -> EulerParams { + let k0 = th0 + th1; + let dth = th1 - th0; + let d2 = dth * dth; + let k2 = k0 * k0; + var a = 6.0; + a -= d2 * (1. / 70.); + a -= (d2 * d2) * (1. / 10780.); + a += (d2 * d2 * d2) * 2.769178184818219e-07; + let b = -0.1 + d2 * (1. / 4200.) + d2 * d2 * 1.6959677820260655e-05; + let c = -1. / 1400. + d2 * 6.84915970574303e-05 - k2 * 7.936475029053326e-06; + a += (b + c * k2) * k2; + let k1 = dth * a; + + // calculation of chord + var ch = 1.0; + ch -= d2 * (1. / 40.); + ch += (d2 * d2) * 0.00034226190482569864; + ch -= (d2 * d2 * d2) * 1.9349474568904524e-06; + let b_ = -1. / 24. + d2 * 0.0024702380951963226 - d2 * d2 * 3.7297408997537985e-05; + let c_ = 1. / 1920. - d2 * 4.87350869747975e-05 - k2 * 3.1001936068463107e-06; + ch += (b_ + c_ * k2) * k2; + return EulerParams(th0, k0, k1, ch); +} + +fn es_params_eval_th(params: EulerParams, t: f32) -> f32 { + return (params.k0 + 0.5 * params.k1 * (t - 1.0)) * t - params.th0; +} + +// Integrate Euler spiral. +fn integ_euler_10(k0: f32, k1: f32) -> vec2f { + let t1_1 = k0; + let t1_2 = 0.5 * k1; + let t2_2 = t1_1 * t1_1; + let t2_3 = 2. * (t1_1 * t1_2); + let t2_4 = t1_2 * t1_2; + let t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + let t3_6 = t2_4 * t1_2; + let t4_4 = t2_2 * t2_2; + let t4_5 = 2. * (t2_2 * t2_3); + let t4_6 = 2. * (t2_2 * t2_4) + t2_3 * t2_3; + let t4_7 = 2. * (t2_3 * t2_4); + let t4_8 = t2_4 * t2_4; + let t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + let t5_8 = t4_6 * t1_2 + t4_7 * t1_1; + let t6_6 = t4_4 * t2_2; + let t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + let t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + let t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + let t8_8 = t6_6 * t2_2; + var u = 1.; + u -= (1. / 24.) * t2_2 + (1. / 160.) * t2_4; + u += (1. / 1920.) * t4_4 + (1. / 10752.) * t4_6 + (1. / 55296.) * t4_8; + u -= (1. / 322560.) * t6_6 + (1. / 1658880.) * t6_8; + u += (1. / 92897280.) * t8_8; + var v = (1. / 12.) * t1_2; + v -= (1. / 480.) * t3_4 + (1. / 2688.) * t3_6; + v += (1. / 53760.) * t5_6 + (1. / 276480.) * t5_8; + v -= (1. / 11612160.) * t7_8; + return vec2(u, v); +} + +fn es_params_eval(params: EulerParams, t: f32) -> vec2f { + let thm = es_params_eval_th(params, t * 0.5); + let k0 = params.k0; + let k1 = params.k1; + let uv = integ_euler_10((k0 + k1 * (0.5 * t - 0.5)) * t, k1 * t * t); + let scale = t / params.ch; + let s = scale * sin(thm); + let c = scale * cos(thm); + let x = uv.x * c - uv.y * s; + let y = -uv.y * c - uv.x * s; + return vec2(x, y); +} + +fn es_params_eval_with_offset(params: EulerParams, t: f32, offset: f32) -> vec2f { + let th = es_params_eval_th(params, t); + let v = offset * vec2f(sin(th), cos(th)); + return es_params_eval(params, t) + v; +} + +fn es_seg_from_params(p0: vec2f, p1: vec2f, params: EulerParams) -> EulerSeg { + return EulerSeg(p0, p1, params); +} + +// Note: offset provided is scaled so that 1 = chord length +fn es_seg_eval_with_offset(es: EulerSeg, t: f32, normalized_offset: f32) -> vec2f { + let chord = es.p1 - es.p0; + let xy = es_params_eval_with_offset(es.params, t, normalized_offset); + return es.p0 + vec2f(chord.x * xy.x - chord.y * xy.y, chord.x * xy.y + chord.y * xy.x); +} + +fn pow_1_5_signed(x: f32) -> f32 { + return x * sqrt(abs(x)); +} + +const BREAK1: f32 = 0.8; +const BREAK2: f32 = 1.25; +const BREAK3: f32 = 2.1; +const SIN_SCALE: f32 = 1.0976991822760038; +const QUAD_A1: f32 = 0.6406; +const QUAD_B1: f32 = -0.81; +const QUAD_C1: f32 = 0.9148117935952064; +const QUAD_A2: f32 = 0.5; +const QUAD_B2: f32 = -0.156; +const QUAD_C2: f32 = 0.16145779359520596; +const QUAD_W1: f32 = 0.5 * QUAD_B1 / QUAD_A1; +const QUAD_V1: f32 = 1.0 / QUAD_A1; +const QUAD_U1: f32 = QUAD_W1 * QUAD_W1 - QUAD_C1 / QUAD_A1; +const QUAD_W2: f32 = 0.5 * QUAD_B2 / QUAD_A2; +const QUAD_V2: f32 = 1.0 / QUAD_A2; +const QUAD_U2: f32 = QUAD_W2 * QUAD_W2 - QUAD_C2 / QUAD_A2; +const FRAC_PI_4: f32 = 0.7853981633974483; +const CBRT_9_8: f32 = 1.040041911525952; + +fn espc_int_approx(x: f32) -> f32 { + let y = abs(x); + var a: f32; + if y < BREAK1 { + a = sin(SIN_SCALE * y) * (1.0 / SIN_SCALE); + } else if y < BREAK2 { + a = (sqrt(8.0) / 3.0) * pow_1_5_signed(y - 1.0) + FRAC_PI_4; + } else { + let abc = select(vec3(QUAD_A2, QUAD_B2, QUAD_C2), vec3(QUAD_A1, QUAD_B1, QUAD_C1), y < BREAK3); + a = (abc.x * y + abc.y) * y + abc.z; + }; + return a * sign(x); +} + +fn espc_int_inv_approx(x: f32) -> f32 { + let y = abs(x); + var a: f32; + if y < 0.7010707591262915 { + a = asin(y * SIN_SCALE) * (1.0 / SIN_SCALE); + } else if y < 0.903249293595206 { + let b = y - FRAC_PI_4; + let u = pow(abs(b), 2. / 3.) * sign(b); + a = u * CBRT_9_8 + 1.0; + } else { + let uvw = select(vec3(QUAD_U2, QUAD_V2, QUAD_W2), vec3(QUAD_U1, QUAD_V1, QUAD_W1), y < 2.038857793595206); + a = sqrt(uvw.x + uvw.y * y) - uvw.z; + } + return a * sign(x); +} + +struct PointDeriv { + point: vec2f, + deriv: vec2f, +} + +fn eval_cubic_and_deriv(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f, t: f32) -> PointDeriv { + let m = 1.0 - t; + let mm = m * m; + let mt = m * t; + let tt = t * t; + let p = p0 * (mm * m) + (p1 * (3.0 * mm) + p2 * (3.0 * mt) + p3 * tt) * t; + let q = (p1 - p0) * mm + (p2 - p1) * (2.0 * mt) + (p3 - p2) * tt; + return PointDeriv(p, q); +} + +fn cubic_start_tangent(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { + let EPS = 1e-12; + let d01 = p1 - p0; + let d02 = p2 - p0; + let d03 = p3 - p0; + return select(select(d03, d02, dot(d02, d02) > EPS), d01, dot(d01, d01) > EPS); +} + +fn cubic_end_tangent(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { + let EPS = 1e-12; + let d23 = p3 - p2; + let d13 = p3 - p1; + let d03 = p3 - p0; + return select(select(d03, d13, dot(d13, d13) > EPS), d23, dot(d23, d23) > EPS); +} + +fn cubic_start_normal(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { + let tangent = normalize(cubic_start_tangent(p0, p1, p2, p3)); + return vec2(-tangent.y, tangent.x); +} + +fn cubic_end_normal(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { + let tangent = normalize(cubic_end_tangent(p0, p1, p2, p3)); + return vec2(-tangent.y, tangent.x); +} + +const ESPC_ROBUST_NORMAL = 0; +const ESPC_ROBUST_LOW_K1 = 1; +const ESPC_ROBUST_LOW_DIST = 2; + +// This function flattens a cubic Bézier by first converting it into Euler spiral +// segments, and then computes a near-optimal flattening of the parallel curves of +// the Euler spiral segments. +fn flatten_euler( + cubic: CubicPoints, + path_ix: u32, + local_to_device: Transform, + offset: f32, + start_p: vec2f, + end_p: vec2f, +) { + var p0: vec2f; + var p1: vec2f; + var p2: vec2f; + var p3: vec2f; + var scale: f32; + var transform: Transform; + var t_start = start_p; + var t_end = end_p; + if offset == 0. { + let t = local_to_device; + p0 = transform_apply(t, cubic.p0); + p1 = transform_apply(t, cubic.p1); + p2 = transform_apply(t, cubic.p2); + p3 = transform_apply(t, cubic.p3); + scale = 1.; + transform = transform_identity(); + t_start = p0; + t_end = p3; + } else { + p0 = cubic.p0; + p1 = cubic.p1; + p2 = cubic.p2; + p3 = cubic.p3; + + transform = local_to_device; + let mat = transform.mat; + // The scale is the semi-major axis of the ellipse given by the unit + // circle transformed by `transform`. This is the greater of the two + // singular values of the 2x2 `transform` matrix (ignoring + // translation). + scale = 0.5 * (length(vec2(mat.x + mat.w, mat.y - mat.z)) + + length(vec2(mat.x - mat.w, mat.y + mat.z))); + } + + // Drop zero length lines. This is an exact equality test because dropping very short + // line segments may result in loss of watertightness. + if all(p0 == p1) && all(p0 == p2) && all(p0 == p3) { + return; + } + + let tol = 0.25; + var t0_u = 0u; + var dt = 1.0; + var last_p = p0; + var last_q = p1 - p0; + if dot(last_q, last_q) < DERIV_THRESH_SQUARED { + last_q = eval_cubic_and_deriv(p0, p1, p2, p3, DERIV_EPS).deriv; + } + var last_t = 0.0; + var lp0 = t_start; + loop { + let t0 = f32(t0_u) * dt; + if t0 == 1.0 { + break; + } + var t1 = t0 + dt; + let this_p0 = last_p; + let this_q0 = last_q; + var this_pq1 = eval_cubic_and_deriv(p0, p1, p2, p3, t1); + if dot(this_pq1.deriv, this_pq1.deriv) < DERIV_THRESH_SQUARED { + let new_pq1 = eval_cubic_and_deriv(p0, p1, p2, p3, t1 - DERIV_EPS); + this_pq1.deriv = new_pq1.deriv; + if t1 < 1.0 { + this_pq1.point = new_pq1.point; + t1 = t1 - DERIV_EPS; + } + } + let actual_dt = t1 - last_t; + let cubic_params = cubic_from_points_derivs(this_p0, this_pq1.point, this_q0, this_pq1.deriv, actual_dt); + if cubic_params.err * scale <= tol || dt <= SUBDIV_LIMIT { + let euler_params = es_params_from_angles(cubic_params.th0, cubic_params.th1); + let es = es_seg_from_params(this_p0, this_pq1.point, euler_params); + let k0 = es.params.k0 - 0.5 * es.params.k1; + let k1 = es.params.k1; + let normalized_offset = offset / cubic_params.chord_len; + let dist_scaled = normalized_offset * es.params.ch; + let scale_multiplier = sqrt(0.125 * scale * cubic_params.chord_len / (es.params.ch * tol)); + var a = 0.0; + var b = 0.0; + var integral = 0.0; + var int0 = 0.0; + var n_frac: f32; + var robust = ESPC_ROBUST_NORMAL; + if abs(k1) < K1_THRESH { + let k = es.params.k0; + n_frac = sqrt(abs(k * (k * dist_scaled + 1.0))); + robust = ESPC_ROBUST_LOW_K1; + } else if abs(dist_scaled) < DIST_THRESH { + a = k1; + b = k0; + int0 = pow_1_5_signed(b); + let int1 = pow_1_5_signed(a + b); + integral = int1 - int0; + n_frac = (2. / 3.) * integral / a; + robust = ESPC_ROBUST_LOW_DIST; + } else { + a = -2.0 * dist_scaled * k1; + b = -1.0 - 2.0 * dist_scaled * k0; + int0 = espc_int_approx(b); + let int1 = espc_int_approx(a + b); + integral = int1 - int0; + let k_peak = k0 - k1 * b / a; + let integrand_peak = sqrt(abs(k_peak * (k_peak * dist_scaled + 1.0))); + n_frac = integral * integrand_peak / a; + } + // Bound number of subdivisions to a reasonable number when the scale is huge. + // This may give slightly incorrect rendering but avoids hangs. + // TODO: aggressively cull to viewport + let n = clamp(ceil(n_frac * scale_multiplier), 1.0, 100.0); + for (var i = 0u; i < u32(n); i++) { + var lp1: vec2f; + if i + 1u == u32(n) && t1 == 1.0 { + lp1 = t_end; + } else { + let t = f32(i + 1u) / n; + var s = t; + if robust != ESPC_ROBUST_LOW_K1 { + let u = integral * t + int0; + var inv: f32; + if robust == ESPC_ROBUST_LOW_DIST { + inv = pow(abs(u), 2. / 3.) * sign(u); + } else { + inv = espc_int_inv_approx(u); + } + s = (inv - b) / a; + } + lp1 = es_seg_eval_with_offset(es, s, normalized_offset); + } + let l0 = select(lp1, lp0, offset >= 0.); + let l1 = select(lp0, lp1, offset >= 0.); + output_line_with_transform(path_ix, l0, l1, transform); + lp0 = lp1; + } + last_p = this_pq1.point; + last_q = this_pq1.deriv; + last_t = t1; + t0_u += 1u; + let shift = countTrailingZeros(t0_u); + t0_u >>= shift; + dt *= f32(1u << shift); + } else { + t0_u = t0_u * 2u; + dt *= 0.5; + } + } +} + +// Flattens the circular arc that subtends the angle begin-center-end. It is assumed that +// ||begin - center|| == ||end - center||. `begin`, `end`, and `center` are defined in the path's +// local coordinate space. +// +// The direction of the arc is always a counter-clockwise (Y-down) rotation starting from `begin`, +// towards `end`, centered at `center`, and will be subtended by `angle` (which is assumed to be +// positive). A line segment will always be drawn from the arc's terminus to `end`, regardless of +// `angle`. +// +// `begin`, `end`, center`, and `angle` should be chosen carefully to ensure a smooth arc with the +// correct winding. +fn flatten_arc( + path_ix: u32, begin: vec2f, end: vec2f, center: vec2f, angle: f32, transform: Transform +) { + var p0 = transform_apply(transform, begin); + var r = begin - center; + + let MIN_THETA = 0.0001; + let tol = 0.25; + let radius = max(tol, length(p0 - transform_apply(transform, center))); + let theta = max(MIN_THETA, 2. * acos(1. - tol / radius)); + + // Always output at least one line so that we always draw the chord. + let n_lines = max(1u, u32(ceil(angle / theta))); + + let c = cos(theta); + let s = sin(theta); + let rot = mat2x2(c, -s, s, c); + + let line_ix = atomicAdd(&bump.lines, n_lines); + for (var i = 0u; i < n_lines - 1u; i += 1u) { + r = rot * r; + let p1 = transform_apply(transform, center + r); + write_line(line_ix + i, path_ix, p0, p1); + p0 = p1; + } + let p1 = transform_apply(transform, end); + write_line(line_ix + n_lines - 1u, path_ix, p0, p1); +} + +fn draw_cap( + path_ix: u32, cap_style: u32, point: vec2f, + cap0: vec2f, cap1: vec2f, offset_tangent: vec2f, + transform: Transform, +) { + if cap_style == STYLE_FLAGS_CAP_ROUND { + flatten_arc(path_ix, cap0, cap1, point, 3.1415927, transform); + return; + } + + var start = cap0; + var end = cap1; + let is_square = (cap_style == STYLE_FLAGS_CAP_SQUARE); + let line_ix = atomicAdd(&bump.lines, select(1u, 3u, is_square)); + if is_square { + let v = offset_tangent; + let p0 = start + v; + let p1 = end + v; + write_line_with_transform(line_ix + 1u, path_ix, start, p0, transform); + write_line_with_transform(line_ix + 2u, path_ix, p1, end, transform); + start = p0; + end = p1; + } + write_line_with_transform(line_ix, path_ix, start, end, transform); +} + +fn draw_join( + path_ix: u32, style_flags: u32, p0: vec2f, + tan_prev: vec2f, tan_next: vec2f, + n_prev: vec2f, n_next: vec2f, + transform: Transform, +) { + var front0 = p0 + n_prev; + let front1 = p0 + n_next; + var back0 = p0 - n_next; + let back1 = p0 - n_prev; + + let cr = tan_prev.x * tan_next.y - tan_prev.y * tan_next.x; + let d = dot(tan_prev, tan_next); + + switch style_flags & STYLE_FLAGS_JOIN_MASK { + case STYLE_FLAGS_JOIN_BEVEL: { + output_two_lines_with_transform(path_ix, front0, front1, back0, back1, transform); + } + case STYLE_FLAGS_JOIN_MITER: { + let hypot = length(vec2f(cr, d)); + let miter_limit = unpack2x16float(style_flags & STYLE_MITER_LIMIT_MASK)[0]; + + var line_ix: u32; + // Given the two tangents `tan_prev` and `tan_next` arranged tail-to-tail, the + // miter length ratio is `1 / |cos(theta/2)|`, where `theta` is the angle + // between the tangents. + // + // `hypot` is `|tan_prev| * |tan_next|` (since cr^2 + d^2 = |a|^2 |b|^2) and + // `hypot + d` is `2 * |tan_prev| * |tan_next| * cos^2(theta/2)`. After + // rearranging, the following tests whether `1/|cos(theta/2)| < miter_limit`. + // + // Also avoid the miter computation when `cr` is very small; the intersection + // math divides by `cr` and becomes numerically unstable for near-collinear + // tangents. + if 2. * hypot < (hypot + d) * miter_limit * miter_limit + && abs(cr) > TANGENT_THRESH * TANGENT_THRESH + { + let is_backside = cr > 0.; + let fp_last = select(front0, back1, is_backside); + let fp_this = select(front1, back0, is_backside); + let p = select(front0, back0, is_backside); + + let v = fp_this - fp_last; + let h = (tan_prev.x * v.y - tan_prev.y * v.x) / cr; + let miter_pt = fp_this - tan_next * h; + + line_ix = atomicAdd(&bump.lines, 3u); + write_line_with_transform(line_ix, path_ix, p, miter_pt, transform); + line_ix += 1u; + + if is_backside { + back0 = miter_pt; + } else { + front0 = miter_pt; + } + } else { + line_ix = atomicAdd(&bump.lines, 2u); + } + write_line_with_transform(line_ix, path_ix, front0, front1, transform); + write_line_with_transform(line_ix + 1u, path_ix, back0, back1, transform); + } + case STYLE_FLAGS_JOIN_ROUND: { + var arc0: vec2f; + var arc1: vec2f; + var other0: vec2f; + var other1: vec2f; + if cr > 0. { + arc0 = back0; + arc1 = back1; + other0 = front0; + other1 = front1; + } else { + arc0 = front0; + arc1 = front1; + other0 = back0; + other1 = back1; + } + flatten_arc(path_ix, arc0, arc1, p0, abs(atan2(cr, d)), transform); + output_line_with_transform(path_ix, other0, other1, transform); + } + default: {} + } +} + +fn read_f32_point(ix: u32) -> vec2f { + let x = bitcast(scene[pathdata_base + ix]); + let y = bitcast(scene[pathdata_base + ix + 1u]); + return vec2(x, y); +} + +fn read_i16_point(ix: u32) -> vec2f { + let raw = scene[pathdata_base + ix]; + let x = f32(i32(raw << 16u) >> 16u); + let y = f32(i32(raw) >> 16u); + return vec2(x, y); +} + +struct Transform { + mat: vec4f, + translate: vec2f, +} + +fn transform_identity() -> Transform { + return Transform(vec4(1., 0., 0., 1.), vec2(0.)); +} + +fn read_transform(transform_base: u32, ix: u32) -> Transform { + let base = transform_base + ix * 6u; + let c0 = bitcast(scene[base]); + let c1 = bitcast(scene[base + 1u]); + let c2 = bitcast(scene[base + 2u]); + let c3 = bitcast(scene[base + 3u]); + let c4 = bitcast(scene[base + 4u]); + let c5 = bitcast(scene[base + 5u]); + let mat = vec4(c0, c1, c2, c3); + let translate = vec2(c4, c5); + return Transform(mat, translate); +} + +fn transform_apply(transform: Transform, p: vec2f) -> vec2f { + let px = fma(transform.mat.x, p.x, fma(transform.mat.z, p.y, transform.translate.x)); + let py = fma(transform.mat.y, p.x, fma(transform.mat.w, p.y, transform.translate.y)); + return vec2(px, py); +} + +fn round_down(x: f32) -> i32 { + return i32(floor(x)); +} + +fn round_up(x: f32) -> i32 { + return i32(ceil(x)); +} + +struct PathTagData { + tag_byte: u32, + monoid: TagMonoid, +} + +fn compute_tag_monoid(ix: u32) -> PathTagData { + let tag_word = scene[config.pathtag_base + (ix >> 2u)]; + let shift = (ix & 3u) * 8u; + var tm = reduce_tag(tag_word & ((1u << shift) - 1u)); + // TODO: this can be a read buf overflow. Conditionalize by tag byte? + tm = combine_tag_monoid(tag_monoids[ix >> 2u], tm); + var tag_byte = (tag_word >> shift) & 0xffu; + // We no longer encode an initial transform and style so these + // are off by one. + // Note: an alternative would be to adjust config.transform_base and + // config.style_base. + tm.trans_ix -= 1u; + tm.style_ix -= STYLE_SIZE_IN_WORDS; + return PathTagData(tag_byte, tm); +} + +struct CubicPoints { + p0: vec2f, + p1: vec2f, + p2: vec2f, + p3: vec2f, +} + +fn read_path_segment(tag: PathTagData, is_stroke: bool) -> CubicPoints { + var p0: vec2f; + var p1: vec2f; + var p2: vec2f; + var p3: vec2f; + + var seg_type = tag.tag_byte & PATH_TAG_SEG_TYPE; + let pathseg_offset = tag.monoid.pathseg_offset; + let is_stroke_cap_marker = is_stroke && (tag.tag_byte & PATH_TAG_SUBPATH_END) != 0u; + let is_open = seg_type == PATH_TAG_QUADTO; + + if (tag.tag_byte & PATH_TAG_F32) != 0u { + p0 = read_f32_point(pathseg_offset); + p1 = read_f32_point(pathseg_offset + 2u); + if seg_type >= PATH_TAG_QUADTO { + p2 = read_f32_point(pathseg_offset + 4u); + if seg_type == PATH_TAG_CUBICTO { + p3 = read_f32_point(pathseg_offset + 6u); + } + } + } else { + p0 = read_i16_point(pathseg_offset); + p1 = read_i16_point(pathseg_offset + 1u); + if seg_type >= PATH_TAG_QUADTO { + p2 = read_i16_point(pathseg_offset + 2u); + if seg_type == PATH_TAG_CUBICTO { + p3 = read_i16_point(pathseg_offset + 3u); + } + } + } + + if is_stroke_cap_marker && is_open { + // The stroke cap marker for an open path is encoded as a quadto where the p1 and p2 store + // the start control point of the subpath and together with p2 forms the start tangent. p0 + // is ignored. + // + // This is encoded this way because encoding this as a lineto would require adding a moveto, + // which would terminate the subpath too early (by setting the SUBPATH_END on the + // segment preceding the cap marker). This scheme is only used for strokes. + p0 = p1; + p1 = p2; + seg_type = PATH_TAG_LINETO; + } + + // Degree-raise + if seg_type == PATH_TAG_LINETO { + p3 = p1; + p2 = p3 + (1.0 / 3.0) * (p0 - p3); + p1 = p0 + (1.0 / 3.0) * (p3 - p0); + } else if seg_type == PATH_TAG_QUADTO { + p3 = p2; + p2 = p1 + (1.0 / 3.0) * (p2 - p1); + p1 = p1 + (1.0 / 3.0) * (p0 - p1); + } + + return CubicPoints(p0, p1, p2, p3); +} + +// Writes a line into a the `lines` buffer at a pre-allocated location designated by `line_ix`. +fn write_line(line_ix: u32, path_ix: u32, p0: vec2f, p1: vec2f) { + bbox = vec4(min(bbox.xy, min(p0, p1)), max(bbox.zw, max(p0, p1))); + if line_ix < config.lines_size { + lines[line_ix] = LineSoup(path_ix, p0, p1); + } +} + +fn write_line_with_transform(line_ix: u32, path_ix: u32, p0: vec2f, p1: vec2f, t: Transform) { + let tp0 = transform_apply(t, p0); + let tp1 = transform_apply(t, p1); + write_line(line_ix, path_ix, tp0, tp1); +} + +fn output_line(path_ix: u32, p0: vec2f, p1: vec2f) { + let line_ix = atomicAdd(&bump.lines, 1u); + write_line(line_ix, path_ix, p0, p1); +} + +fn output_line_with_transform(path_ix: u32, p0: vec2f, p1: vec2f, transform: Transform) { + let line_ix = atomicAdd(&bump.lines, 1u); + write_line_with_transform(line_ix, path_ix, p0, p1, transform); +} + +fn output_two_lines_with_transform( + path_ix: u32, + p00: vec2f, p01: vec2f, + p10: vec2f, p11: vec2f, + transform: Transform +) { + let line_ix = atomicAdd(&bump.lines, 2u); + write_line_with_transform(line_ix, path_ix, p00, p01, transform); + write_line_with_transform(line_ix + 1u, path_ix, p10, p11, transform); +} + +struct NeighboringSegment { + do_join: bool, + + // Device-space start tangent vector + tangent: vec2f, +} + +fn read_neighboring_segment(ix: u32) -> NeighboringSegment { + let tag = compute_tag_monoid(ix); + let pts = read_path_segment(tag, true); + + let is_closed = (tag.tag_byte & PATH_TAG_SEG_TYPE) == PATH_TAG_LINETO; + let is_stroke_cap_marker = (tag.tag_byte & PATH_TAG_SUBPATH_END) != 0u; + let do_join = !is_stroke_cap_marker || is_closed; + var tangent = pts.p3 - pts.p0; + if !is_stroke_cap_marker { + tangent = cubic_start_tangent(pts.p0, pts.p1, pts.p2, pts.p3); + } + return NeighboringSegment(do_join, tangent); +} + +// `pathdata_base` is decoded once and reused by helpers above. +var pathdata_base: u32; + +// This is the bounding box of the shape flattened by a single shader invocation. It gets modified +// during LineSoup generation. +var bbox: vec4f; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, +) { + let ix = global_id.x; + pathdata_base = config.pathdata_base; + bbox = vec4(1e31, 1e31, -1e31, -1e31); + + let tag = compute_tag_monoid(ix); + let path_ix = tag.monoid.path_ix; + let style_ix = tag.monoid.style_ix; + let trans_ix = tag.monoid.trans_ix; + + let out = &path_bboxes[path_ix]; + let style_flags = scene[config.style_base + style_ix]; + let fill_rule = select(DRAW_INFO_FLAGS_FILL_RULE_BIT, 0u, (style_flags & STYLE_FLAGS_FILL) == 0u); + let blend_mode = ((style_flags & 0x00003ffeu) >> 1u) << DRAW_FLAGS_BLEND_MODE_SHIFT; + let blend_alpha = ((style_flags & 0x3fffc000u) >> 14u) << DRAW_FLAGS_BLEND_ALPHA_SHIFT; + let draw_flags = fill_rule | blend_mode | blend_alpha; + if (tag.tag_byte & PATH_TAG_PATH) != 0u { + (*out).draw_flags = draw_flags; + (*out).trans_ix = trans_ix; + } + // Decode path data + let seg_type = tag.tag_byte & PATH_TAG_SEG_TYPE; + if seg_type != 0u { + let is_stroke = (style_flags & STYLE_FLAGS_STYLE) != 0u; + let transform = read_transform(config.transform_base, trans_ix); + let pts = read_path_segment(tag, is_stroke); + + if is_stroke { + let linewidth = bitcast(scene[config.style_base + style_ix + 1u]); + let offset = 0.5 * linewidth; + + let is_open = (tag.tag_byte & PATH_TAG_SEG_TYPE) != PATH_TAG_LINETO; + let is_stroke_cap_marker = (tag.tag_byte & PATH_TAG_SUBPATH_END) != 0u; + if is_stroke_cap_marker { + if is_open { + // Draw start cap + let tangent = pts.p3 - pts.p0; + let offset_tangent = offset * normalize(tangent); + let n = offset_tangent.yx * vec2f(-1., 1.); + draw_cap(path_ix, (style_flags & STYLE_FLAGS_START_CAP_MASK) >> 2u, + pts.p0, pts.p0 - n, pts.p0 + n, -offset_tangent, transform); + } else { + // Don't draw anything if the path is closed. + } + } else { + // Read the neighboring segment. + let neighbor = read_neighboring_segment(ix + 1u); + var tan_start = cubic_start_tangent(pts.p0, pts.p1, pts.p2, pts.p3); + if dot(tan_start, tan_start) < TANGENT_THRESH * TANGENT_THRESH { + tan_start = vec2(TANGENT_THRESH, 0.); + } + var tan_prev = cubic_end_tangent(pts.p0, pts.p1, pts.p2, pts.p3); + if dot(tan_prev, tan_prev) < TANGENT_THRESH * TANGENT_THRESH { + tan_prev = vec2(TANGENT_THRESH, 0.); + } + var tan_next = neighbor.tangent; + if dot(tan_next, tan_next) < TANGENT_THRESH * TANGENT_THRESH { + tan_next = vec2(TANGENT_THRESH, 0.); + } + let n_start = offset * normalize(vec2(-tan_start.y, tan_start.x)); + let offset_tangent = offset * normalize(tan_prev); + let n_prev = offset_tangent.yx * vec2f(-1., 1.); + let n_next = offset * normalize(tan_next).yx * vec2f(-1., 1.); + + // Render offset curves + flatten_euler(pts, path_ix, transform, offset, pts.p0 + n_start, pts.p3 + n_prev); + flatten_euler(pts, path_ix, transform, -offset, pts.p0 - n_start, pts.p3 - n_prev); + + if neighbor.do_join { + draw_join(path_ix, style_flags, pts.p3, tan_prev, tan_next, + n_prev, n_next, transform); + } else { + // Draw end cap. + draw_cap(path_ix, (style_flags & STYLE_FLAGS_END_CAP_MASK), + pts.p3, pts.p3 + n_prev, pts.p3 - n_prev, offset_tangent, transform); + } + } + } else { + let offset = 0.; + flatten_euler(pts, path_ix, transform, offset, pts.p0, pts.p3); + } + // Update bounding box using atomics only. Computing a monoid is a + // potential future optimization. + if bbox.z > bbox.x || bbox.w > bbox.y { + atomicMin(&(*out).x0, round_down(bbox.x)); + atomicMin(&(*out).y0, round_down(bbox.y)); + atomicMax(&(*out).x1, round_up(bbox.z)); + atomicMax(&(*out).y1, round_up(bbox.w)); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/path_count.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/path_count.wgsl new file mode 100644 index 000000000..9a489c209 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/path_count.wgsl @@ -0,0 +1,202 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Stage to compute counts of number of segments in each tile + +#import bump +#import config +#import segment +#import tile + +// TODO: this is cut'n'pasted from path_coarse. +struct AtomicTile { + backdrop: atomic, + segment_count_or_ix: atomic, +} + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var bump: BumpAllocators; + +@group(0) @binding(2) +var lines: array; + +@group(0) @binding(3) +var paths: array; + +@group(0) @binding(4) +var tile: array; + +@group(0) @binding(5) +var seg_counts: array; + +// number of integer cells spanned by interval defined by a, b +fn span(a: f32, b: f32) -> u32 { + return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); +} + +// See cpu_shaders/util.rs for explanation of these. +const ONE_MINUS_ULP: f32 = 0.99999994; +const ROBUST_EPSILON: f32 = 2e-7; + +// Note regarding clipping to bounding box: +// +// We have to do the backdrop bumps for all tiles to the left of the bbox. +// This should probably be a separate loop. This also causes a numerical +// robustness issue. + +// This shader is dispatched with one thread for each line. +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, +) { + let n_lines = atomicLoad(&bump.lines); + var count = 0u; + if global_id.x < n_lines { + let line = lines[global_id.x]; + // coarse rasterization logic to count number of tiles touched by line + let is_down = line.p1.y >= line.p0.y; + let xy0 = select(line.p1, line.p0, is_down); + let xy1 = select(line.p0, line.p1, is_down); + let s0 = xy0 * TILE_SCALE; + let s1 = xy1 * TILE_SCALE; + let count_x = span(s0.x, s1.x) - 1u; + count = count_x + span(s0.y, s1.y); + let line_ix = global_id.x; + + let dx = abs(s1.x - s0.x); + let dy = s1.y - s0.y; + if dx + dy == 0.0 { + // Zero-length segment, drop it. Note, this could be culled in the + // flattening stage, but eliding the test here would be fragile, as + // it would be pretty bad to let it slip through. + return; + } + if dy == 0.0 && floor(s0.y) == s0.y { + return; + } + let idxdy = 1.0 / (dx + dy); + var a = dx * idxdy; + let is_positive_slope = s1.x >= s0.x; + let x_sign = select(-1.0, 1.0, is_positive_slope); + let xt0 = floor(s0.x * x_sign); + let c = s0.x * x_sign - xt0; + let y0 = floor(s0.y); + let ytop = select(y0 + 1.0, ceil(s0.y), s0.y == s1.y); + let b = min((dy * c + dx * (ytop - s0.y)) * idxdy, ONE_MINUS_ULP); + let robust_err = floor(a * (f32(count) - 1.0) + b) - f32(count_x); + if robust_err != 0.0 { + a -= ROBUST_EPSILON * sign(robust_err); + } + let x0 = xt0 * x_sign + select(-1.0, 0.0, is_positive_slope); + + let path = paths[line.path_ix]; + let bbox = vec4(path.bbox); + let xmin = min(s0.x, s1.x); + // If line is to left of bbox, we may still need to do backdrop + let stride = bbox.z - bbox.x; + if s0.y >= f32(bbox.w) || s1.y <= f32(bbox.y) || xmin >= f32(bbox.z) || stride == 0 { + return; + } + // Clip line to bounding box. Clipping is done in "i" space. + var imin = 0u; + if s0.y < f32(bbox.y) { + var iminf = round((f32(bbox.y) - y0 + b - a) / (1.0 - a)) - 1.0; + // Numerical robustness: goal is to find the first i value for which + // the following predicate is false. Above formula is designed to + // undershoot by 0.5. + if y0 + iminf - floor(a * iminf + b) < f32(bbox.y) { + iminf += 1.0; + } + imin = u32(iminf); + } + var imax = count; + if s1.y > f32(bbox.w) { + var imaxf = round((f32(bbox.w) - y0 + b - a) / (1.0 - a)) - 1.0; + if y0 + imaxf - floor(a * imaxf + b) < f32(bbox.w) { + imaxf += 1.0; + } + imax = u32(imaxf); + } + let delta = select(1, -1, is_down); + var ymin = 0; + var ymax = 0; + if max(s0.x, s1.x) <= f32(bbox.x) { + ymin = i32(ceil(s0.y)); + ymax = i32(ceil(s1.y)); + imax = imin; + } else { + let fudge = select(1.0, 0.0, is_positive_slope); + if xmin < f32(bbox.x) { + var f = round((x_sign * (f32(bbox.x) - x0) - b + fudge) / a); + if (x0 + x_sign * floor(a * f + b) < f32(bbox.x)) == is_positive_slope { + f += 1.0; + } + let ynext = i32(y0 + f - floor(a * f + b) + 1.0); + if is_positive_slope { + if u32(f) > imin { + ymin = i32(y0 + select(1.0, 0.0, y0 == s0.y)); + ymax = ynext; + imin = u32(f); + } + } else { + if u32(f) < imax { + ymin = ynext; + ymax = i32(ceil(s1.y)); + imax = u32(f); + } + } + } + if max(s0.x, s1.x) > f32(bbox.z) { + var f = round((x_sign * (f32(bbox.z) - x0) - b + fudge) / a); + if (x0 + x_sign * floor(a * f + b) < f32(bbox.z)) == is_positive_slope { + f += 1.0; + } + if is_positive_slope { + imax = min(imax, u32(f)); + } else { + imin = max(imin, u32(f)); + } + } + } + imax = max(imin, imax); + // Apply backdrop for part of line left of bbox + ymin = max(ymin, bbox.y); + ymax = min(ymax, bbox.w); + for (var y = ymin; y < ymax; y++) { + let base = i32(path.tiles) + (y - bbox.y) * stride; + atomicAdd(&tile[base].backdrop, delta); + } + var last_z = floor(a * (f32(imin) - 1.0) + b); + let seg_base = atomicAdd(&bump.seg_counts, imax - imin); + for (var i = imin; i < imax; i++) { + let subix = i; + // coarse rasterization logic + // Note: we hope fast-math doesn't strength reduce this. + let zf = a * f32(subix) + b; + let z = floor(zf); + // x, y are tile coordinates relative to render target + let y = i32(y0 + f32(subix) - z); + let x = i32(x0 + x_sign * z); + let base = i32(path.tiles) + (y - bbox.y) * stride - bbox.x; + let top_edge = select(last_z == z, y0 == s0.y, subix == 0u); + if top_edge && x + 1 < bbox.z { + let x_bump = max(x + 1, bbox.x); + atomicAdd(&tile[base + x_bump].backdrop, delta); + } + let seg_within_slice = atomicAdd(&tile[base + x].segment_count_or_ix, 1u); + // Pack two count values into a single u32 + let counts = (seg_within_slice << 16u) | subix; + let seg_count = SegmentCount(line_ix, counts); + let seg_ix = seg_base + i - imin; + if seg_ix < config.seg_counts_size { + seg_counts[seg_ix] = seg_count; + } + // Note: since we're iterating, we have a reliable value for + // last_z. + last_z = z; + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/path_count_setup.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/path_count_setup.wgsl new file mode 100644 index 000000000..2daa7fd1f --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/path_count_setup.wgsl @@ -0,0 +1,27 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Set up dispatch size for path count stage. + +#import bump + +@group(0) @binding(0) +var bump: BumpAllocators; + +@group(0) @binding(1) +var indirect: IndirectCount; + +// Partition size for path count stage +const WG_SIZE = 256u; + +@compute @workgroup_size(1) +fn main() { + if atomicLoad(&bump.failed) != 0u { + indirect.count_x = 0u; + } else { + let lines = atomicLoad(&bump.lines); + indirect.count_x = (lines + (WG_SIZE - 1u)) / WG_SIZE; + } + indirect.count_y = 1u; + indirect.count_z = 1u; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/path_tiling.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/path_tiling.wgsl new file mode 100644 index 000000000..e15dc75bd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/path_tiling.wgsl @@ -0,0 +1,173 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Write path segments + +#import bump +#import config +#import segment +#import tile + +@group(0) @binding(0) +var bump: BumpAllocators; + +@group(0) @binding(1) +var seg_counts: array; + +@group(0) @binding(2) +var lines: array; + +@group(0) @binding(3) +var paths: array; + +@group(0) @binding(4) +var tiles: array; + +@group(0) @binding(5) +var segments: array; + +fn span(a: f32, b: f32) -> u32 { + return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); +} + +// See cpu_shaders/util.rs for explanation of these. +const ONE_MINUS_ULP: f32 = 0.99999994; +const ROBUST_EPSILON: f32 = 2e-7; + +// One invocation for each tile that is to be written. +// Total number of invocations = bump.seg_counts +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, +) { + let n_segments = atomicLoad(&bump.seg_counts); + if global_id.x < n_segments { + let seg_count = seg_counts[global_id.x]; + let line = lines[seg_count.line_ix]; + let counts = seg_count.counts; + let seg_within_slice = counts >> 16u; + let seg_within_line = counts & 0xffffu; + + // coarse rasterization logic + let is_down = line.p1.y >= line.p0.y; + var xy0 = select(line.p1, line.p0, is_down); + var xy1 = select(line.p0, line.p1, is_down); + let s0 = xy0 * TILE_SCALE; + let s1 = xy1 * TILE_SCALE; + let count_x = span(s0.x, s1.x) - 1u; + let count = count_x + span(s0.y, s1.y); + let dx = abs(s1.x - s0.x); + let dy = s1.y - s0.y; + // Division by zero can't happen because zero-length lines + // have already been discarded in the path_count stage. + let idxdy = 1.0 / (dx + dy); + var a = dx * idxdy; + let is_positive_slope = s1.x >= s0.x; + let x_sign = select(-1.0, 1.0, is_positive_slope); + let xt0 = floor(s0.x * x_sign); + let c = s0.x * x_sign - xt0; + let y0i = floor(s0.y); + let ytop = select(y0i + 1.0, ceil(s0.y), s0.y == s1.y); + let b = min((dy * c + dx * (ytop - s0.y)) * idxdy, ONE_MINUS_ULP); + let robust_err = floor(a * (f32(count) - 1.0) + b) - f32(count_x); + if robust_err != 0.0 { + a -= ROBUST_EPSILON * sign(robust_err); + } + let x0i = i32(xt0 * x_sign + 0.5 * (x_sign - 1.0)); + let z = floor(a * f32(seg_within_line) + b); + let x = x0i + i32(x_sign * z); + let y = i32(y0i + f32(seg_within_line) - z); + + let path = paths[line.path_ix]; + let bbox = vec4(path.bbox); + let stride = bbox.z - bbox.x; + let tile_ix = i32(path.tiles) + (y - bbox.y) * stride + x - bbox.x; + let tile = tiles[tile_ix]; + let seg_start = ~tile.segment_count_or_ix; + if i32(seg_start) < 0 { + return; + } + let tile_xy = vec2(f32(x) * f32(TILE_WIDTH), f32(y) * f32(TILE_HEIGHT)); + let tile_xy1 = tile_xy + vec2(f32(TILE_WIDTH), f32(TILE_HEIGHT)); + + if seg_within_line > 0u { + let z_prev = floor(a * (f32(seg_within_line) - 1.0) + b); + if z == z_prev { + // Top edge is clipped + var xt = xy0.x + (xy1.x - xy0.x) * (tile_xy.y - xy0.y) / (xy1.y - xy0.y); + // TODO: we want to switch to tile-relative coordinates + xt = clamp(xt, tile_xy.x + 1e-3, tile_xy1.x); + xy0 = vec2(xt, tile_xy.y); + } else { + // If is_positive_slope, left edge is clipped, otherwise right + let x_clip = select(tile_xy1.x, tile_xy.x, is_positive_slope); + var yt = xy0.y + (xy1.y - xy0.y) * (x_clip - xy0.x) / (xy1.x - xy0.x); + yt = clamp(yt, tile_xy.y + 1e-3, tile_xy1.y); + xy0 = vec2(x_clip, yt); + } + } + if seg_within_line < count - 1u { + let z_next = floor(a * (f32(seg_within_line) + 1.0) + b); + if z == z_next { + // Bottom edge is clipped + var xt = xy0.x + (xy1.x - xy0.x) * (tile_xy1.y - xy0.y) / (xy1.y - xy0.y); + xt = clamp(xt, tile_xy.x + 1e-3, tile_xy1.x); + xy1 = vec2(xt, tile_xy1.y); + } else { + // If is_positive_slope, right edge is clipped, otherwise left + let x_clip = select(tile_xy.x, tile_xy1.x, is_positive_slope); + var yt = xy0.y + (xy1.y - xy0.y) * (x_clip - xy0.x) / (xy1.x - xy0.x); + yt = clamp(yt, tile_xy.y + 1e-3, tile_xy1.y); + xy1 = vec2(x_clip, yt); + } + } + var y_edge = 1e9; + // Apply numerical robustness logic + var p0 = xy0 - tile_xy; + var p1 = xy1 - tile_xy; + // When we move to f16, this will be f16::MIN_POSITIVE + let EPSILON = 1e-6; + if p0.x == 0.0 { + if p1.x == 0.0 { + p0.x = EPSILON; + if p0.y == 0.0 { + // Entire tile + p1.x = EPSILON; + p1.y = f32(TILE_HEIGHT); + } else { + // Make segment disappear + p1.x = 2.0 * EPSILON; + p1.y = p0.y; + } + } else if p0.y == 0.0 { + p0.x = EPSILON; + } else { + y_edge = p0.y; + } + } else if p1.x == 0.0 { + if p1.y == 0.0 { + p1.x = EPSILON; + } else { + y_edge = p1.y; + } + } + // Hacky approach to numerical robustness in fine. + // This just makes sure there are no vertical lines aligned to + // the pixel grid internal to the tile. It's faster to do this + // logic here rather than in fine, but at some point we might + // rework it. + if p0.x == floor(p0.x) && p0.x != 0.0 { + p0.x -= EPSILON; + } + if p1.x == floor(p1.x) && p1.x != 0.0 { + p1.x -= EPSILON; + } + if !is_down { + let tmp = p0; + p0 = p1; + p1 = tmp; + } + let segment = Segment(p0, p1, y_edge); + segments[seg_start + seg_within_slice] = segment; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/path_tiling_setup.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/path_tiling_setup.wgsl new file mode 100644 index 000000000..8698a6c26 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/path_tiling_setup.wgsl @@ -0,0 +1,32 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Set up dispatch size for path tiling stage. + +#import bump + +@group(0) @binding(0) +var bump: BumpAllocators; + +@group(0) @binding(1) +var indirect: IndirectCount; + +@group(0) @binding(2) +var ptcl: array; + +// Partition size for path tiling stage +const WG_SIZE = 256u; + +@compute @workgroup_size(1) +fn main() { + if atomicLoad(&bump.failed) != 0u { + indirect.count_x = 0u; + // signal fine rasterizer that failure happened (it doesn't bind bump) + ptcl[0] = ~0u; + } else { + let segments = atomicLoad(&bump.seg_counts); + indirect.count_x = (segments + (WG_SIZE - 1u)) / WG_SIZE; + } + indirect.count_y = 1u; + indirect.count_z = 1u; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_reduce.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_reduce.wgsl new file mode 100644 index 000000000..beefd56e8 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_reduce.wgsl @@ -0,0 +1,42 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +#import config +#import pathtag + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var scene: array; + +@group(0) @binding(2) +var reduced: array; + +const LG_WG_SIZE = 8u; +const WG_SIZE = 256u; + +var sh_scratch: array; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, +) { + let ix = global_id.x; + let tag_word = scene[config.pathtag_base + ix]; + var agg = reduce_tag(tag_word); + sh_scratch[local_id.x] = agg; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_scratch[local_id.x + (1u << i)]; + agg = combine_tag_monoid(agg, other); + } + workgroupBarrier(); + sh_scratch[local_id.x] = agg; + } + if local_id.x == 0u { + reduced[ix >> LG_WG_SIZE] = agg; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_reduce2.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_reduce2.wgsl new file mode 100644 index 000000000..fe47a62a1 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_reduce2.wgsl @@ -0,0 +1,41 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// This shader is the second stage of reduction for the pathtag +// monoid scan, needed when the number of tags is large. + +#import config +#import pathtag + +@group(0) @binding(0) +var reduced_in: array; + +@group(0) @binding(1) +var reduced: array; + +const LG_WG_SIZE = 8u; +const WG_SIZE = 256u; + +var sh_scratch: array; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, +) { + let ix = global_id.x; + var agg = reduced_in[ix]; + sh_scratch[local_id.x] = agg; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_scratch[local_id.x + (1u << i)]; + agg = combine_tag_monoid(agg, other); + } + workgroupBarrier(); + sh_scratch[local_id.x] = agg; + } + if local_id.x == 0u { + reduced[ix >> LG_WG_SIZE] = agg; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_scan.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_scan.wgsl new file mode 100644 index 000000000..686fae813 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_scan.wgsl @@ -0,0 +1,76 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +#import config +#import pathtag + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var scene: array; + +@group(0) @binding(2) +var reduced: array; + +@group(0) @binding(3) +var tag_monoids: array; + +const LG_WG_SIZE = 8u; +const WG_SIZE = 256u; + +#ifdef small +var sh_parent: array; +#endif +// These could be combined? +var sh_monoid: array; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { +#ifdef small + var agg = tag_monoid_identity(); + if local_id.x < wg_id.x { + agg = reduced[local_id.x]; + } + sh_parent[local_id.x] = agg; + for (var i = 0u; i < LG_WG_SIZE; i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_parent[local_id.x + (1u << i)]; + agg = combine_tag_monoid(agg, other); + } + workgroupBarrier(); + sh_parent[local_id.x] = agg; + } +#endif + + let ix = global_id.x; + let tag_word = scene[config.pathtag_base + ix]; + var agg_part = reduce_tag(tag_word); + sh_monoid[local_id.x] = agg_part; + for (var i = 0u; i < LG_WG_SIZE; i += 1u) { + workgroupBarrier(); + if local_id.x >= 1u << i { + let other = sh_monoid[local_id.x - (1u << i)]; + agg_part = combine_tag_monoid(other, agg_part); + } + workgroupBarrier(); + sh_monoid[local_id.x] = agg_part; + } + workgroupBarrier(); + // prefix up to this workgroup +#ifdef small + var tm = sh_parent[0]; +#else + var tm = reduced[wg_id.x]; +#endif + if local_id.x > 0u { + tm = combine_tag_monoid(tm, sh_monoid[local_id.x - 1u]); + } + // exclusive prefix sum, granularity of 4 tag bytes + tag_monoids[ix] = tm; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_scan1.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_scan1.wgsl new file mode 100644 index 000000000..fc5236b0c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/pathtag_scan1.wgsl @@ -0,0 +1,67 @@ +// Copyright 2023 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// This shader computes the scan of reduced tag monoids given +// two levels of reduction. + +#import config +#import pathtag + +@group(0) @binding(0) +var reduced: array; + +@group(0) @binding(1) +var reduced2: array; + +@group(0) @binding(2) +var tag_monoids: array; + +const LG_WG_SIZE = 8u; +const WG_SIZE = 256u; + +var sh_parent: array; +// These could be combined? +var sh_monoid: array; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, + @builtin(workgroup_id) wg_id: vec3, +) { + var agg = tag_monoid_identity(); + if local_id.x < wg_id.x { + agg = reduced2[local_id.x]; + } + sh_parent[local_id.x] = agg; + for (var i = 0u; i < LG_WG_SIZE; i += 1u) { + workgroupBarrier(); + if local_id.x + (1u << i) < WG_SIZE { + let other = sh_parent[local_id.x + (1u << i)]; + agg = combine_tag_monoid(agg, other); + } + workgroupBarrier(); + sh_parent[local_id.x] = agg; + } + + let ix = global_id.x; + agg = reduced[ix]; + sh_monoid[local_id.x] = agg; + for (var i = 0u; i < LG_WG_SIZE; i += 1u) { + workgroupBarrier(); + if local_id.x >= 1u << i { + let other = sh_monoid[local_id.x - (1u << i)]; + agg = combine_tag_monoid(other, agg); + } + workgroupBarrier(); + sh_monoid[local_id.x] = agg; + } + workgroupBarrier(); + // prefix up to this workgroup + var tm = sh_parent[0]; + if local_id.x > 0u { + tm = combine_tag_monoid(tm, sh_monoid[local_id.x - 1u]); + } + // exclusive prefix sum, granularity of 4 tag bytes * workgroup size + tag_monoids[ix] = tm; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/tile_alloc.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/tile_alloc.wgsl new file mode 100644 index 000000000..00915fefe --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/tile_alloc.wgsl @@ -0,0 +1,123 @@ +// Copyright 2022 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense + +// Tile allocation (and zeroing of tiles) + +#import config +#import bump +#import drawtag +#import tile + +@group(0) @binding(0) +var config: Config; + +@group(0) @binding(1) +var scene: array; + +@group(0) @binding(2) +var draw_bboxes: array>; + +@group(0) @binding(3) +var bump: BumpAllocators; + +@group(0) @binding(4) +var paths: array; + +@group(0) @binding(5) +var tiles: array; + +const WG_SIZE = 256u; + +var sh_tile_count: array; +var sh_tile_offset: u32; +var sh_previous_failed: u32; + +@compute @workgroup_size(256) +fn main( + @builtin(global_invocation_id) global_id: vec3, + @builtin(local_invocation_id) local_id: vec3, +) { + // Exit early if prior stages failed, as we can't run this stage. + // We need to check only prior stages, as if this stage has failed in another workgroup, + // we still want to know this workgroup's memory requirement. + if local_id.x == 0u { + let failed = (atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_FLATTEN)) != 0u; + sh_previous_failed = u32(failed); + } + let failed = workgroupUniformLoad(&sh_previous_failed); + if failed != 0u { + return; + } + // scale factors useful for converting coordinates to tiles + // TODO: make into constants + let SX = 1.0 / f32(TILE_WIDTH); + let SY = 1.0 / f32(TILE_HEIGHT); + + let drawobj_ix = global_id.x; + var drawtag = DRAWTAG_NOP; + if drawobj_ix < config.n_drawobj { + drawtag = scene[config.drawtag_base + drawobj_ix]; + } + var x0 = 0; + var y0 = 0; + var x1 = 0; + var y1 = 0; + if drawtag != DRAWTAG_NOP && drawtag != DRAWTAG_END_CLIP { + let bbox = draw_bboxes[drawobj_ix]; + + // Don't round up the bottom-right corner of the bbox if the area is zero and leave the + // coordinates at 0. This will make `tile_count` zero as the shape is clipped out. + if bbox.x < bbox.z && bbox.y < bbox.w { + x0 = i32(floor(bbox.x * SX)); + y0 = i32(floor(bbox.y * SY)); + x1 = i32(ceil(bbox.z * SX)); + y1 = i32(ceil(bbox.w * SY)); + } + } + let ux0 = u32(clamp(x0, 0, i32(config.width_in_tiles))); + let uy0 = u32(clamp(y0, 0, i32(config.height_in_tiles))); + let ux1 = u32(clamp(x1, 0, i32(config.width_in_tiles))); + let uy1 = u32(clamp(y1, 0, i32(config.height_in_tiles))); + let tile_count = (ux1 - ux0) * (uy1 - uy0); + var total_tile_count = tile_count; + sh_tile_count[local_id.x] = tile_count; + for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { + workgroupBarrier(); + if local_id.x >= (1u << i) { + total_tile_count += sh_tile_count[local_id.x - (1u << i)]; + } + workgroupBarrier(); + sh_tile_count[local_id.x] = total_tile_count; + } + if local_id.x == WG_SIZE - 1u { + let count = sh_tile_count[WG_SIZE - 1u]; + var offset = atomicAdd(&bump.tile, count); + if offset + count > config.tiles_size { + offset = 0u; + atomicOr(&bump.failed, STAGE_TILE_ALLOC); + } + paths[drawobj_ix].tiles = offset; + } + // Using storage barriers is a workaround for what appears to be a miscompilation + // when a normal workgroup-shared variable is used to broadcast the value. + storageBarrier(); + let tile_offset = paths[drawobj_ix | (WG_SIZE - 1u)].tiles; + storageBarrier(); + if drawobj_ix < config.n_drawobj { + let tile_subix = select(0u, sh_tile_count[local_id.x - 1u], local_id.x > 0u); + let bbox = vec4(ux0, uy0, ux1, uy1); + let path = Path(bbox, tile_offset + tile_subix); + paths[drawobj_ix] = path; + } + + // zero allocated memory + // Note: if the number of draw objects is small, utilization will be poor. + // There are two things that can be done to improve that. One would be a + // separate (indirect) dispatch. Another would be to have each workgroup + // process fewer draw objects than the number of threads in the wg. + let total_count = sh_tile_count[WG_SIZE - 1u]; + for (var i = local_id.x; i < total_count; i += WG_SIZE) { + // Note: could format output buffer as u32 for even better load balancing. + tiles[tile_offset + i] = Tile(0, 0u); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md new file mode 100644 index 000000000..24d83dff7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -0,0 +1,161 @@ +# WebGPUDrawingBackend + +`WebGPUDrawingBackend` is in a transitional state. + +The old scene rasterizer has been removed. Scene flushes now fall back to the CPU backend, and the only active GPU rendering path left in this project is GPU layer composition. This document describes that current reality so the code and docs stay aligned while the replacement rasterizer is built. + +## Overview + +Today the backend does two distinct jobs: + +1. Provide WebGPU-backed layer frames and layer composition when the target surface is native. +2. Defer normal drawing-scene flushes to `DefaultDrawingBackend`. + +That means the backend is still useful, but only for the parts that are already clean and bounded. The removed scene rasterizer is intentionally not described here as an active system. + +## Current Flush Behavior + +For `FlushCompositions(...)`, the backend now behaves as a fallback coordinator: + +```mermaid +flowchart TD + A[DrawingCanvas.Flush] --> B[WebGPUDrawingBackend.FlushCompositions] + B --> C[Update diagnostics] + C --> D[FlushCompositionsFallback] + D --> E[Render scene with DefaultDrawingBackend] + E --> F[Upload result into native WebGPU target] +``` + +This is deliberate. The previous GPU scene path was removed rather than left half-alive. + +### What FlushCompositions Does Now + +`FlushCompositions(...)` in [WebGPUDrawingBackend.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs): + +- records fallback diagnostics +- reports that the WebGPU scene rasterizer is unavailable +- delegates scene rendering to `FlushCompositionsFallback(...)` + +`FlushCompositionsFallback(...)` then: + +- allocates a CPU staging frame +- renders the scene with `DefaultDrawingBackend` +- uploads the final pixel region into the target WebGPU texture + +There is no intermediate GPU scene planner, no GPU edge builder, and no GPU fine raster pass in the current implementation. + +## Active GPU Path: Layer Composition + +Layer composition is still GPU-backed and is the part of the backend that remains active. + +```mermaid +flowchart TD + A[SaveLayer result] --> B[ComposeLayer] + B --> C{Native GPU destination?} + C -->|No| D[Fallback CPU compose] + C -->|Yes| E[TryComposeLayerGpu] + E -->|Success| F[ComposeLayerComputeShader] + E -->|Failure| G[ComposeLayerFallback] + G --> H[CPU compose plus upload] +``` + +This path lives primarily in: + +- [WebGPUDrawingBackend.ComposeLayer.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs) +- [ComposeLayerComputeShader.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs) +- [CompositionShaderSnippets.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs) + +### Why This Path Survived + +The layer-composition path already has the right properties: + +- bounded work +- simple resource lifetime +- no giant flush-wide raster buffers +- direct use of final pixel data instead of a scene-wide vector raster pipeline + +That makes it safe to keep while the scene rasterizer is replaced. + +## Resource Model + +The backend still owns the common WebGPU infrastructure needed by the surviving GPU path: + +- device probing +- transient texture creation +- native layer frame allocation +- command submission +- GPU readback helpers + +Those helpers live in: + +- [WebGPUDrawingBackend.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs) +- [WebGPUFlushContext.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs) +- [WebGPUDrawingBackend.Readback.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs) +- [WebGPUTextureSampleTypeHelper.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUTextureSampleTypeHelper.cs) + +## Layer Frame Lifecycle + +`CreateLayerFrame(...)` still attempts GPU allocation first when the parent surface is native. + +```mermaid +flowchart TD + A[CreateLayerFrame] --> B{Parent has native WebGPU surface?} + B -->|No| C[Fallback CPU frame] + B -->|Yes| D[Check pixel format and required feature] + D --> E[Allocate texture and texture view] + E --> F[Wrap as NativeCanvasFrame] +``` + +`ReleaseFrameResources(...)` releases those texture handles when the frame is GPU-backed, or delegates to the CPU backend otherwise. + +## Diagnostics + +The scene-flush diagnostics now reflect the removed rasterizer: + +- GPU scene execution counters remain unchanged by `FlushCompositions(...)` +- fallback counters increase for every scene flush +- `TestingLastGPUInitializationFailure` is set to `"WebGPU scene rasterizer removed pending replacement."` +- compute-path scene counters are reset to `0` + +That keeps the diagnostic surface honest. + +## What Was Removed + +The following pieces are no longer part of the active backend: + +- the scene flush planner +- GPU edge preparation for scene fills +- the path-tiling compute pass +- the fine raster/composite scene shader +- the old scene-raster buffer packing logic + +Those files were intentionally deleted rather than left dormant. + +## Replacement Direction + +The eventual replacement scene rasterizer is expected to be a bounded staged design rather than a monolithic flush-wide raster pass. This document does not describe that future system yet, because it does not exist in the codebase today. + +Until that replacement lands, the contract is simple: + +- scene flushes use CPU rendering plus upload +- layer composition can still use WebGPU directly + +## Reading Order + +If you want to understand the current backend, read the files in this order: + +1. [WebGPUDrawingBackend.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs) +2. [WebGPUDrawingBackend.ComposeLayer.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs) +3. [WebGPUFlushContext.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs) +4. [ComposeLayerComputeShader.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/Shaders/ComposeLayerComputeShader.cs) +5. [CompositionShaderSnippets.cs](/d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/Shaders/CompositionShaderSnippets.cs) + +## Summary + +The current WebGPU backend is intentionally smaller than before: + +- it no longer contains a scene rasterizer +- it still supports GPU-backed layer composition +- normal scene flushes fall back cleanly to the CPU backend + +That is the accurate implementation state today. diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs new file mode 100644 index 000000000..1d5ee20cf --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Creates a bind group layout for WebGPU composition pipelines. +/// +/// The WebGPU API facade. +/// The device used to create resources. +/// The created bind-group layout. +/// The error message when creation fails. +/// if the layout was created; otherwise . +internal unsafe delegate bool WebGPUCompositeBindGroupLayoutFactory( + WebGPU api, + Device* device, + out BindGroupLayout* bindGroupLayout, + out string? error); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs new file mode 100644 index 000000000..1a68e83ae --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.ComposeLayer.cs @@ -0,0 +1,383 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU layer compositing via . +/// +public sealed unsafe partial class WebGPUDrawingBackend +{ + private const string ComposeLayerPipelineKey = "compose-layer"; + private const string ComposeLayerConfigBufferKey = "compose-layer/config"; + + /// + /// Attempts to composite a source layer onto a destination using a GPU compute shader. + /// Returns when GPU compositing is not available — the caller + /// should fall back to for + /// CPU-backed destinations where the transfer overhead outweighs the GPU benefit. + /// + private bool TryComposeLayerGpu( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature)) + { + return false; + } + + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + + // Only use GPU compositing when the destination is a native surface. + // CPU-backed destinations fall back to DefaultDrawingBackend where a + // simple pixel blend avoids the upload/readback transfer overhead. + if (!destination.TryGetNativeSurface(out NativeSurface? nativeSurface)) + { + return false; + } + + int srcWidth = source.Bounds.Width; + int srcHeight = source.Bounds.Height; + if (srcWidth <= 0 || srcHeight <= 0) + { + return true; // Nothing to composite. + } + + if (!ComposeLayerComputeShader.TryGetCode(textureFormat, out byte[] shaderCode, out _)) + { + return false; + } + + // TryGetCode already validates format support via TryGetInputSampleType internally. + _ = WebGPUTextureSampleTypeHelper.TryGetInputSampleType(textureFormat, out TextureSampleType inputSampleType); + + // Create a flush context against the destination surface. + WebGPUFlushContext? flushContext = WebGPUFlushContext.Create( + destination, + textureFormat, + requiredFeature, + configuration.MemoryAllocator); + + if (flushContext is null) + { + return false; + } + + try + { + if (!flushContext.EnsureCommandEncoder()) + { + return false; + } + + // Acquire the source texture: bind the existing GPU texture if native, + // otherwise upload from CPU pixels. + if (!TryAcquireSourceTexture( + flushContext, + source, + configuration.MemoryAllocator, + out Texture* sourceTexture, + out TextureView* sourceTextureView)) + { + return false; + } + + // The destination texture/view are guaranteed valid by WebGPUFlushContext.Create. + Texture* destTexture = flushContext.TargetTexture; + TextureView* destTextureView = flushContext.TargetView; + + // Create output texture sized to the destination. + int destWidth = destination.Bounds.Width; + int destHeight = destination.Bounds.Height; + + // Clamp compositing region to both source and destination bounds. + int startX = Math.Max(0, -destinationOffset.X); + int startY = Math.Max(0, -destinationOffset.Y); + int endX = Math.Min(srcWidth, destWidth - destinationOffset.X); + int endY = Math.Min(srcHeight, destHeight - destinationOffset.Y); + + if (endX <= startX || endY <= startY) + { + return true; // No overlap. + } + + int compositeWidth = endX - startX; + int compositeHeight = endY - startY; + + if (!TryCreateCompositionTexture(flushContext, compositeWidth, compositeHeight, out Texture* outputTexture, out TextureView* outputTextureView, out _)) + { + return false; + } + + // Get or create the compute pipeline. + string pipelineKey = $"{ComposeLayerPipelineKey}/{textureFormat}"; + bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) + => TryCreateComposeLayerBindGroupLayout( + api, + device, + textureFormat, + inputSampleType, + out layout, + out layoutError); + + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + pipelineKey, + shaderCode, + ComposeLayerComputeShader.EntryPoint, + LayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out _)) + { + return false; + } + + // Create and upload the config uniform. + nuint configSize = (nuint)Unsafe.SizeOf(); + if (!flushContext.DeviceState.TryGetOrCreateSharedBuffer( + ComposeLayerConfigBufferKey, + BufferUsage.Uniform | BufferUsage.CopyDst, + configSize, + out WgpuBuffer* configBuffer, + out _, + out _)) + { + return false; + } + + LayerConfigGpu config = new() + { + SourceWidth = (uint)srcWidth, + SourceHeight = (uint)srcHeight, + DestOffsetX = destinationOffset.X + startX, + DestOffsetY = destinationOffset.Y + startY, + ColorBlendMode = (uint)options.ColorBlendingMode, + AlphaCompositionMode = (uint)options.AlphaCompositionMode, + BlendPercentage = FloatToUInt32Bits(options.BlendPercentage), + Padding = 0 + }; + + flushContext.Api.QueueWriteBuffer(flushContext.Queue, configBuffer, 0, &config, configSize); + + // Create bind group. + BindGroupEntry* bindGroupEntries = stackalloc BindGroupEntry[4]; + bindGroupEntries[0] = new BindGroupEntry { Binding = 0, TextureView = sourceTextureView }; + bindGroupEntries[1] = new BindGroupEntry { Binding = 1, TextureView = destTextureView }; + bindGroupEntries[2] = new BindGroupEntry { Binding = 2, TextureView = outputTextureView }; + bindGroupEntries[3] = new BindGroupEntry + { + Binding = 3, + Buffer = configBuffer, + Offset = 0, + Size = configSize + }; + + BindGroupDescriptor bindGroupDescriptor = new() + { + Layout = bindGroupLayout, + EntryCount = 4, + Entries = bindGroupEntries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in bindGroupDescriptor); + if (bindGroup is null) + { + return false; + } + + flushContext.TrackBindGroup(bindGroup); + + // Dispatch compute. + uint tileCountX = DivideRoundUp(compositeWidth, CompositeTileWidth); + uint tileCountY = DivideRoundUp(compositeHeight, CompositeTileHeight); + + ComputePassDescriptor passDescriptor = default; + ComputePassEncoder* passEncoder = flushContext.Api.CommandEncoderBeginComputePass(flushContext.CommandEncoder, in passDescriptor); + if (passEncoder is null) + { + return false; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, tileCountX, tileCountY, 1); + } + finally + { + flushContext.Api.ComputePassEncoderEnd(passEncoder); + flushContext.Api.ComputePassEncoderRelease(passEncoder); + } + + // Copy output back to destination texture at the compositing offset. + CopyTextureRegion( + flushContext, + outputTexture, + 0, + 0, + destTexture, + destinationOffset.X + startX, + destinationOffset.Y + startY, + compositeWidth, + compositeHeight); + + return TrySubmit(flushContext); + } + finally + { + flushContext.Dispose(); + } + } + + /// + /// Creates the bind-group layout for the layer compositing compute shader. + /// + private static bool TryCreateComposeLayerBindGroupLayout( + WebGPU api, + Device* device, + TextureFormat outputTextureFormat, + TextureSampleType inputTextureSampleType, + out BindGroupLayout* layout, + out string? error) + { + BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + + // Binding 0: source layer texture (read). + entries[0] = new BindGroupLayoutEntry + { + Binding = 0, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = inputTextureSampleType, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + + // Binding 1: destination/backdrop texture (read). + entries[1] = new BindGroupLayoutEntry + { + Binding = 1, + Visibility = ShaderStage.Compute, + Texture = new TextureBindingLayout + { + SampleType = inputTextureSampleType, + ViewDimension = TextureViewDimension.Dimension2D, + Multisampled = false + } + }; + + // Binding 2: output texture (write storage). + entries[2] = new BindGroupLayoutEntry + { + Binding = 2, + Visibility = ShaderStage.Compute, + StorageTexture = new StorageTextureBindingLayout + { + Access = StorageTextureAccess.WriteOnly, + Format = outputTextureFormat, + ViewDimension = TextureViewDimension.Dimension2D + } + }; + + // Binding 3: uniform config buffer. + entries[3] = new BindGroupLayoutEntry + { + Binding = 3, + Visibility = ShaderStage.Compute, + Buffer = new BufferBindingLayout + { + Type = BufferBindingType.Uniform, + HasDynamicOffset = false, + MinBindingSize = (nuint)Unsafe.SizeOf() + } + }; + + BindGroupLayoutDescriptor descriptor = new() + { + EntryCount = 4, + Entries = entries + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create compose-layer bind group layout."; + return false; + } + + error = null; + return true; + } + + /// + /// Acquires a GPU texture and view for the source frame. If the source is already + /// a native GPU texture it is bound directly; otherwise CPU pixels are uploaded + /// to a temporary texture. + /// + private static bool TryAcquireSourceTexture( + WebGPUFlushContext flushContext, + ICanvasFrame source, + MemoryAllocator memoryAllocator, + out Texture* sourceTexture, + out TextureView* sourceTextureView) + where TPixel : unmanaged, IPixel + { + if (source.TryGetNativeSurface(out NativeSurface? sourceSurface) + && sourceSurface.TryGetCapability(out WebGPUSurfaceCapability? srcCapability)) + { + sourceTexture = (Texture*)srcCapability.TargetTexture; + sourceTextureView = (TextureView*)srcCapability.TargetTextureView; + return true; + } + + if (source.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + if (!TryCreateCompositionTexture(flushContext, sourceRegion.Width, sourceRegion.Height, out sourceTexture, out sourceTextureView, out _)) + { + return false; + } + + WebGPUFlushContext.UploadTextureFromRegion( + flushContext.Api, + flushContext.Queue, + sourceTexture, + sourceRegion, + memoryAllocator); + return true; + } + + sourceTexture = null; + sourceTextureView = null; + return false; + } + + /// + /// GPU uniform config matching the WGSL LayerConfig struct layout. + /// + [StructLayout(LayoutKind.Sequential)] + private struct LayerConfigGpu + { + public uint SourceWidth; + public uint SourceHeight; + public int DestOffsetX; + public int DestOffsetY; + public uint ColorBlendMode; + public uint AlphaCompositionMode; + public uint BlendPercentage; + public uint Padding; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs new file mode 100644 index 000000000..f49fe9cd9 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Pixel-format registration for composite session I/O. +/// +/// +/// The map defined by is intentionally explicit and only +/// includes one-to-one format mappings where the GPU texture format can round-trip the pixel payload +/// without channel swizzle or custom conversion logic. +/// Only formats that support storage texture binding (required by the compute compositor) +/// are included. Formats that lack storage support are omitted and fall back to the CPU backend. +/// +public sealed partial class WebGPUDrawingBackend +{ + private static readonly Lazy> CompositeTextureSampleTypes = + new(CreateCompositeTextureSampleTypes); + + private static readonly Lazy> CompositeTextureShaderTraitsMap = + new(CreateCompositeTextureShaderTraits); + + internal enum CompositeTextureEncodingKind + { + Float, + Snorm, + Uint8, + Uint16, + Sint16 + } + + /// + /// Builds the static registration table that maps implementations to + /// compatible WebGPU storage/sampling formats. + /// + /// The registration map used during flush dispatch. + private static Dictionary CreateCompositePixelHandlers() => + + // Only formats with native or feature-gated storage binding support. + new() + { + [typeof(NormalizedByte4)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Snorm, TextureSampleType.Float), + + [typeof(HalfVector4)] = CompositePixelRegistration.Create(TextureFormat.Rgba16float, TextureSampleType.Float), + + [typeof(Rgba32)] = CompositePixelRegistration.Create(TextureFormat.Rgba8Unorm, TextureSampleType.Float), + [typeof(Bgra32)] = CompositePixelRegistration.Create(TextureFormat.Bgra8Unorm, TextureSampleType.Float, FeatureName.Bgra8UnormStorage) + }; + + /// + /// Builds the sampled-texture-type lookup keyed by the explicit composite format registrations. + /// + /// The lookup used by shader/bind-group setup. + private static Dictionary CreateCompositeTextureSampleTypes() + { + Dictionary sampleTypes = []; + foreach (CompositePixelRegistration registration in CompositePixelHandlers.Values) + { + sampleTypes[registration.TextureFormat] = registration.SampleType; + } + + return sampleTypes; + } + + /// + /// Resolves the WebGPU texture format identifier for . + /// + /// The requested pixel type. + /// Receives the mapped texture format identifier on success. + /// + /// when the pixel type has a registered GPU format mapping; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId) + where TPixel : unmanaged, IPixel + { + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration registration)) + { + formatId = default; + return false; + } + + formatId = WebGPUTextureFormatMapper.FromSilk(registration.TextureFormat); + return true; + } + + /// + /// Resolves the WebGPU texture format identifier and any required device feature + /// for . + /// + /// The requested pixel type. + /// Receives the mapped texture format identifier on success. + /// + /// Receives the device feature required for storage binding, or + /// when no special feature is needed. + /// + /// + /// when the pixel type has a registered GPU format mapping; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature) + where TPixel : unmanaged, IPixel + { + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration registration)) + { + formatId = default; + requiredFeature = FeatureName.Undefined; + return false; + } + + formatId = WebGPUTextureFormatMapper.FromSilk(registration.TextureFormat); + requiredFeature = registration.RequiredFeature; + return true; + } + + /// + /// Resolves the unmanaged size in bytes of a registered composite pixel type. + /// + /// The requested pixel type. + /// Receives the unmanaged pixel size in bytes on success. + /// + /// when the pixel type has a registered GPU format mapping; otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCompositePixelSize(out int pixelSizeInBytes) + where TPixel : unmanaged, IPixel + { + if (!CompositePixelHandlers.TryGetValue(typeof(TPixel), out CompositePixelRegistration registration)) + { + pixelSizeInBytes = 0; + return false; + } + + pixelSizeInBytes = registration.PixelSizeInBytes; + return true; + } + + /// + /// Resolves the sampled texture type for a registered composite texture format. + /// + /// The WebGPU texture format. + /// Receives the sampled texture type on success. + /// + /// when the format is one of the explicitly registered composite formats; + /// otherwise . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCompositeTextureSampleType(TextureFormat textureFormat, out TextureSampleType sampleType) + => CompositeTextureSampleTypes.Value.TryGetValue(textureFormat, out sampleType); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool TryGetCompositeTextureShaderTraits(TextureFormat textureFormat, out CompositeTextureShaderTraits traits) + => CompositeTextureShaderTraitsMap.Value.TryGetValue(textureFormat, out traits); + + private static Dictionary CreateCompositeTextureShaderTraits() + => new() + { + [TextureFormat.Rgba8Snorm] = new CompositeTextureShaderTraits("rgba8snorm", "f32", TextureSampleType.Float, CompositeTextureEncodingKind.Snorm), + [TextureFormat.Rgba16float] = new CompositeTextureShaderTraits("rgba16float", "f32", TextureSampleType.Float, CompositeTextureEncodingKind.Float), + [TextureFormat.Rgba8Unorm] = new CompositeTextureShaderTraits("rgba8unorm", "f32", TextureSampleType.Float, CompositeTextureEncodingKind.Float), + [TextureFormat.Bgra8Unorm] = new CompositeTextureShaderTraits("bgra8unorm", "f32", TextureSampleType.Float, CompositeTextureEncodingKind.Float) + }; + + internal readonly struct CompositeTextureShaderTraits( + string outputFormat, + string texelType, + TextureSampleType sampleType, + CompositeTextureEncodingKind encodingKind) + { + public string OutputFormat { get; } = outputFormat; + + public string TexelType { get; } = texelType; + + public TextureSampleType SampleType { get; } = sampleType; + + public CompositeTextureEncodingKind EncodingKind { get; } = encodingKind; + } + + /// + /// Per-pixel registration payload consumed by GPU composition setup. + /// + private readonly struct CompositePixelRegistration + { + /// + /// Initializes a new instance of the struct. + /// + /// The registered pixel CLR type. + /// The matching WebGPU texture format. + /// The sampled texture type for this format. + /// The unmanaged pixel size in bytes. + /// Optional device feature required for storage binding support. + public CompositePixelRegistration( + Type pixelType, + TextureFormat textureFormat, + TextureSampleType sampleType, + int pixelSizeInBytes, + FeatureName requiredFeature) + { + this.PixelType = pixelType; + this.TextureFormat = textureFormat; + this.SampleType = sampleType; + this.PixelSizeInBytes = pixelSizeInBytes; + this.RequiredFeature = requiredFeature; + } + + /// + /// Gets the CLR pixel type registered for this mapping. + /// + public Type PixelType { get; } + + /// + /// Gets the WebGPU texture format used for this pixel type. + /// + public TextureFormat TextureFormat { get; } + + /// + /// Gets the unmanaged size of the pixel type in bytes. + /// + public int PixelSizeInBytes { get; } + + /// + /// Gets the sampled texture type used when reading this format. + /// + public TextureSampleType SampleType { get; } + + /// + /// Gets the optional device feature required for storage binding support. + /// means the format is natively storable. + /// + public FeatureName RequiredFeature { get; } + + /// + /// Creates a registration record for with native storage support. + /// + /// The matching WebGPU texture format. + /// The sampled texture type for this format. + /// The initialized registration. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CompositePixelRegistration Create(TextureFormat textureFormat, TextureSampleType sampleType) + where TPixel : unmanaged, IPixel + => new(typeof(TPixel), textureFormat, sampleType, Unsafe.SizeOf(), FeatureName.Undefined); + + /// + /// Creates a registration record for with a required device feature. + /// + /// The matching WebGPU texture format. + /// The sampled texture type for this format. + /// The device feature required for storage binding. + /// The initialized registration. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static CompositePixelRegistration Create(TextureFormat textureFormat, TextureSampleType sampleType, FeatureName requiredFeature) + where TPixel : unmanaged, IPixel + => new(typeof(TPixel), textureFormat, sampleType, Unsafe.SizeOf(), requiredFeature); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs new file mode 100644 index 000000000..8af5ff8dc --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs @@ -0,0 +1,230 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU readback helpers. +/// +public sealed unsafe partial class WebGPUDrawingBackend +{ + private const int ReadbackCallbackTimeoutMilliseconds = 5000; + + /// + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(target, nameof(target)); + Guard.NotNull(destination.Buffer, nameof(destination)); + + // Readback is only available for native WebGPU targets with valid interop handles. + if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || + !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability) || + capability.Device == 0 || + capability.Queue == 0 || + capability.TargetTexture == 0) + { + return false; + } + + if (!TryGetCompositeTextureFormat(out WebGPUTextureFormatId expectedFormat, out FeatureName requiredFeature) || + expectedFormat != capability.TargetFormat) + { + return false; + } + + // Convert canvas-local source coordinates to absolute native-surface coordinates. + Rectangle absoluteSource = new( + target.Bounds.X + sourceRectangle.X, + target.Bounds.Y + sourceRectangle.Y, + sourceRectangle.Width, + sourceRectangle.Height); + + Rectangle surfaceBounds = new(0, 0, capability.Width, capability.Height); + Rectangle source = Rectangle.Intersect(surfaceBounds, absoluteSource); + + if (source.Width <= 0 || source.Height <= 0) + { + return false; + } + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + Device* device = (Device*)capability.Device; + + if (requiredFeature != FeatureName.Undefined + && !WebGPURuntime.GetOrCreateDeviceState(api, device).HasFeature(requiredFeature)) + { + return false; + } + + Queue* queue = (Queue*)capability.Queue; + + int pixelSizeInBytes = Unsafe.SizeOf(); + int packedRowBytes = checked(source.Width * pixelSizeInBytes); + + // WebGPU copy-to-buffer requires bytes-per-row alignment to 256 bytes. + int readbackRowBytes = Align(packedRowBytes, 256); + ulong readbackByteCount = checked((ulong)readbackRowBytes * (ulong)source.Height); + + WgpuBuffer* readbackBuffer = null; + CommandEncoder* commandEncoder = null; + CommandBuffer* commandBuffer = null; + try + { + BufferDescriptor bufferDescriptor = new() + { + Usage = BufferUsage.CopyDst | BufferUsage.MapRead, + Size = readbackByteCount, + MappedAtCreation = false + }; + + readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor); + if (readbackBuffer is null) + { + return false; + } + + CommandEncoderDescriptor encoderDescriptor = default; + commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor); + if (commandEncoder is null) + { + return false; + } + + // Copy only the requested source rect from the target texture into the readback buffer. + ImageCopyTexture sourceCopy = new() + { + Texture = (Texture*)capability.TargetTexture, + MipLevel = 0, + Origin = new Origin3D((uint)source.X, (uint)source.Y, 0), + Aspect = TextureAspect.All + }; + + ImageCopyBuffer destinationCopy = new() + { + Buffer = readbackBuffer, + Layout = new TextureDataLayout + { + Offset = 0, + BytesPerRow = (uint)readbackRowBytes, + RowsPerImage = (uint)source.Height + } + }; + + Extent3D copySize = new((uint)source.Width, (uint)source.Height, 1); + api.CommandEncoderCopyTextureToBuffer(commandEncoder, in sourceCopy, in destinationCopy, in copySize); + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + return false; + } + + api.QueueSubmit(queue, 1, ref commandBuffer); + api.CommandBufferRelease(commandBuffer); + commandBuffer = null; + api.CommandEncoderRelease(commandEncoder); + commandEncoder = null; + + // Map the GPU buffer and wait for completion before reading host-visible bytes. + BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + using ManualResetEventSlim mapReady = new(false); + void Callback(BufferMapAsyncStatus status, void* userData) + { + _ = userData; + mapStatus = status; + mapReady.Set(); + } + + using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null); + if (!WaitForMapSignal(lease.WgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + { + return false; + } + + void* mapped = api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)readbackByteCount); + if (mapped is null) + { + api.BufferUnmap(readbackBuffer); + return false; + } + + try + { + ReadOnlySpan readback = new(mapped, checked((int)readbackByteCount)); + + // Copy directly from the mapped GPU buffer into the caller's buffer, + // stripping WebGPU row padding in the process. Single copy, no intermediate array. + int copyHeight = Math.Min(source.Height, destination.Height); + for (int y = 0; y < copyHeight; y++) + { + readback + .Slice(y * readbackRowBytes, packedRowBytes) + .CopyTo(MemoryMarshal.AsBytes(destination.DangerousGetRowSpan(y))); + } + + return true; + } + finally + { + api.BufferUnmap(readbackBuffer); + } + } + finally + { + if (commandBuffer is not null) + { + api.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + api.CommandEncoderRelease(commandEncoder); + } + + if (readbackBuffer is not null) + { + api.BufferRelease(readbackBuffer); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Align(int value, int alignment) + => ((value + alignment - 1) / alignment) * alignment; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool WaitForMapSignal(Wgpu? extension, Device* device, ManualResetEventSlim signal) + { + if (extension is null) + { + return signal.Wait(ReadbackCallbackTimeoutMilliseconds); + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < ReadbackCallbackTimeoutMilliseconds) + { + _ = extension.DevicePoll(device, true, (WrappedSubmissionIndex*)null); + } + + return signal.IsSet; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs new file mode 100644 index 000000000..ca401fccc --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -0,0 +1,513 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// WebGPU-backed implementation of . +/// +/// +/// Scene composition uses a staged WebGPU raster path when the target surface and pixel format are supported, +/// and falls back to otherwise. +/// +public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisposable +{ + private const int CompositeTileWidth = 16; + private const int CompositeTileHeight = 16; + private const int UniformBufferOffsetAlignment = 256; + + private readonly DefaultDrawingBackend fallbackBackend; + private static bool? isSupported; + private bool isDisposed; + + private static readonly Dictionary CompositePixelHandlers = CreateCompositePixelHandlers(); + + /// + /// Initializes a new instance of the class. + /// + public WebGPUDrawingBackend() + => this.fallbackBackend = DefaultDrawingBackend.Instance; + + /// + /// Gets a value indicating whether the last flush completed on the staged path. + /// + internal bool TestingLastFlushUsedGPU { get; private set; } + + /// + /// Gets the testing-only diagnostic containing the last GPU initialization failure reason, if any. + /// + internal string? TestingLastGPUInitializationFailure { get; private set; } + + /// + /// Gets a value indicating whether the last flush completed on the staged path. + /// + public bool DiagnosticLastFlushUsedGPU => this.TestingLastFlushUsedGPU; + + /// + /// Gets the last staged-scene creation or dispatch failure that forced CPU fallback. + /// + public string? DiagnosticLastSceneFailure => this.TestingLastGPUInitializationFailure; + + /// + /// Gets a value indicating whether WebGPU is available on the current system. + /// This probes the runtime by attempting to acquire an adapter and device. + /// The result is cached after the first probe. + /// + public bool IsSupported => isSupported ??= ProbeFullSupport(); + + /// + /// Probes whether WebGPU compute is fully supported on the current system. + /// First checks adapter/device availability in-process. If that succeeds, + /// spawns a child process via to test compute + /// pipeline creation, which can crash with an unrecoverable access violation + /// on some systems. If the remote executor is not available, falls back to + /// the device-only check. + /// + /// Returns if WebGPU compute support is available; otherwise, . + private static bool ProbeFullSupport() + { + if (!ProbeSupport()) + { + return false; + } + + if (!RemoteExecutor.IsSupported) + { + return true; + } + + return RemoteExecutor.Invoke(ProbeComputePipelineSupport) == 0; + } + + /// + /// Determines whether WebGPU adapter and device are available on the current system. + /// + /// This method only checks adapter and device availability. It does not attempt + /// compute pipeline creation. Use for a complete check. + /// Returns if a WebGPU device is available; otherwise, . + public static bool ProbeSupport() + { + try + { + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + return WebGPURuntime.TryGetOrCreateDevice(out _, out _, out _); + } + catch + { + return false; + } + } + + /// + /// Probes full WebGPU compute pipeline support by compiling a trivial shader and + /// creating a compute pipeline. This method may crash with an access violation on + /// systems with broken WebGPU compute support - callers should run it in a child + /// process (for example via RemoteExecutor) to isolate the crash. + /// + /// Exit code: 0 on success, 1 on failure. + public static int ProbeComputePipelineSupport() + { + try + { + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + if (!WebGPURuntime.TryGetOrCreateDevice(out Device* device, out _, out _)) + { + return 1; + } + + WebGPU api = lease.Api; + + ReadOnlySpan probeShader = "@compute @workgroup_size(1) fn main() {}\0"u8; + fixed (byte* shaderCodePtr = probeShader) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + ShaderModule* shaderModule = api.DeviceCreateShaderModule(device, in shaderDescriptor); + if (shaderModule is null) + { + return 1; + } + + try + { + ReadOnlySpan entryPoint = "main\0"u8; + fixed (byte* entryPointPtr = entryPoint) + { + ProgrammableStageDescriptor computeStage = new() + { + Module = shaderModule, + EntryPoint = entryPointPtr + }; + + PipelineLayoutDescriptor layoutDescriptor = new() + { + BindGroupLayoutCount = 0, + BindGroupLayouts = null + }; + + PipelineLayout* pipelineLayout = api.DeviceCreatePipelineLayout(device, in layoutDescriptor); + if (pipelineLayout is null) + { + return 1; + } + + try + { + ComputePipelineDescriptor pipelineDescriptor = new() + { + Layout = pipelineLayout, + Compute = computeStage + }; + + ComputePipeline* pipeline = api.DeviceCreateComputePipeline(device, in pipelineDescriptor); + if (pipeline is null) + { + return 1; + } + + api.ComputePipelineRelease(pipeline); + return 0; + } + finally + { + api.PipelineLayoutRelease(pipelineLayout); + } + } + } + finally + { + api.ShaderModuleRelease(shaderModule); + } + } + } + catch + { + return 1; + } + } + + /// + public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + if (compositionScene.Commands.Count == 0) + { + return; + } + + this.TestingLastFlushUsedGPU = false; + this.TestingLastGPUInitializationFailure = null; + + if (!WebGPUSceneDispatch.TryCreateStagedScene(configuration, target, compositionScene.Commands, out bool exceedsBindingLimit, out WebGPUStagedScene stagedScene, out string? error)) + { + this.TestingLastGPUInitializationFailure = exceedsBindingLimit + ? error ?? "The staged WebGPU scene exceeded the current binding limits." + : error ?? "Failed to create the staged WebGPU scene."; + this.FlushCompositionsFallback(configuration, target, compositionScene, compositionBounds: null); + return; + } + + try + { + this.TestingLastFlushUsedGPU = true; + + if (stagedScene.EncodedScene.FillCount == 0) + { + return; + } + + if (WebGPUSceneDispatch.TryRenderStagedScene(ref stagedScene, out error)) + { + return; + } + + this.TestingLastFlushUsedGPU = false; + this.TestingLastGPUInitializationFailure = error ?? "The staged WebGPU scene dispatch failed."; + } + finally + { + stagedScene.Dispose(); + } + + this.FlushCompositionsFallback(configuration, target, compositionScene, compositionBounds: null); + } + + /// + /// Executes the scene on the CPU fallback backend, then uploads the result + /// to the native GPU surface. + /// + private void FlushCompositionsFallback( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene, + Rectangle? compositionBounds) + where TPixel : unmanaged, IPixel + { + _ = target.TryGetNativeSurface(out NativeSurface? nativeSurface); + _ = nativeSurface!.TryGetCapability(out WebGPUSurfaceCapability? capability); + + Rectangle targetBounds = target.Bounds; + using Buffer2D stagingBuffer = + configuration.MemoryAllocator.Allocate2D(targetBounds.Right, targetBounds.Bottom, AllocationOptions.Clean); + + Buffer2DRegion stagingRegion = new(stagingBuffer, targetBounds); + ICanvasFrame stagingFrame = new MemoryCanvasFrame(stagingRegion); + + if (!this.TryReadRegion( + configuration, + target, + new Rectangle(0, 0, targetBounds.Width, targetBounds.Height), + stagingRegion)) + { + return; + } + + this.fallbackBackend.FlushCompositions(configuration, stagingFrame, compositionScene); + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + Buffer2DRegion uploadRegion = compositionBounds is Rectangle cb && cb.Width > 0 && cb.Height > 0 + ? stagingRegion.GetSubRegion(cb) + : stagingRegion; + + uint destX = compositionBounds is Rectangle cbx ? (uint)cbx.X : 0; + uint destY = compositionBounds is Rectangle cby ? (uint)cby.Y : 0; + + WebGPUFlushContext.UploadTextureFromRegion( + lease.Api, + (Queue*)capability!.Queue, + (Texture*)capability.TargetTexture, + uploadRegion, + configuration.MemoryAllocator, + destX, + destY, + 0); + } + + /// + /// CPU fallback for layer compositing when the GPU path is unavailable but the + /// destination is a native GPU surface. + /// + private void ComposeLayerFallback( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + _ = destination.TryGetNativeSurface(out NativeSurface? destSurface); + _ = destSurface!.TryGetCapability(out WebGPUSurfaceCapability? destCapability); + + MemoryAllocator allocator = configuration.MemoryAllocator; + + using Buffer2D destBuffer = allocator.Allocate2D(destination.Bounds.Width, destination.Bounds.Height); + Buffer2DRegion destRegion = new(destBuffer); + if (!this.TryReadRegion(configuration, destination, destination.Bounds, destRegion)) + { + return; + } + + using Buffer2D srcBuffer = allocator.Allocate2D(source.Bounds.Width, source.Bounds.Height); + Buffer2DRegion srcRegion = new(srcBuffer); + if (!this.TryReadRegion(configuration, source, source.Bounds, srcRegion)) + { + return; + } + + ICanvasFrame destFrame = new MemoryCanvasFrame(destRegion); + ICanvasFrame srcFrame = new MemoryCanvasFrame(srcRegion); + + DefaultDrawingBackend.ComposeLayer(configuration, srcFrame, destFrame, destinationOffset, options); + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPUFlushContext.UploadTextureFromRegion( + lease.Api, + (Queue*)destCapability!.Queue, + (Texture*)destCapability.TargetTexture, + destRegion, + configuration.MemoryAllocator); + } + + /// + /// Creates one transient composition texture that can be rendered to, sampled from, and copied. + /// + internal static bool TryCreateCompositionTexture( + WebGPUFlushContext flushContext, + int width, + int height, + out Texture* texture, + out TextureView* textureView, + out string? error) + { + textureView = null; + + TextureDescriptor textureDescriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopySrc | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = flushContext.TextureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in textureDescriptor); + if (texture is null) + { + error = "Failed to create WebGPU composition texture."; + return false; + } + + TextureViewDescriptor textureViewDescriptor = new() + { + Format = flushContext.TextureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); + if (textureView is null) + { + flushContext.Api.TextureRelease(texture); + texture = null; + error = "Failed to create WebGPU composition texture view."; + return false; + } + + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(textureView); + error = null; + return true; + } + + /// + /// Copies one texture region from source to destination texture. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CopyTextureRegion( + WebGPUFlushContext flushContext, + Texture* sourceTexture, + int sourceOriginX, + int sourceOriginY, + Texture* destinationTexture, + int destinationOriginX, + int destinationOriginY, + int width, + int height) + { + ImageCopyTexture source = new() + { + Texture = sourceTexture, + MipLevel = 0, + Origin = new Origin3D((uint)sourceOriginX, (uint)sourceOriginY, 0), + Aspect = TextureAspect.All + }; + + ImageCopyTexture destination = new() + { + Texture = destinationTexture, + MipLevel = 0, + Origin = new Origin3D((uint)destinationOriginX, (uint)destinationOriginY, 0), + Aspect = TextureAspect.All + }; + + Extent3D copySize = new((uint)width, (uint)height, 1); + flushContext.Api.CommandEncoderCopyTextureToTexture(flushContext.CommandEncoder, in source, in destination, in copySize); + } + + /// + /// Divides by and rounds up. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint DivideRoundUp(int value, int divisor) + => (uint)((value + divisor - 1) / divisor); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint FloatToUInt32Bits(float value) + => unchecked((uint)BitConverter.SingleToInt32Bits(value)); + + /// + /// Submits the current command encoder, if any. + /// + internal static bool TrySubmit(WebGPUFlushContext flushContext) + { + CommandEncoder* commandEncoder = flushContext.CommandEncoder; + if (commandEncoder is null) + { + return true; + } + + flushContext.EndComputePassIfOpen(); + flushContext.EndRenderPassIfOpen(); + + CommandBuffer* commandBuffer = null; + try + { + CommandBufferDescriptor descriptor = default; + commandBuffer = flushContext.Api.CommandEncoderFinish(commandEncoder, in descriptor); + if (commandBuffer is null) + { + return false; + } + + flushContext.Api.QueueSubmit(flushContext.Queue, 1, ref commandBuffer); + flushContext.Api.CommandBufferRelease(commandBuffer); + commandBuffer = null; + flushContext.Api.CommandEncoderRelease(commandEncoder); + flushContext.CommandEncoder = null; + return true; + } + finally + { + if (commandBuffer is not null) + { + flushContext.Api.CommandBufferRelease(commandBuffer); + } + } + } + + /// + /// Releases all cached shared WebGPU resources. + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.TestingLastFlushUsedGPU = false; + this.TestingLastGPUInitializationFailure = null; + this.isDisposed = true; + } + + /// + /// Throws when this backend is disposed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ThrowIfDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs new file mode 100644 index 000000000..d98ca794c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -0,0 +1,610 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Blend mode selection for render-pipeline-based composition passes. +/// +internal enum CompositePipelineBlendMode +{ + /// + /// Uses default blending behavior for the render pipeline variant. + /// + None = 0 +} + +/// +/// Per-flush WebGPU execution context created from a single frame target. +/// +internal sealed unsafe class WebGPUFlushContext : IDisposable +{ + private bool disposed; + private bool ownsTargetTexture; + private bool ownsTargetView; + private readonly List transientBindGroups = []; + private readonly List transientBuffers = []; + private readonly List transientTextureViews = []; + private readonly List transientTextures = []; + + // Flush-scoped source image cache: + // key = source Image reference, value = uploaded texture view handle. + // Handles are released when this flush context is disposed. + private readonly Dictionary cachedSourceTextureViews = new(ReferenceEqualityComparer.Instance); + + private WebGPUFlushContext( + WebGPURuntime.Lease runtimeLease, + Device* device, + Queue* queue, + in Rectangle targetBounds, + TextureFormat textureFormat, + MemoryAllocator memoryAllocator, + WebGPURuntime.DeviceSharedState deviceState) + { + this.RuntimeLease = runtimeLease; + this.Api = runtimeLease.Api; + this.Device = device; + this.Queue = queue; + this.TargetBounds = targetBounds; + this.TextureFormat = textureFormat; + this.MemoryAllocator = memoryAllocator; + this.DeviceState = deviceState; + } + + /// + /// Gets the runtime lease that keeps the process-level WebGPU API alive. + /// + public WebGPURuntime.Lease RuntimeLease { get; } + + /// + /// Gets the WebGPU API facade for this flush. + /// + public WebGPU Api { get; } + + /// + /// Gets the device used to create and execute GPU resources. + /// + public Device* Device { get; } + + /// + /// Gets the queue used to submit GPU work. + /// + public Queue* Queue { get; } + + /// + /// Gets the target bounds for this flush context. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the target texture format for this flush. + /// + public TextureFormat TextureFormat { get; } + + /// + /// Gets the allocator used for temporary CPU staging buffers in this flush context. + /// + public MemoryAllocator MemoryAllocator { get; } + + /// + /// Gets device-scoped shared caches and reusable resources. + /// + public WebGPURuntime.DeviceSharedState DeviceState { get; } + + /// + /// Gets the target texture receiving render/composite output. + /// + public Texture* TargetTexture { get; private set; } + + /// + /// Gets the texture view used when binding the target texture. + /// + public TextureView* TargetView { get; private set; } + + /// + /// Gets the shared instance-data buffer used for parameter uploads. + /// + public WgpuBuffer* InstanceBuffer { get; private set; } + + /// + /// Gets the instance buffer capacity in bytes. + /// + public nuint InstanceBufferCapacity { get; private set; } + + /// + /// Gets or sets the current write offset into . + /// + public nuint InstanceBufferWriteOffset { get; internal set; } + + /// + /// Gets or sets the active command encoder. + /// + public CommandEncoder* CommandEncoder { get; set; } + + /// + /// Gets the currently open render pass encoder, if any. + /// + public RenderPassEncoder* PassEncoder { get; private set; } + + /// + /// Gets the currently open compute pass encoder, if any. + /// + public ComputePassEncoder* ComputePassEncoder { get; private set; } + + /// + /// Creates a flush context for a native WebGPU surface. + /// Returns when the frame does not expose a native surface + /// or the device lacks the required feature. + /// + /// The target frame. + /// The expected GPU texture format. + /// + /// A device feature required by the pixel type for storage binding, or + /// when no special feature is needed. + /// + /// The memory allocator for staging buffers. + /// The flush context, or when GPU execution is unavailable. + public static WebGPUFlushContext? Create( + ICanvasFrame frame, + TextureFormat expectedTextureFormat, + FeatureName requiredFeature, + MemoryAllocator memoryAllocator) + where TPixel : unmanaged, IPixel + { + WebGPUSurfaceCapability? nativeCapability = TryGetNativeSurfaceCapability(frame, expectedTextureFormat); + if (nativeCapability is null) + { + return null; + } + + WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + try + { + Device* device = (Device*)nativeCapability.Device; + Queue* queue = (Queue*)nativeCapability.Queue; + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(nativeCapability.TargetFormat); + Rectangle bounds = new(0, 0, nativeCapability.Width, nativeCapability.Height); + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(lease.Api, device); + + if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature)) + { + lease.Dispose(); + return null; + } + + WebGPUFlushContext context = new(lease, device, queue, in bounds, textureFormat, memoryAllocator, deviceState); + context.InitializeNativeTarget(nativeCapability); + return context; + } + catch + { + lease.Dispose(); + throw; + } + } + + /// + /// Ensures that the instance buffer exists and can hold at least the requested number of bytes. + /// + /// The required number of bytes for the current flush. + /// The minimum allocation size to enforce when creating a new buffer. + /// if the buffer is available with sufficient capacity; otherwise . + public bool EnsureInstanceBufferCapacity(nuint requiredBytes, nuint minimumCapacityBytes) + { + if (this.InstanceBuffer is not null && this.InstanceBufferCapacity >= requiredBytes) + { + return true; + } + + if (this.InstanceBuffer is not null) + { + this.Api.BufferRelease(this.InstanceBuffer); + this.InstanceBuffer = null; + this.InstanceBufferCapacity = 0; + } + + nuint targetSize = requiredBytes > minimumCapacityBytes ? requiredBytes : minimumCapacityBytes; + BufferDescriptor descriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = targetSize + }; + + this.InstanceBuffer = this.Api.DeviceCreateBuffer(this.Device, in descriptor); + if (this.InstanceBuffer is null) + { + return false; + } + + this.InstanceBufferCapacity = targetSize; + return true; + } + + /// + /// Ensures that a command encoder is available for recording GPU commands. + /// + /// if an encoder is available; otherwise . + public bool EnsureCommandEncoder() + { + if (this.CommandEncoder is not null) + { + return true; + } + + CommandEncoderDescriptor descriptor = default; + this.CommandEncoder = this.Api.DeviceCreateCommandEncoder(this.Device, in descriptor); + return this.CommandEncoder is not null; + } + + /// + /// Begins a render pass that targets the specified texture view. + /// + public bool BeginRenderPass(TextureView* targetView) + => this.BeginRenderPass(targetView, loadExisting: false); + + /// + /// Begins a render pass that targets the specified texture view, optionally preserving existing contents. + /// + public bool BeginRenderPass(TextureView* targetView, bool loadExisting) + { + if (this.PassEncoder is not null) + { + return true; + } + + if (this.CommandEncoder is null || targetView is null || this.ComputePassEncoder is not null) + { + return false; + } + + RenderPassColorAttachment colorAttachment = new() + { + View = targetView, + ResolveTarget = null, + LoadOp = loadExisting ? LoadOp.Load : LoadOp.Clear, + StoreOp = StoreOp.Store, + ClearValue = default + }; + + RenderPassDescriptor renderPassDescriptor = new() + { + ColorAttachmentCount = 1, + ColorAttachments = &colorAttachment + }; + + this.PassEncoder = this.Api.CommandEncoderBeginRenderPass(this.CommandEncoder, in renderPassDescriptor); + return this.PassEncoder is not null; + } + + /// + /// Ends and releases the current render pass if one is active. + /// + public void EndRenderPassIfOpen() + { + if (this.PassEncoder is null) + { + return; + } + + this.Api.RenderPassEncoderEnd(this.PassEncoder); + this.Api.RenderPassEncoderRelease(this.PassEncoder); + this.PassEncoder = null; + } + + /// + /// Begins a compute pass on the current command encoder. + /// + /// if a compute pass is available; otherwise . + public bool BeginComputePass() + { + if (this.ComputePassEncoder is not null) + { + return true; + } + + if (this.CommandEncoder is null || this.PassEncoder is not null) + { + return false; + } + + ComputePassDescriptor descriptor = default; + this.ComputePassEncoder = this.Api.CommandEncoderBeginComputePass(this.CommandEncoder, in descriptor); + return this.ComputePassEncoder is not null; + } + + /// + /// Ends and releases the current compute pass if one is active. + /// + public void EndComputePassIfOpen() + { + if (this.ComputePassEncoder is null) + { + return; + } + + this.Api.ComputePassEncoderEnd(this.ComputePassEncoder); + this.Api.ComputePassEncoderRelease(this.ComputePassEncoder); + this.ComputePassEncoder = null; + } + + /// + /// Tracks a transient bind group allocated during this flush. + /// + /// The bind group to track. + public void TrackBindGroup(BindGroup* bindGroup) + { + if (bindGroup is not null) + { + this.transientBindGroups.Add((nint)bindGroup); + } + } + + /// + /// Tracks a transient buffer allocated during this flush. + /// + public void TrackBuffer(WgpuBuffer* buffer) + { + if (buffer is not null) + { + this.transientBuffers.Add((nint)buffer); + } + } + + /// + /// Tracks a transient texture view allocated during this flush. + /// + public void TrackTextureView(TextureView* textureView) + { + if (textureView is not null) + { + this.transientTextureViews.Add((nint)textureView); + } + } + + /// + /// Tracks a transient texture allocated during this flush. + /// + public void TrackTexture(Texture* texture) + { + if (texture is not null) + { + this.transientTextures.Add((nint)texture); + } + } + + /// + /// Tries to resolve a cached source texture view for an input image. + /// + /// The source image key. + /// When this method returns , contains the cached texture view. + /// if a cached texture view exists; otherwise . + public bool TryGetCachedSourceTextureView(Image image, out TextureView* textureView) + { + if (this.cachedSourceTextureViews.TryGetValue(image, out nint handle) && handle != 0) + { + textureView = (TextureView*)handle; + return true; + } + + textureView = null; + return false; + } + + /// + /// Caches a source texture view for reuse within the flush. + /// + /// The source image key. + /// The texture view to cache. + public void CacheSourceTextureView(Image image, TextureView* textureView) + => this.cachedSourceTextureViews[image] = (nint)textureView; + + /// + /// Releases transient GPU resources owned by this flush context. + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.EndComputePassIfOpen(); + this.EndRenderPassIfOpen(); + + if (this.CommandEncoder is not null) + { + this.Api.CommandEncoderRelease(this.CommandEncoder); + this.CommandEncoder = null; + } + + if (this.InstanceBuffer is not null) + { + this.Api.BufferRelease(this.InstanceBuffer); + this.InstanceBuffer = null; + this.InstanceBufferCapacity = 0; + } + + this.InstanceBufferWriteOffset = 0; + + if (this.ownsTargetView && this.TargetView is not null) + { + this.Api.TextureViewRelease(this.TargetView); + } + + if (this.ownsTargetTexture && this.TargetTexture is not null) + { + this.Api.TextureRelease(this.TargetTexture); + } + + for (int i = 0; i < this.transientBindGroups.Count; i++) + { + this.Api.BindGroupRelease((BindGroup*)this.transientBindGroups[i]); + } + + for (int i = 0; i < this.transientBuffers.Count; i++) + { + this.Api.BufferRelease((WgpuBuffer*)this.transientBuffers[i]); + } + + for (int i = 0; i < this.transientTextureViews.Count; i++) + { + this.Api.TextureViewRelease((TextureView*)this.transientTextureViews[i]); + } + + for (int i = 0; i < this.transientTextures.Count; i++) + { + this.Api.TextureRelease((Texture*)this.transientTextures[i]); + } + + this.transientBindGroups.Clear(); + this.transientBuffers.Clear(); + this.transientTextureViews.Clear(); + this.transientTextures.Clear(); + + // Cache entries point to transient texture views that are released above. + this.cachedSourceTextureViews.Clear(); + + this.TargetView = null; + this.TargetTexture = null; + this.ownsTargetView = false; + this.ownsTargetTexture = false; + + this.RuntimeLease.Dispose(); + this.disposed = true; + } + + private void InitializeNativeTarget(WebGPUSurfaceCapability capability) + { + this.TargetTexture = (Texture*)capability.TargetTexture; + this.TargetView = (TextureView*)capability.TargetTextureView; + this.ownsTargetTexture = false; + this.ownsTargetView = false; + } + + private static WebGPUSurfaceCapability? TryGetNativeSurfaceCapability(ICanvasFrame frame, TextureFormat expectedTextureFormat) + where TPixel : unmanaged, IPixel + { + if (!frame.TryGetNativeSurface(out NativeSurface? nativeSurface) || + !nativeSurface.TryGetCapability(out WebGPUSurfaceCapability? capability)) + { + return null; + } + + if (capability.Device == 0 || + capability.Queue == 0 || + capability.TargetTextureView == 0 || + WebGPUTextureFormatMapper.ToSilk(capability.TargetFormat) != expectedTextureFormat) + { + return null; + } + + Rectangle bounds = frame.Bounds; + if (bounds.X < 0 || + bounds.Y < 0 || + bounds.Right > capability.Width || + bounds.Bottom > capability.Height) + { + return null; + } + + return capability; + } + + internal static void UploadTextureFromRegion( + WebGPU api, + Queue* queue, + Texture* destinationTexture, + Buffer2DRegion sourceRegion, + MemoryAllocator memoryAllocator) + where TPixel : unmanaged + => UploadTextureFromRegion(api, queue, destinationTexture, sourceRegion, memoryAllocator, 0, 0, 0); + + internal static void UploadTextureFromRegion( + WebGPU api, + Queue* queue, + Texture* destinationTexture, + Buffer2DRegion sourceRegion, + MemoryAllocator memoryAllocator, + uint destinationX, + uint destinationY, + uint destinationLayer) + where TPixel : unmanaged + { + int pixelSizeInBytes = Unsafe.SizeOf(); + ImageCopyTexture destination = new() + { + Texture = destinationTexture, + MipLevel = 0, + Origin = new Origin3D(destinationX, destinationY, destinationLayer), + Aspect = TextureAspect.All + }; + + Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); + int rowBytes = checked(sourceRegion.Width * pixelSizeInBytes); + uint alignedRowBytes = AlignTo256((uint)rowBytes); + + if (sourceRegion.Buffer.DangerousTryGetSingleMemory(out Memory sourceMemory)) + { + int sourceStrideBytes = checked(sourceRegion.Buffer.RowStride * pixelSizeInBytes); + long directByteCount = ((long)sourceStrideBytes * (sourceRegion.Height - 1)) + rowBytes; + long packedByteCountEstimate = alignedRowBytes * sourceRegion.Height; + + // Only use the direct path when the stride satisfies WebGPU's alignment requirement. + if ((uint)sourceStrideBytes == alignedRowBytes && directByteCount <= packedByteCountEstimate * 2) + { + int startPixelIndex = checked((sourceRegion.Rectangle.Y * sourceRegion.Buffer.RowStride) + sourceRegion.Rectangle.X); + int startByteOffset = checked(startPixelIndex * pixelSizeInBytes); + int uploadByteCount = checked((int)directByteCount); + nuint uploadByteCountNuint = checked((nuint)uploadByteCount); + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)sourceStrideBytes, + RowsPerImage = (uint)sourceRegion.Height + }; + + Span sourceBytes = MemoryMarshal.AsBytes(sourceMemory.Span).Slice(startByteOffset, uploadByteCount); + fixed (byte* uploadPtr = sourceBytes) + { + api.QueueWriteTexture(queue, in destination, uploadPtr, uploadByteCountNuint, in layout, in writeSize); + } + + return; + } + } + + int alignedRowBytesInt = checked((int)alignedRowBytes); + int packedByteCount = checked(alignedRowBytesInt * sourceRegion.Height); + using IMemoryOwner packedOwner = memoryAllocator.Allocate(packedByteCount, AllocationOptions.Clean); + Span packedData = packedOwner.Memory.Span; + for (int y = 0; y < sourceRegion.Height; y++) + { + ReadOnlySpan sourceRow = sourceRegion.DangerousGetRowSpan(y); + MemoryMarshal.AsBytes(sourceRow)[..rowBytes].CopyTo(packedData.Slice(y * alignedRowBytesInt, rowBytes)); + } + + TextureDataLayout packedLayout = new() + { + Offset = 0, + BytesPerRow = alignedRowBytes, + RowsPerImage = (uint)sourceRegion.Height + }; + + fixed (byte* uploadPtr = packedData) + { + api.QueueWriteTexture(queue, in destination, uploadPtr, (nuint)packedByteCount, in packedLayout, in writeSize); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint AlignTo256(uint value) => (value + 255U) & ~255U; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs new file mode 100644 index 000000000..014ef8f2c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurfaceFactory.cs @@ -0,0 +1,108 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Creates instances for externally-owned WebGPU targets. +/// +public static class WebGPUNativeSurfaceFactory +{ + /// + /// Creates a WebGPU-backed from opaque native handles. + /// + /// Canvas pixel format. + /// Opaque WGPUDevice* handle. + /// Opaque WGPUQueue* handle. + /// Opaque WGPUTexture* handle for writable uploads. + /// Opaque WGPUTextureView* handle for render target binding. + /// Texture format identifier. + /// Surface width in pixels. + /// Surface height in pixels. + /// A configured instance. + /// + /// The target texture must have been created with the TEXTURE_BINDING usage flag. + /// The backend reads the target texture for Porter-Duff backdrop sampling. + /// + public static NativeSurface Create( + nint deviceHandle, + nint queueHandle, + nint targetTextureHandle, + nint targetTextureViewHandle, + WebGPUTextureFormatId targetFormat, + int width, + int height) + where TPixel : unmanaged, IPixel + { + ValidateCommon( + deviceHandle, + queueHandle, + targetTextureViewHandle, + width, + height); + + ValidatePixelCompatibility(targetFormat); + + NativeSurface nativeSurface = new(TPixel.GetPixelTypeInfo()); + nativeSurface.SetCapability(new WebGPUSurfaceCapability( + deviceHandle, + queueHandle, + targetTextureHandle, + targetTextureViewHandle, + targetFormat, + width, + height)); + return nativeSurface; + } + + private static void ValidateCommon( + nint deviceHandle, + nint queueHandle, + nint targetTextureViewHandle, + int width, + int height) + { + if (deviceHandle == 0) + { + throw new ArgumentOutOfRangeException(nameof(deviceHandle), "Device handle must be non-zero."); + } + + if (queueHandle == 0) + { + throw new ArgumentOutOfRangeException(nameof(queueHandle), "Queue handle must be non-zero."); + } + + if (targetTextureViewHandle == 0) + { + throw new ArgumentOutOfRangeException(nameof(targetTextureViewHandle), "Texture view handle must be non-zero."); + } + + if (width <= 0) + { + throw new ArgumentOutOfRangeException(nameof(width), "Width must be greater than zero."); + } + + if (height <= 0) + { + throw new ArgumentOutOfRangeException(nameof(height), "Height must be greater than zero."); + } + } + + private static void ValidatePixelCompatibility(WebGPUTextureFormatId targetFormat) + where TPixel : unmanaged, IPixel + { + if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId expected)) + { + throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend."); + } + + if (expected != targetFormat) + { + throw new ArgumentException( + $"Target format '{targetFormat}' is not compatible with pixel type '{typeof(TPixel).Name}' (expected '{expected}').", + nameof(targetFormat)); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs new file mode 100644 index 000000000..c83c489fe --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs @@ -0,0 +1,678 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using Silk.NET.WebGPU; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe partial class WebGPURuntime +{ + private static readonly ConcurrentDictionary DeviceStateCache = new(); + + /// + /// Gets or creates process-scoped shared resources for the specified device. + /// + /// The WebGPU API facade used to manage native resources. + /// The device key and owner for the shared state. + /// The shared device state instance for . + internal static DeviceSharedState GetOrCreateDeviceState(WebGPU api, Device* device) + { + nint cacheKey = (nint)device; + if (DeviceStateCache.TryGetValue(cacheKey, out DeviceSharedState? existing)) + { + return existing; + } + + DeviceSharedState created = new(api, device); + DeviceSharedState winner = DeviceStateCache.GetOrAdd(cacheKey, created); + if (!ReferenceEquals(winner, created)) + { + created.Dispose(); + } + + return winner; + } + + /// + /// Disposes all cached device-scoped shared state. + /// + private static void ClearDeviceStateCache() + { + foreach (DeviceSharedState state in DeviceStateCache.Values) + { + state.Dispose(); + } + + DeviceStateCache.Clear(); + } + + /// + /// Shared device-scoped caches for pipelines, bind groups, and reusable GPU resources. + /// + internal sealed class DeviceSharedState : IDisposable + { + private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary compositeComputePipelines = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary sharedBuffers = new(StringComparer.Ordinal); + private readonly HashSet deviceFeatures; + private bool disposed; + + internal DeviceSharedState(WebGPU api, Device* device) + { + this.Api = api; + this.Device = device; + this.deviceFeatures = EnumerateDeviceFeatures(api, device); + } + + private static ReadOnlySpan CompositeVertexEntryPoint => "vs_main\0"u8; + + private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; + + /// + /// Gets the synchronization object used for shared state mutation. + /// + public object SyncRoot { get; } = new(); + + /// + /// Gets the WebGPU API instance used by this shared state. + /// + public WebGPU Api { get; } + + /// + /// Gets the device associated with this shared state. + /// + public Device* Device { get; } + + /// + /// Returns whether the device has the specified feature. + /// + /// The feature to check. + /// when the device has the feature; otherwise . + public bool HasFeature(FeatureName feature) + => this.deviceFeatures.Contains(feature); + + private static HashSet EnumerateDeviceFeatures(WebGPU api, Device* device) + { + if (device is null) + { + return []; + } + + int count = (int)api.DeviceEnumerateFeatures(device, (FeatureName*)null); + if (count <= 0) + { + return []; + } + + FeatureName* features = stackalloc FeatureName[count]; + api.DeviceEnumerateFeatures(device, features); + + HashSet result = new(count); + for (int i = 0; i < count; i++) + { + result.Add(features[i]); + } + + return result; + } + + /// + /// Gets or creates a graphics pipeline used for composite rendering. + /// + public bool TryGetOrCreateCompositePipeline( + string pipelineKey, + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + TextureFormat textureFormat, + CompositePipelineBlendMode blendMode, + out BindGroupLayout* bindGroupLayout, + out RenderPipeline* pipeline, + out string? error) + { + bindGroupLayout = null; + pipeline = null; + + if (this.disposed) + { + error = "WebGPU device state is disposed."; + return false; + } + + if (string.IsNullOrWhiteSpace(pipelineKey)) + { + error = "Composite pipeline key cannot be empty."; + return false; + } + + if (shaderCode.IsEmpty) + { + error = $"Composite shader code is missing for pipeline '{pipelineKey}'."; + return false; + } + + CompositePipelineInfrastructure infrastructure = this.compositePipelines.GetOrAdd( + pipelineKey, + static _ => new CompositePipelineInfrastructure()); + + lock (infrastructure) + { + if (infrastructure.BindGroupLayout is null || + infrastructure.PipelineLayout is null || + infrastructure.ShaderModule is null) + { + if (!this.TryCreateCompositeInfrastructure( + shaderCode, + bindGroupLayoutFactory, + out BindGroupLayout* createdBindGroupLayout, + out PipelineLayout* createdPipelineLayout, + out ShaderModule* createdShaderModule, + out error)) + { + return false; + } + + infrastructure.BindGroupLayout = createdBindGroupLayout; + infrastructure.PipelineLayout = createdPipelineLayout; + infrastructure.ShaderModule = createdShaderModule; + } + + bindGroupLayout = infrastructure.BindGroupLayout; + (TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode) variantKey = (textureFormat, blendMode); + if (infrastructure.Pipelines.TryGetValue(variantKey, out nint cachedPipelineHandle) && cachedPipelineHandle != 0) + { + pipeline = (RenderPipeline*)cachedPipelineHandle; + error = null; + return true; + } + + RenderPipeline* createdPipeline = this.CreateCompositePipeline( + infrastructure.PipelineLayout, + infrastructure.ShaderModule, + textureFormat, + blendMode); + if (createdPipeline is null) + { + error = $"Failed to create composite pipeline '{pipelineKey}' for format '{textureFormat}'."; + return false; + } + + infrastructure.Pipelines[variantKey] = (nint)createdPipeline; + pipeline = createdPipeline; + error = null; + return true; + } + } + + /// + /// Gets or creates a compute pipeline used for composite execution. + /// + public bool TryGetOrCreateCompositeComputePipeline( + string pipelineKey, + ReadOnlySpan shaderCode, + ReadOnlySpan entryPoint, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out string? error) + { + bindGroupLayout = null; + pipeline = null; + + if (this.disposed) + { + error = "WebGPU device state is disposed."; + return false; + } + + if (string.IsNullOrWhiteSpace(pipelineKey)) + { + error = "Composite compute pipeline key cannot be empty."; + return false; + } + + if (shaderCode.IsEmpty) + { + error = $"Composite compute shader code is missing for pipeline '{pipelineKey}'."; + return false; + } + + if (entryPoint.IsEmpty) + { + error = $"Composite compute entry point is missing for pipeline '{pipelineKey}'."; + return false; + } + + CompositeComputePipelineInfrastructure infrastructure = this.compositeComputePipelines.GetOrAdd( + pipelineKey, + static _ => new CompositeComputePipelineInfrastructure()); + + lock (infrastructure) + { + if (infrastructure.BindGroupLayout is null || + infrastructure.PipelineLayout is null || + infrastructure.ShaderModule is null) + { + if (!this.TryCreateCompositeInfrastructure( + shaderCode, + bindGroupLayoutFactory, + out BindGroupLayout* createdBindGroupLayout, + out PipelineLayout* createdPipelineLayout, + out ShaderModule* createdShaderModule, + out error)) + { + return false; + } + + infrastructure.BindGroupLayout = createdBindGroupLayout; + infrastructure.PipelineLayout = createdPipelineLayout; + infrastructure.ShaderModule = createdShaderModule; + } + + bindGroupLayout = infrastructure.BindGroupLayout; + if (infrastructure.Pipeline is not null) + { + pipeline = infrastructure.Pipeline; + error = null; + return true; + } + + ComputePipeline* createdPipeline = this.CreateCompositeComputePipeline( + infrastructure.PipelineLayout, + infrastructure.ShaderModule, + entryPoint); + if (createdPipeline is null) + { + error = $"Failed to create composite compute pipeline '{pipelineKey}'."; + return false; + } + + infrastructure.Pipeline = createdPipeline; + pipeline = createdPipeline; + error = null; + return true; + } + } + + /// + /// Gets or creates a reusable shared buffer for device-scoped operations. + /// + public bool TryGetOrCreateSharedBuffer( + string bufferKey, + BufferUsage usage, + nuint requiredSize, + out WgpuBuffer* buffer, + out nuint capacity, + out string? error) + { + buffer = null; + capacity = 0; + + if (this.disposed) + { + error = "WebGPU device state is disposed."; + return false; + } + + if (string.IsNullOrWhiteSpace(bufferKey)) + { + error = "Shared buffer key cannot be empty."; + return false; + } + + if (requiredSize == 0) + { + error = $"Shared buffer '{bufferKey}' requires a non-zero size."; + return false; + } + + SharedBufferInfrastructure infrastructure = this.sharedBuffers.GetOrAdd( + bufferKey, + static _ => new SharedBufferInfrastructure()); + lock (infrastructure) + { + if (infrastructure.Buffer is not null && + infrastructure.Capacity >= requiredSize && + infrastructure.Usage == usage) + { + buffer = infrastructure.Buffer; + capacity = infrastructure.Capacity; + error = null; + return true; + } + + if (infrastructure.Buffer is not null) + { + this.Api.BufferRelease(infrastructure.Buffer); + infrastructure.Buffer = null; + infrastructure.Capacity = 0; + } + + BufferDescriptor descriptor = new() + { + Usage = usage, + Size = requiredSize + }; + + WgpuBuffer* createdBuffer = this.Api.DeviceCreateBuffer(this.Device, in descriptor); + if (createdBuffer is null) + { + error = $"Failed to create shared buffer '{bufferKey}'."; + return false; + } + + infrastructure.Buffer = createdBuffer; + infrastructure.Capacity = requiredSize; + infrastructure.Usage = usage; + buffer = createdBuffer; + capacity = requiredSize; + error = null; + return true; + } + } + + /// + /// Releases all cached pipelines and buffers owned by this state. + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + foreach (CompositePipelineInfrastructure infrastructure in this.compositePipelines.Values) + { + this.ReleaseCompositeInfrastructure(infrastructure); + } + + this.compositePipelines.Clear(); + + foreach (CompositeComputePipelineInfrastructure infrastructure in this.compositeComputePipelines.Values) + { + this.ReleaseCompositeComputeInfrastructure(infrastructure); + } + + this.compositeComputePipelines.Clear(); + + foreach (SharedBufferInfrastructure infrastructure in this.sharedBuffers.Values) + { + lock (infrastructure) + { + if (infrastructure.Buffer is not null) + { + this.Api.BufferRelease(infrastructure.Buffer); + infrastructure.Buffer = null; + infrastructure.Capacity = 0; + } + } + } + + this.sharedBuffers.Clear(); + + this.disposed = true; + } + + private bool TryCreateCompositeInfrastructure( + ReadOnlySpan shaderCode, + WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, + out BindGroupLayout* bindGroupLayout, + out PipelineLayout* pipelineLayout, + out ShaderModule* shaderModule, + out string? error) + { + bindGroupLayout = null; + pipelineLayout = null; + shaderModule = null; + + if (!bindGroupLayoutFactory(this.Api, this.Device, out bindGroupLayout, out error)) + { + return false; + } + + BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; + bindGroupLayouts[0] = bindGroupLayout; + PipelineLayoutDescriptor pipelineLayoutDescriptor = new() + { + BindGroupLayoutCount = 1, + BindGroupLayouts = bindGroupLayouts + }; + + pipelineLayout = this.Api.DeviceCreatePipelineLayout(this.Device, in pipelineLayoutDescriptor); + if (pipelineLayout is null) + { + this.Api.BindGroupLayoutRelease(bindGroupLayout); + error = "Failed to create composite pipeline layout."; + return false; + } + + shaderModule = this.CreateShaderModule(shaderCode); + + if (shaderModule is null) + { + this.Api.PipelineLayoutRelease(pipelineLayout); + this.Api.BindGroupLayoutRelease(bindGroupLayout); + error = "Failed to create composite shader module."; + return false; + } + + error = null; + return true; + } + + private RenderPipeline* CreateCompositePipeline( + PipelineLayout* pipelineLayout, + ShaderModule* shaderModule, + TextureFormat textureFormat, + CompositePipelineBlendMode blendMode) + { + ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; + ReadOnlySpan fragmentEntryPoint = CompositeFragmentEntryPoint; + fixed (byte* vertexEntryPointPtr = vertexEntryPoint) + { + fixed (byte* fragmentEntryPointPtr = fragmentEntryPoint) + { + return this.CreateCompositePipelineCore( + pipelineLayout, + shaderModule, + vertexEntryPointPtr, + fragmentEntryPointPtr, + textureFormat, + blendMode); + } + } + } + + private RenderPipeline* CreateCompositePipelineCore( + PipelineLayout* pipelineLayout, + ShaderModule* shaderModule, + byte* vertexEntryPointPtr, + byte* fragmentEntryPointPtr, + TextureFormat textureFormat, + CompositePipelineBlendMode blendMode) + { + _ = blendMode; + VertexState vertexState = new() + { + Module = shaderModule, + EntryPoint = vertexEntryPointPtr, + BufferCount = 0, + Buffers = null + }; + + ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; + colorTargets[0] = new ColorTargetState + { + Format = textureFormat, + Blend = null, + WriteMask = ColorWriteMask.All + }; + + FragmentState fragmentState = new() + { + Module = shaderModule, + EntryPoint = fragmentEntryPointPtr, + TargetCount = 1, + Targets = colorTargets + }; + + RenderPipelineDescriptor descriptor = new() + { + Layout = pipelineLayout, + Vertex = vertexState, + Primitive = new PrimitiveState + { + Topology = PrimitiveTopology.TriangleList, + StripIndexFormat = IndexFormat.Undefined, + FrontFace = FrontFace.Ccw, + CullMode = CullMode.None + }, + DepthStencil = null, + Multisample = new MultisampleState + { + Count = 1, + Mask = uint.MaxValue, + AlphaToCoverageEnabled = false + }, + Fragment = &fragmentState + }; + + return this.Api.DeviceCreateRenderPipeline(this.Device, in descriptor); + } + + private ComputePipeline* CreateCompositeComputePipeline( + PipelineLayout* pipelineLayout, + ShaderModule* shaderModule, + ReadOnlySpan entryPoint) + { + fixed (byte* entryPointPtr = entryPoint) + { + ProgrammableStageDescriptor computeState = new() + { + Module = shaderModule, + EntryPoint = entryPointPtr + }; + + ComputePipelineDescriptor descriptor = new() + { + Layout = pipelineLayout, + Compute = computeState + }; + + return this.Api.DeviceCreateComputePipeline(this.Device, in descriptor); + } + } + + private ShaderModule* CreateShaderModule(ReadOnlySpan shaderCode) + { + fixed (byte* shaderCodePtr = shaderCode) + { + ShaderModuleWGSLDescriptor wgslDescriptor = new() + { + Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, + Code = shaderCodePtr + }; + + ShaderModuleDescriptor shaderDescriptor = new() + { + NextInChain = (ChainedStruct*)&wgslDescriptor + }; + + return this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); + } + } + + private void ReleaseCompositeInfrastructure(CompositePipelineInfrastructure infrastructure) + { + foreach (nint pipelineHandle in infrastructure.Pipelines.Values) + { + if (pipelineHandle != 0) + { + this.Api.RenderPipelineRelease((RenderPipeline*)pipelineHandle); + } + } + + infrastructure.Pipelines.Clear(); + + if (infrastructure.PipelineLayout is not null) + { + this.Api.PipelineLayoutRelease(infrastructure.PipelineLayout); + infrastructure.PipelineLayout = null; + } + + if (infrastructure.ShaderModule is not null) + { + this.Api.ShaderModuleRelease(infrastructure.ShaderModule); + infrastructure.ShaderModule = null; + } + + if (infrastructure.BindGroupLayout is not null) + { + this.Api.BindGroupLayoutRelease(infrastructure.BindGroupLayout); + infrastructure.BindGroupLayout = null; + } + } + + private void ReleaseCompositeComputeInfrastructure(CompositeComputePipelineInfrastructure infrastructure) + { + if (infrastructure.Pipeline is not null) + { + this.Api.ComputePipelineRelease(infrastructure.Pipeline); + infrastructure.Pipeline = null; + } + + if (infrastructure.PipelineLayout is not null) + { + this.Api.PipelineLayoutRelease(infrastructure.PipelineLayout); + infrastructure.PipelineLayout = null; + } + + if (infrastructure.ShaderModule is not null) + { + this.Api.ShaderModuleRelease(infrastructure.ShaderModule); + infrastructure.ShaderModule = null; + } + + if (infrastructure.BindGroupLayout is not null) + { + this.Api.BindGroupLayoutRelease(infrastructure.BindGroupLayout); + infrastructure.BindGroupLayout = null; + } + } + + /// + /// Shared render-pipeline infrastructure for compositing variants. + /// + private sealed class CompositePipelineInfrastructure + { + public Dictionary<(TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode), nint> Pipelines { get; } = []; + + public BindGroupLayout* BindGroupLayout { get; set; } + + public PipelineLayout* PipelineLayout { get; set; } + + public ShaderModule* ShaderModule { get; set; } + } + + private sealed class CompositeComputePipelineInfrastructure + { + public BindGroupLayout* BindGroupLayout { get; set; } + + public PipelineLayout* PipelineLayout { get; set; } + + public ShaderModule* ShaderModule { get; set; } + + public ComputePipeline* Pipeline { get; set; } + } + + private sealed class SharedBufferInfrastructure + { + public WgpuBuffer* Buffer { get; set; } + + public nuint Capacity { get; set; } + + public BufferUsage Usage { get; set; } + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs new file mode 100644 index 000000000..24c274529 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs @@ -0,0 +1,452 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; +using Silk.NET.WebGPU.Extensions.WGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Process-level WebGPU API runtime. +/// +/// +/// +/// This type owns the process-level Silk API loader, its +/// optional extension, and a lazily provisioned default +/// device/queue pair used by the GPU backend when no native surface is available. +/// +/// +/// Backends acquire access by taking a via . +/// The lease count is thread-safe and prevents accidental shutdown while active +/// backends are still running. +/// +/// +/// Runtime unload is explicit: +/// +/// +/// when there are no active leases. +/// Best-effort cleanup on process exit. +/// +/// +/// The shutdown path is resilient to duplicate native unload attempts. +/// +/// +internal static unsafe partial class WebGPURuntime +{ + /// + /// Synchronizes all runtime state transitions. + /// + private static readonly object Sync = new(); + + /// + /// Process-level WebGPU API loader. + /// + private static WebGPU? api; + + /// + /// Optional wgpu-native extension facade. + /// + private static Wgpu? wgpuExtension; + + /// + /// Lazily provisioned device handle for CPU-backed frames. + /// + private static nint autoDeviceHandle; + + /// + /// Lazily provisioned queue handle for CPU-backed frames. + /// + private static nint autoQueueHandle; + + /// + /// Number of currently active runtime leases. + /// + private static int leaseCount; + + /// + /// Tracks whether the process-exit hook has been installed. + /// + private static bool processExitHooked; + + /// + /// Timeout for asynchronous WebGPU callbacks. + /// + private const int CallbackTimeoutMilliseconds = 10_000; + + /// + /// Acquires a runtime lease for WebGPU access. + /// + /// A lease that must be disposed when access is no longer required. + /// Thrown when the WebGPU API cannot be initialized. + public static Lease Acquire() + { + lock (Sync) + { + if (!processExitHooked) + { + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + processExitHooked = true; + } + + api ??= WebGPU.GetApi(); + if (api is null) + { + throw new InvalidOperationException("WebGPU.GetApi returned null."); + } + + if (wgpuExtension is null) + { + api.TryGetDeviceExtension(null, out wgpuExtension); + } + + leaseCount++; + return new Lease(api, wgpuExtension); + } + } + + /// + /// Lazily provisions and caches a default device/queue pair for CPU-backed frames. + /// Returns cached handles on subsequent calls. + /// + /// Receives the device pointer on success. + /// Receives the queue pointer on success. + /// Receives an error message on failure. + /// when handles are available; otherwise . + internal static bool TryGetOrCreateDevice(out Device* device, out Queue* queue, out string? error) + { + lock (Sync) + { + // Fast path: return cached handles. + if (autoDeviceHandle != 0 && autoQueueHandle != 0) + { + device = (Device*)autoDeviceHandle; + queue = (Queue*)autoQueueHandle; + error = null; + return true; + } + + if (api is null) + { + device = null; + queue = null; + error = "WebGPU API is not initialized. Call Acquire() first."; + return false; + } + + // Provision: instance → adapter → device → queue. + // The instance and adapter are transient; only the device and queue are cached. + Instance* instance = api.CreateInstance((InstanceDescriptor*)null); + if (instance is null) + { + device = null; + queue = null; + error = "WebGPU.CreateInstance returned null."; + return false; + } + + Adapter* adapter = null; + Device* requestedDevice = null; + Queue* requestedQueue = null; + bool initialized = false; + try + { + if (!TryRequestAdapter(api, instance, out adapter, out error)) + { + device = null; + queue = null; + return false; + } + + if (!TryRequestDevice(api, adapter, out requestedDevice, out error)) + { + device = null; + queue = null; + return false; + } + + requestedQueue = api.DeviceGetQueue(requestedDevice); + if (requestedQueue is null) + { + device = null; + queue = null; + error = "WebGPU.DeviceGetQueue returned null."; + return false; + } + + // Cache for subsequent calls. + autoDeviceHandle = (nint)requestedDevice; + autoQueueHandle = (nint)requestedQueue; + device = requestedDevice; + queue = requestedQueue; + error = null; + initialized = true; + return true; + } + finally + { + // Always release transient handles. + if (adapter is not null) + { + api.AdapterRelease(adapter); + } + + api.InstanceRelease(instance); + + // On failure, release any partially provisioned handles. + if (!initialized) + { + if (requestedQueue is not null) + { + api.QueueRelease(requestedQueue); + } + + if (requestedDevice is not null) + { + api.DeviceRelease(requestedDevice); + } + } + } + } + } + + /// + /// Releases one active runtime lease. + /// + /// + /// Lease release does not automatically unload the runtime. Unload is performed by + /// or by the process-exit handler. + /// + private static void Release() + { + lock (Sync) + { + if (leaseCount <= 0) + { + return; + } + + leaseCount--; + } + } + + /// + /// Shuts down the process-level WebGPU runtime when no leases are active. + /// + /// + /// This call is intended for coordinated application shutdown. Runtime state can be + /// reinitialized later by calling again. + /// + /// Thrown when runtime leases are still active. + public static void Shutdown() + { + lock (Sync) + { + if (leaseCount != 0) + { + throw new InvalidOperationException($"Cannot shut down WebGPU runtime while {leaseCount} lease(s) are active."); + } + + DisposeRuntimeCore(); + } + } + + /// + /// Process-exit cleanup callback. + /// + /// Event sender. + /// Event arguments. + private static void OnProcessExit(object? sender, EventArgs e) + { + _ = sender; + _ = e; + lock (Sync) + { + leaseCount = 0; + DisposeRuntimeCore(); + } + } + + /// + /// Disposes native runtime objects in a safe and idempotent way. + /// + /// + /// Duplicate-dispose exceptions are intentionally swallowed because process-exit + /// teardown may race with other shutdown paths. + /// + private static void DisposeRuntimeCore() + { + ClearDeviceStateCache(); + + autoDeviceHandle = 0; + autoQueueHandle = 0; + + try + { + wgpuExtension?.Dispose(); + } + catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException) + { + // Safe to ignore at process shutdown or double-dispose races. + } + finally + { + wgpuExtension = null; + } + + try + { + api?.Dispose(); + } + catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException) + { + // Safe to ignore at process shutdown or double-dispose races. + } + finally + { + api = null; + } + } + + private static bool TryRequestAdapter(WebGPU api, Instance* instance, out Adapter* adapter, out string? error) + { + RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; + Adapter* callbackAdapter = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, void* userData) + { + callbackStatus = status; + callbackAdapter = adapterPtr; + callbackReady.Set(); + } + + using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); + RequestAdapterOptions options = new() + { + PowerPreference = PowerPreference.HighPerformance + }; + + api.InstanceRequestAdapter(instance, in options, callbackPtr, null); + if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) + { + adapter = null; + error = "Timed out while waiting for WebGPU adapter request callback."; + return false; + } + + adapter = callbackAdapter; + if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) + { + error = $"WebGPU adapter request failed with status '{callbackStatus}'."; + return false; + } + + error = null; + return true; + } + + private static bool TryRequestDevice(WebGPU api, Adapter* adapter, out Device* device, out string? error) + { + RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; + Device* callbackDevice = null; + using ManualResetEventSlim callbackReady = new(false); + void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void* userData) + { + callbackStatus = status; + callbackDevice = devicePtr; + callbackReady.Set(); + } + + using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); + + // Auto-provision a device when no native surface provides one. + // Request optional storage features that are available on this adapter. + // The compute compositor needs storage binding on the transient output texture, + // and some formats (e.g. Bgra8Unorm) require explicit device features. + Span requestedFeatures = stackalloc FeatureName[1]; + int requestedCount = 0; + if (api.AdapterHasFeature(adapter, FeatureName.Bgra8UnormStorage)) + { + requestedFeatures[requestedCount++] = FeatureName.Bgra8UnormStorage; + } + + DeviceDescriptor descriptor; + if (requestedCount > 0) + { + fixed (FeatureName* featuresPtr = requestedFeatures) + { + descriptor = new DeviceDescriptor + { + RequiredFeatureCount = (uint)requestedCount, + RequiredFeatures = featuresPtr, + }; + + api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + } + } + else + { + descriptor = default; + api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); + } + + if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) + { + device = null; + error = "Timed out while waiting for WebGPU device request callback."; + return false; + } + + device = callbackDevice; + if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) + { + error = $"WebGPU device request failed with status '{callbackStatus}'."; + return false; + } + + error = null; + return true; + } + + /// + /// Ref-counted access token for . + /// + /// + /// Disposing the lease decrements the runtime lease count exactly once. + /// + internal sealed class Lease : IDisposable + { + private int disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The shared WebGPU API loader. + /// The shared optional wgpu extension facade. + internal Lease(WebGPU api, Wgpu? wgpuExtension) + { + this.Api = api; + this.WgpuExtension = wgpuExtension; + } + + /// + /// Gets the shared WebGPU API loader. + /// + public WebGPU Api { get; } + + /// + /// Gets the shared optional wgpu extension facade. + /// + public Wgpu? WgpuExtension { get; } + + /// + /// Releases this lease exactly once. + /// + public void Dispose() + { + if (Interlocked.Exchange(ref this.disposed, 1) == 0) + { + Release(); + } + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs new file mode 100644 index 000000000..92abbb6e9 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs @@ -0,0 +1,374 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// CPU-side sizing and dispatch configuration for one staged WebGPU scene. +/// +internal readonly struct WebGPUSceneConfig +{ + public WebGPUSceneConfig(WebGPUSceneWorkgroupCounts workgroupCounts, WebGPUSceneBufferSizes bufferSizes) + { + this.WorkgroupCounts = workgroupCounts; + this.BufferSizes = bufferSizes; + } + + public WebGPUSceneWorkgroupCounts WorkgroupCounts { get; } + + public WebGPUSceneBufferSizes BufferSizes { get; } + + public static WebGPUSceneConfig Create(WebGPUEncodedScene scene) + => new( + WebGPUSceneWorkgroupCounts.Create(scene), + WebGPUSceneBufferSizes.Create(scene)); +} + +/// +/// Dispatch sizes for the currently implemented staged scene passes. +/// +internal readonly struct WebGPUSceneWorkgroupCounts +{ + public WebGPUSceneWorkgroupCounts( + bool useLargePathScan, + uint pathReduceX, + uint pathReduce2X, + uint pathScan1X, + uint pathScanX, + uint bboxClearX, + uint flattenX, + uint drawReduceX, + uint drawLeafX, + uint clipReduceX, + uint clipLeafX, + uint binningX, + uint tileAllocX, + uint pathCountSetupX, + uint pathCountX, + uint backdropX, + uint coarseX, + uint coarseY, + uint pathTilingSetupX, + uint pathTilingX, + uint fineX, + uint fineY) + { + this.UseLargePathScan = useLargePathScan; + this.PathReduceX = pathReduceX; + this.PathReduce2X = pathReduce2X; + this.PathScan1X = pathScan1X; + this.PathScanX = pathScanX; + this.BboxClearX = bboxClearX; + this.FlattenX = flattenX; + this.DrawReduceX = drawReduceX; + this.DrawLeafX = drawLeafX; + this.ClipReduceX = clipReduceX; + this.ClipLeafX = clipLeafX; + this.BinningX = binningX; + this.TileAllocX = tileAllocX; + this.PathCountSetupX = pathCountSetupX; + this.PathCountX = pathCountX; + this.BackdropX = backdropX; + this.CoarseX = coarseX; + this.CoarseY = coarseY; + this.PathTilingSetupX = pathTilingSetupX; + this.PathTilingX = pathTilingX; + this.FineX = fineX; + this.FineY = fineY; + } + + public bool UseLargePathScan { get; } + + public uint PathReduceX { get; } + + public uint PathReduce2X { get; } + + public uint PathScan1X { get; } + + public uint PathScanX { get; } + + public uint BboxClearX { get; } + + public uint FlattenX { get; } + + public uint DrawReduceX { get; } + + public uint DrawLeafX { get; } + + public uint ClipReduceX { get; } + + public uint ClipLeafX { get; } + + public uint BinningX { get; } + + public uint TileAllocX { get; } + + public uint PathCountSetupX { get; } + + public uint PathCountX { get; } + + public uint BackdropX { get; } + + public uint CoarseX { get; } + + public uint CoarseY { get; } + + public uint PathTilingSetupX { get; } + + public uint PathTilingX { get; } + + public uint FineX { get; } + + public uint FineY { get; } + + public static WebGPUSceneWorkgroupCounts Create(WebGPUEncodedScene scene) + { + uint drawObjectCount = checked((uint)scene.DrawTagCount); + uint pathCount = checked((uint)scene.PathCount); + uint lineCount = checked((uint)scene.LineCount); + uint clipCount = checked((uint)scene.ClipCount); + uint pathTagCount = checked((uint)scene.PathTagByteCount); + uint pathTagPadded = AlignUp(pathTagCount, 4U * 256U); + uint pathTagWgs = pathTagPadded / (4U * 256U); + bool useLargePathScan = pathTagWgs > 256U; + uint reducedSize = useLargePathScan ? AlignUp(pathTagWgs, 256U) : pathTagWgs; + uint drawObjectWgs = DivideRoundUp(drawObjectCount, 256U); + uint drawMonoidWgs = Math.Min(drawObjectWgs, 256U); + uint flattenWgs = DivideRoundUp(pathTagCount, 256U); + uint clipReduceWgs = clipCount == 0U ? 0U : (clipCount - 1U) / 256U; + uint clipWgs = DivideRoundUp(clipCount, 256U); + uint pathWgs = DivideRoundUp(pathCount, 256U); + uint widthInBins = DivideRoundUp(checked((uint)scene.TileCountX), 16U); + uint heightInBins = DivideRoundUp(checked((uint)scene.TileCountY), 16U); + + return new WebGPUSceneWorkgroupCounts( + useLargePathScan, + pathTagWgs, + 256U, + reducedSize / 256U, + pathTagWgs, + drawObjectWgs, + flattenWgs, + drawMonoidWgs, + drawMonoidWgs, + clipReduceWgs, + clipWgs, + BinningComputeShader.GetDispatchX(drawObjectCount), + TileAllocComputeShader.GetDispatchX(drawObjectCount), + PathCountSetupComputeShader.GetDispatchX(), + PathCountComputeShader.GetDispatchX(lineCount), + BackdropComputeShader.GetDispatchX(pathCount), + widthInBins, + heightInBins, + PathTilingSetupComputeShader.GetDispatchX(), + 1, + checked((uint)scene.TileCountX), + checked((uint)scene.TileCountY)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint DivideRoundUp(uint value, uint divisor) + => (value + divisor - 1U) / divisor; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint AlignUp(uint value, uint alignment) + => value + (uint)(-(int)value & (alignment - 1U)); +} + +/// +/// Buffer sizes for the currently implemented staged scene passes. +/// +internal readonly struct WebGPUSceneBufferSizes +{ + public WebGPUSceneBufferSizes( + WebGPUSceneBufferSize pathReduced, + WebGPUSceneBufferSize pathReduced2, + WebGPUSceneBufferSize pathReducedScan, + WebGPUSceneBufferSize pathMonoids, + WebGPUSceneBufferSize pathBboxes, + WebGPUSceneBufferSize drawReduced, + WebGPUSceneBufferSize drawMonoids, + WebGPUSceneBufferSize info, + WebGPUSceneBufferSize clipInputs, + WebGPUSceneBufferSize clipElements, + WebGPUSceneBufferSize clipBics, + WebGPUSceneBufferSize clipBboxes, + WebGPUSceneBufferSize drawBboxes, + WebGPUSceneBufferSize bumpAlloc, + WebGPUSceneBufferSize paths, + WebGPUSceneBufferSize lines, + WebGPUSceneBufferSize binHeaders, + WebGPUSceneBufferSize binData, + WebGPUSceneBufferSize indirectCount, + WebGPUSceneBufferSize pathTiles, + WebGPUSceneBufferSize segCounts, + WebGPUSceneBufferSize segments, + WebGPUSceneBufferSize blendSpill, + WebGPUSceneBufferSize ptcl) + { + this.PathReduced = pathReduced; + this.PathReduced2 = pathReduced2; + this.PathReducedScan = pathReducedScan; + this.PathMonoids = pathMonoids; + this.PathBboxes = pathBboxes; + this.DrawReduced = drawReduced; + this.DrawMonoids = drawMonoids; + this.Info = info; + this.ClipInputs = clipInputs; + this.ClipElements = clipElements; + this.ClipBics = clipBics; + this.ClipBboxes = clipBboxes; + this.DrawBboxes = drawBboxes; + this.BumpAlloc = bumpAlloc; + this.Paths = paths; + this.Lines = lines; + this.BinHeaders = binHeaders; + this.BinData = binData; + this.IndirectCount = indirectCount; + this.PathTiles = pathTiles; + this.SegCounts = segCounts; + this.Segments = segments; + this.BlendSpill = blendSpill; + this.Ptcl = ptcl; + } + + public WebGPUSceneBufferSize PathReduced { get; } + + public WebGPUSceneBufferSize PathReduced2 { get; } + + public WebGPUSceneBufferSize PathReducedScan { get; } + + public WebGPUSceneBufferSize PathMonoids { get; } + + public WebGPUSceneBufferSize PathBboxes { get; } + + public WebGPUSceneBufferSize DrawReduced { get; } + + public WebGPUSceneBufferSize DrawMonoids { get; } + + public WebGPUSceneBufferSize Info { get; } + + public WebGPUSceneBufferSize ClipInputs { get; } + + public WebGPUSceneBufferSize ClipElements { get; } + + public WebGPUSceneBufferSize ClipBics { get; } + + public WebGPUSceneBufferSize ClipBboxes { get; } + + public WebGPUSceneBufferSize DrawBboxes { get; } + + public WebGPUSceneBufferSize BumpAlloc { get; } + + public WebGPUSceneBufferSize Paths { get; } + + public WebGPUSceneBufferSize Lines { get; } + + public WebGPUSceneBufferSize BinHeaders { get; } + + public WebGPUSceneBufferSize BinData { get; } + + public WebGPUSceneBufferSize IndirectCount { get; } + + public WebGPUSceneBufferSize PathTiles { get; } + + public WebGPUSceneBufferSize SegCounts { get; } + + public WebGPUSceneBufferSize Segments { get; } + + public WebGPUSceneBufferSize BlendSpill { get; } + + public WebGPUSceneBufferSize Ptcl { get; } + + public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene) + { + WebGPUSceneWorkgroupCounts workgroupCounts = WebGPUSceneWorkgroupCounts.Create(scene); + uint pathTagWgs = workgroupCounts.PathReduceX; + uint reducedSize = workgroupCounts.UseLargePathScan ? AlignUp(pathTagWgs, 256U) : pathTagWgs; + uint pathReducedCount = reducedSize; + uint pathReduced2Count = 256U; + uint pathReducedScanCount = reducedSize; + uint pathMonoidCount = pathTagWgs * 256U; + uint pathBboxCount = checked((uint)scene.PathCount); + uint drawObjectCount = checked((uint)scene.DrawTagCount); + uint drawReducedCount = workgroupCounts.DrawReduceX; + uint drawMonoidCount = drawObjectCount; + uint infoCount = checked((uint)scene.InfoWordCount); + uint clipInputCount = checked((uint)scene.ClipCount); + uint clipElementCount = checked((uint)scene.ClipCount); + uint clipBicCount = clipInputCount / 256U; + uint clipBboxCount = clipInputCount; + uint drawBboxCount = drawObjectCount; + uint drawObjectPartitions = BinningComputeShader.GetDispatchX(drawObjectCount); + uint binHeaderCount = checked(drawObjectPartitions * 256U); + uint pathCount = AlignUp(checked((uint)scene.PathCount), 256U); + uint linesCount = 1U << 21; + uint binDataCount = 1U << 18; + uint pathTileCount = 1U << 21; + uint segmentCount = 1U << 21; + uint segmentCapacity = 1U << 21; + uint blendSpillCount = 1U << 20; + uint ptclCapacity = 1U << 23; + + return new WebGPUSceneBufferSizes( + WebGPUSceneBufferSize.Create(pathReducedCount), + WebGPUSceneBufferSize.Create(pathReduced2Count), + WebGPUSceneBufferSize.Create(pathReducedScanCount), + WebGPUSceneBufferSize.Create(pathMonoidCount), + WebGPUSceneBufferSize.Create(pathBboxCount), + WebGPUSceneBufferSize.Create(drawReducedCount), + WebGPUSceneBufferSize.Create(drawMonoidCount), + WebGPUSceneBufferSize.Create(infoCount), + WebGPUSceneBufferSize.Create(clipInputCount), + WebGPUSceneBufferSize.Create(clipElementCount), + WebGPUSceneBufferSize.Create(clipBicCount), + WebGPUSceneBufferSize.Create(clipBboxCount), + WebGPUSceneBufferSize.Create(drawBboxCount), + WebGPUSceneBufferSize.Create(1), + WebGPUSceneBufferSize.Create(pathCount), + WebGPUSceneBufferSize.Create(linesCount), + WebGPUSceneBufferSize.Create(binHeaderCount), + WebGPUSceneBufferSize.Create(binDataCount), + WebGPUSceneBufferSize.Create(1), + WebGPUSceneBufferSize.Create(pathTileCount), + WebGPUSceneBufferSize.Create(segmentCount), + WebGPUSceneBufferSize.Create(segmentCapacity), + WebGPUSceneBufferSize.Create(blendSpillCount), + WebGPUSceneBufferSize.Create(ptclCapacity)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint AlignUp(uint value, uint alignment) + => value + (uint)(-(int)value & (alignment - 1U)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint DivideRoundUp(uint value, uint divisor) + => value == 0U ? 0U : ((value - 1U) / divisor) + 1U; +} + +/// +/// Typed buffer size primitive mirroring Vello's exact-count planning style. +/// +internal readonly struct WebGPUSceneBufferSize + where T : unmanaged +{ + private WebGPUSceneBufferSize(uint length) + { + // Storage bindings must remain non-zero-sized for validation. + this.Length = length > 0 ? length : 1; + } + + public uint Length { get; } + + public nuint ByteLength + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => checked((nuint)this.Length * (nuint)Unsafe.SizeOf()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static WebGPUSceneBufferSize Create(uint length) => new(length); +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs new file mode 100644 index 000000000..9f92443a8 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs @@ -0,0 +1,2042 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1201 // Phase-1 staged scene types are grouped by pipeline role. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Phase 1 shell for the staged WebGPU scene pipeline. +/// +internal static class WebGPUSceneDispatch +{ + internal const nuint MaxStorageBufferBindingSize = 128U * 1024U * 1024U; + + private const string PathtagReducePipelineKey = "scene/pathtag-reduce"; + private const string PathtagReduce2PipelineKey = "scene/pathtag-reduce2"; + private const string PathtagScan1PipelineKey = "scene/pathtag-scan1"; + private const string PathtagScanPipelineKey = "scene/pathtag-scan"; + private const string PathtagScanSmallPipelineKey = "scene/pathtag-scan-small"; + private const string BboxClearPipelineKey = "scene/bbox-clear"; + private const string FlattenPipelineKey = "scene/flatten"; + private const string DrawReducePipelineKey = "scene/draw-reduce"; + private const string DrawLeafPipelineKey = "scene/draw-leaf"; + private const string ClipReducePipelineKey = "scene/clip-reduce"; + private const string ClipLeafPipelineKey = "scene/clip-leaf"; + private const string BinningPipelineKey = "scene/binning"; + private const string TileAllocPipelineKey = "scene/tile-alloc"; + private const string BackdropPipelineKey = "scene/backdrop"; + private const string PathCountSetupPipelineKey = "scene/path-count-setup"; + private const string PathCountPipelineKey = "scene/path-count"; + private const string CoarsePipelineKey = "scene/coarse"; + private const string PathTilingSetupPipelineKey = "scene/path-tiling-setup"; + private const string PathTilingPipelineKey = "scene/path-tiling"; + private const string FineAreaPipelineKey = "scene/fine-area"; + + /// + /// Builds the flush-scoped encoded scene and uploads its GPU resources. + /// + public static bool TryCreateStagedScene( + Configuration configuration, + ICanvasFrame target, + CompositionScene scene, + out WebGPUStagedScene stagedScene, + out string? error) + where TPixel : unmanaged, IPixel + => TryCreateStagedScene(configuration, target, scene.Commands, out _, out stagedScene, out error); + + /// + /// Builds the flush-scoped encoded scene and uploads its GPU resources. + /// + public static bool TryCreateStagedScene( + Configuration configuration, + ICanvasFrame target, + IReadOnlyList commands, + out bool exceedsBindingLimit, + out WebGPUStagedScene stagedScene, + out string? error) + where TPixel : unmanaged, IPixel + { + stagedScene = default; + exceedsBindingLimit = false; + WebGPUEncodedScene? encodedScene = null; + + if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature)) + { + error = $"The staged WebGPU scene pipeline does not support pixel format '{typeof(TPixel).Name}'."; + return false; + } + + if (!WebGPUSceneEncoder.TryValidateBrushSupport(commands, out error)) + { + return false; + } + + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + + WebGPUFlushContext? flushContext = WebGPUFlushContext.Create( + target, + textureFormat, + requiredFeature, + configuration.MemoryAllocator); + + if (flushContext is null) + { + error = "Failed to create a WebGPU flush context for the staged scene pipeline."; + return false; + } + + try + { + encodedScene = WebGPUSceneEncoder.Encode(commands, flushContext.TargetBounds, flushContext.MemoryAllocator); + WebGPUSceneConfig config = WebGPUSceneConfig.Create(encodedScene); + uint baseColor = 0U; + if (!TryValidateBindingSizes(encodedScene, config, out error)) + { + exceedsBindingLimit = true; + encodedScene.Dispose(); + flushContext.Dispose(); + stagedScene = default; + return false; + } + + if (encodedScene.FillCount == 0) + { + stagedScene = new WebGPUStagedScene(flushContext, encodedScene, config, default); + error = null; + return true; + } + + if (!WebGPUSceneResources.TryCreate(flushContext, encodedScene, config, baseColor, out WebGPUSceneResourceSet resources, out error)) + { + encodedScene.Dispose(); + flushContext.Dispose(); + stagedScene = default; + return false; + } + + stagedScene = new WebGPUStagedScene(flushContext, encodedScene, config, resources); + error = null; + return true; + } + catch + { + encodedScene?.Dispose(); + flushContext.Dispose(); + stagedScene = default; + throw; + } + } + + /// + /// Checks whether one encoded scene can be bound to the current WebGPU staged pipeline. + /// + public static bool TryValidateBindingSizes( + WebGPUEncodedScene encodedScene, + WebGPUSceneConfig config, + out string? error) + { + WebGPUSceneBufferSizes bufferSizes = config.BufferSizes; + nuint infoBinDataByteLength = checked(GetBindingByteLength(encodedScene.InfoWordCount) + config.BufferSizes.BinData.ByteLength); + if (!TryValidateBufferSize(GetBindingByteLength(1), "scene config", out error) || + !TryValidateBufferSize(GetBindingByteLength(encodedScene.SceneWordCount), "scene data", out error) || + !TryValidateBufferSize(bufferSizes.PathReduced.ByteLength, "path reduced", out error) || + !TryValidateBufferSize(bufferSizes.PathReduced2.ByteLength, "path reduced2", out error) || + !TryValidateBufferSize(bufferSizes.PathReducedScan.ByteLength, "path reduced scan", out error) || + !TryValidateBufferSize(bufferSizes.PathMonoids.ByteLength, "path monoids", out error) || + !TryValidateBufferSize(bufferSizes.PathBboxes.ByteLength, "path bboxes", out error) || + !TryValidateBufferSize(bufferSizes.DrawReduced.ByteLength, "draw reduced", out error) || + !TryValidateBufferSize(bufferSizes.DrawMonoids.ByteLength, "draw monoids", out error) || + !TryValidateBufferSize(infoBinDataByteLength, "scene info/bin data", out error) || + !TryValidateBufferSize(bufferSizes.ClipInputs.ByteLength, "clip inputs", out error) || + !TryValidateBufferSize(bufferSizes.ClipElements.ByteLength, "clip elements", out error) || + !TryValidateBufferSize(bufferSizes.ClipBics.ByteLength, "clip bics", out error) || + !TryValidateBufferSize(bufferSizes.ClipBboxes.ByteLength, "clip bboxes", out error) || + !TryValidateBufferSize(bufferSizes.DrawBboxes.ByteLength, "draw bboxes", out error) || + !TryValidateBufferSize(bufferSizes.Paths.ByteLength, "scene paths", out error) || + !TryValidateBufferSize(bufferSizes.Lines.ByteLength, "scene lines", out error) || + !TryValidateBufferSize(bufferSizes.BinHeaders.ByteLength, "bin headers", out error) || + !TryValidateBufferSize(bufferSizes.IndirectCount.ByteLength, "indirect count", out error) || + !TryValidateBufferSize(bufferSizes.PathTiles.ByteLength, "path tiles", out error) || + !TryValidateBufferSize(bufferSizes.SegCounts.ByteLength, "segment counts", out error) || + !TryValidateBufferSize(bufferSizes.Segments.ByteLength, "segments", out error) || + !TryValidateBufferSize(bufferSizes.BlendSpill.ByteLength, "blend spill", out error) || + !TryValidateBufferSize(bufferSizes.Ptcl.ByteLength, "ptcl", out error)) + { + return false; + } + + error = null; + return true; + } + + /// + /// Dispatches the early Vello-style scheduling stages for one staged scene. + /// + public static unsafe bool TryDispatchSchedulingStages( + ref WebGPUStagedScene stagedScene, + out WebGPUSceneSchedulingResources scheduling, + out string? error) + { + scheduling = default; + + WebGPUFlushContext flushContext = stagedScene.FlushContext; + WebGPUEncodedScene encodedScene = stagedScene.EncodedScene; + WebGPUSceneConfig config = stagedScene.Config; + WebGPUSceneBufferSizes bufferSizes = config.BufferSizes; + WebGPUSceneWorkgroupCounts workgroupCounts = config.WorkgroupCounts; + nuint sceneBufferSize = GetBindingByteLength(encodedScene.SceneWordCount); + nuint pathReducedBufferSize = bufferSizes.PathReduced.ByteLength; + nuint pathReduced2BufferSize = bufferSizes.PathReduced2.ByteLength; + nuint pathReducedScanBufferSize = bufferSizes.PathReducedScan.ByteLength; + nuint pathMonoidBufferSize = bufferSizes.PathMonoids.ByteLength; + nuint pathBboxBufferSize = bufferSizes.PathBboxes.ByteLength; + nuint drawReducedBufferSize = bufferSizes.DrawReduced.ByteLength; + nuint drawMonoidBufferSize = bufferSizes.DrawMonoids.ByteLength; + nuint infoBinDataBufferSize = checked(GetBindingByteLength(encodedScene.InfoWordCount) + bufferSizes.BinData.ByteLength); + nuint clipInputBufferSize = bufferSizes.ClipInputs.ByteLength; + nuint clipElementBufferSize = bufferSizes.ClipElements.ByteLength; + nuint clipBicBufferSize = bufferSizes.ClipBics.ByteLength; + nuint clipBboxBufferSize = bufferSizes.ClipBboxes.ByteLength; + nuint drawBboxBufferSize = bufferSizes.DrawBboxes.ByteLength; + nuint pathBufferSize = bufferSizes.Paths.ByteLength; + nuint lineBufferSize = bufferSizes.Lines.ByteLength; + nuint binHeaderBufferSize = bufferSizes.BinHeaders.ByteLength; + nuint indirectCountBufferSize = bufferSizes.IndirectCount.ByteLength; + nuint pathTileBufferSize = bufferSizes.PathTiles.ByteLength; + nuint segCountBufferSize = bufferSizes.SegCounts.ByteLength; + nuint segmentBufferSize = bufferSizes.Segments.ByteLength; + nuint ptclBufferSize = bufferSizes.Ptcl.ByteLength; + if (encodedScene.FillCount == 0) + { + error = null; + return true; + } + + if (!flushContext.EnsureCommandEncoder()) + { + error = "Failed to create a command encoder for the staged WebGPU scene."; + return false; + } + + if (!TryCreateStorageBuffer(flushContext, bufferSizes.BinHeaders.ByteLength, out WgpuBuffer* binHeaderBuffer, out error)) + { + return false; + } + + if (!TryCreateIndirectStorageBuffer(flushContext, indirectCountBufferSize, out WgpuBuffer* indirectCountBuffer, out error)) + { + return false; + } + + if (!TryCreateStorageBuffer(flushContext, bufferSizes.PathTiles.ByteLength, out WgpuBuffer* pathTileBuffer, out error)) + { + return false; + } + + if (!TryCreateStorageBuffer(flushContext, bufferSizes.SegCounts.ByteLength, out WgpuBuffer* segCountBuffer, out error)) + { + return false; + } + + if (!TryCreateStorageBuffer(flushContext, bufferSizes.Segments.ByteLength, out WgpuBuffer* segmentBuffer, out error)) + { + return false; + } + + if (!TryCreateStorageBuffer(flushContext, bufferSizes.BlendSpill.ByteLength, out WgpuBuffer* blendBuffer, out error)) + { + return false; + } + + if (!TryCreateStorageBuffer(flushContext, bufferSizes.Ptcl.ByteLength, out WgpuBuffer* ptclBuffer, out error)) + { + return false; + } + + GpuSceneBumpAllocators bumpAllocators = new() + { + Failed = 0, + Binning = 0, + Ptcl = 0, + Tile = 0, + SegCounts = 0, + Segments = 0, + Blend = 0, + Lines = 0 + }; + + if (!TryCreateAndUploadStorageBuffer(flushContext, in bumpAllocators, out WgpuBuffer* bumpBuffer, out error)) + { + return false; + } + + WebGPUSceneResourceRegistry resourceRegistry = WebGPUSceneResourceRegistry.Create(stagedScene.Resources); + resourceRegistry.RegisterSchedulingBuffers( + binHeaderBuffer, + indirectCountBuffer, + pathTileBuffer, + segCountBuffer, + segmentBuffer, + blendBuffer, + ptclBuffer, + bumpBuffer); + + WebGPUSceneComputeRecording recording = new(resourceRegistry); + + if (!TryDispatchPathtagReduce( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + pathReducedBufferSize, + workgroupCounts.PathReduceX, + out error)) + { + return false; + } + + if (workgroupCounts.UseLargePathScan) + { + if (!TryDispatchPathtagReduce2( + recording, + flushContext, + stagedScene.Resources, + pathReducedBufferSize, + pathReduced2BufferSize, + workgroupCounts.PathReduce2X, + out error)) + { + return false; + } + + if (!TryDispatchPathtagScan1( + recording, + flushContext, + stagedScene.Resources, + pathReducedBufferSize, + pathReduced2BufferSize, + pathReducedScanBufferSize, + workgroupCounts.PathScan1X, + out error)) + { + return false; + } + + if (!TryDispatchPathtagScan( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + pathReducedScanBufferSize, + pathMonoidBufferSize, + workgroupCounts.PathScanX, + useSmallVariant: false, + out error)) + { + return false; + } + } + else if (!TryDispatchPathtagScan( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + pathReducedBufferSize, + pathMonoidBufferSize, + workgroupCounts.PathScanX, + useSmallVariant: true, + out error)) + { + return false; + } + + if (!TryDispatchBboxClear( + recording, + flushContext, + stagedScene.Resources, + pathBboxBufferSize, + workgroupCounts.BboxClearX, + out error)) + { + return false; + } + + if (!TryDispatchFlatten( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + pathMonoidBufferSize, + pathBboxBufferSize, + bumpBuffer, + lineBufferSize, + workgroupCounts.FlattenX, + out error)) + { + return false; + } + + if (!TryDispatchDrawReduce( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + drawReducedBufferSize, + workgroupCounts.DrawReduceX, + out error)) + { + return false; + } + + if (!TryDispatchDrawLeaf( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + drawReducedBufferSize, + pathBboxBufferSize, + drawMonoidBufferSize, + infoBinDataBufferSize, + clipInputBufferSize, + workgroupCounts.DrawLeafX, + out error)) + { + return false; + } + + if (encodedScene.ClipCount > 0) + { + if (!TryDispatchClipReduce( + recording, + flushContext, + stagedScene.Resources, + clipInputBufferSize, + pathBboxBufferSize, + clipBicBufferSize, + clipElementBufferSize, + workgroupCounts.ClipReduceX, + out error)) + { + return false; + } + + if (!TryDispatchClipLeaf( + recording, + flushContext, + stagedScene.Resources, + clipInputBufferSize, + pathBboxBufferSize, + clipBicBufferSize, + clipElementBufferSize, + drawMonoidBufferSize, + clipBboxBufferSize, + workgroupCounts.ClipLeafX, + out error)) + { + return false; + } + } + + if (!TryDispatchBinning( + recording, + flushContext, + stagedScene.Resources, + drawMonoidBufferSize, + pathBboxBufferSize, + clipBboxBufferSize, + drawBboxBufferSize, + infoBinDataBufferSize, + binHeaderBuffer, + binHeaderBufferSize, + bumpBuffer, + workgroupCounts.BinningX, + out error)) + { + return false; + } + + if (!TryDispatchTileAlloc( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + drawBboxBufferSize, + pathBufferSize, + pathTileBuffer, + pathTileBufferSize, + bumpBuffer, + workgroupCounts.TileAllocX, + out error)) + { + return false; + } + + if (!TryDispatchPathCountSetup( + recording, + flushContext, + bumpBuffer, + indirectCountBuffer, + workgroupCounts.PathCountSetupX, + out error)) + { + return false; + } + + if (!TryDispatchPathCount( + recording, + flushContext, + stagedScene.Resources, + bumpBuffer, + pathBufferSize, + pathTileBuffer, + pathTileBufferSize, + segCountBuffer, + segCountBufferSize, + lineBufferSize, + indirectCountBuffer, + out error)) + { + return false; + } + + if (!TryDispatchBackdrop( + recording, + flushContext, + stagedScene.Resources, + bumpBuffer, + pathBufferSize, + pathTileBuffer, + pathTileBufferSize, + workgroupCounts.BackdropX, + out error)) + { + return false; + } + + if (!TryDispatchCoarse( + recording, + flushContext, + stagedScene.Resources, + sceneBufferSize, + drawMonoidBufferSize, + infoBinDataBufferSize, + binHeaderBuffer, + binHeaderBufferSize, + pathBufferSize, + pathTileBuffer, + pathTileBufferSize, + ptclBuffer, + ptclBufferSize, + bumpBuffer, + workgroupCounts.CoarseX, + workgroupCounts.CoarseY, + out error)) + { + return false; + } + + if (!TryDispatchPathTilingSetup( + recording, + flushContext, + bumpBuffer, + indirectCountBuffer, + ptclBuffer, + ptclBufferSize, + workgroupCounts.PathTilingSetupX, + out error)) + { + return false; + } + + if (!TryDispatchPathTiling( + recording, + flushContext, + stagedScene.Resources, + bumpBuffer, + segCountBuffer, + segCountBufferSize, + lineBufferSize, + pathBufferSize, + pathTileBuffer, + pathTileBufferSize, + segmentBuffer, + segmentBufferSize, + indirectCountBuffer, + out error)) + { + return false; + } + + if (!TryExecuteComputeRecording(flushContext, recording, out error)) + { + return false; + } + + scheduling = new WebGPUSceneSchedulingResources( + binHeaderBuffer, + indirectCountBuffer, + pathTileBuffer, + segCountBuffer, + segmentBuffer, + blendBuffer, + ptclBuffer, + bumpBuffer); + error = null; + return true; + } + + /// + /// Executes the staged scene pipeline against the current flush target. + /// + public static unsafe bool TryRenderStagedScene( + ref WebGPUStagedScene stagedScene, + out string? error) + { + error = null; + + WebGPUEncodedScene encodedScene = stagedScene.EncodedScene; + if (encodedScene.FillCount == 0) + { + return true; + } + + if (!TryDispatchSchedulingStages(ref stagedScene, out WebGPUSceneSchedulingResources scheduling, out error)) + { + return false; + } + + if (string.Equals(Environment.GetEnvironmentVariable("IMAGE_SHARP_WEBGPU_DEBUG_SCHED"), "1", StringComparison.Ordinal)) + { + if (!TryReadSchedulingStatus(stagedScene.FlushContext, scheduling.BumpBuffer, out GpuSceneBumpAllocators bumpAllocators, out error)) + { + return false; + } + + WebGPUEncodedScene debugScene = stagedScene.EncodedScene; + error = $"scene fills={debugScene.FillCount} paths={debugScene.PathCount} lines={debugScene.LineCount} pathtag_bytes={debugScene.PathTagByteCount} pathtag_words={debugScene.PathTagWordCount} drawtags={debugScene.DrawTagCount} drawdata={debugScene.DrawDataWordCount} transforms={debugScene.TransformWordCount} styles={debugScene.StyleWordCount}; sched failed={bumpAllocators.Failed} binning={bumpAllocators.Binning} ptcl={bumpAllocators.Ptcl} tile={bumpAllocators.Tile} seg_counts={bumpAllocators.SegCounts} segments={bumpAllocators.Segments} blend={bumpAllocators.Blend} lines={bumpAllocators.Lines}"; + return false; + } + + WebGPUFlushContext flushContext = stagedScene.FlushContext; + int targetWidth = encodedScene.TargetSize.Width; + int targetHeight = encodedScene.TargetSize.Height; + + if (!WebGPUDrawingBackend.TryCreateCompositionTexture(flushContext, targetWidth, targetHeight, out Texture* outputTexture, out TextureView* outputTextureView, out error)) + { + return false; + } + + if (!TryDispatchFineArea( + flushContext, + stagedScene.Resources, + encodedScene, + stagedScene.Config.BufferSizes, + scheduling, + outputTextureView, + (uint)encodedScene.TileCountX, + (uint)encodedScene.TileCountY, + out error)) + { + return false; + } + + WebGPUDrawingBackend.CopyTextureRegion( + flushContext, + outputTexture, + 0, + 0, + flushContext.TargetTexture, + 0, + 0, + targetWidth, + targetHeight); + + return WebGPUDrawingBackend.TrySubmit(flushContext); + } + + private static unsafe bool TryDispatchPathtagReduce( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint sceneBufferSize, + nuint pathReducedBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[3]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); + entries[2] = CreateBufferBinding(2, resources.PathReducedBuffer, pathReducedBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.PathtagReduce, entries, 3, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchPathtagReduce2( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint pathReducedBufferSize, + nuint pathReduced2BufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[2]; + entries[0] = CreateBufferBinding(0, resources.PathReducedBuffer, pathReducedBufferSize); + entries[1] = CreateBufferBinding(1, resources.PathReduced2Buffer, pathReduced2BufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.PathtagReduce2, entries, 2, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchPathtagScan1( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint pathReducedBufferSize, + nuint pathReduced2BufferSize, + nuint pathReducedScanBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[3]; + entries[0] = CreateBufferBinding(0, resources.PathReducedBuffer, pathReducedBufferSize); + entries[1] = CreateBufferBinding(1, resources.PathReduced2Buffer, pathReduced2BufferSize); + entries[2] = CreateBufferBinding(2, resources.PathReducedScanBuffer, pathReducedScanBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.PathtagScan1, entries, 3, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchPathtagScan( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint sceneBufferSize, + nuint parentBufferSize, + nuint pathMonoidBufferSize, + uint dispatchX, + bool useSmallVariant, + out string? error) + { + WgpuBuffer* parentBuffer = useSmallVariant ? resources.PathReducedBuffer : resources.PathReducedScanBuffer; + + BindGroupEntry* entries = stackalloc BindGroupEntry[4]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); + entries[2] = CreateBufferBinding(2, parentBuffer, parentBufferSize); + entries[3] = CreateBufferBinding(3, resources.PathMonoidBuffer, pathMonoidBufferSize); + + return recording.TryRecord(useSmallVariant ? WebGPUSceneShaderId.PathtagScanSmall : WebGPUSceneShaderId.PathtagScan, entries, 4, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchBboxClear( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint pathBboxBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[2]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.PathBboxBuffer, pathBboxBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.BboxClear, entries, 2, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchFlatten( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint sceneBufferSize, + nuint pathMonoidBufferSize, + nuint pathBboxBufferSize, + WgpuBuffer* bumpBuffer, + nuint lineBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[6]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); + entries[2] = CreateBufferBinding(2, resources.PathMonoidBuffer, pathMonoidBufferSize); + entries[3] = CreateBufferBinding(3, resources.PathBboxBuffer, pathBboxBufferSize); + entries[4] = CreateBufferBinding(4, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[5] = CreateBufferBinding(5, resources.LineBuffer, lineBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.Flatten, entries, 6, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchDrawReduce( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint sceneBufferSize, + nuint drawReducedBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[3]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); + entries[2] = CreateBufferBinding(2, resources.DrawReducedBuffer, drawReducedBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.DrawReduce, entries, 3, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchDrawLeaf( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint sceneBufferSize, + nuint drawReducedBufferSize, + nuint pathBboxBufferSize, + nuint drawMonoidBufferSize, + nuint infoBinDataBufferSize, + nuint clipInputBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[7]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); + entries[2] = CreateBufferBinding(2, resources.DrawReducedBuffer, drawReducedBufferSize); + entries[3] = CreateBufferBinding(3, resources.PathBboxBuffer, pathBboxBufferSize); + entries[4] = CreateBufferBinding(4, resources.DrawMonoidBuffer, drawMonoidBufferSize); + entries[5] = CreateBufferBinding(5, resources.InfoBinDataBuffer, infoBinDataBufferSize); + entries[6] = CreateBufferBinding(6, resources.ClipInputBuffer, clipInputBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.DrawLeaf, entries, 7, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchClipReduce( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint clipInputBufferSize, + nuint pathBboxBufferSize, + nuint clipBicBufferSize, + nuint clipElementBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[4]; + entries[0] = CreateBufferBinding(0, resources.ClipInputBuffer, clipInputBufferSize); + entries[1] = CreateBufferBinding(1, resources.PathBboxBuffer, pathBboxBufferSize); + entries[2] = CreateBufferBinding(2, resources.ClipBicBuffer, clipBicBufferSize); + entries[3] = CreateBufferBinding(3, resources.ClipElementBuffer, clipElementBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.ClipReduce, entries, 4, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchClipLeaf( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint clipInputBufferSize, + nuint pathBboxBufferSize, + nuint clipBicBufferSize, + nuint clipElementBufferSize, + nuint drawMonoidBufferSize, + nuint clipBboxBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[7]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.ClipInputBuffer, clipInputBufferSize); + entries[2] = CreateBufferBinding(2, resources.PathBboxBuffer, pathBboxBufferSize); + entries[3] = CreateBufferBinding(3, resources.ClipBicBuffer, clipBicBufferSize); + entries[4] = CreateBufferBinding(4, resources.ClipElementBuffer, clipElementBufferSize); + entries[5] = CreateBufferBinding(5, resources.DrawMonoidBuffer, drawMonoidBufferSize); + entries[6] = CreateBufferBinding(6, resources.ClipBboxBuffer, clipBboxBufferSize); + + return recording.TryRecord(WebGPUSceneShaderId.ClipLeaf, entries, 7, dispatchX, 1, 1, out error); + } + + private static unsafe bool TryDispatchBinning( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint drawMonoidBufferSize, + nuint pathBboxBufferSize, + nuint clipBboxBufferSize, + nuint drawBboxBufferSize, + nuint infoBinDataBufferSize, + WgpuBuffer* binHeaderBuffer, + nuint binHeaderBufferSize, + WgpuBuffer* bumpBuffer, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[8]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.DrawMonoidBuffer, drawMonoidBufferSize); + entries[2] = CreateBufferBinding(2, resources.PathBboxBuffer, pathBboxBufferSize); + entries[3] = CreateBufferBinding(3, resources.ClipBboxBuffer, clipBboxBufferSize); + entries[4] = CreateBufferBinding(4, resources.DrawBboxBuffer, drawBboxBufferSize); + entries[5] = CreateBufferBinding(5, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[6] = CreateBufferBinding(6, resources.InfoBinDataBuffer, infoBinDataBufferSize); + entries[7] = CreateBufferBinding(7, binHeaderBuffer, binHeaderBufferSize); + + if (!recording.TryRecord(WebGPUSceneShaderId.Binning, entries, 8, dispatchX, 1, 1, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryReadSchedulingStatus( + WebGPUFlushContext flushContext, + WgpuBuffer* bumpBuffer, + out GpuSceneBumpAllocators bumpAllocators, + out string? error) + { + bumpAllocators = default; + + BufferDescriptor readbackDescriptor = new() + { + Usage = BufferUsage.CopyDst | BufferUsage.MapRead, + Size = (nuint)sizeof(GpuSceneBumpAllocators), + MappedAtCreation = false + }; + + WgpuBuffer* readbackBuffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in readbackDescriptor); + if (readbackBuffer is null) + { + error = "Failed to create the staged-scene scheduling readback buffer."; + return false; + } + + try + { + flushContext.EndComputePassIfOpen(); + flushContext.Api.CommandEncoderCopyBufferToBuffer( + flushContext.CommandEncoder, + bumpBuffer, + 0, + readbackBuffer, + 0, + (nuint)sizeof(GpuSceneBumpAllocators)); + + if (!WebGPUDrawingBackend.TrySubmit(flushContext)) + { + error = "Failed to submit staged-scene scheduling work."; + return false; + } + + BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + using ManualResetEventSlim mapReady = new(false); + + void Callback(BufferMapAsyncStatus status, void* userData) + { + _ = userData; + mapStatus = status; + mapReady.Set(); + } + + using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + flushContext.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)sizeof(GpuSceneBumpAllocators), callback, null); + if (!WaitForMapSignal(flushContext.RuntimeLease.WgpuExtension, flushContext.Device, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + { + error = $"Failed to map staged-scene scheduling status with status '{mapStatus}'."; + return false; + } + + void* mapped = flushContext.Api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)sizeof(GpuSceneBumpAllocators)); + if (mapped is null) + { + flushContext.Api.BufferUnmap(readbackBuffer); + error = "Failed to map the staged-scene scheduling status range."; + return false; + } + + try + { + bumpAllocators = Unsafe.Read(mapped); + error = null; + return true; + } + finally + { + flushContext.Api.BufferUnmap(readbackBuffer); + } + } + finally + { + flushContext.Api.BufferRelease(readbackBuffer); + } + } + + private static unsafe bool TryDispatchTileAlloc( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint sceneBufferSize, + nuint drawBboxBufferSize, + nuint pathBufferSize, + WgpuBuffer* pathTileBuffer, + nuint pathTileBufferSize, + WgpuBuffer* bumpBuffer, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[6]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); + entries[2] = CreateBufferBinding(2, resources.DrawBboxBuffer, drawBboxBufferSize); + entries[3] = CreateBufferBinding(3, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[4] = CreateBufferBinding(4, resources.PathBuffer, pathBufferSize); + entries[5] = CreateBufferBinding(5, pathTileBuffer, pathTileBufferSize); + + if (!recording.TryRecord(WebGPUSceneShaderId.TileAlloc, entries, 6, dispatchX, 1, 1, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryDispatchBackdrop( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + WgpuBuffer* bumpBuffer, + nuint pathBufferSize, + WgpuBuffer* pathTileBuffer, + nuint pathTileBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[4]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = CreateBufferBinding(2, resources.PathBuffer, pathBufferSize); + entries[3] = CreateBufferBinding(3, pathTileBuffer, pathTileBufferSize); + + if (!recording.TryRecord(WebGPUSceneShaderId.Backdrop, entries, 4, dispatchX, 1, 1, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryDispatchPathCountSetup( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WgpuBuffer* bumpBuffer, + WgpuBuffer* indirectCountBuffer, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[2]; + entries[0] = CreateBufferBinding(0, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = CreateBufferBinding(1, indirectCountBuffer, (nuint)sizeof(GpuSceneIndirectCount)); + + if (!recording.TryRecord(WebGPUSceneShaderId.PathCountSetup, entries, 2, dispatchX, 1, 1, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryDispatchPathCount( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + WgpuBuffer* bumpBuffer, + nuint pathBufferSize, + WgpuBuffer* pathTileBuffer, + nuint pathTileBufferSize, + WgpuBuffer* segCountBuffer, + nuint segCountBufferSize, + nuint lineBufferSize, + WgpuBuffer* indirectCountBuffer, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[6]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = CreateBufferBinding(2, resources.LineBuffer, lineBufferSize); + entries[3] = CreateBufferBinding(3, resources.PathBuffer, pathBufferSize); + entries[4] = CreateBufferBinding(4, pathTileBuffer, pathTileBufferSize); + entries[5] = CreateBufferBinding(5, segCountBuffer, segCountBufferSize); + + if (!recording.TryRecordIndirect(WebGPUSceneShaderId.PathCount, entries, 6, indirectCountBuffer, 0, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryDispatchCoarse( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + nuint sceneBufferSize, + nuint drawMonoidBufferSize, + nuint infoBinDataBufferSize, + WgpuBuffer* binHeaderBuffer, + nuint binHeaderBufferSize, + nuint pathBufferSize, + WgpuBuffer* pathTileBuffer, + nuint pathTileBufferSize, + WgpuBuffer* ptclBuffer, + nuint ptclBufferSize, + WgpuBuffer* bumpBuffer, + uint dispatchX, + uint dispatchY, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[9]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); + entries[2] = CreateBufferBinding(2, resources.DrawMonoidBuffer, drawMonoidBufferSize); + entries[3] = CreateBufferBinding(3, binHeaderBuffer, binHeaderBufferSize); + entries[4] = CreateBufferBinding(4, resources.InfoBinDataBuffer, infoBinDataBufferSize); + entries[5] = CreateBufferBinding(5, resources.PathBuffer, pathBufferSize); + entries[6] = CreateBufferBinding(6, pathTileBuffer, pathTileBufferSize); + entries[7] = CreateBufferBinding(7, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[8] = CreateBufferBinding(8, ptclBuffer, ptclBufferSize); + + if (!recording.TryRecord(WebGPUSceneShaderId.Coarse, entries, 9, dispatchX, dispatchY, 1, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryDispatchPathTilingSetup( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WgpuBuffer* bumpBuffer, + WgpuBuffer* indirectCountBuffer, + WgpuBuffer* ptclBuffer, + nuint ptclBufferSize, + uint dispatchX, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[3]; + entries[0] = CreateBufferBinding(0, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = CreateBufferBinding(1, indirectCountBuffer, (nuint)sizeof(GpuSceneIndirectCount)); + entries[2] = CreateBufferBinding(2, ptclBuffer, ptclBufferSize); + + if (!recording.TryRecord(WebGPUSceneShaderId.PathTilingSetup, entries, 3, dispatchX, 1, 1, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryDispatchPathTiling( + WebGPUSceneComputeRecording recording, + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + WgpuBuffer* bumpBuffer, + WgpuBuffer* segCountBuffer, + nuint segCountBufferSize, + nuint lineBufferSize, + nuint pathBufferSize, + WgpuBuffer* pathTileBuffer, + nuint pathTileBufferSize, + WgpuBuffer* segmentBuffer, + nuint segmentBufferSize, + WgpuBuffer* indirectCountBuffer, + out string? error) + { + BindGroupEntry* entries = stackalloc BindGroupEntry[6]; + entries[0] = CreateBufferBinding(0, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = CreateBufferBinding(1, segCountBuffer, segCountBufferSize); + entries[2] = CreateBufferBinding(2, resources.LineBuffer, lineBufferSize); + entries[3] = CreateBufferBinding(3, resources.PathBuffer, pathBufferSize); + entries[4] = CreateBufferBinding(4, pathTileBuffer, pathTileBufferSize); + entries[5] = CreateBufferBinding(5, segmentBuffer, segmentBufferSize); + + if (!recording.TryRecordIndirect(WebGPUSceneShaderId.PathTiling, entries, 6, indirectCountBuffer, 0, out error)) + { + return false; + } + + error = null; + return true; + } + + private static unsafe bool TryDispatchFineArea( + WebGPUFlushContext flushContext, + WebGPUSceneResourceSet resources, + WebGPUEncodedScene encodedScene, + WebGPUSceneBufferSizes bufferSizes, + WebGPUSceneSchedulingResources scheduling, + TextureView* outputTextureView, + uint groupCountX, + uint groupCountY, + out string? error) + { + if (!FineAreaComputeShader.TryGetCode(flushContext.TextureFormat, out byte[] shaderCode, out error)) + { + return false; + } + + bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) + => FineAreaComputeShader.TryCreateBindGroupLayout( + api, + device, + flushContext.TextureFormat, + out layout, + out layoutError); + + if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + $"{FineAreaPipelineKey}/{flushContext.TextureFormat}", + shaderCode, + FineAreaComputeShader.EntryPoint, + LayoutFactory, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out error)) + { + return false; + } + + BindGroupEntry* entries = stackalloc BindGroupEntry[9]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, scheduling.SegmentBuffer, bufferSizes.Segments.ByteLength); + entries[2] = CreateBufferBinding(2, scheduling.PtclBuffer, bufferSizes.Ptcl.ByteLength); + entries[3] = CreateBufferBinding(3, resources.InfoBinDataBuffer, checked(GetBindingByteLength(encodedScene.InfoWordCount) + bufferSizes.BinData.ByteLength)); + entries[4] = CreateBufferBinding(4, scheduling.BlendBuffer, bufferSizes.BlendSpill.ByteLength); + entries[5] = new BindGroupEntry { Binding = 5, TextureView = outputTextureView }; + entries[6] = new BindGroupEntry { Binding = 6, TextureView = resources.GradientTextureView }; + entries[7] = new BindGroupEntry { Binding = 7, TextureView = resources.ImageAtlasTextureView }; + entries[8] = new BindGroupEntry { Binding = 8, TextureView = flushContext.TargetView }; + + if (!TryDispatchComputePass(flushContext, bindGroupLayout, pipeline, entries, 9, groupCountX, groupCountY, 1, out error)) + { + return false; + } + + error = null; + return true; + } + + internal static unsafe bool TryDispatchComputePass( + WebGPUFlushContext flushContext, + BindGroupLayout* bindGroupLayout, + ComputePipeline* pipeline, + BindGroupEntry* entries, + uint entryCount, + uint groupCountX, + uint groupCountY, + uint groupCountZ, + out string? error) + { + BindGroupDescriptor descriptor = new() + { + Layout = bindGroupLayout, + EntryCount = entryCount, + Entries = entries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in descriptor); + if (bindGroup is null) + { + error = "Failed to create a staged-scene compute bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + bool ownsPassEncoder = false; + ComputePassEncoder* passEncoder = flushContext.ComputePassEncoder; + if (passEncoder is null) + { + if (!flushContext.BeginComputePass()) + { + error = "Failed to begin a staged-scene compute pass."; + return false; + } + + passEncoder = flushContext.ComputePassEncoder; + ownsPassEncoder = true; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, groupCountX, groupCountY, groupCountZ); + } + finally + { + if (ownsPassEncoder) + { + flushContext.EndComputePassIfOpen(); + } + } + + error = null; + return true; + } + + internal static unsafe bool TryDispatchComputePassIndirect( + WebGPUFlushContext flushContext, + BindGroupLayout* bindGroupLayout, + ComputePipeline* pipeline, + BindGroupEntry* entries, + uint entryCount, + WgpuBuffer* indirectBuffer, + ulong indirectOffset, + out string? error) + { + BindGroupDescriptor descriptor = new() + { + Layout = bindGroupLayout, + EntryCount = entryCount, + Entries = entries + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in descriptor); + if (bindGroup is null) + { + error = "Failed to create a staged-scene compute bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + bool ownsPassEncoder = false; + ComputePassEncoder* passEncoder = flushContext.ComputePassEncoder; + if (passEncoder is null) + { + if (!flushContext.BeginComputePass()) + { + error = "Failed to begin a staged-scene compute pass."; + return false; + } + + passEncoder = flushContext.ComputePassEncoder; + ownsPassEncoder = true; + } + + try + { + flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); + flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(passEncoder, indirectBuffer, indirectOffset); + } + finally + { + if (ownsPassEncoder) + { + flushContext.EndComputePassIfOpen(); + } + } + + error = null; + return true; + } + + private static unsafe bool TryExecuteComputeRecording( + WebGPUFlushContext flushContext, + WebGPUSceneComputeRecording recording, + out string? error) + { + foreach (WebGPUSceneComputeCommand command in recording.Commands) + { + if (!TryResolveComputeShader(flushContext, command.ShaderId, out BindGroupLayout* bindGroupLayout, out ComputePipeline* pipeline, out error)) + { + return false; + } + + if (!flushContext.BeginComputePass()) + { + error = "Failed to begin the staged-scene compute pass."; + return false; + } + + try + { + BindGroupEntry[] entries = command.ResolveEntries(recording.ResourceRegistry); + fixed (BindGroupEntry* entriesPtr = entries) + { + BindGroupDescriptor descriptor = new() + { + Layout = bindGroupLayout, + EntryCount = (uint)entries.Length, + Entries = entriesPtr + }; + + BindGroup* bindGroup = flushContext.Api.DeviceCreateBindGroup(flushContext.Device, in descriptor); + if (bindGroup is null) + { + error = "Failed to create a staged-scene compute bind group."; + return false; + } + + flushContext.TrackBindGroup(bindGroup); + flushContext.Api.ComputePassEncoderSetPipeline(flushContext.ComputePassEncoder, pipeline); + flushContext.Api.ComputePassEncoderSetBindGroup(flushContext.ComputePassEncoder, 0, bindGroup, 0, null); + + if (command.IsIndirect) + { + flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect( + flushContext.ComputePassEncoder, + command.IndirectBuffer, + command.IndirectOffset); + } + else + { + flushContext.Api.ComputePassEncoderDispatchWorkgroups( + flushContext.ComputePassEncoder, + command.GroupCountX, + command.GroupCountY, + command.GroupCountZ); + } + } + } + finally + { + flushContext.EndComputePassIfOpen(); + } + } + + error = null; + return true; + } + + private static unsafe bool TryResolveComputeShader( + WebGPUFlushContext flushContext, + WebGPUSceneShaderId shaderId, + out BindGroupLayout* bindGroupLayout, + out ComputePipeline* pipeline, + out string? error) + { + bindGroupLayout = null; + pipeline = null; + + bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) => + shaderId switch + { + WebGPUSceneShaderId.PathtagReduce => PathtagReduceComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathtagReduce2 => PathtagReduce2ComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathtagScan1 => PathtagScan1ComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathtagScan or WebGPUSceneShaderId.PathtagScanSmall => PathtagScanComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.BboxClear => BboxClearComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.Flatten => FlattenComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.DrawReduce => DrawReduceComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.DrawLeaf => DrawLeafComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.ClipReduce => ClipReduceComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.ClipLeaf => ClipLeafComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.Binning => BinningComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.TileAlloc => TileAllocComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.Backdrop => BackdropComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathCountSetup => PathCountSetupComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathCount => PathCountComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.Coarse => CoarseComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathTilingSetup => PathTilingSetupComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + WebGPUSceneShaderId.PathTiling => PathTilingComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), + _ => throw new UnreachableException() + }; + + ReadOnlySpan shaderCode = shaderId switch + { + WebGPUSceneShaderId.PathtagReduce => PathtagReduceComputeShader.ShaderCode, + WebGPUSceneShaderId.PathtagReduce2 => PathtagReduce2ComputeShader.ShaderCode, + WebGPUSceneShaderId.PathtagScan1 => PathtagScan1ComputeShader.ShaderCode, + WebGPUSceneShaderId.PathtagScan => PathtagScanComputeShader.ShaderCode, + WebGPUSceneShaderId.PathtagScanSmall => PathtagScanComputeShader.SmallShaderCode, + WebGPUSceneShaderId.BboxClear => BboxClearComputeShader.ShaderCode, + WebGPUSceneShaderId.Flatten => FlattenComputeShader.ShaderCode, + WebGPUSceneShaderId.DrawReduce => DrawReduceComputeShader.ShaderCode, + WebGPUSceneShaderId.DrawLeaf => DrawLeafComputeShader.ShaderCode, + WebGPUSceneShaderId.ClipReduce => ClipReduceComputeShader.ShaderCode, + WebGPUSceneShaderId.ClipLeaf => ClipLeafComputeShader.ShaderCode, + WebGPUSceneShaderId.Binning => BinningComputeShader.ShaderCode, + WebGPUSceneShaderId.TileAlloc => TileAllocComputeShader.ShaderCode, + WebGPUSceneShaderId.Backdrop => BackdropComputeShader.ShaderCode, + WebGPUSceneShaderId.PathCountSetup => PathCountSetupComputeShader.ShaderCode, + WebGPUSceneShaderId.PathCount => PathCountComputeShader.ShaderCode, + WebGPUSceneShaderId.Coarse => CoarseComputeShader.ShaderCode, + WebGPUSceneShaderId.PathTilingSetup => PathTilingSetupComputeShader.ShaderCode, + WebGPUSceneShaderId.PathTiling => PathTilingComputeShader.ShaderCode, + _ => throw new UnreachableException() + }; + + ReadOnlySpan entryPoint = shaderId switch + { + WebGPUSceneShaderId.PathtagReduce => PathtagReduceComputeShader.EntryPoint, + WebGPUSceneShaderId.PathtagReduce2 => PathtagReduce2ComputeShader.EntryPoint, + WebGPUSceneShaderId.PathtagScan1 => PathtagScan1ComputeShader.EntryPoint, + WebGPUSceneShaderId.PathtagScan or WebGPUSceneShaderId.PathtagScanSmall => PathtagScanComputeShader.EntryPoint, + WebGPUSceneShaderId.BboxClear => BboxClearComputeShader.EntryPoint, + WebGPUSceneShaderId.Flatten => FlattenComputeShader.EntryPoint, + WebGPUSceneShaderId.DrawReduce => DrawReduceComputeShader.EntryPoint, + WebGPUSceneShaderId.DrawLeaf => DrawLeafComputeShader.EntryPoint, + WebGPUSceneShaderId.ClipReduce => ClipReduceComputeShader.EntryPoint, + WebGPUSceneShaderId.ClipLeaf => ClipLeafComputeShader.EntryPoint, + WebGPUSceneShaderId.Binning => BinningComputeShader.EntryPoint, + WebGPUSceneShaderId.TileAlloc => TileAllocComputeShader.EntryPoint, + WebGPUSceneShaderId.Backdrop => BackdropComputeShader.EntryPoint, + WebGPUSceneShaderId.PathCountSetup => PathCountSetupComputeShader.EntryPoint, + WebGPUSceneShaderId.PathCount => PathCountComputeShader.EntryPoint, + WebGPUSceneShaderId.Coarse => CoarseComputeShader.EntryPoint, + WebGPUSceneShaderId.PathTilingSetup => PathTilingSetupComputeShader.EntryPoint, + WebGPUSceneShaderId.PathTiling => PathTilingComputeShader.EntryPoint, + _ => throw new UnreachableException() + }; + + string pipelineKey = shaderId switch + { + WebGPUSceneShaderId.PathtagReduce => PathtagReducePipelineKey, + WebGPUSceneShaderId.PathtagReduce2 => PathtagReduce2PipelineKey, + WebGPUSceneShaderId.PathtagScan1 => PathtagScan1PipelineKey, + WebGPUSceneShaderId.PathtagScan => PathtagScanPipelineKey, + WebGPUSceneShaderId.PathtagScanSmall => PathtagScanSmallPipelineKey, + WebGPUSceneShaderId.BboxClear => BboxClearPipelineKey, + WebGPUSceneShaderId.Flatten => FlattenPipelineKey, + WebGPUSceneShaderId.DrawReduce => DrawReducePipelineKey, + WebGPUSceneShaderId.DrawLeaf => DrawLeafPipelineKey, + WebGPUSceneShaderId.ClipReduce => ClipReducePipelineKey, + WebGPUSceneShaderId.ClipLeaf => ClipLeafPipelineKey, + WebGPUSceneShaderId.Binning => BinningPipelineKey, + WebGPUSceneShaderId.TileAlloc => TileAllocPipelineKey, + WebGPUSceneShaderId.Backdrop => BackdropPipelineKey, + WebGPUSceneShaderId.PathCountSetup => PathCountSetupPipelineKey, + WebGPUSceneShaderId.PathCount => PathCountPipelineKey, + WebGPUSceneShaderId.Coarse => CoarsePipelineKey, + WebGPUSceneShaderId.PathTilingSetup => PathTilingSetupPipelineKey, + WebGPUSceneShaderId.PathTiling => PathTilingPipelineKey, + _ => throw new UnreachableException() + }; + + return flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + pipelineKey, + shaderCode, + entryPoint, + LayoutFactory, + out bindGroupLayout, + out pipeline, + out error); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffer* buffer, nuint size) + => new() + { + Binding = binding, + Buffer = buffer, + Offset = 0, + Size = size + }; + + private static unsafe bool TryCreateStorageBuffer( + WebGPUFlushContext flushContext, + nuint size, + out WgpuBuffer* buffer, + out string? error) + => TryCreateBuffer( + flushContext, + size, + BufferUsage.Storage | BufferUsage.CopySrc | BufferUsage.CopyDst, + out buffer, + out error); + + private static unsafe bool TryCreateIndirectStorageBuffer( + WebGPUFlushContext flushContext, + nuint size, + out WgpuBuffer* buffer, + out string? error) + => TryCreateBuffer( + flushContext, + size, + BufferUsage.Storage | BufferUsage.Indirect | BufferUsage.CopyDst, + out buffer, + out error); + + private static unsafe bool TryCreateBuffer( + WebGPUFlushContext flushContext, + nuint size, + BufferUsage usage, + out WgpuBuffer* buffer, + out string? error) + { + if (size == 0) + { + size = sizeof(uint); + } + + BufferDescriptor descriptor = new() + { + Usage = usage, + Size = size + }; + + buffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); + if (buffer is null) + { + error = "Failed to create a staged-scene buffer."; + return false; + } + + flushContext.TrackBuffer(buffer); + error = null; + return true; + } + + private static unsafe bool TryCreateAndUploadStorageBuffer( + WebGPUFlushContext flushContext, + in T value, + out WgpuBuffer* buffer, + out string? error) + where T : unmanaged + { + if (!TryCreateStorageBuffer(flushContext, (nuint)sizeof(T), out buffer, out error)) + { + return false; + } + + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + buffer, + 0, + Unsafe.AsPointer(ref Unsafe.AsRef(in value)), + (nuint)sizeof(T)); + error = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nuint GetBindingByteLength(int count) + where T : unmanaged + => checked((nuint)Math.Max(count, 1) * (nuint)Unsafe.SizeOf()); + + private static bool TryValidateBufferSize(nuint byteLength, string bufferName, out string? error) + { + if (byteLength > MaxStorageBufferBindingSize) + { + error = $"The staged-scene {bufferName} buffer requires {byteLength} bytes, exceeding the current WebGPU binding limit of {MaxStorageBufferBindingSize} bytes."; + return false; + } + + error = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe bool WaitForMapSignal(Wgpu? extension, Device* device, ManualResetEventSlim signal) + { + if (extension is null) + { + return signal.Wait(5000); + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < 5000) + { + _ = extension.DevicePoll(device, true, (WrappedSubmissionIndex*)null); + } + + return signal.IsSet; + } +} + +/// +/// Flush-scoped recording of staged-scene compute dispatches. +/// This mirrors the upstream split between command recording and later execution, +/// without introducing another runtime abstraction layer. +/// +internal sealed unsafe class WebGPUSceneComputeRecording +{ + private readonly List commands = []; + + public WebGPUSceneComputeRecording(WebGPUSceneResourceRegistry resourceRegistry) + => this.ResourceRegistry = resourceRegistry; + + public WebGPUSceneResourceRegistry ResourceRegistry { get; } + + /// + /// Gets the recorded compute commands in submission order. + /// + public IReadOnlyList Commands => this.commands; + + /// + /// Records one direct compute dispatch. + /// + public bool TryRecord( + WebGPUSceneShaderId shaderId, + BindGroupEntry* entries, + uint entryCount, + uint groupCountX, + uint groupCountY, + uint groupCountZ, + out string? error) + { + this.commands.Add(new WebGPUSceneComputeCommand( + shaderId, + groupCountX, + groupCountY, + groupCountZ, + this.CopyResources(entries, entryCount), + null, + 0, + isIndirect: false)); + error = null; + return true; + } + + /// + /// Records one indirect compute dispatch. + /// + public bool TryRecordIndirect( + WebGPUSceneShaderId shaderId, + BindGroupEntry* entries, + uint entryCount, + WgpuBuffer* indirectBuffer, + ulong indirectOffset, + out string? error) + { + this.commands.Add(new WebGPUSceneComputeCommand( + shaderId, + 0, + 0, + 0, + this.CopyResources(entries, entryCount), + indirectBuffer, + indirectOffset, + isIndirect: true)); + error = null; + return true; + } + + private WebGPUSceneResourceProxy[] CopyResources(BindGroupEntry* entries, uint entryCount) + { + WebGPUSceneResourceProxy[] resources = new WebGPUSceneResourceProxy[entryCount]; + for (int i = 0; i < resources.Length; i++) + { + resources[i] = this.ResourceRegistry.CreateProxy(entries[i]); + } + + return resources; + } +} + +/// +/// One recorded staged-scene compute dispatch. +/// +internal readonly unsafe struct WebGPUSceneComputeCommand +{ + public WebGPUSceneComputeCommand( + WebGPUSceneShaderId shaderId, + uint groupCountX, + uint groupCountY, + uint groupCountZ, + WebGPUSceneResourceProxy[] resources, + WgpuBuffer* indirectBuffer, + ulong indirectOffset, + bool isIndirect) + { + this.ShaderId = shaderId; + this.Resources = resources; + this.GroupCountX = groupCountX; + this.GroupCountY = groupCountY; + this.GroupCountZ = groupCountZ; + this.IndirectBuffer = indirectBuffer; + this.IndirectOffset = indirectOffset; + this.IsIndirect = isIndirect; + } + + public WebGPUSceneShaderId ShaderId { get; } + + public WebGPUSceneResourceProxy[] Resources { get; } + + public uint GroupCountX { get; } + + public uint GroupCountY { get; } + + public uint GroupCountZ { get; } + + public WgpuBuffer* IndirectBuffer { get; } + + public ulong IndirectOffset { get; } + + public bool IsIndirect { get; } + + public BindGroupEntry[] ResolveEntries(WebGPUSceneResourceRegistry resourceRegistry) + { + BindGroupEntry[] entries = new BindGroupEntry[this.Resources.Length]; + for (int i = 0; i < entries.Length; i++) + { + entries[i] = resourceRegistry.Resolve(this.Resources[i]); + } + + return entries; + } +} + +internal enum WebGPUSceneShaderId +{ + PathtagReduce = 0, + PathtagReduce2 = 1, + PathtagScan1 = 2, + PathtagScan = 3, + PathtagScanSmall = 4, + BboxClear = 5, + Flatten = 6, + DrawReduce = 7, + DrawLeaf = 8, + ClipReduce = 9, + ClipLeaf = 10, + Binning = 11, + TileAlloc = 12, + Backdrop = 13, + PathCountSetup = 14, + PathCount = 15, + Coarse = 16, + PathTilingSetup = 17, + PathTiling = 18 +} + +internal readonly struct WebGPUSceneResourceProxy +{ + private WebGPUSceneResourceProxy( + uint binding, + nuint offset, + nuint size, + uint resourceId, + WebGPUSceneResourceProxyKind kind) + { + this.Binding = binding; + this.Offset = offset; + this.Size = size; + this.ResourceId = resourceId; + this.Kind = kind; + } + + public uint Binding { get; } + + public nuint Offset { get; } + + public nuint Size { get; } + + public uint ResourceId { get; } + + public WebGPUSceneResourceProxyKind Kind { get; } + + public static WebGPUSceneResourceProxy CreateBuffer(uint binding, uint resourceId, nuint offset, nuint size) + => new(binding, offset, size, resourceId, WebGPUSceneResourceProxyKind.Buffer); + + public static WebGPUSceneResourceProxy CreateTextureView(uint binding, uint resourceId) + => new(binding, 0, 0, resourceId, WebGPUSceneResourceProxyKind.TextureView); +} + +internal enum WebGPUSceneResourceProxyKind +{ + Buffer = 0, + TextureView = 1 +} + +internal sealed unsafe class WebGPUSceneResourceRegistry +{ + private uint nextResourceId = 1; + private readonly Dictionary bufferIds = []; + private readonly Dictionary textureViewIds = []; + private readonly Dictionary buffers = []; + private readonly Dictionary textureViews = []; + + private WebGPUSceneResourceRegistry() + { + } + + public static WebGPUSceneResourceRegistry Create(WebGPUSceneResourceSet resources) + { + WebGPUSceneResourceRegistry registry = new(); + registry.RegisterBuffer(resources.HeaderBuffer); + registry.RegisterBuffer(resources.SceneBuffer); + registry.RegisterBuffer(resources.PathReducedBuffer); + registry.RegisterBuffer(resources.PathReduced2Buffer); + registry.RegisterBuffer(resources.PathReducedScanBuffer); + registry.RegisterBuffer(resources.PathMonoidBuffer); + registry.RegisterBuffer(resources.PathBboxBuffer); + registry.RegisterBuffer(resources.DrawReducedBuffer); + registry.RegisterBuffer(resources.DrawMonoidBuffer); + registry.RegisterBuffer(resources.InfoBinDataBuffer); + registry.RegisterBuffer(resources.ClipInputBuffer); + registry.RegisterBuffer(resources.ClipElementBuffer); + registry.RegisterBuffer(resources.ClipBicBuffer); + registry.RegisterBuffer(resources.ClipBboxBuffer); + registry.RegisterBuffer(resources.DrawBboxBuffer); + registry.RegisterBuffer(resources.PathBuffer); + registry.RegisterBuffer(resources.LineBuffer); + registry.RegisterTextureView(resources.GradientTextureView); + registry.RegisterTextureView(resources.ImageAtlasTextureView); + return registry; + } + + public void RegisterSchedulingBuffers( + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* indirectCountBuffer, + WgpuBuffer* pathTileBuffer, + WgpuBuffer* segCountBuffer, + WgpuBuffer* segmentBuffer, + WgpuBuffer* blendBuffer, + WgpuBuffer* ptclBuffer, + WgpuBuffer* bumpBuffer) + { + this.RegisterBuffer(binHeaderBuffer); + this.RegisterBuffer(indirectCountBuffer); + this.RegisterBuffer(pathTileBuffer); + this.RegisterBuffer(segCountBuffer); + this.RegisterBuffer(segmentBuffer); + this.RegisterBuffer(blendBuffer); + this.RegisterBuffer(ptclBuffer); + this.RegisterBuffer(bumpBuffer); + } + + public WebGPUSceneResourceProxy CreateProxy(BindGroupEntry entry) + => entry.TextureView is not null + ? WebGPUSceneResourceProxy.CreateTextureView(entry.Binding, this.GetTextureViewId(entry.TextureView)) + : WebGPUSceneResourceProxy.CreateBuffer(entry.Binding, this.GetBufferId(entry.Buffer), checked((nuint)entry.Offset), checked((nuint)entry.Size)); + + public BindGroupEntry Resolve(WebGPUSceneResourceProxy proxy) + => proxy.Kind == WebGPUSceneResourceProxyKind.TextureView + ? new BindGroupEntry { Binding = proxy.Binding, TextureView = (TextureView*)this.textureViews[proxy.ResourceId] } + : new BindGroupEntry + { + Binding = proxy.Binding, + Buffer = (WgpuBuffer*)this.buffers[proxy.ResourceId], + Offset = proxy.Offset, + Size = proxy.Size + }; + + private void RegisterBuffer(WgpuBuffer* buffer) + { + if (buffer is null) + { + return; + } + + nint handle = (nint)buffer; + if (this.bufferIds.ContainsKey(handle)) + { + return; + } + + uint id = this.nextResourceId++; + this.bufferIds[handle] = id; + this.buffers[id] = (nint)buffer; + } + + private void RegisterTextureView(TextureView* textureView) + { + if (textureView is null) + { + return; + } + + nint handle = (nint)textureView; + if (this.textureViewIds.ContainsKey(handle)) + { + return; + } + + uint id = this.nextResourceId++; + this.textureViewIds[handle] = id; + this.textureViews[id] = (nint)textureView; + } + + private uint GetBufferId(WgpuBuffer* buffer) => this.bufferIds[(nint)buffer]; + + private uint GetTextureViewId(TextureView* textureView) => this.textureViewIds[(nint)textureView]; +} + +/// +/// One flush-scoped staged-scene instance produced during the WebGPU rasterizer replacement. +/// +internal readonly struct WebGPUStagedScene : IDisposable +{ + public WebGPUStagedScene( + WebGPUFlushContext flushContext, + WebGPUEncodedScene encodedScene, + WebGPUSceneConfig config, + WebGPUSceneResourceSet resources) + { + this.FlushContext = flushContext; + this.EncodedScene = encodedScene; + this.Config = config; + this.Resources = resources; + } + + public WebGPUFlushContext FlushContext { get; } + + public WebGPUEncodedScene EncodedScene { get; } + + public WebGPUSceneConfig Config { get; } + + public WebGPUSceneResourceSet Resources { get; } + + public void Dispose() + { + this.EncodedScene.Dispose(); + this.FlushContext.Dispose(); + } +} + +/// +/// Flush-scoped GPU buffers produced by the early scheduling stages. +/// +internal readonly unsafe struct WebGPUSceneSchedulingResources +{ + public WebGPUSceneSchedulingResources( + WgpuBuffer* binHeaderBuffer, + WgpuBuffer* indirectCountBuffer, + WgpuBuffer* pathTileBuffer, + WgpuBuffer* segCountBuffer, + WgpuBuffer* segmentBuffer, + WgpuBuffer* blendBuffer, + WgpuBuffer* ptclBuffer, + WgpuBuffer* bumpBuffer) + { + this.BinHeaderBuffer = binHeaderBuffer; + this.IndirectCountBuffer = indirectCountBuffer; + this.PathTileBuffer = pathTileBuffer; + this.SegCountBuffer = segCountBuffer; + this.SegmentBuffer = segmentBuffer; + this.BlendBuffer = blendBuffer; + this.PtclBuffer = ptclBuffer; + this.BumpBuffer = bumpBuffer; + } + + public WgpuBuffer* BinHeaderBuffer { get; } + + public WgpuBuffer* IndirectCountBuffer { get; } + + public WgpuBuffer* PathTileBuffer { get; } + + public WgpuBuffer* SegCountBuffer { get; } + + public WgpuBuffer* SegmentBuffer { get; } + + public WgpuBuffer* BlendBuffer { get; } + + public WgpuBuffer* PtclBuffer { get; } + + public WgpuBuffer* BumpBuffer { get; } +} + +#pragma warning restore SA1201 diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs new file mode 100644 index 000000000..2288e01e1 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs @@ -0,0 +1,1243 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1201 // Phase-1 scene-model types are grouped together in one file for now. + +using System.Buffers; +using System.Diagnostics; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Builds the flush-scoped scene payload consumed by the staged WebGPU rasterizer. +/// +internal static class WebGPUSceneEncoder +{ + private const int GradientWidth = 512; + private const int TileWidth = 16; + private const int TileHeight = 16; + private const int FixedShift = 8; + private const int FixedOne = 1 << FixedShift; + private const byte PathTagLineToF32 = 0x09; + private const byte PathTagTransform = 0x20; + private const byte PathTagPath = 0x10; + private const byte PathTagStyle = 0x40; + private const byte PathTagSubpathEnd = 0x04; + private const int StyleBlendModeShift = 1; + private const uint StyleBlendModeMask = 0x1FFFU << StyleBlendModeShift; + private const int StyleBlendAlphaShift = 14; + private const uint StyleBlendAlphaMask = 0xFFFFU << StyleBlendAlphaShift; + private const uint StyleFlagsFill = 0x40000000U; + private static readonly GraphicsOptions DefaultClipGraphicsOptions = new(); + + /// + /// Encodes prepared composition commands into flush-scoped scene buffers. + /// + public static WebGPUEncodedScene Encode( + IReadOnlyList commands, + in Rectangle targetBounds, + MemoryAllocator allocator) + { + if (commands.Count == 0) + { + return WebGPUEncodedScene.Empty; + } + + SupportedSubsetSceneEncoding encoding = SupportedSubsetSceneEncoding.Create(commands, targetBounds, allocator); + try + { + if (encoding.IsEmpty) + { + return WebGPUEncodedScene.Empty; + } + + return SupportedSubsetSceneResolver.Resolve(ref encoding, targetBounds, allocator); + } + finally + { + encoding.Dispose(); + } + } + + public static bool TryValidateBrushSupport(IReadOnlyList commands, out string? error) + { + for (int i = 0; i < commands.Count; i++) + { + CompositionCommand command = commands[i]; + if (command.Kind is not CompositionCommandKind.FillLayer || !command.IsVisible) + { + continue; + } + + if (!IsSupportedBrush(command.Brush)) + { + error = $"The staged WebGPU scene pipeline does not support brush type '{command.Brush.GetType().Name}'."; + return false; + } + } + + error = null; + return true; + } + + private ref struct SupportedSubsetSceneEncoding + { + private bool hasLastStyle; + private bool gradientPixelsDetached; + private uint lastStyle0; + private uint lastStyle1; + private readonly Rectangle rootTargetBounds; + private List? openLayerBounds; + + private SupportedSubsetSceneEncoding(MemoryAllocator allocator, int commandCount, in Rectangle rootTargetBounds) + { + this.PathTags = new OwnedStream(allocator, Math.Max(commandCount * 8, 256)); + this.PathData = new OwnedStream(allocator, Math.Max(commandCount * 16, 256)); + this.DrawTags = new OwnedStream(allocator, Math.Max(commandCount, 16)); + this.DrawData = new OwnedStream(allocator, Math.Max(commandCount, 16)); + this.Transforms = new OwnedStream(allocator, 8); + this.Styles = new OwnedStream(allocator, Math.Max(commandCount * 2, 16)); + this.GradientPixels = new OwnedStream(allocator, Math.Max(commandCount * GradientWidth, GradientWidth)); + this.Images = []; + this.FillCount = 0; + this.PathCount = 0; + this.LineCount = 0; + this.InfoWordCount = 0; + this.ClipCount = 0; + this.TotalTileMembershipCount = 0; + this.GradientRowCount = 0; + this.hasLastStyle = false; + this.gradientPixelsDetached = false; + this.lastStyle0 = 0; + this.lastStyle1 = 0; + this.rootTargetBounds = rootTargetBounds; + this.openLayerBounds = null; + + this.PathTags.Add(PathTagTransform); + AppendIdentityTransform(ref this.Transforms); + } + + public OwnedStream PathTags; + + public OwnedStream PathData; + + public OwnedStream DrawTags; + + public OwnedStream DrawData; + + public OwnedStream Transforms; + + public OwnedStream Styles; + + public OwnedStream GradientPixels; + + public List Images; + + public int FillCount { get; private set; } + + public int PathCount { get; private set; } + + public int LineCount { get; private set; } + + public int InfoWordCount { get; private set; } + + public int ClipCount { get; private set; } + + public int TotalTileMembershipCount { get; private set; } + + public int GradientRowCount { get; private set; } + + public readonly bool IsEmpty => this.FillCount == 0; + + public static SupportedSubsetSceneEncoding Create( + IReadOnlyList commands, + in Rectangle targetBounds, + MemoryAllocator allocator) + { + SupportedSubsetSceneEncoding encoding = new(allocator, commands.Count, targetBounds); + encoding.Build(commands); + return encoding; + } + + public void Dispose() + { + if (!this.gradientPixelsDetached) + { + this.GradientPixels.Dispose(); + } + + this.Styles.Dispose(); + this.Transforms.Dispose(); + this.DrawData.Dispose(); + this.DrawTags.Dispose(); + this.PathData.Dispose(); + this.PathTags.Dispose(); + } + + public void MarkGradientPixelsDetached() => this.gradientPixelsDetached = true; + + private void Build(IReadOnlyList commands) + { + for (int i = 0; i < commands.Count; i++) + { + this.Append(commands[i]); + } + } + + private void Append(in CompositionCommand command) + { + switch (command.Kind) + { + case CompositionCommandKind.FillLayer: + if (!command.IsVisible) + { + return; + } + + IPath preparedPath = command.PreparedPath!; + this.AppendPlainFill(command, preparedPath); + return; + + case CompositionCommandKind.BeginLayer: + this.AppendBeginLayer(command); + return; + + case CompositionCommandKind.EndLayer: + this.AppendEndLayer(); + return; + + default: + return; + } + } + + private void AppendPlainFill(in CompositionCommand command, IPath preparedPath) + { + uint drawTag = GetDrawTag(command); + GpuSceneDrawMonoid drawTagMonoid = GpuSceneDrawTag.Map(drawTag); + (uint style0, uint style1) = GetFillStyle(command.GraphicsOptions, command.RasterizerOptions.IntersectionRule); + int pathTagCheckpoint = this.PathTags.Count; + int styleCheckpoint = this.Styles.Count; + + if (!this.hasLastStyle || style0 != this.lastStyle0 || style1 != this.lastStyle1) + { + this.PathTags.Add(PathTagStyle); + this.Styles.Add(style0); + this.Styles.Add(style1); + } + + int encodedPathCount = EncodePath( + command, + preparedPath, + this.rootTargetBounds, + ref this.PathTags, + ref this.PathData, + out int geometryLineCount, + out _); + + if (encodedPathCount == 0) + { + this.PathTags.SetCount(pathTagCheckpoint); + this.Styles.SetCount(styleCheckpoint); + return; + } + + this.hasLastStyle = true; + this.lastStyle0 = style0; + this.lastStyle1 = style1; + this.FillCount++; + this.PathCount += encodedPathCount; + this.LineCount += geometryLineCount; + this.InfoWordCount += (int)drawTagMonoid.InfoOffset; + this.DrawTags.Add(drawTag); + int gradientRowCount = this.GradientRowCount; + AppendDrawData( + command, + drawTag, + ref this.DrawData, + ref this.GradientPixels, + this.Images, + ref gradientRowCount); + this.GradientRowCount = gradientRowCount; + + this.TotalTileMembershipCount += CountTileMembership(GetTargetLocalDestination(command, this.rootTargetBounds)); + } + + private void AppendBeginLayer(in CompositionCommand command) + { + Rectangle layerBounds = ToTargetLocal(command.LayerBounds, this.rootTargetBounds); + (uint style0, uint style1) = GetFillStyle(DefaultClipGraphicsOptions, IntersectionRule.NonZero); + int pathTagCheckpoint = this.PathTags.Count; + int styleCheckpoint = this.Styles.Count; + + if (!this.hasLastStyle || style0 != this.lastStyle0 || style1 != this.lastStyle1) + { + this.PathTags.Add(PathTagStyle); + this.Styles.Add(style0); + this.Styles.Add(style1); + } + + if (EncodeRectanglePath(layerBounds, ref this.PathTags, ref this.PathData, out int clipLineCount) == 0) + { + this.PathTags.SetCount(pathTagCheckpoint); + this.Styles.SetCount(styleCheckpoint); + return; + } + + this.hasLastStyle = true; + this.lastStyle0 = style0; + this.lastStyle1 = style1; + this.PathCount++; + this.LineCount += clipLineCount; + this.InfoWordCount += (int)GpuSceneDrawTag.Map(GpuSceneDrawTag.BeginClip).InfoOffset; + this.DrawTags.Add(GpuSceneDrawTag.BeginClip); + AppendBeginClipData(command.GraphicsOptions, ref this.DrawData); + this.ClipCount++; + this.TotalTileMembershipCount += CountTileMembership(layerBounds); + this.openLayerBounds ??= new List(4); + this.openLayerBounds.Add(layerBounds); + } + + private void AppendEndLayer() + { + if (this.openLayerBounds is not { Count: > 0 }) + { + return; + } + + int lastIndex = this.openLayerBounds.Count - 1; + Rectangle layerBounds = this.openLayerBounds[lastIndex]; + this.openLayerBounds.RemoveAt(lastIndex); + + this.DrawTags.Add(GpuSceneDrawTag.EndClip); + this.PathTags.Add(PathTagPath); + this.PathCount++; + this.ClipCount++; + this.TotalTileMembershipCount += CountTileMembership(layerBounds); + } + } + + private static class SupportedSubsetSceneResolver + { + public static WebGPUEncodedScene Resolve( + ref SupportedSubsetSceneEncoding encoding, + in Rectangle targetBounds, + MemoryAllocator allocator) + { + int pathTagByteCount = encoding.PathTags.Count; + int pathTagWordCount = AlignUp(DivideRoundUp(pathTagByteCount, 4), 256); + int pathDataWordCount = encoding.PathData.Count; + int drawTagCount = encoding.DrawTags.Count; + int drawDataWordCount = encoding.DrawData.Count; + int transformWordCount = encoding.Transforms.Count; + int styleWordCount = encoding.Styles.Count; + int drawTagBase = pathTagWordCount + pathDataWordCount; + int drawDataBase = drawTagBase + drawTagCount; + int transformBase = drawDataBase + drawDataWordCount; + int styleBase = transformBase + transformWordCount; + int sceneWordCount = styleBase + styleWordCount; + GpuSceneLayout layout = new( + (uint)drawTagCount, + (uint)encoding.PathCount, + (uint)encoding.ClipCount, + (uint)encoding.InfoWordCount, + 0U, + (uint)pathTagWordCount, + (uint)drawTagBase, + (uint)drawDataBase, + (uint)transformBase, + (uint)styleBase); + + IMemoryOwner sceneDataOwner = allocator.Allocate(sceneWordCount); + try + { + PackSceneData( + layout, + pathTagWordCount, + encoding.PathTags.WrittenSpan, + encoding.PathData.WrittenSpan, + encoding.DrawTags.WrittenSpan, + encoding.DrawData.WrittenSpan, + encoding.Transforms.WrittenSpan, + encoding.Styles.WrittenSpan, + sceneDataOwner.Memory.Span[..sceneWordCount]); + + return new WebGPUEncodedScene( + targetBounds.Size, + encoding.InfoWordCount, + sceneDataOwner, + sceneWordCount, + DetachGradientPixels(ref encoding), + encoding.Images, + encoding.GradientRowCount, + layout, + encoding.FillCount, + encoding.PathCount, + encoding.LineCount, + pathTagByteCount, + pathTagWordCount, + pathDataWordCount, + drawTagCount, + drawDataWordCount, + transformWordCount, + styleWordCount, + encoding.ClipCount, + encoding.FillCount, + 0, + encoding.TotalTileMembershipCount, + 0, + DivideRoundUp(targetBounds.Width, TileWidth), + DivideRoundUp(targetBounds.Height, TileHeight)); + } + catch + { + sceneDataOwner.Dispose(); + throw; + } + } + + private static IMemoryOwner? DetachGradientPixels(ref SupportedSubsetSceneEncoding encoding) + { + if (encoding.GradientRowCount == 0) + { + return null; + } + + encoding.MarkGradientPixelsDetached(); + return encoding.GradientPixels.DetachOwner(); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsSupportedBrush(Brush brush) + => brush is SolidBrush + or RecolorBrush + or LinearGradientBrush + or RadialGradientBrush + or EllipticGradientBrush + or SweepGradientBrush + or PatternBrush + or ImageBrush; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetDrawTag(in CompositionCommand command) + => command.Brush switch + { + SolidBrush => GpuSceneDrawTag.FillColor, + RecolorBrush => GpuSceneDrawTag.FillRecolor, + LinearGradientBrush => GpuSceneDrawTag.FillLinGradient, + RadialGradientBrush => GpuSceneDrawTag.FillRadGradient, + EllipticGradientBrush => GpuSceneDrawTag.FillEllipticGradient, + SweepGradientBrush => GpuSceneDrawTag.FillSweepGradient, + PatternBrush => GpuSceneDrawTag.FillImage, + ImageBrush => GpuSceneDrawTag.FillImage, + _ => throw new UnreachableException($"Unsupported brush type '{command.Brush.GetType().Name}' should have been rejected before scene encoding.") + }; + + private static int EncodePath( + in CompositionCommand command, + IPath preparedPath, + in Rectangle rootTargetBounds, + ref OwnedStream pathTags, + ref OwnedStream pathData, + out int lineCount, + out int pathSegmentCount) + { + Rectangle interest = command.RasterizerOptions.Interest; + float samplingOffset = command.RasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + float targetOffsetX = command.TargetBounds.X - rootTargetBounds.X; + float targetOffsetY = command.TargetBounds.Y - rootTargetBounds.Y; + float translateX = targetOffsetX + command.DestinationRegion.X - command.SourceOffset.X; + float translateY = targetOffsetY + command.DestinationRegion.Y - command.SourceOffset.Y; + + // Move path points from interest-local coverage space into target-local space. + float pointTranslateX = translateX + samplingOffset - interest.Left; + float pointTranslateY = translateY + samplingOffset - interest.Top; + pathSegmentCount = 0; + lineCount = 0; + + foreach (ISimplePath subPath in preparedPath.Flatten()) + { + ReadOnlySpan points = subPath.Points.Span; + if (points.Length < 2) + { + continue; + } + + int segmentCount = subPath.IsClosed ? points.Length : points.Length - 1; + if (segmentCount == 0) + { + continue; + } + + // Emit moveto for the first point of each subpath. + PointF startPoint = points[0]; + float firstX = startPoint.X + pointTranslateX; + float firstY = startPoint.Y + pointTranslateY; + pathData.Add(BitcastSingle(firstX)); + pathData.Add(BitcastSingle(firstY)); + float currentX = firstX; + float currentY = firstY; + + for (int j = 0; j < segmentCount; j++) + { + PointF p0 = points[j]; + PointF p1 = points[(j + 1) % points.Length]; + if (PointsEqual(p0, p1)) + { + continue; + } + + float translatedEndX = p1.X + pointTranslateX; + float translatedEndY = p1.Y + pointTranslateY; + pathData.Add(BitcastSingle(translatedEndX)); + pathData.Add(BitcastSingle(translatedEndY)); + pathTags.Add(PathTagLineToF32); + pathSegmentCount++; + currentX = translatedEndX; + currentY = translatedEndY; + + float y0 = ((p0.Y - interest.Top) + samplingOffset) + translateY; + float y1 = ((p1.Y - interest.Top) + samplingOffset) + translateY; + int fy0 = (int)MathF.Round(y0 * FixedOne); + int fy1 = (int)MathF.Round(y1 * FixedOne); + if (fy0 != fy1) + { + lineCount++; + } + } + + // Close the subpath. + if (!PointsEqual(firstX, firstY, currentX, currentY)) + { + pathData.Add(BitcastSingle(firstX)); + pathData.Add(BitcastSingle(firstY)); + pathTags.Add(PathTagLineToF32); + pathSegmentCount++; + } + + if (pathTags.Count > 0) + { + pathTags[^1] |= PathTagSubpathEnd; + } + } + + if (pathSegmentCount == 0) + { + return 0; + } + + pathTags.Add(PathTagPath); + return 1; + } + + private static int EncodeRectanglePath( + in Rectangle rectangle, + ref OwnedStream pathTags, + ref OwnedStream pathData, + out int lineCount) + { + lineCount = 0; + if (rectangle.Width <= 0 || rectangle.Height <= 0) + { + return 0; + } + + float left = rectangle.Left; + float top = rectangle.Top; + float right = rectangle.Right; + float bottom = rectangle.Bottom; + + pathData.Add(BitcastSingle(left)); + pathData.Add(BitcastSingle(top)); + + pathData.Add(BitcastSingle(right)); + pathData.Add(BitcastSingle(top)); + pathTags.Add(PathTagLineToF32); + + pathData.Add(BitcastSingle(right)); + pathData.Add(BitcastSingle(bottom)); + pathTags.Add(PathTagLineToF32); + + pathData.Add(BitcastSingle(left)); + pathData.Add(BitcastSingle(bottom)); + pathTags.Add(PathTagLineToF32); + + pathData.Add(BitcastSingle(left)); + pathData.Add(BitcastSingle(top)); + pathTags.Add(PathTagLineToF32 | PathTagSubpathEnd); + + pathTags.Add(PathTagPath); + lineCount = 2; + return 1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendTransform(GpuSceneTransform transform, ref OwnedStream transforms) + { + transforms.Add(BitcastSingle(transform.M11)); + transforms.Add(BitcastSingle(transform.M12)); + transforms.Add(BitcastSingle(transform.M21)); + transforms.Add(BitcastSingle(transform.M22)); + transforms.Add(BitcastSingle(transform.Tx)); + transforms.Add(BitcastSingle(transform.Ty)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendIdentityTransform(ref OwnedStream transforms) + => AppendTransform(GpuSceneTransform.Identity, ref transforms); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (uint Style0, uint Style1) GetFillStyle(GraphicsOptions options, IntersectionRule intersectionRule) + { + uint style0 = intersectionRule == IntersectionRule.EvenOdd ? StyleFlagsFill : 0U; + uint packedBlendMode = PackBlendMode(options); + uint packedBlendAlpha = PackBlendAlpha(options.BlendPercentage); + style0 |= (packedBlendMode << StyleBlendModeShift) & StyleBlendModeMask; + style0 |= (packedBlendAlpha << StyleBlendAlphaShift) & StyleBlendAlphaMask; + return (style0, 0U); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint PackBlendAlpha(float blendPercentage) + => (uint)Math.Clamp((int)MathF.Round(Math.Clamp(blendPercentage, 0F, 1F) * 65535F), 0, 65535); + + private static void PackSceneData( + GpuSceneLayout layout, + int pathTagWordCount, + ReadOnlySpan pathTags, + ReadOnlySpan pathData, + ReadOnlySpan drawTags, + ReadOnlySpan drawData, + ReadOnlySpan transforms, + ReadOnlySpan styles, + Span sceneWords) + { + Span sceneBytes = MemoryMarshal.Cast(sceneWords); + int paddedPathTagBytes = checked(pathTagWordCount * sizeof(uint)); + sceneBytes[..paddedPathTagBytes].Clear(); + pathTags.CopyTo(sceneBytes); + pathData.CopyTo(sceneWords[(int)layout.PathDataBase..]); + drawTags.CopyTo(sceneWords[(int)layout.DrawTagBase..]); + drawData.CopyTo(sceneWords[(int)layout.DrawDataBase..]); + transforms.CopyTo(sceneWords[(int)layout.TransformBase..]); + styles.CopyTo(sceneWords[(int)layout.StyleBase..]); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool PointsEqual(PointF a, PointF b) + => a.X == b.X && a.Y == b.Y; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool PointsEqual(float ax, float ay, float bx, float by) + => ax == bx && ay == by; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint BitcastSingle(float value) + => unchecked((uint)BitConverter.SingleToInt32Bits(value)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DivideRoundUp(int value, int divisor) + => (value + divisor - 1) / divisor; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int AlignUp(int value, int alignment) + => value + ((alignment - (value % alignment)) % alignment); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CountTileMembership(in Rectangle destinationRegion) + { + int tileMinX = Math.Max(0, destinationRegion.Left / TileWidth); + int tileMinY = Math.Max(0, destinationRegion.Top / TileHeight); + int tileMaxX = Math.Max(tileMinX + 1, DivideRoundUp(destinationRegion.Right, TileWidth)); + int tileMaxY = Math.Max(tileMinY + 1, DivideRoundUp(destinationRegion.Bottom, TileHeight)); + return (tileMaxX - tileMinX) * (tileMaxY - tileMinY); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Rectangle ToTargetLocal(in Rectangle absoluteBounds, in Rectangle rootTargetBounds) + => new( + absoluteBounds.X - rootTargetBounds.X, + absoluteBounds.Y - rootTargetBounds.Y, + absoluteBounds.Width, + absoluteBounds.Height); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Rectangle GetTargetLocalDestination(in CompositionCommand command, in Rectangle rootTargetBounds) + => new( + (command.TargetBounds.X - rootTargetBounds.X) + command.DestinationRegion.X, + (command.TargetBounds.Y - rootTargetBounds.Y) + command.DestinationRegion.Y, + command.DestinationRegion.Width, + command.DestinationRegion.Height); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint PackSolidColor(SolidBrush solidBrush) + { + Vector4 color = solidBrush.Color.ToScaledVector4(); + color.X *= color.W; + color.Y *= color.W; + color.Z *= color.W; + return Rgba32.FromScaledVector4(color).Rgba; + } + + private static void AppendDrawData( + in CompositionCommand command, + uint drawTag, + ref OwnedStream drawData, + ref OwnedStream gradientPixels, + List images, + ref int gradientRowCount) + { + Brush brush = command.Brush; + switch (drawTag) + { + case GpuSceneDrawTag.BeginClip: + AppendBeginClipData(command.GraphicsOptions, ref drawData); + break; + case GpuSceneDrawTag.FillColor: + drawData.Add(PackSolidColor((SolidBrush)brush)); + break; + case GpuSceneDrawTag.FillRecolor: + AppendRecolorData((RecolorBrush)brush, ref drawData); + break; + case GpuSceneDrawTag.FillLinGradient: + AppendLinearGradientData((LinearGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); + break; + case GpuSceneDrawTag.FillRadGradient: + AppendRadialGradientData((RadialGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); + break; + case GpuSceneDrawTag.FillEllipticGradient: + AppendEllipticGradientData((EllipticGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); + break; + case GpuSceneDrawTag.FillSweepGradient: + AppendSweepGradientData((SweepGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); + break; + case GpuSceneDrawTag.FillImage: + AppendImageData(command, ref drawData, images); + break; + default: + throw new UnreachableException($"Unsupported draw tag '{drawTag}' reached scene draw-data encoding."); + } + } + + private static void AppendBeginClipData(GraphicsOptions options, ref OwnedStream drawData) + { + drawData.Add(PackBlendMode(options)); + drawData.Add(BitcastSingle(Math.Clamp(options.BlendPercentage, 0F, 1F))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint PackBlendMode(GraphicsOptions options) + => (MapColorBlendMode(options.ColorBlendingMode) << 8) | MapAlphaCompositionMode(options.AlphaCompositionMode); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MapColorBlendMode(PixelColorBlendingMode mode) + => mode switch + { + PixelColorBlendingMode.Normal => 0U, + PixelColorBlendingMode.Multiply => 1U, + PixelColorBlendingMode.Add => 16U, + PixelColorBlendingMode.Subtract => 17U, + PixelColorBlendingMode.Screen => 2U, + PixelColorBlendingMode.Darken => 4U, + PixelColorBlendingMode.Lighten => 5U, + PixelColorBlendingMode.Overlay => 3U, + PixelColorBlendingMode.HardLight => 8U, + _ => throw new UnreachableException($"Unsupported color blending mode '{mode}'.") + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MapAlphaCompositionMode(PixelAlphaCompositionMode mode) + => mode switch + { + PixelAlphaCompositionMode.SrcOver => 3U, + PixelAlphaCompositionMode.Src => 1U, + PixelAlphaCompositionMode.SrcAtop => 9U, + PixelAlphaCompositionMode.SrcIn => 5U, + PixelAlphaCompositionMode.SrcOut => 7U, + PixelAlphaCompositionMode.Dest => 2U, + PixelAlphaCompositionMode.DestAtop => 10U, + PixelAlphaCompositionMode.DestOver => 4U, + PixelAlphaCompositionMode.DestIn => 6U, + PixelAlphaCompositionMode.DestOut => 8U, + PixelAlphaCompositionMode.Clear => 0U, + PixelAlphaCompositionMode.Xor => 11U, + _ => throw new UnreachableException($"Unsupported alpha composition mode '{mode}'.") + }; + + private static void AppendRecolorData(RecolorBrush brush, ref OwnedStream drawData) + { + drawData.Add(PackPremultipliedColor(brush.SourceColor)); + drawData.Add(PackPremultipliedColor(brush.TargetColor)); + drawData.Add(BitcastSingle(brush.Threshold * 4F)); + } + + private static void AppendImageData( + in CompositionCommand command, + ref OwnedStream drawData, + List images) + { + Brush brush = command.Brush; + + // The image payload words are patched once the atlas is built because the + // uploader is the first place that knows the concrete TPixel texture format. + int payloadWordOffset = drawData.Count; + drawData.Add(0); + drawData.Add(0); + drawData.Add(0); + if (brush is ImageBrush imageBrush) + { + drawData.Add(BitcastSingle(command.BrushBounds.Left + imageBrush.Offset.X)); + drawData.Add(BitcastSingle(command.BrushBounds.Top + imageBrush.Offset.Y)); + } + else + { + drawData.Add(0); + drawData.Add(0); + } + + images.Add(new GpuImageDescriptor(brush, payloadWordOffset)); + } + + private static void AppendLinearGradientData( + LinearGradientBrush brush, + ref OwnedStream drawData, + ref OwnedStream gradientPixels, + ref int gradientRowCount) + { + uint indexMode = ((uint)gradientRowCount << 2) | MapExtendMode(brush.RepetitionMode); + AppendGradientRamp(brush.ColorStops, ref gradientPixels); + gradientRowCount++; + + drawData.Add(indexMode); + drawData.Add(BitcastSingle(brush.StartPoint.X)); + drawData.Add(BitcastSingle(brush.StartPoint.Y)); + drawData.Add(BitcastSingle(brush.EndPoint.X)); + drawData.Add(BitcastSingle(brush.EndPoint.Y)); + } + + private static void AppendRadialGradientData( + RadialGradientBrush brush, + ref OwnedStream drawData, + ref OwnedStream gradientPixels, + ref int gradientRowCount) + { + uint indexMode = ((uint)gradientRowCount << 2) | MapExtendMode(brush.RepetitionMode); + AppendGradientRamp(brush.ColorStops, ref gradientPixels); + gradientRowCount++; + + PointF center0; + float radius0; + PointF center1; + float radius1; + + if (brush.IsTwoCircle) + { + center0 = brush.Center0; + radius0 = brush.Radius0; + center1 = brush.Center1!.Value; + radius1 = brush.Radius1!.Value; + } + else + { + center0 = brush.Center0; + radius0 = 0F; + center1 = brush.Center0; + radius1 = brush.Radius0; + } + + drawData.Add(indexMode); + drawData.Add(BitcastSingle(center0.X)); + drawData.Add(BitcastSingle(center0.Y)); + drawData.Add(BitcastSingle(center1.X)); + drawData.Add(BitcastSingle(center1.Y)); + drawData.Add(BitcastSingle(radius0)); + drawData.Add(BitcastSingle(radius1)); + } + + private static void AppendEllipticGradientData( + EllipticGradientBrush brush, + ref OwnedStream drawData, + ref OwnedStream gradientPixels, + ref int gradientRowCount) + { + uint indexMode = ((uint)gradientRowCount << 2) | MapExtendMode(brush.RepetitionMode); + AppendGradientRamp(brush.ColorStops, ref gradientPixels); + gradientRowCount++; + + drawData.Add(indexMode); + drawData.Add(BitcastSingle(brush.Center.X)); + drawData.Add(BitcastSingle(brush.Center.Y)); + drawData.Add(BitcastSingle(brush.ReferenceAxisEnd.X)); + drawData.Add(BitcastSingle(brush.ReferenceAxisEnd.Y)); + drawData.Add(BitcastSingle(brush.AxisRatio)); + } + + private static void AppendSweepGradientData( + SweepGradientBrush brush, + ref OwnedStream drawData, + ref OwnedStream gradientPixels, + ref int gradientRowCount) + { + uint indexMode = ((uint)gradientRowCount << 2) | MapExtendMode(brush.RepetitionMode); + AppendGradientRamp(brush.ColorStops, ref gradientPixels); + gradientRowCount++; + + float sweepDegrees = brush.EndAngleDegrees - brush.StartAngleDegrees; + if (MathF.Abs(sweepDegrees) < 1e-6F) + { + sweepDegrees = 360F; + } + + float t0 = brush.StartAngleDegrees / 360F; + float t1 = t0 + (sweepDegrees / 360F); + + drawData.Add(indexMode); + drawData.Add(BitcastSingle(brush.Center.X)); + drawData.Add(BitcastSingle(brush.Center.Y)); + drawData.Add(BitcastSingle(t0)); + drawData.Add(BitcastSingle(t1)); + } + + private static void AppendGradientRamp(ReadOnlySpan colorStops, ref OwnedStream gradientPixels) + { + for (int x = 0; x < GradientWidth; x++) + { + float t = x / (float)(GradientWidth - 1); + gradientPixels.Add(EvaluateGradientColor(colorStops, t)); + } + } + + private static uint EvaluateGradientColor(ReadOnlySpan colorStops, float t) + { + ColorStop from = colorStops[0]; + ColorStop to = colorStops[0]; + for (int i = 0; i < colorStops.Length; i++) + { + to = colorStops[i]; + if (to.Ratio > t) + { + break; + } + + from = to; + } + + if (from.Color.Equals(to.Color) || to.Ratio == from.Ratio) + { + return PackPremultipliedColor(from.Color); + } + + float localT = (t - from.Ratio) / (to.Ratio - from.Ratio); + Vector4 color = Vector4.Lerp(from.Color.ToScaledVector4(), to.Color.ToScaledVector4(), localT); + return PackPremultipliedColor(Color.FromScaledVector(color)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint PackPremultipliedColor(Color color) + { + Vector4 scaled = color.ToScaledVector4(); + scaled.X *= scaled.W; + scaled.Y *= scaled.W; + scaled.Z *= scaled.W; + return Rgba32.FromScaledVector4(scaled).Rgba; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MapExtendMode(GradientRepetitionMode repetitionMode) + => repetitionMode switch + { + GradientRepetitionMode.Repeat => 1U, + GradientRepetitionMode.Reflect => 2U, + _ => 0U + }; +} + +/// +/// Flush-scoped encoded scene payload. +/// +internal sealed class WebGPUEncodedScene : IDisposable +{ + public static WebGPUEncodedScene Empty { get; } = new( + Size.Empty, + 0, + null, + 0, + null, + [], + 0, + default, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0); + + private readonly IMemoryOwner? sceneDataOwner; + private readonly IMemoryOwner? gradientPixelsOwner; + private readonly List images; + private bool disposed; + + public WebGPUEncodedScene( + Size targetSize, + int infoWordCount, + IMemoryOwner? sceneDataOwner, + int sceneWordCount, + IMemoryOwner? gradientPixelsOwner, + List images, + int gradientRowCount, + GpuSceneLayout layout, + int fillCount, + int pathCount, + int lineCount, + int pathTagByteCount, + int pathTagWordCount, + int pathDataWordCount, + int drawTagCount, + int drawDataWordCount, + int transformWordCount, + int styleWordCount, + int clipCount, + int uniqueDefinitionCount, + int totalBinMembershipCount, + int totalTileMembershipCount, + int totalLineSliceCount, + int tileCountX, + int tileCountY) + { + this.TargetSize = targetSize; + this.InfoWordCount = infoWordCount; + this.sceneDataOwner = sceneDataOwner; + this.gradientPixelsOwner = gradientPixelsOwner; + this.images = images; + this.SceneWordCount = sceneWordCount; + this.GradientRowCount = gradientRowCount; + this.Layout = layout; + this.FillCount = fillCount; + this.PathCount = pathCount; + this.LineCount = lineCount; + this.PathTagByteCount = pathTagByteCount; + this.PathTagWordCount = pathTagWordCount; + this.PathDataWordCount = pathDataWordCount; + this.DrawTagCount = drawTagCount; + this.DrawDataWordCount = drawDataWordCount; + this.TransformWordCount = transformWordCount; + this.StyleWordCount = styleWordCount; + this.ClipCount = clipCount; + this.UniqueDefinitionCount = uniqueDefinitionCount; + this.TotalBinMembershipCount = totalBinMembershipCount; + this.TotalTileMembershipCount = totalTileMembershipCount; + this.TotalLineSliceCount = totalLineSliceCount; + this.TileCountX = tileCountX; + this.TileCountY = tileCountY; + } + + public Size TargetSize { get; } + + public ReadOnlyMemory SceneData + => this.sceneDataOwner is null ? ReadOnlyMemory.Empty : this.sceneDataOwner.Memory[..this.SceneWordCount]; + + public ReadOnlyMemory GradientPixels + => this.gradientPixelsOwner is null ? ReadOnlyMemory.Empty : this.gradientPixelsOwner.Memory[..(this.GradientRowCount * 512)]; + + public IReadOnlyList Images => this.images; + + public int FillCount { get; } + + public int InfoWordCount { get; } + + public int SceneWordCount { get; } + + public int GradientRowCount { get; } + + public int PathCount { get; } + + public int LineCount { get; } + + public int PathTagByteCount { get; } + + public int PathTagWordCount { get; } + + public int PathDataWordCount { get; } + + public int DrawTagCount { get; } + + public int DrawDataWordCount { get; } + + public int TransformWordCount { get; } + + public int StyleWordCount { get; } + + public int ClipCount { get; } + + public int UniqueDefinitionCount { get; } + + public int TotalBinMembershipCount { get; } + + public int TotalTileMembershipCount { get; } + + public int TotalLineSliceCount { get; } + + public int TileCountX { get; } + + public int TileCountY { get; } + + public int TileCount => this.TileCountX * this.TileCountY; + + public GpuSceneLayout Layout { get; } + + public void SetSceneWord(int index, uint value) + { + if (this.sceneDataOwner is null) + { + throw new InvalidOperationException("The scene buffer is not available."); + } + + this.sceneDataOwner.Memory.Span[index] = value; + } + + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.disposed = true; + this.sceneDataOwner?.Dispose(); + this.gradientPixelsOwner?.Dispose(); + } +} + +internal readonly struct GpuImageDescriptor +{ + public GpuImageDescriptor(Brush brush, int drawDataWordOffset) + { + this.Brush = brush; + this.DrawDataWordOffset = drawDataWordOffset; + } + + public Brush Brush { get; } + + public int DrawDataWordOffset { get; } +} + +internal readonly struct GpuSceneTransform : IEquatable +{ + public static readonly GpuSceneTransform Identity = new(1F, 0F, 0F, 1F, 0F, 0F); + + public GpuSceneTransform(float m11, float m12, float m21, float m22, float tx, float ty) + { + this.M11 = m11; + this.M12 = m12; + this.M21 = m21; + this.M22 = m22; + this.Tx = tx; + this.Ty = ty; + } + + public float M11 { get; } + + public float M12 { get; } + + public float M21 { get; } + + public float M22 { get; } + + public float Tx { get; } + + public float Ty { get; } + + public Vector2 Translation => new(this.Tx, this.Ty); + + public bool Equals(GpuSceneTransform other) + => this.M11 == other.M11 + && this.M12 == other.M12 + && this.M21 == other.M21 + && this.M22 == other.M22 + && this.Tx == other.Tx + && this.Ty == other.Ty; + + public override bool Equals(object? obj) => obj is GpuSceneTransform other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine(this.M11, this.M12, this.M21, this.M22, this.Tx, this.Ty); +} + +internal ref struct OwnedStream + where T : unmanaged +{ + private readonly MemoryAllocator allocator; + private IMemoryOwner owner; + private Span span; + + public OwnedStream(MemoryAllocator allocator, int initialCapacity) + { + this.allocator = allocator; + this.owner = allocator.Allocate(Math.Max(initialCapacity, 16)); + this.span = this.owner.Memory.Span; + this.Count = 0; + } + + public int Count { get; private set; } + + public ref T this[int index] => ref this.span[index]; + + public readonly ReadOnlySpan WrittenSpan => this.span[..this.Count]; + + public void Add(T value) + { + this.EnsureCapacity(this.Count + 1); + this.span[this.Count++] = value; + } + + public void SetCount(int count) => this.Count = count; + + public void Dispose() + { + this.owner.Dispose(); + this.span = default; + this.Count = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IMemoryOwner DetachOwner() + { + IMemoryOwner detached = this.owner; + this.span = default; + this.Count = 0; + return detached; + } + + private void EnsureCapacity(int requiredCapacity) + { + if (requiredCapacity <= this.span.Length) + { + return; + } + + IMemoryOwner next = this.allocator.Allocate(Math.Max(requiredCapacity, this.span.Length * 2)); + this.span[..this.Count].CopyTo(next.Memory.Span); + this.owner.Dispose(); + this.owner = next; + this.span = next.Memory.Span; + } +} + +#pragma warning restore SA1201 diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs new file mode 100644 index 000000000..995c16075 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs @@ -0,0 +1,1191 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1201 // Phase-1 staged scene types are grouped by pipeline role. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.PixelFormats; +using WgpuBuffer = Silk.NET.WebGPU.Buffer; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Uploads one encoded scene into flush-scoped GPU resources for the staged WebGPU rasterizer. +/// +internal static unsafe class WebGPUSceneResources +{ + /// + /// Creates the flush-scoped GPU resources required by the staged scene pipeline. + /// + public static bool TryCreate( + WebGPUFlushContext flushContext, + WebGPUEncodedScene scene, + WebGPUSceneConfig config, + uint baseColor, + out WebGPUSceneResourceSet resources, + out string? error) + where TPixel : unmanaged, IPixel + { + resources = default; + + if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId expectedFormatId)) + { + error = $"The staged WebGPU scene pipeline does not support pixel format '{typeof(TPixel).Name}'."; + return false; + } + + TextureFormat expectedTextureFormat = WebGPUTextureFormatMapper.ToSilk(expectedFormatId); + if (flushContext.TextureFormat != expectedTextureFormat) + { + error = $"Scene resource texture format '{flushContext.TextureFormat}' does not match the required '{expectedTextureFormat}' for pixel type '{typeof(TPixel).Name}'."; + return false; + } + + if (!TryCreateAndUploadCombinedInfoBinDataBuffer(flushContext, scene.InfoWordCount, config.BufferSizes.BinData.ByteLength, out WgpuBuffer* infoBinDataBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathReduced.Length, out WgpuBuffer* pathReducedBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathReduced2.Length, out WgpuBuffer* pathReduced2Buffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathReducedScan.Length, out WgpuBuffer* pathReducedScanBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathMonoids.Length, out WgpuBuffer* pathMonoidBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathBboxes.Length, out WgpuBuffer* pathBboxBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.DrawReduced.Length, out WgpuBuffer* drawReducedBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.DrawMonoids.Length, out WgpuBuffer* drawMonoidBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipInputs.Length, out WgpuBuffer* clipInputBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipElements.Length, out WgpuBuffer* clipElementBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipBics.Length, out WgpuBuffer* clipBicBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipBboxes.Length, out WgpuBuffer* clipBboxBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.DrawBboxes.Length, out WgpuBuffer* drawBboxBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.Paths.Length, out WgpuBuffer* pathBuffer, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, [], config.BufferSizes.Lines.Length, out WgpuBuffer* lineBuffer, out error)) + { + return false; + } + + if (!TryCreateGradientTexture(flushContext, scene, out TextureView* gradientTextureView, out error)) + { + return false; + } + + if (!TryCreateImageAtlasTexture(flushContext, scene, expectedTextureFormat, out TextureView* imageAtlasTextureView, out error)) + { + return false; + } + + if (!TryCreateAndUploadBuffer(flushContext, scene.SceneData.Span, (uint)scene.SceneData.Length, out WgpuBuffer* sceneBuffer, out error)) + { + return false; + } + + GpuSceneConfig header = new( + (uint)scene.TileCountX, + (uint)scene.TileCountY, + (uint)scene.TargetSize.Width, + (uint)scene.TargetSize.Height, + baseColor, + scene.Layout, + config.BufferSizes.Lines.Length, + config.BufferSizes.BinData.Length, + config.BufferSizes.PathTiles.Length, + config.BufferSizes.SegCounts.Length, + config.BufferSizes.Segments.Length, + config.BufferSizes.BlendSpill.Length, + config.BufferSizes.Ptcl.Length); + + if (!TryCreateAndUploadScalarBuffer(flushContext, in header, out WgpuBuffer* headerBuffer, out error)) + { + return false; + } + + resources = new WebGPUSceneResourceSet( + headerBuffer, + sceneBuffer, + pathReducedBuffer, + pathReduced2Buffer, + pathReducedScanBuffer, + pathMonoidBuffer, + pathBboxBuffer, + drawReducedBuffer, + drawMonoidBuffer, + infoBinDataBuffer, + clipInputBuffer, + clipElementBuffer, + clipBicBuffer, + clipBboxBuffer, + drawBboxBuffer, + pathBuffer, + lineBuffer, + gradientTextureView, + imageAtlasTextureView); + error = null; + return true; + } + + private static bool TryCreateImageAtlasTexture( + WebGPUFlushContext flushContext, + WebGPUEncodedScene scene, + TextureFormat textureFormat, + out TextureView* textureView, + out string? error) + where TPixel : unmanaged, IPixel + { + if (scene.Images.Count == 0) + { + return TryCreateTransparentSampledTexture(flushContext, textureFormat, out _, out textureView, out error); + } + + int atlasWidth = 1; + int atlasHeight = 0; + foreach (GpuImageDescriptor descriptor in scene.Images) + { + GetImageEntrySize(descriptor.Brush, out int width, out int height); + atlasWidth = Math.Max(atlasWidth, width); + atlasHeight += height; + } + + if (!TryCreateTexture(flushContext, textureFormat, atlasWidth, atlasHeight, "image atlas", out Texture* texture, out textureView, out error)) + { + return false; + } + + TPixel[] rowBuffer = GC.AllocateUninitializedArray(atlasWidth); + int atlasY = 0; + foreach (GpuImageDescriptor descriptor in scene.Images) + { + if (!TryUploadImageEntry( + flushContext, + texture, + descriptor.Brush, + atlasY, + rowBuffer, + out int entryWidth, + out int entryHeight, + out error)) + { + return false; + } + + int sceneIndex = (int)scene.Layout.DrawDataBase + descriptor.DrawDataWordOffset; + scene.SetSceneWord(sceneIndex, PackImageAtlasOffset(0, atlasY)); + scene.SetSceneWord(sceneIndex + 1, PackImageExtents(entryWidth, entryHeight)); + scene.SetSceneWord(sceneIndex + 2, PackImageSampleInfo(textureFormat, xExtendMode: 1U, yExtendMode: 1U)); + atlasY += entryHeight; + } + + error = null; + return true; + } + + private static bool TryCreateGradientTexture( + WebGPUFlushContext flushContext, + WebGPUEncodedScene scene, + out TextureView* textureView, + out string? error) + { + if (scene.GradientRowCount == 0) + { + return TryCreateTransparentSampledTexture(flushContext, TextureFormat.Rgba8Unorm, out _, out textureView, out error); + } + + TextureDescriptor textureDescriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D(512, (uint)scene.GradientRowCount, 1), + Format = TextureFormat.Rgba8Unorm, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in textureDescriptor); + if (texture is null) + { + textureView = null; + error = "Failed to create a gradient texture."; + return false; + } + + TextureViewDescriptor textureViewDescriptor = new() + { + Format = TextureFormat.Rgba8Unorm, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); + if (textureView is null) + { + flushContext.Api.TextureRelease(texture); + error = "Failed to create a gradient texture view."; + return false; + } + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = 512 * 4, + RowsPerImage = (uint)scene.GradientRowCount + }; + + ImageCopyTexture destination = new() + { + Texture = texture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + fixed (uint* pixelPtr = scene.GradientPixels.Span) + { + Extent3D extent = new(512, (uint)scene.GradientRowCount, 1); + flushContext.Api.QueueWriteTexture( + flushContext.Queue, + in destination, + pixelPtr, + (nuint)(scene.GradientPixels.Length * sizeof(uint)), + in layout, + in extent); + } + + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(textureView); + error = null; + return true; + } + + private static void GetImageEntrySize(Brush brush, out int width, out int height) + { + if (brush is PatternBrush patternBrush) + { + width = patternBrush.Pattern.Columns; + height = patternBrush.Pattern.Rows; + return; + } + + ImageBrush imageBrush = (ImageBrush)brush; + Rectangle sourceRegion = Rectangle.Intersect(imageBrush.UntypedImage.Bounds, (Rectangle)imageBrush.SourceRegion); + width = sourceRegion.Width; + height = sourceRegion.Height; + } + + private static bool TryUploadImageEntry( + WebGPUFlushContext flushContext, + Texture* texture, + Brush brush, + int atlasY, + TPixel[] rowBuffer, + out int entryWidth, + out int entryHeight, + out string? error) + where TPixel : unmanaged, IPixel + { + if (brush is PatternBrush patternBrush) + { + return TryUploadPatternEntry(flushContext, texture, patternBrush, atlasY, rowBuffer, out entryWidth, out entryHeight, out error); + } + + // We can safely cast the untyped image to a typed image because the type constraint is tightly + // controlled by the caller based on the flush context's texture format, which is determined by the pixel type. + return TryUploadImageBrushEntry(flushContext, texture, (ImageBrush)brush, atlasY, out entryWidth, out entryHeight, out error); + } + + private static bool TryUploadPatternEntry( + WebGPUFlushContext flushContext, + Texture* texture, + PatternBrush patternBrush, + int atlasY, + TPixel[] rowBuffer, + out int entryWidth, + out int entryHeight, + out string? error) + where TPixel : unmanaged, IPixel + { + DenseMatrix pattern = patternBrush.Pattern; + entryWidth = pattern.Columns; + entryHeight = pattern.Rows; + + for (int y = 0; y < entryHeight; y++) + { + Span rowPixels = rowBuffer.AsSpan(0, entryWidth); + for (int x = 0; x < entryWidth; x++) + { + rowPixels[x] = pattern[y, x].ToPixel(); + } + + if (!TryWriteTextureRegion(flushContext, texture, 0, atlasY + y, entryWidth, 1, rowPixels, out error)) + { + return false; + } + } + + error = null; + return true; + } + + private static bool TryUploadImageBrushEntry( + WebGPUFlushContext flushContext, + Texture* texture, + ImageBrush imageBrush, + int atlasY, + out int entryWidth, + out int entryHeight, + out string? error) + where TPixel : unmanaged, IPixel + { + Rectangle sourceRegion = Rectangle.Intersect(imageBrush.UntypedImage.Bounds, (Rectangle)imageBrush.SourceRegion); + entryWidth = sourceRegion.Width; + entryHeight = sourceRegion.Height; + + ImageFrame sourceFrame = imageBrush.SourceImage.Frames.RootFrame; + for (int y = 0; y < entryHeight; y++) + { + ReadOnlySpan sourceRow = sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceRegion.Y + y).Slice(sourceRegion.X, entryWidth); + + if (!TryWriteTextureRegion(flushContext, texture, 0, atlasY + y, entryWidth, 1, sourceRow, out error)) + { + return false; + } + } + + error = null; + return true; + } + + private static bool TryCreateAndUploadCombinedInfoBinDataBuffer( + WebGPUFlushContext flushContext, + int infoWordCount, + nuint dynamicBinByteLength, + out WgpuBuffer* buffer, + out string? error) + { + nuint infoByteLength = checked((nuint)infoWordCount * (nuint)Unsafe.SizeOf()); + nuint totalByteLength = checked(infoByteLength + dynamicBinByteLength); + if (totalByteLength == 0) + { + totalByteLength = (nuint)Unsafe.SizeOf(); + } + + BufferDescriptor descriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = totalByteLength + }; + + buffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); + if (buffer is null) + { + error = "Failed to create the staged-scene info/bin-data buffer."; + return false; + } + + flushContext.TrackBuffer(buffer); + error = null; + return true; + } + + private static bool TryCreateTransparentSampledTexture( + WebGPUFlushContext flushContext, + TextureFormat textureFormat, + out Texture* texture, + out TextureView* textureView, + out string? error) + { + TextureDescriptor textureDescriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D(1, 1, 1), + Format = textureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in textureDescriptor); + if (texture is null) + { + textureView = null; + error = "Failed to create a sampled scene texture."; + return false; + } + + TextureViewDescriptor textureViewDescriptor = new() + { + Format = textureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); + if (textureView is null) + { + flushContext.Api.TextureRelease(texture); + texture = null; + error = "Failed to create a sampled scene texture view."; + return false; + } + + uint pixel = 0; + ImageCopyTexture destination = new() + { + Texture = texture, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = 4, + RowsPerImage = 1 + }; + + Extent3D size = new(1, 1, 1); + flushContext.Api.QueueWriteTexture(flushContext.Queue, in destination, &pixel, 4, in layout, in size); + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(textureView); + error = null; + return true; + } + + private static bool TryCreateTexture( + WebGPUFlushContext flushContext, + TextureFormat textureFormat, + int width, + int height, + string textureName, + out Texture* texture, + out TextureView* textureView, + out string? error) + { + TextureDescriptor textureDescriptor = new() + { + Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = textureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + texture = flushContext.Api.DeviceCreateTexture(flushContext.Device, in textureDescriptor); + if (texture is null) + { + textureView = null; + error = $"Failed to create a {textureName} texture."; + return false; + } + + TextureViewDescriptor textureViewDescriptor = new() + { + Format = textureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); + if (textureView is null) + { + flushContext.Api.TextureRelease(texture); + texture = null; + error = $"Failed to create a {textureName} texture view."; + return false; + } + + flushContext.TrackTexture(texture); + flushContext.TrackTextureView(textureView); + error = null; + return true; + } + + private static bool TryWriteTextureRegion( + WebGPUFlushContext flushContext, + Texture* texture, + int x, + int y, + int width, + int height, + ReadOnlySpan pixels, + out string? error) + where TPixel : unmanaged + { + TextureDataLayout layout = new() + { + Offset = 0, + BytesPerRow = (uint)(width * Unsafe.SizeOf()), + RowsPerImage = (uint)height + }; + + fixed (TPixel* pixelPtr = pixels) + { + ImageCopyTexture destination = new() + { + Texture = texture, + MipLevel = 0, + Origin = new Origin3D((uint)x, (uint)y, 0), + Aspect = TextureAspect.All + }; + + Extent3D extent = new((uint)width, (uint)height, 1); + flushContext.Api.QueueWriteTexture( + flushContext.Queue, + in destination, + pixelPtr, + (nuint)(pixels.Length * Unsafe.SizeOf()), + in layout, + in extent); + } + + error = null; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint PackImageAtlasOffset(int x, int y) + => ((uint)x << 16) | (uint)y; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint PackImageExtents(int width, int height) + => ((uint)width << 16) | (uint)height; + + private static uint PackImageSampleInfo(TextureFormat textureFormat, uint xExtendMode, uint yExtendMode) + { + const uint alpha = 0xFFU; + const uint qualityLow = 0U; + const uint alphaTypeStraight = 0U; + uint format = textureFormat == TextureFormat.Bgra8Unorm ? 1U : 0U; + return alpha + | (yExtendMode << 8) + | (xExtendMode << 10) + | (qualityLow << 12) + | (alphaTypeStraight << 14) + | (format << 15); + } + + private static bool TryCreateAndUploadScalarBuffer( + WebGPUFlushContext flushContext, + in T value, + out WgpuBuffer* buffer, + out string? error) + where T : unmanaged + { + nuint byteLength = (nuint)Unsafe.SizeOf(); + BufferDescriptor descriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.Uniform | BufferUsage.CopyDst, + Size = byteLength + }; + + buffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); + if (buffer is null) + { + error = $"Failed to create a staged-scene scalar buffer for '{typeof(T).Name}'."; + return false; + } + + flushContext.TrackBuffer(buffer); + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + buffer, + 0, + Unsafe.AsPointer(ref Unsafe.AsRef(in value)), + byteLength); + error = null; + return true; + } + + private static bool TryCreateAndUploadBuffer( + WebGPUFlushContext flushContext, + ReadOnlySpan values, + uint minimumLength, + out WgpuBuffer* buffer, + out string? error) + where T : unmanaged + { + uint elementCount = Math.Max(Math.Max((uint)values.Length, minimumLength), 1U); + nuint byteLength = checked(elementCount * (nuint)Unsafe.SizeOf()); + BufferDescriptor descriptor = new() + { + Usage = BufferUsage.Storage | BufferUsage.CopyDst, + Size = byteLength + }; + + buffer = flushContext.Api.DeviceCreateBuffer(flushContext.Device, in descriptor); + if (buffer is null) + { + error = $"Failed to create a staged-scene buffer for '{typeof(T).Name}'."; + return false; + } + + flushContext.TrackBuffer(buffer); + if (!values.IsEmpty) + { + nuint uploadByteLength = checked((nuint)values.Length * (nuint)Unsafe.SizeOf()); + fixed (T* dataPtr = values) + { + flushContext.Api.QueueWriteBuffer( + flushContext.Queue, + buffer, + 0, + dataPtr, + uploadByteLength); + } + } + + error = null; + return true; + } +} + +/// +/// Flush-scoped GPU resources produced from one encoded scene. +/// +internal readonly unsafe struct WebGPUSceneResourceSet +{ + public WebGPUSceneResourceSet( + WgpuBuffer* headerBuffer, + WgpuBuffer* sceneBuffer, + WgpuBuffer* pathReducedBuffer, + WgpuBuffer* pathReduced2Buffer, + WgpuBuffer* pathReducedScanBuffer, + WgpuBuffer* pathMonoidBuffer, + WgpuBuffer* pathBboxBuffer, + WgpuBuffer* drawReducedBuffer, + WgpuBuffer* drawMonoidBuffer, + WgpuBuffer* infoBinDataBuffer, + WgpuBuffer* clipInputBuffer, + WgpuBuffer* clipElementBuffer, + WgpuBuffer* clipBicBuffer, + WgpuBuffer* clipBboxBuffer, + WgpuBuffer* drawBboxBuffer, + WgpuBuffer* pathBuffer, + WgpuBuffer* lineBuffer, + TextureView* gradientTextureView, + TextureView* imageAtlasTextureView) + { + this.HeaderBuffer = headerBuffer; + this.SceneBuffer = sceneBuffer; + this.PathReducedBuffer = pathReducedBuffer; + this.PathReduced2Buffer = pathReduced2Buffer; + this.PathReducedScanBuffer = pathReducedScanBuffer; + this.PathMonoidBuffer = pathMonoidBuffer; + this.PathBboxBuffer = pathBboxBuffer; + this.DrawReducedBuffer = drawReducedBuffer; + this.DrawMonoidBuffer = drawMonoidBuffer; + this.InfoBinDataBuffer = infoBinDataBuffer; + this.ClipInputBuffer = clipInputBuffer; + this.ClipElementBuffer = clipElementBuffer; + this.ClipBicBuffer = clipBicBuffer; + this.ClipBboxBuffer = clipBboxBuffer; + this.DrawBboxBuffer = drawBboxBuffer; + this.PathBuffer = pathBuffer; + this.LineBuffer = lineBuffer; + this.GradientTextureView = gradientTextureView; + this.ImageAtlasTextureView = imageAtlasTextureView; + } + + public WgpuBuffer* HeaderBuffer { get; } + + public WgpuBuffer* SceneBuffer { get; } + + public WgpuBuffer* PathReducedBuffer { get; } + + public WgpuBuffer* PathReduced2Buffer { get; } + + public WgpuBuffer* PathReducedScanBuffer { get; } + + public WgpuBuffer* PathMonoidBuffer { get; } + + public WgpuBuffer* PathBboxBuffer { get; } + + public WgpuBuffer* DrawReducedBuffer { get; } + + public WgpuBuffer* DrawMonoidBuffer { get; } + + public WgpuBuffer* InfoBinDataBuffer { get; } + + public WgpuBuffer* ClipInputBuffer { get; } + + public WgpuBuffer* ClipElementBuffer { get; } + + public WgpuBuffer* ClipBicBuffer { get; } + + public WgpuBuffer* ClipBboxBuffer { get; } + + public WgpuBuffer* DrawBboxBuffer { get; } + + public WgpuBuffer* PathBuffer { get; } + + public WgpuBuffer* LineBuffer { get; } + + public TextureView* GradientTextureView { get; } + + public TextureView* ImageAtlasTextureView { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal struct GpuSceneBumpAllocators +{ + public uint Failed; + public uint Binning; + public uint Ptcl; + public uint Tile; + public uint SegCounts; + public uint Segments; + public uint Blend; + public uint Lines; +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuTagMonoid +{ + public GpuTagMonoid(uint transIndex, uint pathSegmentIndex, uint pathSegmentOffset, uint styleIndex, uint pathIndex) + { + this.TransIndex = transIndex; + this.PathSegmentIndex = pathSegmentIndex; + this.PathSegmentOffset = pathSegmentOffset; + this.StyleIndex = styleIndex; + this.PathIndex = pathIndex; + } + + public uint TransIndex { get; } + + public uint PathSegmentIndex { get; } + + public uint PathSegmentOffset { get; } + + public uint StyleIndex { get; } + + public uint PathIndex { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuPathBbox +{ + public GpuPathBbox(int x0, int y0, int x1, int y1, uint drawFlags, uint transIndex) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + this.DrawFlags = drawFlags; + this.TransIndex = transIndex; + } + + public int X0 { get; } + + public int Y0 { get; } + + public int X1 { get; } + + public int Y1 { get; } + + public uint DrawFlags { get; } + + public uint TransIndex { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuClipInp +{ + public GpuClipInp(uint drawIndex, int pathIndex) + { + this.DrawIndex = drawIndex; + this.PathIndex = pathIndex; + } + + public uint DrawIndex { get; } + + public int PathIndex { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuBic +{ + public GpuBic(uint a, uint b) + { + this.A = a; + this.B = b; + } + + public uint A { get; } + + public uint B { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuClipElement +{ + public GpuClipElement(uint parentIndex, Vector4 bbox) + { + this.ParentIndex = parentIndex; + this.Bbox = bbox; + } + + public uint ParentIndex { get; } + + public Vector4 Bbox { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuDrawBbox +{ + public GpuDrawBbox(Vector4 bbox) => this.Bbox = bbox; + + public Vector4 Bbox { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuSceneBinHeader +{ + public GpuSceneBinHeader(uint elementCount, uint chunkOffset) + { + this.ElementCount = elementCount; + this.ChunkOffset = chunkOffset; + } + + public uint ElementCount { get; } + + public uint ChunkOffset { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal struct GpuSceneIndirectCount +{ + public uint CountX; + public uint CountY; + public uint CountZ; + public uint Pad0; +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuSceneLayout +{ + public GpuSceneLayout( + uint drawObjectCount, + uint pathCount, + uint clipCount, + uint binDataStart, + uint pathTagBase, + uint pathDataBase, + uint drawTagBase, + uint drawDataBase, + uint transformBase, + uint styleBase) + { + this.DrawObjectCount = drawObjectCount; + this.PathCount = pathCount; + this.ClipCount = clipCount; + this.BinDataStart = binDataStart; + this.PathTagBase = pathTagBase; + this.PathDataBase = pathDataBase; + this.DrawTagBase = drawTagBase; + this.DrawDataBase = drawDataBase; + this.TransformBase = transformBase; + this.StyleBase = styleBase; + } + + public uint DrawObjectCount { get; } + + public uint PathCount { get; } + + public uint ClipCount { get; } + + public uint BinDataStart { get; } + + public uint PathTagBase { get; } + + public uint PathDataBase { get; } + + public uint DrawTagBase { get; } + + public uint DrawDataBase { get; } + + public uint TransformBase { get; } + + public uint StyleBase { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuSceneConfig +{ + public GpuSceneConfig( + uint widthInTiles, + uint heightInTiles, + uint targetWidth, + uint targetHeight, + uint baseColor, + GpuSceneLayout layout, + uint linesSize, + uint binningSize, + uint tilesSize, + uint segCountsSize, + uint segmentsSize, + uint blendSize, + uint ptclSize) + { + this.WidthInTiles = widthInTiles; + this.HeightInTiles = heightInTiles; + this.TargetWidth = targetWidth; + this.TargetHeight = targetHeight; + this.BaseColor = baseColor; + this.Layout = layout; + this.LinesSize = linesSize; + this.BinningSize = binningSize; + this.TilesSize = tilesSize; + this.SegCountsSize = segCountsSize; + this.SegmentsSize = segmentsSize; + this.BlendSize = blendSize; + this.PtclSize = ptclSize; + } + + public uint WidthInTiles { get; } + + public uint HeightInTiles { get; } + + public uint TargetWidth { get; } + + public uint TargetHeight { get; } + + public uint BaseColor { get; } + + public GpuSceneLayout Layout { get; } + + public uint LinesSize { get; } + + public uint BinningSize { get; } + + public uint TilesSize { get; } + + public uint SegCountsSize { get; } + + public uint SegmentsSize { get; } + + public uint BlendSize { get; } + + public uint PtclSize { get; } +} + +internal static class GpuSceneDrawTag +{ + // TODO: Why is this not an enum? + // It's really hard to understand what these magic numbers mean without documentation. + public const uint Nop = 0U; + public const uint FillColor = 0x44U; + public const uint FillRecolor = 0x4CU; + public const uint FillLinGradient = 0x114U; + public const uint FillRadGradient = 0x29CU; + public const uint FillEllipticGradient = 0x1D8U; + public const uint FillSweepGradient = 0x254U; + public const uint FillImage = 0x294U; + public const uint BeginClip = 0x49U; + public const uint EndClip = 0x21U; + public const uint FillInfoFlagsFillRuleBit = 1U; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuSceneDrawMonoid Map(uint tagWord) + + // TODO: This needs documentation. It's unintelligable. + => new( + tagWord != Nop ? 1U : 0U, + tagWord & 1U, + (tagWord >> 2) & 0x07U, + (tagWord >> 6) & 0x0FU); +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuSceneDrawMonoid +{ + public GpuSceneDrawMonoid(uint pathIndex, uint clipIndex, uint sceneOffset, uint infoOffset) + { + this.PathIndex = pathIndex; + this.ClipIndex = clipIndex; + this.SceneOffset = sceneOffset; + this.InfoOffset = infoOffset; + } + + public uint PathIndex { get; } + + public uint ClipIndex { get; } + + public uint SceneOffset { get; } + + public uint InfoOffset { get; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuSceneDrawMonoid Combine(in GpuSceneDrawMonoid a, in GpuSceneDrawMonoid b) + => new( + a.PathIndex + b.PathIndex, + a.ClipIndex + b.ClipIndex, + a.SceneOffset + b.SceneOffset, + a.InfoOffset + b.InfoOffset); +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuScenePath +{ + private readonly uint padding0; + private readonly uint padding1; + private readonly uint padding2; + + public GpuScenePath(uint bboxMinX, uint bboxMinY, uint bboxMaxX, uint bboxMaxY, uint tiles) + { + this.BboxMinX = bboxMinX; + this.BboxMinY = bboxMinY; + this.BboxMaxX = bboxMaxX; + this.BboxMaxY = bboxMaxY; + this.Tiles = tiles; + this.padding0 = 0; + this.padding1 = 0; + this.padding2 = 0; + } + + public uint BboxMinX { get; } + + public uint BboxMinY { get; } + + public uint BboxMaxX { get; } + + public uint BboxMaxY { get; } + + public uint Tiles { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuSceneLine +{ + private readonly uint padding0; + + public GpuSceneLine(uint pathIndex, Vector2 point0, Vector2 point1) + { + this.PathIndex = pathIndex; + this.padding0 = 0; + this.Point0 = point0; + this.Point1 = point1; + } + + public uint PathIndex { get; } + + public Vector2 Point0 { get; } + + public Vector2 Point1 { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal struct GpuPathTile +{ + public GpuPathTile(int backdrop, uint segmentCountOrIndex) + { + this.Backdrop = backdrop; + this.SegmentCountOrIndex = segmentCountOrIndex; + } + + public int Backdrop; + + public uint SegmentCountOrIndex; +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuSegmentCount +{ + public GpuSegmentCount(uint lineIndex, uint counts) + { + this.LineIndex = lineIndex; + this.Counts = counts; + } + + public uint LineIndex { get; } + + public uint Counts { get; } +} + +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuPathSegment +{ + private readonly float padding0; + + public GpuPathSegment(Vector2 point0, Vector2 point1, float yEdge) + { + this.Point0 = point0; + this.Point1 = point1; + this.YEdge = yEdge; + this.padding0 = 0; + } + + public Vector2 Point0 { get; } + + public Vector2 Point1 { get; } + + public float YEdge { get; } +} + +#pragma warning restore SA1201 diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs new file mode 100644 index 000000000..6a62cc51a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceCapability.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Native WebGPU surface capability attached to . +/// +/// +/// The handle must remain valid for the lifetime of any +/// that processes frames using this capability. +/// The backend caches per-device GPU resources (pipelines, buffers) that reference +/// the device internally. Ensure the device is not released while any backend +/// instance may still reference it. +/// +public sealed class WebGPUSurfaceCapability +{ + /// + /// Initializes a new instance of the class. + /// + /// Opaque WGPUDevice* handle. Must remain valid for the lifetime of any backend that uses this capability. + /// Opaque WGPUQueue* handle. + /// Opaque WGPUTexture* handle for the current frame when writable upload is supported. + /// Opaque WGPUTextureView* handle for the current frame. + /// Native render target texture format identifier. + /// Surface width in pixels. + /// Surface height in pixels. + public WebGPUSurfaceCapability( + nint device, + nint queue, + nint targetTexture, + nint targetTextureView, + WebGPUTextureFormatId targetFormat, + int width, + int height) + { + this.Device = device; + this.Queue = queue; + this.TargetTexture = targetTexture; + this.TargetTextureView = targetTextureView; + this.TargetFormat = targetFormat; + this.Width = width; + this.Height = height; + } + + /// + /// Gets the opaque WGPUDevice* handle. + /// + public nint Device { get; } + + /// + /// Gets the opaque WGPUQueue* handle. + /// + public nint Queue { get; } + + /// + /// Gets the opaque WGPUTexture* handle for the current frame. + /// + public nint TargetTexture { get; } + + /// + /// Gets the opaque WGPUTextureView* handle for the current frame. + /// + public nint TargetTextureView { get; } + + /// + /// Gets the native render target texture format identifier. + /// + public WebGPUTextureFormatId TargetFormat { get; } + + /// + /// Gets the surface width in pixels. + /// + public int Width { get; } + + /// + /// Gets the surface height in pixels. + /// + public int Height { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs new file mode 100644 index 000000000..09a0dfdec --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTestNativeSurfaceAllocator.cs @@ -0,0 +1,383 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Silk.NET.WebGPU; +using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Internal helper for benchmark/test-only native WebGPU target allocation. +/// +internal static unsafe class WebGPUTestNativeSurfaceAllocator +{ + private const int CallbackTimeoutMilliseconds = 5000; + + /// + /// Tries to allocate a native WebGPU texture + view pair and wrap them in a . + /// + internal static bool TryCreate( + int width, + int height, + out NativeSurface surface, + out nint textureHandle, + out nint textureViewHandle, + out string error) + where TPixel : unmanaged, IPixel + { + if (!WebGPUDrawingBackend.TryGetCompositeTextureFormat(out WebGPUTextureFormatId formatId, out FeatureName requiredFeature)) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = $"Pixel type '{typeof(TPixel).Name}' is not supported by the WebGPU backend."; + return false; + } + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + + if (!WebGPURuntime.TryGetOrCreateDevice(out Device* device, out Queue* queue, out string? deviceError)) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = deviceError ?? "WebGPU device auto-provisioning failed."; + return false; + } + + if (requiredFeature != FeatureName.Undefined + && !WebGPURuntime.GetOrCreateDeviceState(api, device).HasFeature(requiredFeature)) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = $"Device does not support required feature '{requiredFeature}' for pixel type '{typeof(TPixel).Name}'."; + return false; + } + + TextureFormat textureFormat = WebGPUTextureFormatMapper.ToSilk(formatId); + TextureDescriptor targetTextureDescriptor = new() + { + Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding | TextureUsage.StorageBinding, + Dimension = TextureDimension.Dimension2D, + Size = new Extent3D((uint)width, (uint)height, 1), + Format = textureFormat, + MipLevelCount = 1, + SampleCount = 1 + }; + + Texture* targetTexture = api.DeviceCreateTexture(device, in targetTextureDescriptor); + if (targetTexture is null) + { + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = "WebGPU.DeviceCreateTexture returned null."; + return false; + } + + TextureViewDescriptor targetViewDescriptor = new() + { + Format = textureFormat, + Dimension = TextureViewDimension.Dimension2D, + BaseMipLevel = 0, + MipLevelCount = 1, + BaseArrayLayer = 0, + ArrayLayerCount = 1, + Aspect = TextureAspect.All + }; + + TextureView* targetView = api.TextureCreateView(targetTexture, in targetViewDescriptor); + if (targetView is null) + { + api.TextureRelease(targetTexture); + surface = new NativeSurface(TPixel.GetPixelTypeInfo()); + textureHandle = 0; + textureViewHandle = 0; + error = "WebGPU.TextureCreateView returned null."; + return false; + } + + nint deviceHandle = (nint)device; + nint queueHandle = (nint)queue; + textureHandle = (nint)targetTexture; + textureViewHandle = (nint)targetView; + surface = WebGPUNativeSurfaceFactory.Create( + deviceHandle, + queueHandle, + textureHandle, + textureViewHandle, + formatId, + width, + height); + error = string.Empty; + return true; + } + + /// + /// Tries to upload CPU pixel data to an existing native WebGPU texture handle. + /// + internal static bool TryWriteTexture( + nint textureHandle, + int width, + int height, + Image image, + out string error) + where TPixel : unmanaged, IPixel + { + if (textureHandle == 0) + { + error = "Texture handle is zero."; + return false; + } + + if (image.Width != width || image.Height != height) + { + error = "Source image dimensions must match the target texture dimensions."; + return false; + } + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + if (!WebGPURuntime.TryGetOrCreateDevice(out _, out Queue* queue, out string? deviceError)) + { + error = deviceError ?? "WebGPU device auto-provisioning failed."; + return false; + } + + try + { + Buffer2DRegion sourceRegion = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + WebGPUFlushContext.UploadTextureFromRegion( + lease.Api, + queue, + (Texture*)textureHandle, + sourceRegion, + Configuration.Default.MemoryAllocator); + error = string.Empty; + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + + /// + /// Tries to read pixels from a native WebGPU texture handle into an . + /// + internal static bool TryReadTexture( + nint textureHandle, + int width, + int height, + out Image? image, + out string error) + where TPixel : unmanaged, IPixel + { + image = null; + if (textureHandle == 0) + { + error = "Texture handle is zero."; + return false; + } + + if (width <= 0 || height <= 0) + { + error = "Texture dimensions must be greater than zero."; + return false; + } + + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + if (!WebGPURuntime.TryGetOrCreateDevice(out Device* device, out Queue* queue, out string? deviceError)) + { + error = deviceError ?? "WebGPU device auto-provisioning failed."; + return false; + } + + WebGPU api = lease.Api; + int pixelSizeInBytes = Unsafe.SizeOf(); + int packedRowBytes = checked(width * pixelSizeInBytes); + int readbackRowBytes = Align(packedRowBytes, 256); + int packedByteCount = checked(packedRowBytes * height); + ulong readbackByteCount = checked((ulong)readbackRowBytes * (ulong)height); + + Silk.NET.WebGPU.Buffer* readbackBuffer = null; + CommandEncoder* commandEncoder = null; + CommandBuffer* commandBuffer = null; + try + { + BufferDescriptor bufferDescriptor = new() + { + Usage = BufferUsage.CopyDst | BufferUsage.MapRead, + Size = readbackByteCount, + MappedAtCreation = false + }; + + readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor); + if (readbackBuffer is null) + { + error = "WebGPU.DeviceCreateBuffer returned null for readback."; + return false; + } + + CommandEncoderDescriptor encoderDescriptor = default; + commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor); + if (commandEncoder is null) + { + error = "WebGPU.DeviceCreateCommandEncoder returned null."; + return false; + } + + ImageCopyTexture source = new() + { + Texture = (Texture*)textureHandle, + MipLevel = 0, + Origin = new Origin3D(0, 0, 0), + Aspect = TextureAspect.All + }; + + ImageCopyBuffer destination = new() + { + Buffer = readbackBuffer, + Layout = new TextureDataLayout + { + Offset = 0, + BytesPerRow = (uint)readbackRowBytes, + RowsPerImage = (uint)height + } + }; + + Extent3D copySize = new((uint)width, (uint)height, 1); + api.CommandEncoderCopyTextureToBuffer(commandEncoder, in source, in destination, in copySize); + + CommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + error = "WebGPU.CommandEncoderFinish returned null."; + return false; + } + + api.QueueSubmit(queue, 1, ref commandBuffer); + api.CommandBufferRelease(commandBuffer); + commandBuffer = null; + api.CommandEncoderRelease(commandEncoder); + commandEncoder = null; + + BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + using ManualResetEventSlim mapReady = new(false); + void Callback(BufferMapAsyncStatus status, void* userData) + { + _ = userData; + mapStatus = status; + mapReady.Set(); + } + + using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null); + if (!WaitForSignal(lease.WgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + { + error = $"WebGPU readback map failed with status '{mapStatus}'."; + return false; + } + + void* mapped = api.BufferGetConstMappedRange(readbackBuffer, 0, (nuint)readbackByteCount); + if (mapped is null) + { + api.BufferUnmap(readbackBuffer); + error = "WebGPU.BufferGetConstMappedRange returned null."; + return false; + } + + try + { + ReadOnlySpan readback = new(mapped, checked((int)readbackByteCount)); + byte[] packed = new byte[packedByteCount]; + Span packedSpan = packed; + for (int y = 0; y < height; y++) + { + readback + .Slice(y * readbackRowBytes, packedRowBytes) + .CopyTo(packedSpan.Slice(y * packedRowBytes, packedRowBytes)); + } + + image = Image.LoadPixelData(packed, width, height); + error = string.Empty; + return true; + } + finally + { + api.BufferUnmap(readbackBuffer); + } + } + finally + { + if (commandBuffer is not null) + { + api.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + api.CommandEncoderRelease(commandEncoder); + } + + if (readbackBuffer is not null) + { + api.BufferRelease(readbackBuffer); + } + } + } + + /// + /// Releases native texture and texture-view handles allocated for tests. + /// + /// The native texture handle. + /// The native texture-view handle. + internal static void Release(nint textureHandle, nint textureViewHandle) + { + if (textureHandle == 0 && textureViewHandle == 0) + { + return; + } + + // Keep the runtime alive while releasing native handles. + using WebGPURuntime.Lease lease = WebGPURuntime.Acquire(); + WebGPU api = lease.Api; + if (textureViewHandle != 0) + { + api.TextureViewRelease((TextureView*)textureViewHandle); + } + + if (textureHandle != 0) + { + api.TextureRelease((Texture*)textureHandle); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Align(int value, int alignment) + => ((value + alignment - 1) / alignment) * alignment; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool WaitForSignal(Wgpu? extension, Device* device, ManualResetEventSlim signal) + { + if (extension is null) + { + return signal.Wait(CallbackTimeoutMilliseconds); + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < CallbackTimeoutMilliseconds) + { + _ = extension.DevicePoll(device, true, (WrappedSubmissionIndex*)null); + } + + return signal.IsSet; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs new file mode 100644 index 000000000..40251527e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatId.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Supported WebGPU texture format identifiers used by . +/// +/// +/// Only formats with storage texture binding support are included. +/// Numeric values match the WebGPU WGPUTextureFormat constants. +/// +public enum WebGPUTextureFormatId +{ + /// + /// Four-channel 8-bit normalized unsigned RGBA format. + /// + Rgba8Unorm = 0x12, + + /// + /// Four-channel 8-bit normalized signed format. + /// + Rgba8Snorm = 0x14, + + /// + /// Four-channel 8-bit normalized unsigned BGRA format. + /// + Bgra8Unorm = 0x17, + + /// + /// Four-channel 16-bit floating-point format. + /// + Rgba16Float = 0x22 +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs new file mode 100644 index 000000000..21d79f482 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Maps public WebGPU texture format identifiers to Silk.NET texture formats and back. +/// +internal static class WebGPUTextureFormatMapper +{ + /// + /// Converts a public WebGPU texture format identifier to the corresponding Silk.NET texture format. + /// + /// The public texture format identifier. + /// The matching value. + public static TextureFormat ToSilk(WebGPUTextureFormatId formatId) + => (TextureFormat)(int)formatId; + + /// + /// Converts a Silk.NET texture format to the corresponding public WebGPU texture format identifier. + /// + /// The Silk.NET texture format. + /// The matching value. + public static WebGPUTextureFormatId FromSilk(TextureFormat textureFormat) + => (WebGPUTextureFormatId)(int)textureFormat; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureSampleTypeHelper.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureSampleTypeHelper.cs new file mode 100644 index 000000000..f8cfaafc7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureSampleTypeHelper.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Resolves the sampled texture type for formats explicitly supported by +/// composite registrations. +/// +internal static class WebGPUTextureSampleTypeHelper +{ + public static bool TryGetInputSampleType(TextureFormat textureFormat, out TextureSampleType sampleType) + => WebGPUDrawingBackend.TryGetCompositeTextureSampleType(textureFormat, out sampleType); +} diff --git a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs b/src/ImageSharp.Drawing/ArcLineSegment.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs rename to src/ImageSharp.Drawing/ArcLineSegment.cs index 994753da2..eed5dafa5 100644 --- a/src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs +++ b/src/ImageSharp.Drawing/ArcLineSegment.cs @@ -80,10 +80,7 @@ public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAn } } - private ArcLineSegment(PointF[] linePoints) - { - this.linePoints = linePoints; - } + private ArcLineSegment(PointF[] linePoints) => this.linePoints = linePoints; /// public PointF EndPoint => this.linePoints[^1]; @@ -96,7 +93,7 @@ private ArcLineSegment(PointF[] linePoints) /// /// The transformation matrix. /// An with the matrix applied to it. - public ILineSegment Transform(Matrix3x2 matrix) + public ILineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -113,7 +110,7 @@ public ILineSegment Transform(Matrix3x2 matrix) } /// - ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix); + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); private static PointF[] EllipticArcFromEndParams( PointF from, @@ -203,7 +200,7 @@ private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, V prev = p2; } - return points.ToArray(); + return [.. points]; } private static void EndpointToCenterArcParams( diff --git a/src/ImageSharp.Drawing/Shapes/BooleanOperation.cs b/src/ImageSharp.Drawing/BooleanOperation.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/BooleanOperation.cs rename to src/ImageSharp.Drawing/BooleanOperation.cs diff --git a/src/ImageSharp.Drawing/ClipPathExtensions.cs b/src/ImageSharp.Drawing/ClipPathExtensions.cs new file mode 100644 index 000000000..9ad53bbff --- /dev/null +++ b/src/ImageSharp.Drawing/ClipPathExtensions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.PolygonGeometry; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Provides extension methods to that allow the clipping of shapes. +/// +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 . + public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths) + => subjectPath.Clip(DefaultOptions, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths. + /// + /// The subject path. + /// The shape options. + /// The clipping paths. + /// The clipped . + public static IPath Clip( + this IPath subjectPath, + ShapeOptions options, + params IPath[] clipPaths) + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths. + /// + /// The subject path. + /// The clipping paths. + /// The clipped . + public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) + => subjectPath.Clip(DefaultOptions, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths. + /// + /// The subject path. + /// The shape options. + /// The clipping paths. + /// The clipped . + public static IPath Clip( + this IPath subjectPath, + ShapeOptions options, + IEnumerable clipPaths) + => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); +} diff --git a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs b/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs deleted file mode 100644 index 299ac3338..000000000 --- a/src/ImageSharp.Drawing/Common/Extensions/GraphicsOptionsExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Extensions methods fpor the class. -/// -internal static class GraphicsOptionsExtensions -{ - /// - /// Evaluates if a given SOURCE color can completely replace a BACKDROP color given the current blending and composition settings. - /// - /// The graphics options. - /// The source color. - /// true if the color can be considered opaque - /// - /// Blending and composition is an expensive operation, in some cases, like - /// filling with a solid color, the blending can be avoided by a plain color replacement. - /// This method can be useful for such processors to select the fast path. - /// - public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Color color) - { - if (options.ColorBlendingMode != PixelColorBlendingMode.Normal) - { - return false; - } - - if (options.AlphaCompositionMode is not PixelAlphaCompositionMode.SrcOver and not PixelAlphaCompositionMode.Src) - { - return false; - } - - const float opaque = 1f; - - if (options.BlendPercentage != opaque) - { - return false; - } - - if (color.ToScaledVector4().W != opaque) - { - return false; - } - - return true; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs b/src/ImageSharp.Drawing/ComplexPolygon.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs rename to src/ImageSharp.Drawing/ComplexPolygon.cs index dcdda4067..845a3b41b 100644 --- a/src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs +++ b/src/ImageSharp.Drawing/ComplexPolygon.cs @@ -67,7 +67,7 @@ public ComplexPolygon(params IPath[] paths) public RectangleF Bounds => this.bounds ??= this.CalcBounds(); /// - public IPath Transform(Matrix3x2 matrix) + public IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -88,13 +88,13 @@ public IPath Transform(Matrix3x2 matrix) /// public IEnumerable Flatten() { - List paths = []; + List paths = new(this.paths.Length); foreach (IPath path in this.Paths) { paths.AddRange(path.Flatten()); } - return paths.ToArray(); + return paths; } /// diff --git a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs similarity index 76% rename from src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs rename to src/ImageSharp.Drawing/CubicBezierLineSegment.cs index e9a44bd37..214a0019e 100644 --- a/src/ImageSharp.Drawing/Shapes/CubicBezierLineSegment.cs +++ b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing; @@ -54,7 +55,7 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control /// public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end) - : this(new[] { start, controlPoint1, controlPoint2, end }) + : this([start, controlPoint1, controlPoint2, end]) { } @@ -80,7 +81,7 @@ public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF control /// /// The matrix. /// A line segment with the matrix applied to it. - public CubicBezierLineSegment Transform(Matrix3x2 matrix) + public CubicBezierLineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -99,7 +100,7 @@ public CubicBezierLineSegment Transform(Matrix3x2 matrix) } /// - ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix); + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); private static PointF[] GetDrawingPoints(PointF[] controlPoints) { @@ -108,48 +109,33 @@ private static PointF[] GetDrawingPoints(PointF[] controlPoints) for (int curveIndex = 0; curveIndex < curveCount; curveIndex++) { - List bezierCurveDrawingPoints = FindDrawingPoints(curveIndex, controlPoints); - - if (curveIndex != 0) + if (curveIndex == 0) { - // remove the fist point, as it coincides with the last point of the previous Bezier curve. - bezierCurveDrawingPoints.RemoveAt(0); + drawingPoints.Add(CalculateBezierPoint(curveIndex, 0, controlPoints)); } - drawingPoints.AddRange(bezierCurveDrawingPoints); + SubdivideAndAppend(curveIndex, 0, 1, controlPoints, drawingPoints, 0); + drawingPoints.Add(CalculateBezierPoint(curveIndex, 1, controlPoints)); } - return drawingPoints.ToArray(); + return [.. drawingPoints]; } - private static List FindDrawingPoints(int curveIndex, PointF[] controlPoints) - { - List pointList = []; - - Vector2 left = CalculateBezierPoint(curveIndex, 0, controlPoints); - Vector2 right = CalculateBezierPoint(curveIndex, 1, controlPoints); - - pointList.Add(left); - pointList.Add(right); - - FindDrawingPoints(curveIndex, 0, 1, pointList, 1, controlPoints, 0); - - return pointList; - } - - private static int FindDrawingPoints( + /// + /// Recursively subdivides a cubic bezier curve segment and appends points in left-to-right order. + /// Points are appended (not inserted), avoiding O(n) shifts per point. + /// + private static void SubdivideAndAppend( int curveIndex, float t0, float t1, - List pointList, - int insertionIndex, PointF[] controlPoints, + List output, int depth) { - // max recursive depth for control points, means this is approx the max number of points discoverable if (depth > 999) { - return 0; + return; } Vector2 left = CalculateBezierPoint(curveIndex, t0, controlPoints); @@ -157,7 +143,7 @@ private static int FindDrawingPoints( if ((left - right).LengthSquared() < MinimumSqrDistance) { - return 0; + return; } float midT = (t0 + t1) / 2; @@ -168,17 +154,11 @@ private static int FindDrawingPoints( if (Vector2.Dot(leftDirection, rightDirection) > DivisionThreshold || Math.Abs(midT - 0.5f) < 0.0001f) { - int pointsAddedCount = 0; - - pointsAddedCount += FindDrawingPoints(curveIndex, t0, midT, pointList, insertionIndex, controlPoints, depth + 1); - pointList.Insert(insertionIndex + pointsAddedCount, mid); - pointsAddedCount++; - pointsAddedCount += FindDrawingPoints(curveIndex, midT, t1, pointList, insertionIndex + pointsAddedCount, controlPoints, depth + 1); - - return pointsAddedCount; + // Recurse left half, emit midpoint, recurse right half — all in order. + SubdivideAndAppend(curveIndex, t0, midT, controlPoints, output, depth + 1); + output.Add(mid); + SubdivideAndAppend(curveIndex, midT, t1, controlPoints, output, depth + 1); } - - return 0; } private static PointF CalculateBezierPoint(int curveIndex, float t, PointF[] controlPoints) diff --git a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs b/src/ImageSharp.Drawing/EllipsePolygon.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs rename to src/ImageSharp.Drawing/EllipsePolygon.cs index 6b9cac56f..4456c4bf5 100644 --- a/src/ImageSharp.Drawing/Shapes/EllipsePolygon.cs +++ b/src/ImageSharp.Drawing/EllipsePolygon.cs @@ -59,7 +59,7 @@ public EllipsePolygon(float x, float y, float radius) } /// - public override IPath Transform(Matrix3x2 matrix) + public override IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/Shapes/EmptyPath.cs b/src/ImageSharp.Drawing/EmptyPath.cs similarity index 94% rename from src/ImageSharp.Drawing/Shapes/EmptyPath.cs rename to src/ImageSharp.Drawing/EmptyPath.cs index 6789db399..ef39f221c 100644 --- a/src/ImageSharp.Drawing/Shapes/EmptyPath.cs +++ b/src/ImageSharp.Drawing/EmptyPath.cs @@ -35,5 +35,5 @@ public sealed class EmptyPath : IPath public IEnumerable Flatten() => []; /// - public IPath Transform(Matrix3x2 matrix) => this; + public IPath Transform(Matrix4x4 matrix) => this; } diff --git a/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs b/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs new file mode 100644 index 000000000..33a658455 --- /dev/null +++ b/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Helpers; + +/// +/// Extension methods for arrays. +/// +internal static class ArrayExtensions +{ + /// + /// Merges two arrays into one. + /// + /// the type of the array + /// The first source array. + /// The second source array. + /// + /// A new array containing the elements of both source arrays. + /// + public static T[] Merge(this T[] source1, T[] source2) + { + if (source2 is null || source2.Length == 0) + { + return source1; + } + + T[] target = new T[source1.Length + source2.Length]; + + for (int i = 0; i < source1.Length; i++) + { + target[i] = source1[i]; + } + + for (int i = 0; i < source2.Length; i++) + { + target[i + source1.Length] = source2[i]; + } + + return target; + } +} diff --git a/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs b/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs new file mode 100644 index 000000000..6d0168044 --- /dev/null +++ b/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Helpers; + +/// +/// Provides helper methods for extracting properties from transformation matrices. +/// +internal static class MatrixUtilities +{ + /// + /// Extracts the average 2D scale factor from a . + /// This is the mean of the X and Y axis scale magnitudes, suitable for + /// uniformly scaling radii under non-uniform or projective transforms. + /// + /// The transformation matrix. + /// The average scale factor. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float GetAverageScale(in Matrix4x4 matrix) + { + float sx = MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)); + float sy = MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22)); + return (sx + sy) * 0.5f; + } +} diff --git a/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs b/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs new file mode 100644 index 000000000..e035fc940 --- /dev/null +++ b/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs @@ -0,0 +1,125 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing; + +namespace SixLabors.ImageSharp.Drawing.Helpers; + +/// +/// Provides low-level geometry helpers for polygon winding and segment intersection. +/// +/// +/// Polygon methods expect a closed ring where the first point is repeated as the last point. +/// Orientation signs are defined using world-space math conventions (Y points up): +/// positive signed area is counter-clockwise and negative signed area is clockwise. +/// In screen space (Y points down), the visual winding appears inverted. +/// +internal static class PolygonUtilities +{ + // Epsilon used for floating-point tolerance. Values within +-Eps are treated as zero. + // This reduces instability when segments are nearly parallel or endpoints are close. + private const float Eps = 1e-3f; + private const float MinusEps = -Eps; + private const float OnePlusEps = 1 + Eps; + + /// + /// Ensures that a closed polygon ring matches the expected orientation. + /// + /// Polygon ring to normalize in place. + /// + /// Expected orientation sign: + /// positive for counter-clockwise in world space, negative for clockwise in world space. + /// + /// + /// The ring is reversed only when its orientation sign disagrees with + /// . Degenerate rings (zero area) are not changed. + /// + public static void EnsureOrientation(Span polygon, int expectedOrientation) + { + if (GetPolygonOrientation(polygon) * expectedOrientation < 0) + { + polygon.Reverse(); + } + } + + /// + /// Returns the orientation sign of a closed polygon ring using the shoelace sum. + /// + /// Closed polygon ring. + /// + /// -1 for clockwise, 1 for counter-clockwise, or 0 for degenerate (zero-area) input. + /// + private static int GetPolygonOrientation(ReadOnlySpan polygon) + { + float sum = 0f; + for (int i = 0; i < polygon.Length - 1; ++i) + { + PointF current = polygon[i]; + PointF next = polygon[i + 1]; + sum += (current.X * next.Y) - (next.X * current.Y); + } + + // A tolerant compare could be used here, but edge scanning does not special-case + // zero-area or near-zero-area input, so we keep this strict sign check. + return Math.Sign(sum); + } + + /// + /// Tests whether two line segments intersect, excluding collinear overlap cases. + /// + /// Start point of segment A. + /// End point of segment A. + /// Start point of segment B. + /// End point of segment B. + /// + /// Receives the intersection point when an intersection is found. + /// If no intersection is detected, the value is not modified. + /// + /// + /// when the segments intersect within their extents + /// (including endpoints); otherwise . + /// + /// + /// This solves the two segment equations in parametric form and accepts values in [0, 1] + /// with an epsilon margin for floating-point tolerance. + /// Parallel and collinear pairs are rejected early (cross product ~= 0). + /// + 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 the direction vectors. Near zero means parallel/collinear. + float crossD = (-dbx * day) + (dax * dby); + + // Reject parallel and collinear lines. Collinear overlap is intentionally not handled. + 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); + intersectionPoint.Y = a0.Y + (t * day); + return true; + } + + return false; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs b/src/ImageSharp.Drawing/IInternalPathOwner.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IInternalPathOwner.cs rename to src/ImageSharp.Drawing/IInternalPathOwner.cs diff --git a/src/ImageSharp.Drawing/Shapes/ILineSegment.cs b/src/ImageSharp.Drawing/ILineSegment.cs similarity index 82% rename from src/ImageSharp.Drawing/Shapes/ILineSegment.cs rename to src/ImageSharp.Drawing/ILineSegment.cs index 19d485e7d..d4c72e878 100644 --- a/src/ImageSharp.Drawing/Shapes/ILineSegment.cs +++ b/src/ImageSharp.Drawing/ILineSegment.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; @@ -16,18 +16,18 @@ public interface ILineSegment /// /// The end point. /// - PointF EndPoint { get; } + public PointF EndPoint { get; } /// /// Converts the into a simple linear path.. /// /// Returns the current as simple linear path. - ReadOnlyMemory Flatten(); + public ReadOnlyMemory Flatten(); /// /// Transforms the current LineSegment using specified matrix. /// /// The matrix. /// A line segment with the matrix applied to it. - ILineSegment Transform(Matrix3x2 matrix); + public ILineSegment Transform(Matrix4x4 matrix); } diff --git a/src/ImageSharp.Drawing/Shapes/IPath.cs b/src/ImageSharp.Drawing/IPath.cs similarity index 96% rename from src/ImageSharp.Drawing/Shapes/IPath.cs rename to src/ImageSharp.Drawing/IPath.cs index 4e8be5840..bd305e38e 100644 --- a/src/ImageSharp.Drawing/Shapes/IPath.cs +++ b/src/ImageSharp.Drawing/IPath.cs @@ -31,7 +31,7 @@ public interface IPath /// /// The matrix. /// A new path with the matrix applied to it. - public IPath Transform(Matrix3x2 matrix); + public IPath Transform(Matrix4x4 matrix); /// /// Returns this path with all figures closed. diff --git a/src/ImageSharp.Drawing/Shapes/IPathCollection.cs b/src/ImageSharp.Drawing/IPathCollection.cs similarity index 91% rename from src/ImageSharp.Drawing/Shapes/IPathCollection.cs rename to src/ImageSharp.Drawing/IPathCollection.cs index ef2834721..5d2780235 100644 --- a/src/ImageSharp.Drawing/Shapes/IPathCollection.cs +++ b/src/ImageSharp.Drawing/IPathCollection.cs @@ -20,5 +20,5 @@ public interface IPathCollection : IEnumerable /// /// The matrix. /// A new path collection with the matrix applied to it. - public IPathCollection Transform(Matrix3x2 matrix); + public IPathCollection Transform(Matrix4x4 matrix); } diff --git a/src/ImageSharp.Drawing/Shapes/IPathInternals.cs b/src/ImageSharp.Drawing/IPathInternals.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IPathInternals.cs rename to src/ImageSharp.Drawing/IPathInternals.cs diff --git a/src/ImageSharp.Drawing/Shapes/ISimplePath.cs b/src/ImageSharp.Drawing/ISimplePath.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/ISimplePath.cs rename to src/ImageSharp.Drawing/ISimplePath.cs diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 153c102b7..dc53c9127 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -13,6 +13,7 @@ An extension to ImageSharp that allows the drawing of images, paths, and text. Debug;Release true + @@ -25,7 +26,7 @@ enable Nullable - + @@ -38,15 +39,17 @@ - + - - - + + + + + diff --git a/src/ImageSharp.Drawing/Shapes/InnerJoin.cs b/src/ImageSharp.Drawing/InnerJoin.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/InnerJoin.cs rename to src/ImageSharp.Drawing/InnerJoin.cs diff --git a/src/ImageSharp.Drawing/InternalPath.cs b/src/ImageSharp.Drawing/InternalPath.cs new file mode 100644 index 000000000..f63b05831 --- /dev/null +++ b/src/ImageSharp.Drawing/InternalPath.cs @@ -0,0 +1,403 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Internal logic for integrating linear paths. +/// +internal class InternalPath +{ + /// + /// The epsilon for float comparison + /// + private const float Epsilon = 0.003f; + private const float Epsilon2 = 0.2f; + + /// + /// The points. + /// + private readonly PointData[] points; + + /// + /// Materialized points projected from . + /// + private PointF[]? materializedPoints; + + /// + /// The closed path. + /// + private readonly bool closedPath; + + /// + /// Initializes a new instance of the class. + /// + /// The segments. + /// if set to true [is closed path]. + /// Whether to remove close and collinear vertices + internal InternalPath(IReadOnlyList segments, bool isClosedPath, bool removeCloseAndCollinear = true) + : this(Simplify(segments, isClosedPath, removeCloseAndCollinear), isClosedPath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The segment. + /// if set to true [is closed path]. + internal InternalPath(ILineSegment segment, bool isClosedPath) + : this(segment?.Flatten() ?? Array.Empty(), isClosedPath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + /// if set to true [is closed path]. + internal InternalPath(ReadOnlyMemory points, bool isClosedPath) + : this(Simplify(points.Span, isClosedPath, true), isClosedPath) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The points. + /// if set to true [is closed path]. + private InternalPath(PointData[] points, bool isClosedPath) + { + this.points = points; + this.closedPath = isClosedPath; + + if (this.points.Length > 0) + { + float minX, minY, maxX, maxY, length; + length = 0; + minX = minY = float.MaxValue; + maxX = maxY = float.MinValue; + + foreach (PointData point in this.points) + { + length += point.Length; + minX = Math.Min(point.Point.X, minX); + minY = Math.Min(point.Point.Y, minY); + maxX = Math.Max(point.Point.X, maxX); + maxY = Math.Max(point.Point.Y, maxY); + } + + this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); + this.Length = length; + } + else + { + this.Bounds = RectangleF.Empty; + this.Length = 0; + } + } + + /// + /// Gets the bounds. + /// + /// + /// The bounds. + /// + public RectangleF Bounds { get; } + + /// + /// Gets the length. + /// + /// + /// The length. + /// + public float Length { get; } + + /// + /// Gets the length. + /// + public int PointCount => this.points.Length; + + /// + /// Gets the points. + /// + /// The + internal ReadOnlyMemory Points() => this.materializedPoints ??= this.CreatePoints(); + + /// + /// Calculates the point a certain distance a path. + /// + /// The distance along the path to find details of. + /// + /// Returns details about a point along a path. + /// + /// Thrown if no points found. + internal SegmentInfo PointAlongPath(float distanceAlongPath) + { + int pointCount = this.PointCount; + if (this.closedPath) + { + // Move the distance back to the beginning since this is a closed polygon. + distanceAlongPath %= this.Length; + pointCount--; + } + + for (int i = 0; i < pointCount; i++) + { + int next = WrapArrayIndex(i + 1, this.PointCount); + if (distanceAlongPath < this.points[next].Length) + { + float t = distanceAlongPath / this.points[next].Length; + Vector2 point = Vector2.Lerp(this.points[i].Point, this.points[next].Point, t); + Vector2 diff = this.points[i].Point - this.points[next].Point; + + return new SegmentInfo + { + Point = point, + Angle = (float)(Math.Atan2(diff.Y, diff.X) % (Math.PI * 2)) + }; + } + + distanceAlongPath -= this.points[next].Length; + } + + // Closed paths will never reach this point. + // For open paths we're going to create a new virtual point that extends past the path. + // The position and angle for that point are calculated based upon the last two points. + PointF a = this.points[Math.Max(this.points.Length - 2, 0)].Point; + PointF b = this.points[^1].Point; + Vector2 delta = a - b; + float angle = (float)(Math.Atan2(delta.Y, delta.X) % (Math.PI * 2)); + + Matrix4x4 transform = Matrix4x4.CreateRotationZ(angle - MathF.PI) * Matrix4x4.CreateTranslation(b.X, b.Y, 0); + + return new SegmentInfo + { + Point = PointF.Transform(new PointF(distanceAlongPath, 0), transform), + Angle = angle + }; + } + + // Modulo is a very slow operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength; + + private PointF[] CreatePoints() + { + PointF[] result = new PointF[this.points.Length]; + for (int i = 0; i < result.Length; i++) + { + result[i] = this.points[i].Point; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vector2 r) + { + // See http://www.geeksforgeeks.org/orientation-3-ordered-points/ + // for details of below formula. + Vector2 qp = q - p; + Vector2 rq = r - q; + float val = (qp.Y * rq.X) - (qp.X * rq.Y); + + if (val is > -Epsilon and < Epsilon) + { + return PointOrientation.Collinear; // colinear + } + + return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise + } + + /// + /// Simplifies the collection of segments. + /// + /// The segments. + /// Weather the path is closed or open. + /// Whether to remove close and collinear vertices + /// + /// The . + /// + private static PointData[] Simplify(IReadOnlyList segments, bool isClosed, bool removeCloseAndCollinear) + { + // Pre-compute capacity from cached flattened lengths to avoid List resizing. + int totalPoints = 0; + for (int s = 0; s < segments.Count; s++) + { + totalPoints += segments[s].Flatten().Length; + } + + List simplified = new(totalPoints); + + // Track indices where collinear direction reversals represent user-intended + // geometry: interior points of multi-point linear segments, and junction + // points between two linear segments (e.g. PathBuilder LineTo → LineTo). + // Reversals at all other indices (flattened curves, curve junctions) are + // artifacts and should be removed normally. + HashSet? linearReversalIndices = null; + ILineSegment? prevSeg = null; + + foreach (ILineSegment seg in segments) + { + int start = simplified.Count; + ReadOnlyMemory points = seg.Flatten(); + simplified.AddRange(points.Span); + + if (seg is LinearLineSegment) + { + // Interior points of a multi-point linear segment (e.g. DrawLine with 3+ points). + if (points.Length > 2) + { + linearReversalIndices ??= []; + for (int i = start + 1; i < start + points.Length - 1; i++) + { + _ = linearReversalIndices.Add(i); + } + } + + // Junction between two linear segments (e.g. PathBuilder LineTo → LineTo). + if (prevSeg is LinearLineSegment && start > 0) + { + linearReversalIndices ??= []; + _ = linearReversalIndices.Add(start); + } + } + + prevSeg = seg; + } + + return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear, linearReversalIndices); + } + + private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, bool removeCloseAndCollinear, HashSet? linearReversalIndices = null) + { + int polyCorners = points.Length; + if (polyCorners == 0) + { + return []; + } + + List results = new(polyCorners); + Vector2 lastPoint = points[0]; + + if (!isClosed) + { + results.Add(new PointData + { + Point = points[0], + Orientation = PointOrientation.Collinear, + Length = 0 + }); + } + else + { + int prev = polyCorners; + do + { + prev--; + if (prev == 0) + { + // All points are common, shouldn't match anything + results.Add( + new PointData + { + Point = points[0], + Orientation = PointOrientation.Collinear, + Length = 0, + }); + + return [.. results]; + } + } + while (removeCloseAndCollinear && Equivalent(points[0], points[prev], Epsilon2)); // skip points too close together + + polyCorners = prev + 1; + lastPoint = points[prev]; + + results.Add( + new PointData + { + Point = points[0], + Orientation = CalculateOrientation(lastPoint, points[0], points[1]), + Length = Vector2.Distance(lastPoint, points[0]), + }); + + lastPoint = points[0]; + } + + for (int i = 1; i < polyCorners; i++) + { + int next = WrapArrayIndex(i + 1, polyCorners); + PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]); + if (removeCloseAndCollinear && or == PointOrientation.Collinear && next != 0) + { + // Preserve collinear points that represent a direction reversal (U-turn) + // within a single segment. E.g. (10,10)→(90,10)→(20,10): the middle point + // is collinear but the stroker needs to see the reversal. + // Don't preserve reversals at segment boundaries — these arise from joining + // different path segments (e.g. arc-to-arc) and are not user-intended. + bool preserve = false; + if (linearReversalIndices == null || linearReversalIndices.Contains(i)) + { + Vector2 incoming = (Vector2)points[i] - lastPoint; + Vector2 outgoing = (Vector2)points[next] - (Vector2)points[i]; + float inLen = incoming.Length(); + float outLen = outgoing.Length(); + preserve = inLen > Epsilon && outLen > Epsilon && Vector2.Dot(incoming, outgoing) < 0; + } + + if (!preserve) + { + continue; + } + } + + results.Add( + new PointData + { + Point = points[i], + Orientation = or, + Length = Vector2.Distance(lastPoint, points[i]), + }); + lastPoint = points[i]; + } + + if (isClosed && removeCloseAndCollinear) + { + // walk back removing collinear points + while (results.Count > 2 && results[^1].Orientation == PointOrientation.Collinear) + { + results.RemoveAt(results.Count - 1); + } + } + + return [.. results]; + } + + /// + /// Merges the specified source2. + /// + /// The source1. + /// The source2. + /// The threshold. + /// + /// the Merged arrays + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool Equivalent(PointF source1, PointF source2, float threshold) + { + Vector2 abs = Vector2.Abs(source1 - source2); + return abs.X < threshold && abs.Y < threshold; + } + + private struct PointData + { + public PointF Point; + public PointOrientation Orientation; + public float Length; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/IntersectionRule.cs b/src/ImageSharp.Drawing/IntersectionRule.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/IntersectionRule.cs rename to src/ImageSharp.Drawing/IntersectionRule.cs diff --git a/src/ImageSharp.Drawing/Shapes/LineCap.cs b/src/ImageSharp.Drawing/LineCap.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/LineCap.cs rename to src/ImageSharp.Drawing/LineCap.cs diff --git a/src/ImageSharp.Drawing/Shapes/LineJoin.cs b/src/ImageSharp.Drawing/LineJoin.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/LineJoin.cs rename to src/ImageSharp.Drawing/LineJoin.cs diff --git a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs b/src/ImageSharp.Drawing/LinearLineSegment.cs similarity index 94% rename from src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs rename to src/ImageSharp.Drawing/LinearLineSegment.cs index f1170baf3..f410b7063 100644 --- a/src/ImageSharp.Drawing/Shapes/LinearLineSegment.cs +++ b/src/ImageSharp.Drawing/LinearLineSegment.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing; @@ -71,7 +72,7 @@ public LinearLineSegment(PointF[] points) /// /// A line segment with the matrix applied to it. /// - public LinearLineSegment Transform(Matrix3x2 matrix) + public LinearLineSegment Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -94,5 +95,5 @@ public LinearLineSegment Transform(Matrix3x2 matrix) /// /// The matrix. /// A line segment with the matrix applied to it. - ILineSegment ILineSegment.Transform(Matrix3x2 matrix) => this.Transform(matrix); + ILineSegment ILineSegment.Transform(Matrix4x4 matrix) => this.Transform(matrix); } diff --git a/src/ImageSharp.Drawing/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/OutlinePathExtensions.cs new file mode 100644 index 000000000..77f40996f --- /dev/null +++ b/src/ImageSharp.Drawing/OutlinePathExtensions.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.PolygonGeometry; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Extensions to that allow the generation of outlines. +/// +public static class OutlinePathExtensions +{ + private static readonly StrokeOptions DefaultOptions = new(); + + /// + /// Generates an outline of the path. + /// + /// The path to outline + /// The outline width. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width) + => GenerateOutline(path, width, DefaultOptions); + + /// + /// Generates an outline of the path. + /// + /// The path to outline + /// The outline width. + /// The stroke geometry options. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, StrokeOptions strokeOptions) + { + if (width <= 0) + { + return Path.Empty; + } + + return StrokedShapeGenerator.GenerateStrokedShapes(path, width, strokeOptions); + } + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline + /// The outline width. + /// The pattern made of multiples of the width. + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern) + => path.GenerateOutline(width, pattern, false); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline + /// The outline width. + /// The pattern made of multiples of the width. + /// The stroke geometry options. + /// A new representing the outline. + 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. + /// + /// The 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. + /// A new representing the outline. + 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. + /// + /// The path to outline + /// The outline width. + /// The pattern made of multiples of the width. + /// Whether the first item in the pattern is on or off. + /// The stroke geometry options. + /// A new representing the outline. + public static IPath GenerateOutline( + this IPath path, + float width, + ReadOnlySpan pattern, + bool startOff, + StrokeOptions strokeOptions) + { + if (width <= 0) + { + return Path.Empty; + } + + if (pattern.Length < 2) + { + return path.GenerateOutline(width, strokeOptions); + } + + IPath dashed = path.GenerateDashes(width, pattern, startOff); + + // GenerateDashes returns the original path when the pattern is degenerate + // or when segmentation would exceed safety limits; stroke it as solid. + if (ReferenceEquals(dashed, path)) + { + return path.GenerateOutline(width, strokeOptions); + } + + if (dashed == Path.Empty) + { + return Path.Empty; + } + + // Each dash segment is an open sub-path; stroke expansion and boolean merge + // are handled by the generator. + return StrokedShapeGenerator.GenerateStrokedShapes(dashed, width, strokeOptions); + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Path.cs b/src/ImageSharp.Drawing/Path.cs similarity index 91% rename from src/ImageSharp.Drawing/Shapes/Path.cs rename to src/ImageSharp.Drawing/Path.cs index 4ddf0c421..23bde31d0 100644 --- a/src/ImageSharp.Drawing/Shapes/Path.cs +++ b/src/ImageSharp.Drawing/Path.cs @@ -31,7 +31,7 @@ public Path(PointF[] points) /// /// The segments. public Path(IEnumerable segments) - : this(segments.ToArray()) + : this(GetSegmentArray(segments)) { } @@ -93,7 +93,7 @@ public Path(params ILineSegment[] segments) this.innerPath ??= new InternalPath(this.lineSegments, this.IsClosed, this.RemoveCloseAndCollinearPoints); /// - public virtual IPath Transform(Matrix3x2 matrix) + public virtual IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { @@ -135,6 +135,12 @@ SegmentInfo IPathInternals.PointAlongPath(float distance) IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() => this.internalPathRings ??= [this.InnerPath]; + private static ILineSegment[] GetSegmentArray(IEnumerable segments) + { + Guard.NotNull(segments, nameof(segments)); + return segments as ILineSegment[] ?? [.. segments]; + } + /// /// Converts an SVG path string into an . /// @@ -383,10 +389,42 @@ private static ReadOnlySpan FindScaler(ReadOnlySpan str, out float s str = TrimSeparator(str); scaler = 0; + bool hasDot = false; for (int i = 0; i < str.Length; i++) { - if (IsSeparator(str[i]) || i == str.Length) + char ch = str[i]; + + if (IsSeparator(ch)) + { + scaler = ParseFloat(str[..i]); + return str[i..]; + } + + if (ch == '.') + { + if (hasDot) + { + // Second decimal point starts a new number. + scaler = ParseFloat(str[..i]); + return str[i..]; + } + + hasDot = true; + } + else if ((ch is '-' or '+') && i > 0) + { + // A sign character mid-number starts a new number, + // unless it follows an exponent indicator. + char prev = str[i - 1]; + if (prev is not 'e' and not 'E') + { + scaler = ParseFloat(str[..i]); + return str[i..]; + } + } + else if (char.IsLetter(ch)) { + // Hit a command letter; end this number. scaler = ParseFloat(str[..i]); return str[i..]; } diff --git a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs b/src/ImageSharp.Drawing/PathBuilder.cs similarity index 95% rename from src/ImageSharp.Drawing/Shapes/PathBuilder.cs rename to src/ImageSharp.Drawing/PathBuilder.cs index c29510e4f..f18b0d92b 100644 --- a/src/ImageSharp.Drawing/Shapes/PathBuilder.cs +++ b/src/ImageSharp.Drawing/PathBuilder.cs @@ -12,17 +12,17 @@ namespace SixLabors.ImageSharp.Drawing; public class PathBuilder { private readonly List
figures = []; - private readonly Matrix3x2 defaultTransform; + private readonly Matrix4x4 defaultTransform; private Figure currentFigure; - private Matrix3x2 currentTransform; - private Matrix3x2 setTransform; + private Matrix4x4 currentTransform; + private Matrix4x4 setTransform; private Vector2 currentPoint; /// /// Initializes a new instance of the class. /// public PathBuilder() - : this(Matrix3x2.Identity) + : this(Matrix4x4.Identity) { } @@ -30,7 +30,7 @@ public PathBuilder() /// Initializes a new instance of the class. /// /// The default transform. - public PathBuilder(Matrix3x2 defaultTransform) + public PathBuilder(Matrix4x4 defaultTransform) { this.defaultTransform = defaultTransform; this.Clear(); @@ -41,19 +41,19 @@ public PathBuilder(Matrix3x2 defaultTransform) /// Gets the current transformation matrix. /// /// - /// Returns a copy of the matrix. Because is a value type, + /// Returns a copy of the matrix. Because is a value type, /// modifications to the returned value do not affect the internal state. To change the transform, - /// call . + /// call . /// /// The current transformation matrix. - public Matrix3x2 Transform => this.currentTransform; + public Matrix4x4 Transform => this.currentTransform; /// /// Sets the translation to be applied to all items to follow being applied to the . /// /// The transform. /// The . - public PathBuilder SetTransform(Matrix3x2 transform) + public PathBuilder SetTransform(Matrix4x4 transform) { this.setTransform = transform; this.currentTransform = this.setTransform * this.defaultTransform; @@ -68,7 +68,7 @@ public PathBuilder SetTransform(Matrix3x2 transform) public PathBuilder SetOrigin(PointF origin) { // The new origin should be transformed based on the default transform - this.setTransform.Translation = origin; + this.setTransform.Translation = new Vector3(origin.X, origin.Y, 0); this.currentTransform = this.setTransform * this.defaultTransform; return this; @@ -80,7 +80,7 @@ public PathBuilder SetOrigin(PointF origin) /// The . public PathBuilder ResetTransform() { - this.setTransform = Matrix3x2.Identity; + this.setTransform = Matrix4x4.Identity; this.currentTransform = this.setTransform * this.defaultTransform; return this; @@ -92,7 +92,7 @@ public PathBuilder ResetTransform() /// The . public PathBuilder ResetOrigin() { - this.setTransform.Translation = Vector2.Zero; + this.setTransform.Translation = Vector3.Zero; this.currentTransform = this.setTransform * this.defaultTransform; return this; @@ -110,6 +110,15 @@ public PathBuilder MoveTo(PointF point) return this; } + /// + /// Moves to current point to the supplied vector. + /// + /// The x-coordinate. + /// The y-coordinate. + /// The + public PathBuilder MoveTo(float x, float y) + => this.MoveTo(new PointF(x, y)); + /// /// Draws the line connecting the current the current point to the new point. /// @@ -456,7 +465,7 @@ public PathBuilder Reset() /// /// Clears all drawn paths, Leaving any applied transforms. /// - [MemberNotNull(nameof(this.currentFigure))] + [MemberNotNull(nameof(currentFigure))] public void Clear() { this.currentFigure = new Figure(); @@ -476,7 +485,7 @@ private class Figure public IPath Build() => this.IsClosed - ? new Polygon(this.segments.ToArray(), true) + ? new Polygon([.. this.segments], true) : new Path(this.segments.ToArray()); } } diff --git a/src/ImageSharp.Drawing/Shapes/PathCollection.cs b/src/ImageSharp.Drawing/PathCollection.cs similarity index 89% rename from src/ImageSharp.Drawing/Shapes/PathCollection.cs rename to src/ImageSharp.Drawing/PathCollection.cs index 9ae4bc739..ebbdb299e 100644 --- a/src/ImageSharp.Drawing/Shapes/PathCollection.cs +++ b/src/ImageSharp.Drawing/PathCollection.cs @@ -20,7 +20,7 @@ public class PathCollection : IPathCollection /// /// The collection of paths public PathCollection(IEnumerable paths) - : this(paths.ToArray()) + : this(GetPathArray(paths)) { } @@ -63,7 +63,7 @@ private RectangleF CalcBounds() public IEnumerator GetEnumerator() => ((IEnumerable)this.paths).GetEnumerator(); /// - public IPathCollection Transform(Matrix3x2 matrix) + public IPathCollection Transform(Matrix4x4 matrix) { IPath[] result = new IPath[this.paths.Length]; @@ -77,4 +77,10 @@ public IPathCollection Transform(Matrix3x2 matrix) /// IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.paths).GetEnumerator(); + + private static IPath[] GetPathArray(IEnumerable paths) + { + Guard.NotNull(paths, nameof(paths)); + return paths as IPath[] ?? [.. paths]; + } } diff --git a/src/ImageSharp.Drawing/PathExtensions.Internal.cs b/src/ImageSharp.Drawing/PathExtensions.Internal.cs new file mode 100644 index 000000000..40329b910 --- /dev/null +++ b/src/ImageSharp.Drawing/PathExtensions.Internal.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Convenience methods that can be applied to shapes and paths. +/// +public static partial class PathExtensions +{ + /// + /// Create a path with the segment order reversed. + /// + /// The path to reverse. + /// The reversed . + internal static IPath Reverse(this IPath path) + { + // TODO. Make this a void. We can reverse the segments in place and then reverse the points in place as well. + IEnumerable segments = path.Flatten().Select(static p => new LinearLineSegment(ReversePoints(p.Points.Span))); + bool closed = false; + if (path is ISimplePath sp) + { + closed = sp.IsClosed; + } + + return closed ? new Polygon(segments) : new Path(segments); + } + + private static PointF[] ReversePoints(ReadOnlySpan points) + { + PointF[] reversed = new PointF[points.Length]; + for (int i = 0; i < reversed.Length; i++) + { + reversed[i] = points[points.Length - 1 - i]; + } + + return reversed; + } +} diff --git a/src/ImageSharp.Drawing/Shapes/PathExtensions.cs b/src/ImageSharp.Drawing/PathExtensions.cs similarity index 91% rename from src/ImageSharp.Drawing/Shapes/PathExtensions.cs rename to src/ImageSharp.Drawing/PathExtensions.cs index a2cffd2c5..a0dcd38ca 100644 --- a/src/ImageSharp.Drawing/Shapes/PathExtensions.cs +++ b/src/ImageSharp.Drawing/PathExtensions.cs @@ -17,7 +17,7 @@ public static partial class PathExtensions /// The radians to rotate the path. /// A with a rotate transform applied. public static IPathCollection Rotate(this IPathCollection path, float radians) - => path.Transform(Matrix3x2Extensions.CreateRotation(radians, RectangleF.Center(path.Bounds))); + => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); /// /// Creates a path rotated by the specified degrees around its center. @@ -35,7 +35,7 @@ public static IPathCollection RotateDegree(this IPathCollection shape, float deg /// The translation position. /// A with a translate transform applied. public static IPathCollection Translate(this IPathCollection path, PointF position) - => path.Transform(Matrix3x2.CreateTranslation(position)); + => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); /// /// Creates a path translated by the supplied position @@ -55,7 +55,7 @@ public static IPathCollection Translate(this IPathCollection path, float x, floa /// The amount to scale along the Y axis. /// A with a translate transform applied. public static IPathCollection Scale(this IPathCollection path, float scaleX, float scaleY) - => path.Transform(Matrix3x2.CreateScale(scaleX, scaleY, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Creates a path translated by the supplied position @@ -64,7 +64,7 @@ public static IPathCollection Scale(this IPathCollection path, float scaleX, flo /// The amount to scale along both the x and y axis. /// A with a translate transform applied. public static IPathCollection Scale(this IPathCollection path, float scale) - => path.Transform(Matrix3x2.CreateScale(scale, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Creates a path rotated by the specified radians around its center. @@ -73,7 +73,7 @@ public static IPathCollection Scale(this IPathCollection path, float scale) /// The radians to rotate the path. /// A with a rotate transform applied. public static IPath Rotate(this IPath path, float radians) - => path.Transform(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds))); + => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); /// /// Creates a path rotated by the specified degrees around its center. @@ -91,7 +91,7 @@ public static IPath RotateDegree(this IPath shape, float degree) /// The translation position. /// A with a translate transform applied. public static IPath Translate(this IPath path, PointF position) - => path.Transform(Matrix3x2.CreateTranslation(position)); + => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); /// /// Creates a path translated by the supplied position @@ -111,7 +111,7 @@ public static IPath Translate(this IPath path, float x, float y) /// The amount to scale along the Y axis. /// A with a translate transform applied. public static IPath Scale(this IPath path, float scaleX, float scaleY) - => path.Transform(Matrix3x2.CreateScale(scaleX, scaleY, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Creates a path translated by the supplied position @@ -120,7 +120,7 @@ public static IPath Scale(this IPath path, float scaleX, float scaleY) /// The amount to scale along both the x and y axis. /// A with a translate transform applied. public static IPath Scale(this IPath path, float scale) - => path.Transform(Matrix3x2.CreateScale(scale, RectangleF.Center(path.Bounds))); + => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// /// Calculates the approximate length of the path as though each segment were unrolled into a line. diff --git a/src/ImageSharp.Drawing/Shapes/PathTypes.cs b/src/ImageSharp.Drawing/PathTypes.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PathTypes.cs rename to src/ImageSharp.Drawing/PathTypes.cs diff --git a/src/ImageSharp.Drawing/Shapes/PointOrientation.cs b/src/ImageSharp.Drawing/PointOrientation.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/PointOrientation.cs rename to src/ImageSharp.Drawing/PointOrientation.cs diff --git a/src/ImageSharp.Drawing/Shapes/Polygon.cs b/src/ImageSharp.Drawing/Polygon.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/Polygon.cs rename to src/ImageSharp.Drawing/Polygon.cs index e928d32e6..ef9e690e9 100644 --- a/src/ImageSharp.Drawing/Shapes/Polygon.cs +++ b/src/ImageSharp.Drawing/Polygon.cs @@ -24,7 +24,7 @@ public Polygon(PointF[] points) /// /// The segments. public Polygon(params ILineSegment[] segments) - : base(segments.ToArray()) + : base(segments) { } @@ -78,7 +78,7 @@ internal Polygon(ILineSegment[] segments, bool owned) public override bool IsClosed => true; /// - public override IPath Transform(Matrix3x2 matrix) + public override IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs similarity index 98% rename from src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs rename to src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs index d423b57aa..6f2e36f75 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClippedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs @@ -5,7 +5,7 @@ using PCPolygon = SixLabors.PolygonClipper.Polygon; using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; /// /// Generates clipped shapes from one or more input paths using polygon boolean operations. diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs rename to src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs index dfe11f4d4..454820488 100644 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/PolygonClipperFactory.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs @@ -4,7 +4,7 @@ using SixLabors.PolygonClipper; using PCPolygon = SixLabors.PolygonClipper.Polygon; -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; /// /// Builders for from ImageSharp paths. diff --git a/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs new file mode 100644 index 000000000..d93f0fd6d --- /dev/null +++ b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs @@ -0,0 +1,116 @@ +// 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.PolygonGeometry; + +/// +/// Generates stroked and merged shapes using polygon stroking and boolean clipping. +/// +internal static class StrokedShapeGenerator +{ + /// + /// 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, + 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, + }, + NormalizeOutput = options.NormalizeOutput + }; + + return o; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs new file mode 100644 index 000000000..2714017f2 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Frame adapter that exposes a clipped subregion of another frame. +/// +/// The pixel format. +internal sealed class CanvasRegionFrame : ICanvasFrame + where TPixel : unmanaged, IPixel +{ + private readonly ICanvasFrame parent; + private readonly Rectangle region; + + public CanvasRegionFrame(ICanvasFrame parent, Rectangle region) + { + Guard.NotNull(parent, nameof(parent)); + Guard.MustBeGreaterThanOrEqualTo(region.Width, 0, nameof(region)); + Guard.MustBeGreaterThanOrEqualTo(region.Height, 0, nameof(region)); + this.parent = parent; + this.region = region; + } + + public Rectangle Bounds => new( + this.parent.Bounds.X + this.region.X, + this.parent.Bounds.Y + this.region.Y, + this.region.Width, + this.region.Height); + + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + if (!this.parent.TryGetCpuRegion(out Buffer2DRegion parentRegion)) + { + region = default; + return false; + } + + region = parentRegion.GetSubRegion(this.region); + return true; + } + + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + => this.parent.TryGetNativeSurface(out surface); +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs new file mode 100644 index 000000000..2d2bc0244 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -0,0 +1,376 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies the flush-time operation carried by a . +/// +public enum CompositionCommandKind : byte +{ + /// + /// A prepared fill or stroke command. + /// + FillLayer = 0, + + /// + /// Starts an isolated compositing layer. + /// + BeginLayer = 1, + + /// + /// Ends the most recently opened layer. + /// + EndLayer = 2 +} + +/// +/// One normalized composition command queued by . +/// After is called by the batcher, every +/// command is an immutable prepared fill ready for backend execution. +/// +public struct CompositionCommand +{ + private static readonly Brush SentinelBrush = new SolidBrush(Color.Transparent); + private static readonly GraphicsOptions SentinelGraphicsOptions = new(); + private static readonly ShapeOptions SentinelShapeOptions = new(); + + private readonly Pen? pen; + private readonly IPath sourcePath; + private readonly Matrix4x4 transform; + private readonly IReadOnlyList? clipPaths; + private readonly ShapeOptions shapeOptions; + + private CompositionCommand( + CompositionCommandKind kind, + int definitionKey, + IPath sourcePath, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Rectangle layerBounds, + Point destinationOffset, + Pen? pen, + Matrix4x4 transform, + IReadOnlyList? clipPaths, + ShapeOptions shapeOptions) + { + this.Kind = kind; + this.DefinitionKey = definitionKey; + this.sourcePath = sourcePath; + this.PreparedPath = null; + this.IsVisible = false; + this.Brush = brush; + this.GraphicsOptions = graphicsOptions; + this.RasterizerOptions = rasterizerOptions; + this.TargetBounds = targetBounds; + this.LayerBounds = layerBounds; + this.DestinationOffset = destinationOffset; + this.DestinationRegion = default; + this.SourceOffset = default; + this.pen = pen; + this.transform = transform; + this.clipPaths = clipPaths; + this.shapeOptions = shapeOptions; + } + + /// + /// Gets the command kind. + /// + public CompositionCommandKind Kind { get; } + + /// + /// Gets a stable definition key used for composition-level caching. + /// Recomputed by after path replacement. + /// + public int DefinitionKey { get; private set; } + + /// + /// Gets the prepared path to rasterize in target-local coordinates. + /// This is the post-transform, post-stroke, post-clip path populated by . + /// Backends walk this path directly to produce their native rasterization format. + /// + public IPath? PreparedPath { get; private set; } + + /// + /// Gets a value indicating whether this command is visible after clipping to its logical target. + /// Populated by . + /// + public bool IsVisible { get; private set; } + + /// + /// Gets the absolute bounds of the logical target for this command. + /// For fills this is the command target frame; for begin-layer commands this is the layer bounds. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the absolute bounds of the layer opened by this command. + /// Only meaningful for . + /// + public Rectangle LayerBounds { get; } + + /// + /// Gets the destination region in target-local coordinates. + /// Populated by . + /// + public Rectangle DestinationRegion { get; private set; } + + /// + /// Gets the source offset into the coverage map. + /// Populated by . + /// + public Point SourceOffset { get; private set; } + + /// + /// Gets the brush used during composition. + /// After this is transformed to match the path coordinate space. + /// + public Brush Brush { get; private set; } + + /// + /// Gets brush bounds used for applicator creation. + /// + public Rectangle BrushBounds { get; private set; } + + /// + /// Gets graphics options used for composition or layer compositing. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets rasterizer options used to generate coverage. + /// After the interest rect reflects the final path bounds. + /// + public RasterizerOptions RasterizerOptions { get; private set; } + + /// + /// Gets the absolute destination offset where the local coverage should be composited. + /// + public Point DestinationOffset { get; } + + /// + /// Creates a fill composition command. + /// + /// Path in target-local coordinates. + /// Brush used during composition. + /// Graphics options used for composition. + /// Rasterizer options used to generate coverage. + /// Shape options for clip operations. + /// Transform matrix to apply during preparation. + /// The absolute bounds of the logical target for this command. + /// Absolute destination offset where coverage is composited. + /// Optional pen for stroke commands. The batcher expands strokes to fills. + /// Optional clip paths to apply during preparation. + /// The composition command. + public static CompositionCommand Create( + IPath path, + Brush brush, + GraphicsOptions graphicsOptions, + in RasterizerOptions rasterizerOptions, + ShapeOptions shapeOptions, + Matrix4x4 transform, + Rectangle targetBounds, + Point destinationOffset = default, + Pen? pen = null, + IReadOnlyList? clipPaths = null) + { + int definitionKey = ComputeCoverageDefinitionKey(path, in rasterizerOptions); + + return new( + CompositionCommandKind.FillLayer, + definitionKey, + path, + brush, + graphicsOptions, + in rasterizerOptions, + targetBounds, + default, + destinationOffset, + pen, + transform, + clipPaths, + shapeOptions); + } + + /// + /// Creates a begin-layer composition command. + /// + /// The absolute bounds of the layer. + /// The compositing options used when the layer closes. + /// The begin-layer command. + public static CompositionCommand CreateBeginLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions) + => new( + CompositionCommandKind.BeginLayer, + 0, + EmptyPath.ClosedPath, + SentinelBrush, + graphicsOptions, + default, + layerBounds, + layerBounds, + default, + null, + Matrix4x4.Identity, + null, + SentinelShapeOptions); + + /// + /// Creates an end-layer composition command. + /// + /// The end-layer command. + public static CompositionCommand CreateEndLayer() + => new( + CompositionCommandKind.EndLayer, + 0, + EmptyPath.ClosedPath, + SentinelBrush, + SentinelGraphicsOptions, + default, + default, + default, + default, + null, + Matrix4x4.Identity, + null, + SentinelShapeOptions); + + /// + /// Prepares this command for backend execution. Expands strokes to fills, + /// clips, transforms the source path, and clips to the logical target. + /// After this call the command is a fill with an immutable prepared path + /// and pre-computed visibility against the target. + /// + internal void Prepare() + { + if (this.Kind is not CompositionCommandKind.FillLayer) + { + this.IsVisible = true; + return; + } + + // Expand the queued draw into the final fill geometry the backend will consume. + IPath preparedPath = this.BuildPreparedPath(); + this.PreparedPath = preparedPath; + + // Transform the brush to match the path coordinate space. + if (!this.transform.IsIdentity) + { + this.Brush = this.Brush.Transform(this.transform); + } + + // Recompute interest, brush bounds, and definition key from the final path. + RasterizerOptions old = this.RasterizerOptions; + RectangleF bounds = preparedPath.Bounds; + + // Pixel-center stroke sampling nudges the realized bounds by half a pixel. + if (old.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + } + + // Coverage is generated in path-local interest coordinates. + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + this.RasterizerOptions = new RasterizerOptions( + interest, + old.IntersectionRule, + old.RasterizationMode, + old.SamplingOrigin, + old.AntialiasThreshold); + + this.BrushBounds = new Rectangle( + interest.X + this.DestinationOffset.X, + interest.Y + this.DestinationOffset.Y, + interest.Width, + interest.Height); + + RasterizerOptions updated = this.RasterizerOptions; + this.DefinitionKey = ComputeCoverageDefinitionKey(preparedPath, in updated); + + // Move the interest rect into absolute destination space, then clip it back to the command target. + Rectangle commandDestination = new( + this.DestinationOffset.X + interest.X, + this.DestinationOffset.Y + interest.Y, + interest.Width, + interest.Height); + + Rectangle clippedDestination = Rectangle.Intersect(this.TargetBounds, commandDestination); + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + this.IsVisible = false; + return; + } + + // DestinationRegion is target-local. SourceOffset keeps coverage aligned after clipping. + this.DestinationRegion = new Rectangle( + clippedDestination.X - this.TargetBounds.X, + clippedDestination.Y - this.TargetBounds.Y, + clippedDestination.Width, + clippedDestination.Height); + + this.SourceOffset = new Point( + clippedDestination.X - commandDestination.X, + clippedDestination.Y - commandDestination.Y); + + this.IsVisible = true; + } + + /// + /// Builds the prepared path for this command without consulting any external cache. + /// Applies transform, stroke expansion, and clipping. The returned path is ready + /// for backends to walk directly via . + /// + /// The prepared path. + internal readonly IPath BuildPreparedPath() + { + IPath path = this.sourcePath; + + // Transform to world space once so subsequent stroke and clip work operate in final coordinates. + if (!this.transform.IsIdentity) + { + path = path.Transform(this.transform); + } + + // Stroke commands are lowered to fills before clipping and rasterization. + if (this.pen is not null) + { + path = this.pen.GeneratePath(path); + } + + // Clip — path and clip paths are both interpreted in the prepared command space. + if (this.clipPaths is { Count: > 0 }) + { + path = path.Clip(this.shapeOptions, this.clipPaths); + } + + return path; + } + + /// + /// Computes a coverage definition key from path identity and rasterization state. + /// + /// Path to rasterize. + /// Rasterizer options used for coverage generation. + /// A stable key for coverage-equivalent commands. + public static int ComputeCoverageDefinitionKey( + IPath path, + in RasterizerOptions rasterizerOptions) + { + int pathIdentity = RuntimeHelpers.GetHashCode(path); + int rasterState = HashCode.Combine( + rasterizerOptions.Interest.Size, + (int)rasterizerOptions.IntersectionRule, + (int)rasterizerOptions.RasterizationMode, + (int)rasterizerOptions.SamplingOrigin); + return HashCode.Combine(pathIdentity, rasterState); + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommandPreparer.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommandPreparer.cs new file mode 100644 index 000000000..682f9d355 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommandPreparer.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Clips and normalizes composition commands for backend execution. +/// +public static class CompositionCommandPreparer +{ + /// + /// Clips one scene command to target bounds and computes coverage source offset mapping. + /// + /// The source command. + /// Target frame bounds in absolute coordinates. + /// Prepared command when clipping produces visible output. + /// when the command has visible output in target bounds. + public static bool TryPrepareCommand( + in CompositionCommand command, + in Rectangle targetBounds, + out PreparedCompositionCommand prepared) + { + Rectangle interest = command.RasterizerOptions.Interest; + Rectangle commandDestination = new( + command.DestinationOffset.X + interest.X, + command.DestinationOffset.Y + interest.Y, + interest.Width, + interest.Height); + + Rectangle clippedDestination = Rectangle.Intersect(targetBounds, commandDestination); + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + prepared = default; + return false; + } + + Rectangle destinationLocalRegion = new( + clippedDestination.X - targetBounds.X, + clippedDestination.Y - targetBounds.Y, + clippedDestination.Width, + clippedDestination.Height); + + Point sourceOffset = new( + clippedDestination.X - commandDestination.X, + clippedDestination.Y - commandDestination.Y); + + prepared = new PreparedCompositionCommand( + destinationLocalRegion, + sourceOffset, + command.Brush, + command.BrushBounds, + command.GraphicsOptions, + command.DestinationOffset); + + return true; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs new file mode 100644 index 000000000..3b3fb73eb --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCoverageDefinition.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// One coverage definition that can be rasterized once and reused by multiple composition commands. +/// +public readonly struct CompositionCoverageDefinition +{ + /// + /// Initializes a new instance of the struct. + /// + /// The stable key for this coverage definition. + /// The prepared path used to generate coverage. + /// The rasterizer options used to generate coverage. + /// The absolute destination offset where coverage is composited. + public CompositionCoverageDefinition( + int definitionKey, + IPath preparedPath, + in RasterizerOptions rasterizerOptions, + Point destinationOffset = default) + { + this.DefinitionKey = definitionKey; + this.PreparedPath = preparedPath; + this.RasterizerOptions = rasterizerOptions; + this.DestinationOffset = destinationOffset; + } + + /// + /// Gets the stable key for this coverage definition. + /// + public int DefinitionKey { get; } + + /// + /// Gets the prepared path used to generate coverage. + /// + public IPath PreparedPath { get; } + + /// + /// Gets the rasterizer options used to generate coverage. + /// + public RasterizerOptions RasterizerOptions { get; } + + /// + /// Gets the absolute destination offset where coverage is composited. + /// + public Point DestinationOffset { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs new file mode 100644 index 000000000..926f6de6a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionScene.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// One flush-time scene packet containing normalized composition commands in draw order. +/// +public sealed class CompositionScene +{ + /// + /// Initializes a new instance of the class. + /// + /// The composition commands in submission order. + /// Indicates whether the command stream contains layer boundaries. + public CompositionScene(IReadOnlyList commands, bool hasLayers) + { + this.Commands = commands; + this.HasLayers = hasLayers; + } + + /// + /// Gets normalized composition commands in submission order. + /// + public IReadOnlyList Commands { get; } + + /// + /// Gets a value indicating whether this scene contains inline layer commands. + /// + public bool HasLayers { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs deleted file mode 100644 index cd2c22ed1..000000000 --- a/src/ImageSharp.Drawing/Processing/Backends/CpuDrawingBackend.cs +++ /dev/null @@ -1,333 +0,0 @@ -// 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/DEFAULT_DRAWING_BACKEND.md b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md new file mode 100644 index 000000000..1eeaecf22 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md @@ -0,0 +1,474 @@ +# DefaultDrawingBackend + +`DefaultDrawingBackend` is the CPU drawing backend for ImageSharp.Drawing. It is responsible for taking a flush worth of prepared drawing commands, converting them into row-local raster work, and composing the resulting coverage into a CPU pixel buffer. + +This document explains the current CPU backend: + +- a flush-scoped `FlushScene` +- row-first execution +- row-local raster payloads +- reusable worker-local raster and brush scratch + +The goal is to make the code readable as a system, not just as a collection of methods. + +## 1. What The Backend Does + +At a high level, the CPU backend has three jobs: + +1. Accept a `CompositionScene` from `DrawingCanvas`. +2. Build a flush-scoped execution plan that is cheap to traverse in parallel. +3. Rasterize and compose that plan into a CPU destination frame. + +`DefaultDrawingBackend` itself is intentionally small. Most of the real work lives in two places: + +- `FlushScene.cs` +- `DefaultRasterizer.cs` + +That split is deliberate: + +- `DefaultDrawingBackend` owns backend policy. +- `FlushScene` owns flush-local scheduling and prepared row data. +- `DefaultRasterizer` owns scan conversion and coverage emission. + +## 2. End-To-End Flow + +The full CPU fill path looks like this: + +```mermaid +flowchart TD + A[DrawingCanvas.Flush] --> B[DefaultDrawingBackend.FlushCompositions] + B --> C[target.TryGetCpuRegion] + B --> D[FlushScene.Create] + D --> E[Prepare and clip commands] + E --> F[Build row membership] + F --> G[Prebuild rasterizable row bands] + G --> H[FlushScene.Execute] + H --> I[Create BrushRenderer per command] + I --> J[Parallel.For over scene rows] + J --> K[DefaultRasterizer.ExecuteRasterizableBand] + K --> L[Emit coverage rows] + L --> M[BrushRenderer.Apply] + M --> N[Destination pixels updated] +``` + +There are two important ideas in that flow: + +- the execution unit is a flush-scoped row scene, not a batch of commands sharing a coverage definition +- the rasterizer consumes row-local prepared data, not raw paths + +## 3. DefaultDrawingBackend.cs + +`DefaultDrawingBackend` is intentionally thin. + +### `FlushCompositions` + +`FlushCompositions()` does four things: + +1. It returns immediately when there are no commands. +2. It asks the target for a CPU region. +3. It creates a `FlushScene` from the scene commands. +4. It executes that scene against the destination frame. + +In other words, the backend delegates flush-local planning and execution to `FlushScene`. + +### `ComposeLayer` + +`ComposeLayer()` is separate from shape rasterization. It composites one CPU frame into another using `PixelBlender` and a reusable `amounts` buffer filled with `BlendPercentage`. + +This is the backend's layer-composition path, not the path/shape fill path. + +### `CreateLayerFrame`, `ReleaseFrameResources`, `TryReadRegion` + +These methods are backend services: + +- allocate a CPU frame +- release CPU-backed frame resources +- copy a region out of a CPU target + +They are not part of the raster architecture, but they are part of the backend contract. + +## 4. The Flush Scene + +`FlushScene` is the real execution model for the CPU backend. + +It exists only for the lifetime of one flush and owns: + +- the visible prepared commands +- per-command row-band membership +- prebuilt row-local raster payloads +- row-major execution order +- scene-wide scratch size maxima + +That means the expensive, shape-dependent preparation work happens once per flush, then execution can focus on scan conversion and composition. + +### Why A Flush-Scoped Scene Exists + +Without a flush scene, the backend would have to rediscover the same facts repeatedly: + +- which rows a command touches +- which segments matter for a given row band +- how large worker scratch must be +- how to traverse commands in row order while preserving submission order + +`FlushScene.Create()` centralizes that work. + +## 5. FlushScene.Create In Detail + +The builder runs in several phases. Each phase changes the shape of the data so the next phase can be cheaper. + +```mermaid +flowchart LR + A[CompositionCommand] --> B[Prepare and clip] + + B --> C[Compact visible items] + C --> D[Count segment refs per band] + D --> E[Fill dense segment-ref table] + E --> F[Build row counts and row offsets] + F --> G[Create row-major item list] + G --> H[Prebuild RasterizableBandInfo + line/start-cover storage] + H --> I[FlushScene] +``` + +### Phase 1: Prepare And Clip Commands + +`FlushScene.Create()` calls `CompositionCommandPreparer.TryPrepareCommand(...)` for each command. + +Despite the type name, in the CPU path this API is used as a command-normalization helper: + +- reject invisible commands +- compute clipped destination region +- expose prepared geometry + +This phase is embarrassingly parallel, so it runs across command ranges with `Parallel.ForEach`. + +### Phase 2: Compact Visible Items + +The preparation pass writes into fixed-size temporary arrays indexed by original command position. The next phase compacts only visible commands into dense arrays: + +- `preparedCommands` +- `geometries` +- `rasterizerOptions` +- destination offsets +- band ranges + +This matters because the execution path should not keep paying for invisible commands through sparse scans and conditional branches. + +### Phase 3: Compute Scene-Wide Maxima + +The scene computes: + +- maximum item width +- maximum bit-vector width +- maximum cover stride +- maximum band capacity + +Those values drive worker scratch sizing. Each worker can allocate once and reuse the same buffers for the whole flush. + +### Phase 4: Count Segment References Per Band + +For each visible geometry, the scene counts how many prepared line segments touch each local row band. + +This is not rasterization yet. It is an indexing pass. + +The important output is a dense offset table for: + +- item -> local band -> segment reference range + +Once that exists, later phases can read band membership directly from the dense offset table. + +### Phase 5: Materialize Dense Segment References + +The scene allocates one dense `int[]` of segment indices and fills it so each item's band range points into a contiguous slice of that array. + +Conceptually: + +```text +item 0 + band 0 -> segmentIndices[0..7) + band 1 -> segmentIndices[7..11) + +item 1 + band 0 -> segmentIndices[11..14) +``` + +This is the bridge from prepared geometry to row-local raster preparation. + +### Phase 6: Build Row Membership + +The scene converts item-local band ranges into scene-row counts, then into prefix-summed `rowOffsets`. + +That produces a classic CSR-style row structure: + +- `rowOffsets[row]` +- `rowOffsets[row + 1]` + +Everything between those offsets belongs to that scene row. + +### Phase 7: Build Row-Major Execution Order + +The builder creates `PendingRowItem[]` in row-major order. + +Each pending item records: + +- which command it belongs to +- which local band it represents +- where its segment refs begin +- how many segment refs it owns +- the clipped destination region for that band + +Submission order is preserved inside each row. That is critical because composition still has to respect draw order. + +### Phase 8: Prebuild Rasterizable Bands + +Finally, the scene allocates flush-owned storage for: + +- `RasterLineData` +- start-cover seeds +- `RasterizableBandInfo` + +Then it calls `DefaultRasterizer.TryBuildRasterizableBand(...)` once per row item. + +This is where the scene becomes execution-ready. After this point, row execution can consume prebuilt line lists and left-of-band winding seeds directly. + +## 6. What A Row Item Actually Contains + +Each `RowItem` is small on purpose. It contains: + +- `ItemIndex` +- `LineStart` +- `StartCoverStart` +- `DestinationRegion` + +The item does not own its own arrays. Instead it points into large flush-owned buffers: + +- one dense `RasterLineData` store +- one dense start-cover store + +That layout reduces per-item allocation churn and keeps traversal cache-friendly. + +## 7. Rasterizable Bands + +The fill rasterizer operates on a row-local representation rather than on a general scene-wide edge table. + +`RasterizableBandInfo` describes one band: + +- line count +- band height +- width +- bit-vector width +- cover stride +- destination top +- fill rule +- raster mode +- antialias threshold +- whether the band has non-zero start covers + +`RasterizableBand` is the execution view: + +- `ReadOnlySpan Lines` +- `ReadOnlySpan StartCovers` +- the raster metadata above + +This separation matters: + +- `RasterizableBandInfo` is storable +- `RasterizableBand` is a cheap span-based view built on demand + +## 8. DefaultRasterizer: Fill Path + +The fill rasterizer has two distinct steps: + +1. build a rasterizable band +2. execute a rasterizable band + +### 8.1 Band Building + +`TryBuildRasterizableBand(...)` converts prepared line segments into row-local raster data. + +For each candidate segment in the band: + +1. translate from geometry space into interest-local coordinates +2. apply sampling origin offset +3. clip vertically to the band +4. convert to 24.8 fixed point +5. send left-of-visible-X coverage into start covers +6. clip visible portions horizontally to the band +7. emit a `RasterLineData` record for the visible part + +The key idea is step 5. + +Segments that begin left of the visible X range still affect winding. Instead of keeping those off-screen edges around forever, the builder folds their effect into `StartCovers`. Execution can then begin with the correct carry state and only process visible lines. + +### 8.2 Band Execution + +`ExecuteRasterizableBand(...)` is the hot scan-conversion entrypoint. + +It does four things: + +1. `Context.Reconfigure(...)` +2. `Context.SeedStartCovers(...)` +3. `Context.RasterizePreparedLines(...)` +4. `Context.EmitCoverageRows(...)` + +Then it resets touched rows so the same scratch can be reused for the next band. + +## 9. The Scanner Context + +`DefaultRasterizer.Context` is the mutable fixed-point scanner state. It is a `ref struct` so the hot-path spans stay stack-bound. + +The important buffers are: + +- `bitVectors`: sparse touched-column tracking +- `coverArea`: winding delta and area accumulation +- `startCover`: left-of-band winding seeds +- `rowMinTouchedColumn` / `rowMaxTouchedColumn`: row-local sparse bounds +- `rowTouched` / `rowHasBits` / `touchedRows`: fast reset bookkeeping + +Coverage emission works row-by-row: + +1. find touched columns from the bit vectors +2. accumulate winding and area +3. resolve the fill rule +4. apply aliased or antialiased coverage conversion +5. coalesce spans +6. invoke the row callback + +The fixed-point core is still the same coverage engine. What changed is the data shape feeding it. + +## 10. Brush Composition + +Rasterization emits coverage. It does not write colors directly. + +Composition happens in `FlushScene.Execute()`: + +1. create one `BrushRenderer` per command +2. run `Parallel.For` over scene rows +3. reuse one `WorkerScratch` and one `BrushWorkspace` per worker +4. for each row item: + - build a span-based `RasterizableBand` + - execute the raster band + - route coverage rows into `ItemRowOperation.InvokeCoverageRow(...)` + +`ItemRowOperation` maps emitted coverage back to the destination slice: + +- convert `startX` to destination X using the command's interest left +- slice the destination row from `Buffer2DRegion` +- call `BrushRenderer.Apply(...)` + +That means the rasterizer never needs to know about brushes, colors, gradients, or destination frame layout. + +## 11. Why BrushRenderer Is Target-Unbound + +`BrushRenderer` is target-unbound. Its `Apply(...)` method receives: + +- `destinationRow` +- `coverage` +- `x` +- `y` +- `BrushWorkspace` + +That separation is important for the row-first backend: + +- renderers can be created once per command +- destination slicing is owned by the executor +- worker-local scratch can be reused across many brush applications + +The renderer is now a row shader, not a row shader plus frame binding. + +## 12. Stroke Rasterization + +The standalone stroke API in `DefaultRasterizer` is still separate from the fill builder described above. + +`RasterizeStrokeRows(...)`: + +1. flattens contours +2. builds `StrokeEdgeData` +3. band-sorts those stroke descriptors +4. expands them into outline coverage during rasterization + +This still uses the same fixed-point scan conversion context, but its input representation is different because strokes begin as centerlines that must be expanded into an outline. + +That distinction is important: + +- fill path: prepared geometry -> rasterizable row bands +- stroke path: centerline descriptors -> expanded outline coverage + +## 13. Memory And Lifetime + +The CPU backend deliberately aligns ownership with work lifetime. + +### Flush-owned + +Owned by `FlushScene`: + +- command arrays +- row offsets +- row items +- dense line data +- dense start-cover data + +Disposed when the flush ends. + +### Worker-owned + +Owned per worker during execution: + +- `WorkerScratch` +- `BrushWorkspace` + +Disposed when the worker completes. + +### Command-owned + +Owned per command during execution: + +- `BrushRenderer` + +Created once before row execution begins and disposed after the row pass finishes. + +That ownership model keeps allocation and disposal aligned with the real execution lifetime of the work. + +## 14. Other Backend Operations + +Two backend operations are easy to overlook because they are not part of the raster path. + +### `ComposeLayer` + +This composites one frame into another using `PixelBlender`. It is used for layer flattening, not for path coverage rasterization. + +### `TryReadRegion` + +This copies a rectangular region out of a CPU target into a destination buffer. It is the backend's readback path. + +## 15. Reading The Code In Order + +If you want to understand the implementation in the same order the CPU backend executes it, read the files in this order: + +1. `DefaultDrawingBackend.cs` +2. `FlushScene.cs` +3. `BrushRenderer.cs` +4. `DefaultRasterizer.cs` + +Inside `DefaultRasterizer.cs`, read in this order: + +1. `TryBuildRasterizableBand` +2. `ExecuteRasterizableBand` +3. `Context` +4. `WorkerScratch` + +That order mirrors the real runtime path. + +## 16. Summary + +The current CPU backend is built around one core idea: + +> convert a flush into row-local raster work once, then execute rows directly with reusable worker scratch + +Everything else supports that idea: + +- `FlushScene` turns commands into row-major work +- `RasterizableBandInfo` turns geometry into row-local raster data +- `Context` performs fixed-point scan conversion +- `BrushRenderer` composes coverage into pixels + +That is the architecture to keep in mind when changing the backend or the rasterizer. diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs new file mode 100644 index 000000000..847990e4e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// CPU backend that executes path coverage rasterization and brush composition directly against a CPU region. +/// +public sealed class DefaultDrawingBackend : IDrawingBackend +{ + /// + /// Gets the default backend instance. + /// + public static DefaultDrawingBackend Instance { get; } = new(); + + /// + public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene) + where TPixel : unmanaged, IPixel + { + if (compositionScene.Commands.Count == 0) + { + return; + } + + if (!target.TryGetCpuRegion(out Buffer2DRegion destinationFrame)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible frame targets."); + } + + if (compositionScene.HasLayers) + { + using FlushScene layeredScene = FlushScene.Create( + compositionScene.Commands, + target.Bounds, + configuration.MemoryAllocator); + + if (layeredScene.ItemCount == 0) + { + return; + } + + layeredScene.ExecuteLayered(configuration, destinationFrame, compositionScene.Commands); + return; + } + + using FlushScene scene = FlushScene.Create( + compositionScene.Commands, + target.Bounds, + configuration.MemoryAllocator); + + if (scene.ItemCount == 0) + { + return; + } + + scene.Execute(configuration, destinationFrame); + } + + /// + /// Composites one CPU-backed frame onto another using the supplied graphics options. + /// + /// The pixel format. + /// The active processing configuration. + /// The source frame. + /// The destination frame. + /// The destination offset relative to . + /// The graphics options controlling composition. + public static void ComposeLayer( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame destination, + Point destinationOffset, + GraphicsOptions options) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + + if (!source.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames."); + } + + if (!destination.TryGetCpuRegion(out Buffer2DRegion destinationRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible destination frames."); + } + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(options); + float blendPercentage = options.BlendPercentage; + + int srcWidth = sourceRegion.Width; + int srcHeight = sourceRegion.Height; + int dstWidth = destinationRegion.Width; + int dstHeight = destinationRegion.Height; + + // Clamp the compositing region to both source and destination bounds. + int startX = Math.Max(0, -destinationOffset.X); + int startY = Math.Max(0, -destinationOffset.Y); + int endX = Math.Min(srcWidth, dstWidth - destinationOffset.X); + int endY = Math.Min(srcHeight, dstHeight - destinationOffset.Y); + + if (endX <= startX || endY <= startY) + { + return; + } + + int width = endX - startX; + + // Allocate a reusable per-row amount buffer from the memory pool. + using IMemoryOwner amountsOwner = configuration.MemoryAllocator.Allocate(width); + Span amounts = amountsOwner.Memory.Span; + amounts.Fill(blendPercentage); + + for (int y = startY; y < endY; y++) + { + Span srcRow = sourceRegion.DangerousGetRowSpan(y).Slice(startX, width); + int dstX = destinationOffset.X + startX; + int dstY = destinationOffset.Y + y; + Span dstRow = destinationRegion.DangerousGetRowSpan(dstY).Slice(dstX, width); + + blender.Blend(configuration, dstRow, dstRow, srcRow, amounts); + } + } + + /// + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(destination.Buffer, nameof(destination)); + + // CPU backend readback is available only when the target exposes CPU pixels. + if (!target.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + return false; + } + + // Clamp the request to the target region to avoid out-of-range row slicing. + Rectangle clipped = Rectangle.Intersect( + new Rectangle(0, 0, sourceRegion.Width, sourceRegion.Height), + sourceRectangle); + + if (clipped.Width <= 0 || clipped.Height <= 0) + { + return false; + } + + int copyWidth = Math.Min(clipped.Width, destination.Width); + int copyHeight = Math.Min(clipped.Height, destination.Height); + for (int y = 0; y < copyHeight; y++) + { + sourceRegion.DangerousGetRowSpan(clipped.Y + y) + .Slice(clipped.X, copyWidth) + .CopyTo(destination.DangerousGetRowSpan(y)); + } + + return true; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs new file mode 100644 index 000000000..5016d43c3 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -0,0 +1,2105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Default fixed-point rasterizer that converts polygon edges into per-row coverage. +/// +/// +/// Fill rasterization is organized around scene-aligned row bands. Each band builds a compact +/// line list and optional start-cover seed, then executes directly against worker-local scratch. +/// Stroke rasterization uses the same fixed-point scan conversion core, but expands centerline +/// edges into outline geometry before rasterization. +/// +internal static class DefaultRasterizer +{ + // 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; + + // Tile height used by the parallel row-tiling pipeline. + private const int DefaultTileHeight = 16; + + // Cap worker fan-out for coverage emission + composition callbacks. + // Higher counts increased scheduling overhead for medium geometry workloads. + private const int MaxParallelWorkerCount = 12; + + 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; + + /// + /// Gets the preferred scene row height used by the CPU rasterizer. + /// + internal static int PreferredRowHeight => DefaultTileHeight; + + /// + /// Converts one row band of a materialized path into a compact rasterizable payload. + /// + /// + /// The builder emits two complementary data sets: + /// + /// for visible line segments inside the band. + /// Start-cover seeds for segments that begin left of the visible X range. + /// + /// That split keeps the execution hot path small: the scanner seeds left-of-band winding once, + /// then only walks visible lines during the actual coverage pass. + /// + internal static bool TryBuildRasterizableBand( + MaterializedPath path, + ReadOnlySpan bandSegmentIndices, + int translateX, + int translateY, + in RasterizerOptions options, + int bandIndex, + Span lineDestination, + Span startCoverDestination, + out RasterizableBandInfo rasterizableBandInfo) + { + Rectangle interest = options.Interest; + int width = interest.Width; + int height = interest.Height; + + if (width <= 0 || height <= 0 || bandSegmentIndices.Length == 0 || bandIndex < 0) + { + rasterizableBandInfo = default; + return false; + } + + int tileHeight = DefaultTileHeight; + int firstBandIndex = FloorDiv(interest.Top, tileHeight); + int lastBandIndex = FloorDiv(interest.Bottom - 1, tileHeight); + int tileCount = (lastBandIndex - firstBandIndex) + 1; + + if ((uint)bandIndex >= (uint)tileCount) + { + rasterizableBandInfo = default; + return false; + } + + int bandTopStart = (firstBandIndex * tileHeight) - interest.Top; + int bandTop = bandTopStart + (bandIndex * tileHeight); + int bandHeight = tileHeight; + + if (startCoverDestination.Length < bandHeight || lineDestination.Length < bandSegmentIndices.Length) + { + ThrowBandBufferTooSmall(); + } + + int wordsPerRow = BitVectorsForMaxBitCount(width); + long coverStrideLong = (long)width * 2; + + if (coverStrideLong > int.MaxValue) + { + ThrowInterestBoundsTooLarge(); + } + + int coverStride = (int)coverStrideLong; + bool samplePixelCenter = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter; + float samplingOffsetX = samplePixelCenter ? 0.5F : 0F; + float samplingOffsetY = samplePixelCenter ? 0.5F : 0F; + float clipTop = MathF.Max(0F, bandTop); + float clipBottom = MathF.Min(height, bandTop + bandHeight); + + if (clipBottom <= clipTop) + { + rasterizableBandInfo = default; + return false; + } + + Span startCovers = startCoverDestination[..bandHeight]; + startCovers.Clear(); + Span rowMinTouchedColumn = stackalloc int[tileHeight]; + Span rowMaxTouchedColumn = stackalloc int[tileHeight]; + Span rowHasBits = stackalloc byte[tileHeight]; + Span rowTouched = stackalloc byte[tileHeight]; + Span touchedRows = stackalloc int[tileHeight]; + Context startCoverContext = new( + [], + [], + startCovers, + rowMinTouchedColumn, + rowMaxTouchedColumn, + rowHasBits, + rowTouched, + touchedRows, + width: 0, + height: bandHeight, + wordsPerRow: 0, + coverStride: 0, + IntersectionRule.NonZero, + RasterizationMode.Antialiased, + antialiasThreshold: 0F); + + int maxVisibleX = width * FixedOne; + int lineCount = 0; + int subPathHint = 0; + + for (int i = 0; i < bandSegmentIndices.Length; i++) + { + path.GetSegment(bandSegmentIndices[i], out PointF p0, out PointF p1, ref subPathHint); + + float x0 = ((p0.X + translateX) - interest.Left) + samplingOffsetX; + float y0 = ((p0.Y + translateY) - interest.Top) + samplingOffsetY; + float x1 = ((p1.X + translateX) - interest.Left) + samplingOffsetX; + float y1 = ((p1.Y + translateY) - interest.Top) + samplingOffsetY; + if (!float.IsFinite(x0) || !float.IsFinite(y0) || !float.IsFinite(x1) || !float.IsFinite(y1)) + { + continue; + } + + if (!ClipToVerticalBounds(ref x0, ref y0, ref x1, ref y1, clipTop, clipBottom)) + { + continue; + } + + int fx0 = FloatToFixed24Dot8(x0); + int fy0 = FloatToFixed24Dot8(y0 - bandTop); + int fx1 = FloatToFixed24Dot8(x1); + int fy1 = FloatToFixed24Dot8(y1 - bandTop); + if (fy0 == fy1) + { + continue; + } + + int minX = Math.Min(fx0, fx1); + int maxX = Math.Max(fx0, fx1); + + if (minX < 0) + { + startCoverContext.RasterizeLineSegment(fx0, fy0, fx1, fy1); + } + + if (maxX < 0 || minX > maxVisibleX) + { + continue; + } + + int clippedX0 = fx0; + int clippedY0 = fy0; + int clippedX1 = fx1; + int clippedY1 = fy1; + + if (!ClipToHorizontalBoundsFixed(ref clippedX0, ref clippedY0, ref clippedX1, ref clippedY1, 0, maxVisibleX)) + { + continue; + } + + if (clippedY0 == clippedY1) + { + continue; + } + + lineDestination[lineCount++] = new RasterLineData(clippedX0, clippedY0, clippedX1, clippedY1); + } + + bool hasStartCovers = false; + + for (int i = 0; i < bandHeight; i++) + { + if (startCovers[i] != 0) + { + hasStartCovers = true; + break; + } + } + + if (lineCount <= 0 && !hasStartCovers) + { + rasterizableBandInfo = default; + return false; + } + + RasterizationMode rasterizationMode = options.RasterizationMode == RasterizationMode.Antialiased + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + rasterizableBandInfo = new RasterizableBandInfo( + lineCount, + bandHeight, + width, + wordsPerRow, + coverStride, + interest.Top + bandTop, + options.IntersectionRule, + rasterizationMode, + options.AntialiasThreshold, + hasStartCovers); + + return true; + } + + /// + /// Executes one rasterizable band against a reusable scanner context. + /// + internal static void ExecuteRasterizableBand( + ref Context context, + in RasterizableBand rasterizableBand, + Span scanline, + RasterizerCoverageRowHandler rowHandler) + { + context.Reconfigure( + rasterizableBand.Width, + rasterizableBand.WordsPerRow, + rasterizableBand.CoverStride, + rasterizableBand.BandHeight, + rasterizableBand.IntersectionRule, + rasterizableBand.RasterizationMode, + rasterizableBand.AntialiasThreshold); + + context.SeedStartCovers(rasterizableBand.StartCovers); + context.RasterizePreparedLines(rasterizableBand.Lines); + context.EmitCoverageRows(rasterizableBand.DestinationTop, scanline, rowHandler); + context.ResetTouchedRows(); + } + + /// + /// 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; + + /// + /// Integer floor division for potentially negative values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FloorDiv(int value, int divisor) + { + int quotient = value / divisor; + int remainder = value % divisor; + if (remainder != 0 && ((remainder < 0) != (divisor < 0))) + { + quotient--; + } + + return quotient; + } + + /// + /// Converts a float coordinate to signed 24.8 fixed-point. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); + + /// + /// Clips a fixed-point segment against horizontal bounds. + /// + private static bool ClipToHorizontalBoundsFixed(ref int x0, ref int y0, ref int x1, ref int y1, int minX, int maxX) + { + 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)dx, originX0 - (double)minX, ref t0, ref t1)) + { + return false; + } + + if (!ClipTestFixed(dx, maxX - (double)originX0, ref t0, ref t1)) + { + return false; + } + + if (t1 < 1D) + { + x1 = originX0 + (int)Math.Round(dx * t1); + y1 = originY0 + (int)Math.Round(dy * t1); + } + + if (t0 > 0D) + { + x0 = originX0 + (int)Math.Round(dx * t0); + y0 = originY0 + (int)Math.Round(dy * t0); + } + + return y0 != y1; + } + + /// + /// Clips a segment against vertical bounds using Liang-Barsky style parametric tests. + /// + /// 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; + } + + /// + /// One Liang-Barsky clip test step. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTest(float p, float q, ref float t0, ref float t1) + { + if (p == 0F) + { + return q >= 0F; + } + + float r = q / p; + if (p < 0F) + { + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } + } + else + { + if (r < t0) + { + return false; + } + + if (r < t1) + { + t1 = r; + } + } + + return true; + } + + /// + /// One Liang-Barsky clip test step for fixed-point clipping. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool ClipTestFixed(double p, double q, ref double t0, ref double t1) + { + if (p == 0D) + { + return q >= 0D; + } + + double r = q / p; + if (p < 0D) + { + if (r > t1) + { + return false; + } + + if (r > t0) + { + t0 = r; + } + } + else + { + if (r < t0) + { + return false; + } + + if (r < t1) + { + t1 = r; + } + } + + return true; + } + + /// + /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. + /// This is used to keep edge ownership consistent for vertical lines. + /// + [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; + } + + /// + /// 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); + + /// + /// Throws when the requested raster interest exceeds the scanner's indexing limits. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInterestBoundsTooLarge() + => throw new ImageProcessingException("The rasterizer interest bounds are too large for DefaultRasterizer buffers."); + + /// + /// Throws when the caller-provided band buffers are smaller than the requested raster band. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowBandBufferTooSmall() + => throw new ImageProcessingException("The destination raster band buffer is too small for the requested operation."); + + /// + /// Throws when a worker scratch instance is reused for a band taller than it was allocated for. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowBandHeightExceedsScratchCapacity() + => throw new ImageProcessingException("Requested band height exceeds worker scratch capacity."); + + /// + /// Metadata that describes one prepared rasterizable band. + /// + internal readonly struct RasterizableBandInfo + { + /// + /// Initializes a new instance of the struct. + /// + public RasterizableBandInfo( + int lineCount, + int bandHeight, + int width, + int wordsPerRow, + int coverStride, + int destinationTop, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold, + bool hasStartCovers) + { + this.LineCount = lineCount; + this.BandHeight = bandHeight; + this.Width = width; + this.WordsPerRow = wordsPerRow; + this.CoverStride = coverStride; + this.DestinationTop = destinationTop; + this.IntersectionRule = intersectionRule; + this.RasterizationMode = rasterizationMode; + this.AntialiasThreshold = antialiasThreshold; + this.HasStartCovers = hasStartCovers; + } + + /// + /// Gets the number of visible raster lines stored for the band. + /// + public int LineCount { get; } + + /// + /// Gets the band height in pixels. + /// + public int BandHeight { get; } + + /// + /// Gets the visible band width in pixels. + /// + public int Width { get; } + + /// + /// Gets the bit-vector width in machine words. + /// + public int WordsPerRow { get; } + + /// + /// Gets the scanner cover/area stride. + /// + public int CoverStride { get; } + + /// + /// Gets the absolute destination Y coordinate of the band's top row. + /// + public int DestinationTop { get; } + + /// + /// Gets the fill rule used when resolving accumulated winding. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the coverage mode used by the band. + /// + public RasterizationMode RasterizationMode { get; } + + /// + /// Gets the aliased threshold used when the band runs in aliased mode. + /// + public float AntialiasThreshold { get; } + + /// + /// Gets a value indicating whether the band has non-zero start-cover seeds. + /// + public bool HasStartCovers { get; } + + /// + /// Gets a value indicating whether the band would emit any coverage. + /// + public bool HasCoverage => this.LineCount > 0 || this.HasStartCovers; + + /// + /// Creates a lightweight band view over caller-owned line and start-cover buffers. + /// + public RasterizableBand CreateRasterizableBand(ReadOnlySpan lines, ReadOnlySpan startCovers) + { + if (lines.Length < this.LineCount || startCovers.Length < this.BandHeight) + { + ThrowBandBufferTooSmall(); + } + + return new RasterizableBand( + lines[..this.LineCount], + startCovers[..this.BandHeight], + this.Width, + this.WordsPerRow, + this.CoverStride, + this.BandHeight, + this.DestinationTop, + this.IntersectionRule, + this.RasterizationMode, + this.AntialiasThreshold); + } + } + + /// + /// Prepared raster payload for one row band. + /// + internal readonly ref struct RasterizableBand + { + /// + /// Initializes a new instance of the struct. + /// + public RasterizableBand( + ReadOnlySpan lines, + ReadOnlySpan startCovers, + int width, + int wordsPerRow, + int coverStride, + int bandHeight, + int destinationTop, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold) + { + this.Lines = lines; + this.StartCovers = startCovers; + this.Width = width; + this.WordsPerRow = wordsPerRow; + this.CoverStride = coverStride; + this.BandHeight = bandHeight; + this.DestinationTop = destinationTop; + this.IntersectionRule = intersectionRule; + this.RasterizationMode = rasterizationMode; + this.AntialiasThreshold = antialiasThreshold; + } + + /// + /// Gets the clipped line list to rasterize. + /// + public ReadOnlySpan Lines { get; } + + /// + /// Gets the per-row start-cover seeds for off-screen left coverage. + /// + public ReadOnlySpan StartCovers { get; } + + /// + /// Gets the visible band width in pixels. + /// + public int Width { get; } + + /// + /// Gets the bit-vector width in machine words. + /// + public int WordsPerRow { get; } + + /// + /// Gets the scanner cover/area stride. + /// + public int CoverStride { get; } + + /// + /// Gets the band height in pixels. + /// + public int BandHeight { get; } + + /// + /// Gets the absolute destination Y coordinate of the band's top row. + /// + public int DestinationTop { get; } + + /// + /// Gets the fill rule used when resolving accumulated winding. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the coverage mode used by the band. + /// + public RasterizationMode RasterizationMode { get; } + + /// + /// Gets the aliased threshold used when the band runs in aliased mode. + /// + public float AntialiasThreshold { get; } + } + + /// + /// 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. + /// + internal ref struct Context + { + private readonly Span bitVectors; + private readonly Span coverArea; + private readonly Span startCover; + private readonly Span rowMinTouchedColumn; + private readonly Span rowMaxTouchedColumn; + private readonly Span rowHasBits; + private readonly Span rowTouched; + private readonly Span touchedRows; + private readonly int widthCapacity; + private readonly int heightCapacity; + private readonly int wordsPerRowCapacity; + private readonly int coverStrideCapacity; + private int width; + private int height; + private int wordsPerRow; + private int coverStride; + private IntersectionRule intersectionRule; + private RasterizationMode rasterizationMode; + private float antialiasThreshold; + private int touchedRowCount; + + /// + /// Initializes a new instance of the struct. + /// + public Context( + Span bitVectors, + Span coverArea, + Span startCover, + Span rowMinTouchedColumn, + Span rowMaxTouchedColumn, + Span rowHasBits, + Span rowTouched, + Span touchedRows, + int width, + int height, + int wordsPerRow, + int coverStride, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold) + { + this.bitVectors = bitVectors; + this.coverArea = coverArea; + this.startCover = startCover; + this.rowMinTouchedColumn = rowMinTouchedColumn; + this.rowMaxTouchedColumn = rowMaxTouchedColumn; + this.rowHasBits = rowHasBits; + this.rowTouched = rowTouched; + this.touchedRows = touchedRows; + this.widthCapacity = width; + this.heightCapacity = height; + this.wordsPerRowCapacity = wordsPerRow; + this.coverStrideCapacity = coverStride; + this.width = width; + this.height = height; + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.intersectionRule = intersectionRule; + this.rasterizationMode = rasterizationMode; + this.antialiasThreshold = antialiasThreshold; + this.touchedRowCount = 0; + } + + public void Reconfigure( + int width, + int wordsPerRow, + int coverStride, + int height, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold) + { + if ((uint)height > (uint)this.heightCapacity || + (uint)wordsPerRow > (uint)this.wordsPerRowCapacity || + (uint)coverStride > (uint)this.coverStrideCapacity || + (uint)width > (uint)this.widthCapacity) + { + ThrowBandHeightExceedsScratchCapacity(); + } + + this.width = width; + this.height = height; + this.wordsPerRow = wordsPerRow; + this.coverStride = coverStride; + this.intersectionRule = intersectionRule; + this.rasterizationMode = rasterizationMode; + this.antialiasThreshold = antialiasThreshold; + } + + public void SeedStartCovers(ReadOnlySpan startCovers) + { + int count = Math.Min(this.height, startCovers.Length); + for (int i = 0; i < count; i++) + { + int cover = startCovers[i]; + if (cover == 0) + { + continue; + } + + this.startCover[i] += cover; + this.MarkRowTouched(i); + } + } + + public void RasterizePreparedLines(ReadOnlySpan lines) + { + for (int i = 0; i < lines.Length; i++) + { + ref readonly RasterLineData line = ref lines[i]; + this.RasterizeLine(line.X0, line.Y0, line.X1, line.Y1); + } + } + + public void RasterizeLineSegment(int x0, int y0, int x1, int y1) + => this.RasterizeLine(x0, y0, x1, y1); + + /// + /// Converts accumulated cover/area tables into non-zero coverage span callbacks. + /// + /// Absolute destination Y corresponding to row zero in this context. + /// Reusable scanline scratch buffer used to materialize emitted spans. + /// Coverage callback invoked for each emitted non-zero span. + public readonly void EmitCoverageRows(int destinationTop, Span scanline, RasterizerCoverageRowHandler rowHandler) + { + // Iterate only rows that actually received coverage contributions. + // MarkRowTouched is called from AddCell for all contributions, including + // column-less startCover accumulations, so touchedRows is complete. + for (int i = 0; i < this.touchedRowCount; i++) + { + int row = this.touchedRows[i]; + int rowCover = this.startCover[row]; + bool rowHasBits = this.rowHasBits[row] != 0; + if (rowCover == 0 && !rowHasBits) + { + // Safety guard — should not fire in practice. + continue; + } + + if (!rowHasBits) + { + // No touched cells in this row, but carry cover from x < 0 can still + // produce a full-width constant span. + float coverage = this.AreaToCoverage(rowCover << AreaToCoverageShift); + if (coverage > 0F) + { + scanline[..this.width].Fill(coverage); + rowHandler(destinationTop + row, 0, scanline[..this.width]); + } + + continue; + } + + int minTouchedColumn = this.rowMinTouchedColumn[row]; + int maxTouchedColumn = this.rowMaxTouchedColumn[row]; + ReadOnlySpan rowBitVectors = this.bitVectors.Slice(row * this.wordsPerRow, this.wordsPerRow); + this.EmitRowCoverage( + rowBitVectors, + row, + rowCover, + minTouchedColumn, + maxTouchedColumn, + destinationTop + row, + scanline, + rowHandler); + } + } + + /// + /// 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; + + // Clear only touched bitset words for this row. + int minWord = this.rowMinTouchedColumn[row] / WordBitCount; + int maxWord = this.rowMaxTouchedColumn[row] / WordBitCount; + int wordCount = (maxWord - minWord) + 1; + this.bitVectors.Slice((row * this.wordsPerRow) + minWord, wordCount).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. + /// Minimum touched column index in this row. + /// Maximum touched column index in this row. + /// Absolute destination y for this row. + /// Reusable scanline coverage buffer used for per-span materialization. + /// Coverage callback invoked for each emitted non-zero span. + private readonly void EmitRowCoverage( + ReadOnlySpan rowBitVectors, + int row, + int cover, + int minTouchedColumn, + int maxTouchedColumn, + int destinationY, + Span scanline, + RasterizerCoverageRowHandler rowHandler) + { + int rowOffset = row * this.coverStride; + int spanStart = 0; + int spanEnd = 0; + float spanCoverage = 0F; + int runStart = -1; + int runEnd = -1; + int minWord = minTouchedColumn / WordBitCount; + int maxWord = maxTouchedColumn / WordBitCount; + + for (int wordIndex = minWord; wordIndex <= maxWord; 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) + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + spanStart = x + 1; + spanEnd = spanStart; + spanCoverage = 0F; + } + else if (coverage == spanCoverage) + { + spanEnd = x + 1; + } + else + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + 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) + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else + { + float gapCoverage = this.AreaToCoverage(cover << AreaToCoverageShift); + if (gapCoverage <= 0F) + { + // Even-odd can map non-zero winding to zero coverage. + // Treat this as a hard run break so we don't bridge holes. + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + else if (spanCoverage == gapCoverage) + { + if (coverage == gapCoverage) + { + spanEnd = x + 1; + } + else + { + WriteSpan(scanline, spanStart, x, spanCoverage, ref runStart, ref runEnd); + spanStart = x; + spanEnd = x + 1; + spanCoverage = coverage; + } + } + else + { + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + WriteSpan(scanline, spanEnd, x, gapCoverage, ref runStart, ref runEnd); + 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. + WriteSpan(scanline, spanStart, spanEnd, spanCoverage, ref runStart, ref runEnd); + + if (cover != 0 && spanEnd < this.width) + { + WriteSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift), ref runStart, ref runEnd); + } + + EmitRun(rowHandler, destinationY, scanline, ref runStart, ref runEnd); + } + + /// + /// 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 + // using the configurable threshold from GraphicsOptions.AntialiasThreshold. + return coverage >= this.antialiasThreshold ? 1F : 0F; + } + + return coverage; + } + + /// + /// Writes one non-zero coverage segment into the scanline and expands the active run. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteSpan( + Span scanline, + int start, + int end, + float coverage, + ref int runStart, + ref int runEnd) + { + if (coverage <= 0F || end <= start) + { + return; + } + + scanline[start..end].Fill(coverage); + + if (runStart < 0) + { + runStart = start; + runEnd = end; + return; + } + + if (end > runEnd) + { + runEnd = end; + } + } + + /// + /// Emits the currently accumulated non-zero run, if any. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EmitRun( + RasterizerCoverageRowHandler rowHandler, + int destinationY, + Span scanline, + ref int runStart, + ref int runEnd) + { + if (runStart < 0) + { + return; + } + + rowHandler(destinationY, runStart, scanline[runStart..runEnd]); + runStart = -1; + runEnd = -1; + } + + /// + /// Sets a row/column bit and reports whether it was newly set. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly bool ConditionalSetBit(int row, int column, out bool rowHadBits) + { + 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; + + // Single read of rowHasBits serves both the conditional store + // and the caller's min/max column tracking. + rowHadBits = this.rowHasBits[row] != 0; + if (!rowHadBits) + { + 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, out bool rowHadBits)) + { + // 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; + } + + if (!rowHadBits) + { + this.rowMinTouchedColumn[row] = column; + this.rowMaxTouchedColumn[row] = column; + } + else + { + if (column < this.rowMinTouchedColumn[row]) + { + this.rowMinTouchedColumn[row] = column; + } + + if (column > this.rowMaxTouchedColumn[row]) + { + this.rowMaxTouchedColumn[row] = column; + } + } + } + + /// + /// 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 (16 bytes). + /// + /// + /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path + /// access without per-read unpacking. Row bounds are computed inline from Y coordinates + /// where needed. + /// + internal 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; + + /// + /// Initializes a new instance of the struct. + /// + public EdgeData(int x0, int y0, int x1, int y1) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + } + } + + /// + /// Immutable line record stored in band-local raster coordinates. + /// + internal readonly struct RasterLineData + { + public readonly int X0; + + public readonly int Y0; + + public readonly int X1; + + public readonly int Y1; + + public RasterLineData(int x0, int y0, int x1, int y1) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + } + } + + /// + /// Stroke centerline edge descriptor used for per-band parallel stroke expansion. + /// + /// + /// + /// Each descriptor represents one centerline edge with associated join/cap metadata. + /// During rasterization, each descriptor is expanded into outline polygon edges that + /// are rasterized directly via . + /// + /// + /// The layout mirrors the GPU StrokeExpandComputeShader edge format: + /// + /// Side edge (flags=0): (X0,Y0)→(X1,Y1) is the centerline segment. + /// Join edge (): (X0,Y0) is the vertex, (X1,Y1) is the previous endpoint, (AdjX,AdjY) is the next endpoint. + /// Cap edge (/): (X0,Y0) is the cap vertex, (X1,Y1) is the adjacent endpoint. + /// + /// + /// All coordinates are in scanner-local float space (relative to interest top-left with sampling offset). + /// + internal readonly struct StrokeEdgeData + { + public readonly float X0; + public readonly float Y0; + public readonly float X1; + public readonly float Y1; + public readonly float AdjX; + public readonly float AdjY; + public readonly StrokeEdgeFlags Flags; + + public StrokeEdgeData(float x0, float y0, float x1, float y1, StrokeEdgeFlags flags, float adjX = 0, float adjY = 0) + { + this.X0 = x0; + this.Y0 = y0; + this.X1 = x1; + this.Y1 = y1; + this.Flags = flags; + this.AdjX = adjX; + this.AdjY = adjY; + } + } + + /// + /// Reusable per-worker scratch buffers used by raster band execution. + /// + internal 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 rowMinTouchedColumnOwner; + private readonly IMemoryOwner rowMaxTouchedColumnOwner; + 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 rowMinTouchedColumnOwner, + IMemoryOwner rowMaxTouchedColumnOwner, + 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.rowMinTouchedColumnOwner = rowMinTouchedColumnOwner; + this.rowMaxTouchedColumnOwner = rowMaxTouchedColumnOwner; + 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; + + /// + /// Returns when this scratch has compatible dimensions and sufficient + /// capacity for the requested parameters, making it safe to reuse without reallocation. + /// + internal bool CanReuse(int requiredWordsPerRow, int requiredCoverStride, int requiredWidth, int minCapacity) + => this.wordsPerRow >= requiredWordsPerRow + && this.coverStride >= requiredCoverStride + && this.width >= requiredWidth + && this.tileCapacity >= minCapacity; + + /// + /// 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 rowMinTouchedColumnOwner = allocator.Allocate(tileCapacity); + IMemoryOwner rowMaxTouchedColumnOwner = allocator.Allocate(tileCapacity); + 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, + rowMinTouchedColumnOwner, + rowMaxTouchedColumnOwner, + rowHasBitsOwner, + rowTouchedOwner, + touchedRowsOwner, + scanlineOwner); + } + + /// + /// Creates a context view over a compatible prefix of this scratch for the requested geometry width. + /// + public Context CreateContext( + int width, + int wordsPerRow, + int coverStride, + int bandHeight, + IntersectionRule intersectionRule, + RasterizationMode rasterizationMode, + float antialiasThreshold) + { + if ((uint)bandHeight > (uint)this.tileCapacity || + (uint)wordsPerRow > (uint)this.wordsPerRow || + (uint)coverStride > (uint)this.coverStride || + (uint)width > (uint)this.width) + { + ThrowBandHeightExceedsScratchCapacity(); + } + + int bitVectorCount = checked(wordsPerRow * bandHeight); + int coverAreaCount = checked(coverStride * bandHeight); + return new Context( + this.bitVectorsOwner.Memory.Span[..bitVectorCount], + this.coverAreaOwner.Memory.Span[..coverAreaCount], + this.startCoverOwner.Memory.Span[..bandHeight], + this.rowMinTouchedColumnOwner.Memory.Span[..bandHeight], + this.rowMaxTouchedColumnOwner.Memory.Span[..bandHeight], + this.rowHasBitsOwner.Memory.Span[..bandHeight], + this.rowTouchedOwner.Memory.Span[..bandHeight], + this.touchedRowsOwner.Memory.Span[..bandHeight], + width, + bandHeight, + wordsPerRow, + coverStride, + intersectionRule, + rasterizationMode, + antialiasThreshold); + } + + /// + /// Releases worker-local scratch buffers back to the allocator. + /// + public void Dispose() + { + this.bitVectorsOwner.Dispose(); + this.coverAreaOwner.Dispose(); + this.startCoverOwner.Dispose(); + this.rowMinTouchedColumnOwner.Dispose(); + this.rowMaxTouchedColumnOwner.Dispose(); + this.rowHasBitsOwner.Dispose(); + this.rowTouchedOwner.Dispose(); + this.touchedRowsOwner.Dispose(); + this.scanlineOwner.Dispose(); + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs new file mode 100644 index 000000000..e0fc66b0e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs @@ -0,0 +1,1495 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Collections.Concurrent; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Flush-scoped CPU scene realized in destination coordinates. +/// +/// +/// +/// One flush is converted into a set of destination-row-local raster items with prepared geometry +/// and row membership lists. +/// +/// +/// The scene owns flush-local scheduling data plus row-local raster payloads that are prebuilt in +/// destination coordinates. Scene-row execution can then rasterize directly from those prepared +/// row items without rebuilding coverage state for every command during composition. +/// +/// +internal sealed class FlushScene : IDisposable +{ + // Keep row-level parallelism bounded so small scenes do not overpay in scheduling overhead. + private const int MaxParallelWorkerCount = 12; + + private readonly CompositionCommand[] commands; + private readonly int[] interestLefts; + private readonly IMemoryOwner? lineDataOwner; + private readonly IMemoryOwner? startCoverOwner; + private readonly DefaultRasterizer.RasterizableBandInfo[] rasterizableBands; + private readonly int[] rowOffsets; + private readonly RowItem[] rowItems; + private readonly int firstSceneBandTop; + private readonly int maxWidth; + private readonly int maxWordsPerRow; + private readonly int maxCoverStride; + private readonly int maxBandCapacity; + + /// + /// Initializes a new instance of the class. + /// + private FlushScene( + CompositionCommand[] commands, + int[] interestLefts, + IMemoryOwner? lineDataOwner, + IMemoryOwner? startCoverOwner, + DefaultRasterizer.RasterizableBandInfo[] rasterizableBands, + int[] rowOffsets, + RowItem[] rowItems, + int firstSceneBandTop, + int maxWidth, + int maxWordsPerRow, + int maxCoverStride, + int maxBandCapacity, + long totalEdgeCount, + int singleBandItemCount, + int smallEdgeItemCount) + { + this.commands = commands; + this.interestLefts = interestLefts; + this.lineDataOwner = lineDataOwner; + this.startCoverOwner = startCoverOwner; + this.rasterizableBands = rasterizableBands; + this.rowOffsets = rowOffsets; + this.rowItems = rowItems; + this.firstSceneBandTop = firstSceneBandTop; + this.maxWidth = maxWidth; + this.maxWordsPerRow = maxWordsPerRow; + this.maxCoverStride = maxCoverStride; + this.maxBandCapacity = maxBandCapacity; + this.TotalEdgeCount = totalEdgeCount; + this.SingleBandItemCount = singleBandItemCount; + this.SmallEdgeItemCount = smallEdgeItemCount; + } + + /// + /// Gets the number of visible raster items in the scene. + /// + public int ItemCount => this.commands.Length; + + /// + /// Gets the number of destination scene rows in this flush. + /// + public int RowCount => this.rowOffsets.Length == 0 ? 0 : this.rowOffsets.Length - 1; + + /// + /// Gets the total number of row-local raster items in this flush. + /// + public int RowItemCount => this.rowItems.Length; + + /// + /// Gets the total prepared edge count across all raster items. + /// + public long TotalEdgeCount { get; } + + /// + /// Gets the count of items whose realized interest fits in one raster row band. + /// + public int SingleBandItemCount { get; } + + /// + /// Gets the count of items with small prepared geometry. + /// + public int SmallEdgeItemCount { get; } + + /// + /// Builds a flush-scoped CPU scene from prepared commands. + /// + public static FlushScene Create( + IReadOnlyList commands, + in Rectangle targetBounds, + MemoryAllocator allocator) + => Create(commands, 0, commands.Count, targetBounds, allocator); + + /// + /// Builds a flush-scoped CPU scene from a contiguous command range. + /// + public static FlushScene Create( + IReadOnlyList commands, + int start, + int length, + in Rectangle targetBounds, + MemoryAllocator allocator) + { + Rectangle sceneBounds = targetBounds; + int rowHeight = DefaultRasterizer.PreferredRowHeight; + int commandCount = length; + if (commandCount == 0) + { + return Empty(); + } + + // Phase 1: materialize paths and compute band layout for visible commands. + // Visibility clipping was already done during Prepare(), so we just skip non-visible commands. + // All temporary buffers are pooled via the allocator to avoid GC pressure on large command counts. + IMemoryOwner interestLeftBufferOwner = allocator.Allocate(commandCount); + IMemoryOwner firstBandIndexBufferOwner = allocator.Allocate(commandCount); + IMemoryOwner bandCountBufferOwner = allocator.Allocate(commandCount); + IMemoryOwner visibleItemFlagsOwner = allocator.Allocate(commandCount, AllocationOptions.Clean); + MaterializedPath[] pathBuffer = new MaterializedPath[commandCount]; + RasterizerOptions[] rasterizerOptionsBuffer = new RasterizerOptions[commandCount]; + + Memory interestLeftBuffer = interestLeftBufferOwner.Memory; + Memory firstBandIndexBuffer = firstBandIndexBufferOwner.Memory; + Memory bandCountBuffer = bandCountBufferOwner.Memory; + Memory visibleItemFlags = visibleItemFlagsOwner.Memory; + + _ = Parallel.ForEach(Partitioner.Create(0, commandCount), range => + { + Span ilSpan = interestLeftBuffer.Span; + Span fbSpan = firstBandIndexBuffer.Span; + Span bcSpan = bandCountBuffer.Span; + Span vfSpan = visibleItemFlags.Span; + + for (int i = range.Item1; i < range.Item2; i++) + { + CompositionCommand command = commands[start + i]; + if (command.Kind is not CompositionCommandKind.FillLayer || !command.IsVisible) + { + continue; + } + + IPath preparedPath = command.PreparedPath!; + + MaterializedPath materializedPath = MaterializedPath.Create(preparedPath); + if (materializedPath.TotalSegmentCount == 0) + { + continue; + } + + Rectangle destinationInterest = new( + command.TargetBounds.X + command.DestinationRegion.X, + command.TargetBounds.Y + command.DestinationRegion.Y, + command.DestinationRegion.Width, + command.DestinationRegion.Height); + + RasterizerOptions sourceOptions = command.RasterizerOptions; + RasterizerOptions itemOptions = new( + destinationInterest, + sourceOptions.IntersectionRule, + sourceOptions.RasterizationMode, + sourceOptions.SamplingOrigin, + sourceOptions.AntialiasThreshold); + + int firstBandIndex = FloorDiv(destinationInterest.Top, rowHeight); + int lastBandIndex = FloorDiv(destinationInterest.Bottom - 1, rowHeight); + int bandCount = (lastBandIndex - firstBandIndex) + 1; + + if (bandCount <= 0) + { + continue; + } + + pathBuffer[i] = materializedPath; + rasterizerOptionsBuffer[i] = itemOptions; + ilSpan[i] = destinationInterest.Left; + fbSpan[i] = firstBandIndex; + bcSpan[i] = bandCount; + vfSpan[i] = 1; + } + }); + + int visibleItemCount = 0; + + // TODO: SIMD sum over the byte span. + Span flagSpan = visibleItemFlags.Span; + + for (int i = 0; i < flagSpan.Length; i++) + { + visibleItemCount += flagSpan[i]; + } + + if (visibleItemCount == 0) + { + return Empty(); + } + + // Phase 2: compact in-place — move visible entries to the front of the Phase 1 arrays. + // This avoids a second allocation for MaterializedPath[] and RasterizerOptions[]. + CompositionCommand[] compactedCommands = new CompositionCommand[visibleItemCount]; + int[] interestLefts = new int[visibleItemCount]; + IMemoryOwner itemFirstBandIndicesOwner = allocator.Allocate(visibleItemCount); + IMemoryOwner itemBandCountsOwner = allocator.Allocate(visibleItemCount); + Memory itemFirstBandIndices = itemFirstBandIndicesOwner.Memory; + Memory itemBandCounts = itemBandCountsOwner.Memory; + + int writeIndex = 0; + { + Span mpSpan = pathBuffer.AsSpan(); + Span roSpan = rasterizerOptionsBuffer.AsSpan(); + Span ilSpan = interestLeftBuffer.Span; + Span fbSrc = firstBandIndexBuffer.Span; + Span bcSrc = bandCountBuffer.Span; + Span fbDst = itemFirstBandIndices.Span; + Span bcDst = itemBandCounts.Span; + + for (int i = 0; i < commandCount; i++) + { + if (flagSpan[i] == 0) + { + continue; + } + + compactedCommands[writeIndex] = commands[start + i]; + mpSpan[writeIndex] = mpSpan[i]; + roSpan[writeIndex] = roSpan[i]; + interestLefts[writeIndex] = ilSpan[i]; + fbDst[writeIndex] = fbSrc[i]; + bcDst[writeIndex] = bcSrc[i]; + writeIndex++; + } + } + + // Phase 1 buffers for int/byte are no longer needed — release them now. + bandCountBufferOwner.Dispose(); + firstBandIndexBufferOwner.Dispose(); + interestLeftBufferOwner.Dispose(); + visibleItemFlagsOwner.Dispose(); + + int minBandIndex = int.MaxValue; + int maxBandIndex = int.MinValue; + int maxWidth = 0; + int maxWordsPerRow = 0; + int maxCoverStride = 0; + int maxBandCapacity = rowHeight; + long totalEdgeCount = 0; + int singleBandItemCount = 0; + int smallEdgeItemCount = 0; + + // Segment start offsets into the band assignment cache — one entry per visible item. + // Used in Phases 4 and 5 to slice each item's portion of the cache. + int[] itemSegmentStarts = new int[visibleItemCount]; + + // Phase 3: derive scene-wide maxima once so every worker can allocate one reusable scratch set. + { + Span roSpan = rasterizerOptionsBuffer.AsSpan(); + Span fbSpan = itemFirstBandIndices.Span; + Span bcSpan = itemBandCounts.Span; + Span mpSpan = pathBuffer.AsSpan(); + + for (int i = 0; i < visibleItemCount; i++) + { + RasterizerOptions itemOptions = roSpan[i]; + Rectangle interest = itemOptions.Interest; + int width = interest.Width; + long coverStride = (long)width * 2; + + if (coverStride > int.MaxValue) + { + throw new InvalidOperationException("Interest bounds exceed rasterizer limits."); + } + + minBandIndex = Math.Min(minBandIndex, fbSpan[i]); + maxBandIndex = Math.Max(maxBandIndex, fbSpan[i] + bcSpan[i] - 1); + maxWidth = Math.Max(maxWidth, width); + maxWordsPerRow = Math.Max(maxWordsPerRow, BitVectorsForMaxBitCount(width)); + maxCoverStride = Math.Max(maxCoverStride, (int)coverStride); + itemSegmentStarts[i] = (int)totalEdgeCount; + totalEdgeCount += mpSpan[i].TotalSegmentCount; + + if (bcSpan[i] == 1) + { + singleBandItemCount++; + } + + if (mpSpan[i].TotalSegmentCount <= 64) + { + smallEdgeItemCount++; + } + } + } + + // Band assignment cache: stores the packed (firstBand | lastBand << 16) for each segment, + // or -1 if the segment falls outside the item's interest rectangle. + // Built during Phase 4 so Phase 5 can scatter from integers instead of re-enumerating path geometry. + IMemoryOwner bandAssignmentCacheOwner = allocator.Allocate(Math.Max((int)totalEdgeCount, 1)); + Memory bandAssignmentCache = bandAssignmentCacheOwner.Memory; + + IMemoryOwner itemBandOffsetStartsOwner = allocator.Allocate(visibleItemCount + 1); + int totalBandOffsetCount = 0; + { + Span offsetSpan = itemBandOffsetStartsOwner.Memory.Span; + Span bcSpan = itemBandCounts.Span; + for (int i = 0; i < visibleItemCount; i++) + { + offsetSpan[i] = totalBandOffsetCount; + totalBandOffsetCount += bcSpan[i] + 1; + } + + offsetSpan[visibleItemCount] = totalBandOffsetCount; + } + + IMemoryOwner bandSegmentOffsetsOwner = allocator.Allocate(totalBandOffsetCount); + long totalBandSegmentRefs = 0; + + // Phase 4: count how many prepared segments each item contributes to each row band, + // and record the packed band assignment for each segment in the cache so Phase 5 + // can scatter from integers rather than re-enumerating path geometry. + _ = Parallel.ForEach( + Partitioner.Create(0, visibleItemCount), + () => 0L, + (range, _, localTotal) => + { + Span mpSpan = pathBuffer.AsSpan(); + Span roSpan = rasterizerOptionsBuffer.AsSpan(); + Span bcSpan = itemBandCounts.Span; + Span offsetSpan = itemBandOffsetStartsOwner.Memory.Span; + Span bsoSpan = bandSegmentOffsetsOwner.Memory.Span; + Span bacSpan = bandAssignmentCache.Span; + + for (int i = range.Item1; i < range.Item2; i++) + { + localTotal += CountAndStoreBandSegmentRefs( + mpSpan[i], + compactedCommands[i].DestinationOffset.Y, + in roSpan[i], + bcSpan[i], + bsoSpan.Slice(offsetSpan[i], bcSpan[i]), + bacSpan.Slice(itemSegmentStarts[i], mpSpan[i].TotalSegmentCount)); + } + + return localTotal; + }, + localTotal => Interlocked.Add(ref totalBandSegmentRefs, localTotal)); + + if (totalBandSegmentRefs > int.MaxValue) + { + throw new InvalidOperationException("Flush scene exceeds row-local segment indexing limits."); + } + + int runningSegmentOffset = 0; + { + Span offsetSpan = itemBandOffsetStartsOwner.Memory.Span; + Span bcSpan = itemBandCounts.Span; + Span bsoSpan = bandSegmentOffsetsOwner.Memory.Span; + for (int i = 0; i < visibleItemCount; i++) + { + int bandOffsetStart = offsetSpan[i]; + int bandCount = bcSpan[i]; + for (int bandIndex = 0; bandIndex < bandCount; bandIndex++) + { + int segmentCount = bsoSpan[bandOffsetStart + bandIndex]; + bsoSpan[bandOffsetStart + bandIndex] = runningSegmentOffset; + runningSegmentOffset += segmentCount; + } + + bsoSpan[bandOffsetStart + bandCount] = runningSegmentOffset; + } + } + + IMemoryOwner bandSegmentIndicesOwner = allocator.Allocate(Math.Max(runningSegmentOffset, 1)); + + // Phase 5: scatter segment indices into the dense per-band lists using offsets from the prefix sum. + // Reads the compact band assignment cache built in Phase 4 — no path geometry re-enumeration. + _ = Parallel.ForEach( + Partitioner.Create(0, visibleItemCount), + () => Array.Empty(), + (range, _, bandCursorBuffer) => + { + Span mpSpan = pathBuffer.AsSpan(); + Span bcSpan = itemBandCounts.Span; + Span offsetSpan = itemBandOffsetStartsOwner.Memory.Span; + Span bsoSpan = bandSegmentOffsetsOwner.Memory.Span; + Span bsiSpan = bandSegmentIndicesOwner.Memory.Span; + Span bacSpan = bandAssignmentCache.Span; + + for (int i = range.Item1; i < range.Item2; i++) + { + int bandCount = bcSpan[i]; + if (bandCount <= 0) + { + continue; + } + + if (bandCursorBuffer.Length < bandCount) + { + bandCursorBuffer = new int[bandCount]; + } + + ReadOnlySpan bandOffsets = bsoSpan.Slice(offsetSpan[i], bandCount + 1); + Span bandCursors = bandCursorBuffer.AsSpan(0, bandCount); + bandOffsets[..bandCount].CopyTo(bandCursors); + + FillBandSegmentRefsFromCache( + bacSpan.Slice(itemSegmentStarts[i], mpSpan[i].TotalSegmentCount), + bandCursors, + bsiSpan); + } + + return bandCursorBuffer; + }, + static _ => { }); + + bandAssignmentCacheOwner.Dispose(); + + int rowCount = (maxBandIndex - minBandIndex) + 1; + int[] rowCounts = new int[rowCount]; + int totalRefs = 0; + + // Phase 6: convert item-local bands into scene-row membership counts. + { + Span fbSpan = itemFirstBandIndices.Span; + Span bcSpan = itemBandCounts.Span; + Span offsetSpan = itemBandOffsetStartsOwner.Memory.Span; + Span bsoSpan = bandSegmentOffsetsOwner.Memory.Span; + + for (int i = 0; i < visibleItemCount; i++) + { + int itemFirstActiveRow = fbSpan[i] - minBandIndex; + int bandOffsetStart = offsetSpan[i]; + for (int localBandIndex = 0; localBandIndex < bcSpan[i]; localBandIndex++) + { + if (bsoSpan[bandOffsetStart + localBandIndex + 1] <= bsoSpan[bandOffsetStart + localBandIndex]) + { + continue; + } + + int sceneRow = itemFirstActiveRow + localBandIndex; + rowCounts[sceneRow]++; + totalRefs++; + } + } + } + + if (totalRefs == 0) + { + itemFirstBandIndicesOwner.Dispose(); + itemBandCountsOwner.Dispose(); + itemBandOffsetStartsOwner.Dispose(); + bandSegmentOffsetsOwner.Dispose(); + bandSegmentIndicesOwner.Dispose(); + return Empty(); + } + + int[] rowOffsets = new int[rowCount + 1]; + int runningOffset = 0; + for (int i = 0; i < rowCount; i++) + { + rowOffsets[i] = runningOffset; + runningOffset += rowCounts[i]; + } + + rowOffsets[rowCount] = runningOffset; + + PendingRowItem[] pendingRowItems = new PendingRowItem[totalRefs]; + int[] rowCursors = new int[rowCount]; + Array.Copy(rowOffsets, rowCursors, rowCount); + + // Phase 7: build the row-major execution order while preserving command submission order per row. + { + Span fbSpan = itemFirstBandIndices.Span; + Span bcSpan = itemBandCounts.Span; + Span offsetSpan = itemBandOffsetStartsOwner.Memory.Span; + Span bsoSpan = bandSegmentOffsetsOwner.Memory.Span; + + for (int i = 0; i < visibleItemCount; i++) + { + int itemFirstActiveRow = fbSpan[i] - minBandIndex; + int bandOffsetStart = offsetSpan[i]; + CompositionCommand command = compactedCommands[i]; + for (int localBandIndex = 0; localBandIndex < bcSpan[i]; localBandIndex++) + { + int segmentStart = bsoSpan[bandOffsetStart + localBandIndex]; + int segmentEnd = bsoSpan[bandOffsetStart + localBandIndex + 1]; + int segmentCount = segmentEnd - segmentStart; + if (segmentCount <= 0) + { + continue; + } + + int sceneRow = itemFirstActiveRow + localBandIndex; + int absoluteBandIndex = fbSpan[i] + localBandIndex; + int bandTop = (absoluteBandIndex * rowHeight) - command.TargetBounds.Y; + Rectangle rowDestinationRegion = Rectangle.Intersect( + command.DestinationRegion, + new Rectangle( + command.DestinationRegion.X, + bandTop, + command.DestinationRegion.Width, + rowHeight)); + pendingRowItems[rowCursors[sceneRow]++] = new PendingRowItem( + i, + localBandIndex, + segmentStart, + segmentCount, + rowDestinationRegion); + } + } + } + + // Dispose band layout temporaries — no longer needed after Phase 7. + itemFirstBandIndicesOwner.Dispose(); + itemBandCountsOwner.Dispose(); + itemBandOffsetStartsOwner.Dispose(); + bandSegmentOffsetsOwner.Dispose(); + + IMemoryOwner? lineDataOwner = + runningSegmentOffset > 0 ? allocator.Allocate(runningSegmentOffset) : null; + IMemoryOwner? startCoverOwner = totalRefs > 0 ? allocator.Allocate(totalRefs * rowHeight) : null; + + DefaultRasterizer.RasterizableBandInfo[] rasterizableBands = new DefaultRasterizer.RasterizableBandInfo[totalRefs]; + RowItem[] rowItems = new RowItem[totalRefs]; + long totalLineCount = 0; + + if (lineDataOwner is not null && startCoverOwner is not null) + { + Memory lineData = lineDataOwner.Memory; + Memory startCoverData = startCoverOwner.Memory; + + // Phase 8: prebuild each row-local raster band once so execution only performs scan conversion. + _ = Parallel.ForEach(Partitioner.Create(0, totalRefs), range => + { + Span mpSpan = pathBuffer.AsSpan(); + Span roSpan = rasterizerOptionsBuffer.AsSpan(); + Span bsiSpan = bandSegmentIndicesOwner.Memory.Span; + + long localLineCount = 0; + for (int rowPosition = range.Item1; rowPosition < range.Item2; rowPosition++) + { + PendingRowItem pendingRowItem = pendingRowItems[rowPosition]; + int itemIndex = pendingRowItem.ItemIndex; + rowItems[rowPosition] = new RowItem( + itemIndex, + pendingRowItem.SegmentStart, + rowPosition * rowHeight, + pendingRowItem.DestinationRegion); + + _ = DefaultRasterizer.TryBuildRasterizableBand( + mpSpan[itemIndex], + bsiSpan.Slice(pendingRowItem.SegmentStart, pendingRowItem.SegmentCount), + compactedCommands[itemIndex].DestinationOffset.X, + compactedCommands[itemIndex].DestinationOffset.Y, + in roSpan[itemIndex], + pendingRowItem.LocalBandIndex, + lineData.Span.Slice(pendingRowItem.SegmentStart, pendingRowItem.SegmentCount), + startCoverData.Span.Slice(rowPosition * rowHeight, rowHeight), + out rasterizableBands[rowPosition]); + localLineCount += rasterizableBands[rowPosition].LineCount; + } + + _ = Interlocked.Add(ref totalLineCount, localLineCount); + }); + } + + // Dispose remaining temporary. + bandSegmentIndicesOwner.Dispose(); + + return new FlushScene( + compactedCommands, + interestLefts, + lineDataOwner, + startCoverOwner, + rasterizableBands, + rowOffsets, + rowItems, + minBandIndex * rowHeight, + maxWidth, + maxWordsPerRow, + maxCoverStride, + maxBandCapacity, + totalLineCount, + singleBandItemCount, + smallEdgeItemCount); + } + + /// + /// Executes the scene against a CPU destination region. + /// + /// The configuration that supplies memory allocation and pixel operations. + /// The CPU destination region that receives the composed pixels. + public void Execute( + Configuration configuration, + Buffer2DRegion destinationFrame) + where TPixel : unmanaged, IPixel => this.ExecuteCore(configuration, destinationFrame, this.commands); + + /// + /// Executes the scene against a CPU destination region while honoring inline layer commands. + /// + /// The configuration that supplies memory allocation and pixel operations. + /// The CPU destination region that receives the composed pixels. + /// The full ordered command stream, including BeginLayer and EndLayer. + public void ExecuteLayered( + Configuration configuration, + Buffer2DRegion destinationFrame, + IReadOnlyList sourceCommands) + where TPixel : unmanaged, IPixel => this.ExecuteCore(configuration, destinationFrame, sourceCommands); + + /// + /// Executes the scene against a CPU destination region using either the flat fill path or the inline layer path. + /// + private void ExecuteCore( + Configuration configuration, + Buffer2DRegion destinationFrame, + IReadOnlyList sourceCommands) + where TPixel : unmanaged, IPixel + { + if (this.RowCount == 0) + { + return; + } + + MemoryAllocator allocator = configuration.MemoryAllocator; + ReadOnlyMemory lineData = + this.lineDataOwner?.Memory ?? Memory.Empty; + + ReadOnlyMemory startCovers = + this.startCoverOwner?.Memory ?? Memory.Empty; + + ParallelOptions options = new() + { + MaxDegreeOfParallelism = Math.Min( + MaxParallelWorkerCount, + Math.Min(Environment.ProcessorCount, Math.Max(1, this.RowCount))), + }; + + BrushRenderer[] preparedBrushes = new BrushRenderer[this.commands.Length]; + _ = Parallel.ForEach(Partitioner.Create(0, this.commands.Length), range => + { + for (int i = range.Item1; i < range.Item2; i++) + { + CompositionCommand command = this.commands[i]; + preparedBrushes[i] = command.Brush.CreateRenderer( + configuration, + command.GraphicsOptions, + destinationFrame.Width, + command.BrushBounds); + } + }); + + try + { + _ = Parallel.For( + 0, + this.RowCount, + options, + () => new ExecuteWorkerState( + allocator, + this.maxWordsPerRow, + this.maxCoverStride, + this.maxWidth, + this.maxBandCapacity), + (sceneRow, _, state) => + { + DefaultRasterizer.WorkerScratch scratch = state.Scratch!; + DefaultRasterizer.Context context = scratch.CreateContext( + this.maxWidth, + this.maxWordsPerRow, + this.maxCoverStride, + this.maxBandCapacity, + IntersectionRule.NonZero, + RasterizationMode.Antialiased, + antialiasThreshold: 0F); + + int rowStart = this.rowOffsets[sceneRow]; + int rowEnd = this.rowOffsets[sceneRow + 1]; + if (rowStart >= rowEnd) + { + return state; + } + + BandTarget rootTarget = BandTarget.CreateRoot(destinationFrame); + this.ExecuteBand( + configuration, + sourceCommands, + preparedBrushes, + lineData.Span, + startCovers.Span, + sceneRow, + rowStart, + rowEnd, + ref context, + state, + rootTarget); + return state; + }, + state => state.Dispose()); + } + finally + { + for (int i = 0; i < preparedBrushes.Length; i++) + { + preparedBrushes[i].Dispose(); + } + } + } + + /// + /// Executes one band of the command stream while honoring inline layer boundaries. + /// + private void ExecuteBand( + Configuration configuration, + IReadOnlyList sourceCommands, + BrushRenderer[] preparedBrushes, + ReadOnlySpan lineData, + ReadOnlySpan startCovers, + int sceneRow, + int rowStart, + int rowEnd, + ref DefaultRasterizer.Context context, + ExecuteWorkerState state, + BandTarget rootTarget) + where TPixel : unmanaged, IPixel + { + int bandTop = this.firstSceneBandTop + (sceneRow * DefaultRasterizer.PreferredRowHeight); + + // Mirror the GPU control flow: one structural layer stack driven by BeginLayer/EndLayer. + // CPU-only backdrop state is stored separately by depth. + Span layerStack = state.GetLayerStack(); + int layerCount = 0; + + BandTarget target = rootTarget; + int rowCursor = rowStart; + int visibleFillIndex = 0; + + for (int commandIndex = 0; commandIndex < sourceCommands.Count; commandIndex++) + { + CompositionCommand command = sourceCommands[commandIndex]; + switch (command.Kind) + { + case CompositionCommandKind.FillLayer: + if (command.IsVisible) + { + if (rowCursor < rowEnd && this.rowItems[rowCursor].ItemIndex == visibleFillIndex) + { + this.ExecuteRowItem( + preparedBrushes, + lineData, + startCovers, + rowCursor, + ref context, + state, + target); + rowCursor++; + } + + visibleFillIndex++; + } + + break; + + case CompositionCommandKind.BeginLayer: + BandTarget layerTarget = BandTarget.CreateLayer( + target, + command.LayerBounds, + bandTop, + DefaultRasterizer.PreferredRowHeight); + + int layerDepth = layerCount; + layerStack = state.EnsureLayerDepth(layerDepth); + layerStack[layerDepth] = layerTarget.ActiveBounds; + layerCount++; + + if (layerTarget.ActiveBounds.Width > 0 && layerTarget.ActiveBounds.Height > 0) + { + // CPU execution still needs preserved pixels so EndLayer can composite back correctly. + Buffer2DRegion backdrop = state.GetLayerBackdropRegion( + layerDepth, + layerTarget.ActiveBounds.Width, + layerTarget.ActiveBounds.Height); + SaveBackdropAndClear(layerTarget, backdrop); + state.SetLayerCompositeState(layerDepth, backdrop, command.GraphicsOptions, hasBackdrop: true); + } + else + { + state.SetLayerCompositeState(layerDepth, default, command.GraphicsOptions, hasBackdrop: false); + } + + target = layerTarget; + break; + + case CompositionCommandKind.EndLayer: + if (layerCount == 0) + { + continue; + } + + int layerIndex = layerCount - 1; + Rectangle layerBounds = layerStack[layerIndex]; + layerCount--; + + LayerCompositeState layer = state.GetLayerCompositeState(layerIndex); + BandTarget compositeTarget = new( + rootTarget.Region, + rootTarget.OriginX, + rootTarget.OriginY, + layerBounds); + + if (layer.HasBackdrop) + { + ComposeLayerBand(configuration, layer.Backdrop, compositeTarget, layer.Options, state.BrushWorkspace); + } + + // The structural stack determines the current target after the pop. + target = layerCount == 0 + ? rootTarget + : new BandTarget( + rootTarget.Region, + rootTarget.OriginX, + rootTarget.OriginY, + layerStack[layerCount - 1]); + break; + } + } + } + + /// + /// Rasterizes and composites one prepared row item into the current band target. + /// + private void ExecuteRowItem( + BrushRenderer[] preparedBrushes, + ReadOnlySpan lineData, + ReadOnlySpan startCovers, + int rowPosition, + ref DefaultRasterizer.Context context, + ExecuteWorkerState state, + BandTarget target) + where TPixel : unmanaged, IPixel + { + ref readonly RowItem rowItem = ref this.rowItems[rowPosition]; + int itemIndex = rowItem.ItemIndex; + DefaultRasterizer.RasterizableBandInfo rasterizableBandInfo = this.rasterizableBands[rowPosition]; + + if (!rasterizableBandInfo.HasCoverage || rowItem.DestinationRegion.Width <= 0 || rowItem.DestinationRegion.Height <= 0) + { + return; + } + + BandRowOperation operation = new( + preparedBrushes[itemIndex], + target, + this.interestLefts[itemIndex], + state.BrushWorkspace); + DefaultRasterizer.RasterizableBand rasterizableBand = rasterizableBandInfo.CreateRasterizableBand( + lineData.Slice(rowItem.LineStart, rasterizableBandInfo.LineCount), + startCovers.Slice(rowItem.StartCoverStart, rasterizableBandInfo.BandHeight)); + DefaultRasterizer.ExecuteRasterizableBand( + ref context, + in rasterizableBand, + state.Scratch!.Scanline, + operation.InvokeCoverageRow); + } + + /// + /// Blends an isolated layer result back over the saved backdrop for the current band. + /// + private static void ComposeLayerBand( + Configuration configuration, + Buffer2DRegion backdrop, + BandTarget destination, + GraphicsOptions options, + BrushWorkspace workspace) + where TPixel : unmanaged, IPixel + { + Rectangle activeBounds = destination.ActiveBounds; + if (activeBounds.Width <= 0 || activeBounds.Height <= 0) + { + return; + } + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(options); + Span amounts = workspace.GetAmounts(activeBounds.Width); + amounts.Fill(options.BlendPercentage); + Span sourceRowCopy = workspace.GetOverlays(activeBounds.Width); + + for (int y = activeBounds.Top; y < activeBounds.Bottom; y++) + { + int localY = y - activeBounds.Top; + Span dstRow = destination.GetRow(y, activeBounds.Left, activeBounds.Width); + + // Preserve the isolated layer result, restore the saved backdrop, then blend the layer over it. + dstRow.CopyTo(sourceRowCopy); + backdrop.DangerousGetRowSpan(localY)[..activeBounds.Width].CopyTo(dstRow); + blender.Blend(configuration, dstRow, dstRow, sourceRowCopy, amounts); + } + } + + /// + /// Saves the pixels covered by the active layer band and clears that region for isolated drawing. + /// + private static void SaveBackdropAndClear(BandTarget target, Buffer2DRegion backdrop) + where TPixel : unmanaged, IPixel + { + Rectangle activeBounds = target.ActiveBounds; + for (int y = activeBounds.Top; y < activeBounds.Bottom; y++) + { + int localY = y - activeBounds.Top; + Span dstRow = target.GetRow(y, activeBounds.Left, activeBounds.Width); + + // Layer draws start from transparent black; the old pixels are restored during composition. + dstRow.CopyTo(backdrop.DangerousGetRowSpan(localY)[..activeBounds.Width]); + dstRow.Clear(); + } + } + + /// + public void Dispose() + { + this.startCoverOwner?.Dispose(); + this.lineDataOwner?.Dispose(); + } + + /// + /// Creates an empty scene instance. + /// + private static FlushScene Empty() + => new( + [], + [], + null, + null, + [], + [], + [], + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0); + + /// + /// Counts how many references each band needs for one materialized path and records the packed + /// band assignment (firstBand | lastBand << 16) for each segment in , + /// or -1 for segments that fall outside the item's interest rectangle. + /// The cache is consumed by in Phase 5, eliminating a + /// second enumeration of path geometry. + /// + private static int CountAndStoreBandSegmentRefs( + MaterializedPath path, + int translateY, + in RasterizerOptions options, + int bandCount, + Span bandCounts, + Span bandAssignments) + { + bandCounts.Clear(); + bandAssignments.Fill(-1); + + if (bandCount <= 0 || path.TotalSegmentCount == 0) + { + return 0; + } + + Rectangle interest = options.Interest; + float samplingOffsetY = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; + int rowHeight = DefaultRasterizer.PreferredRowHeight; + int firstSceneBandIndex = FloorDiv(interest.Top, rowHeight); + int bandTopStart = (firstSceneBandIndex * rowHeight) - interest.Top; + int totalCount = 0; + + int segmentIndex = 0; + MaterializedPath.SegmentEnumerator enumerator = path.GetSegmentEnumerator(); + while (enumerator.MoveNext()) + { + if (TryGetLocalBandSpan( + enumerator.CurrentMinY, + enumerator.CurrentMaxY, + translateY, + interest.Top, + interest.Height, + samplingOffsetY, + bandTopStart, + rowHeight, + bandCount, + out int firstBandIndex, + out int lastBandIndex)) + { + bandAssignments[segmentIndex] = firstBandIndex | (lastBandIndex << 16); + for (int bandIndex = firstBandIndex; bandIndex <= lastBandIndex; bandIndex++) + { + bandCounts[bandIndex]++; + totalCount++; + } + } + + segmentIndex++; + } + + return totalCount; + } + + /// + /// Scatters segment indices into the dense per-band reference table by reading the compact + /// band assignment cache built during Phase 4. No path geometry is accessed. + /// + private static void FillBandSegmentRefsFromCache( + ReadOnlySpan bandAssignments, + Span bandCursors, + Span bandSegmentIndices) + { + for (int segmentIndex = 0; segmentIndex < bandAssignments.Length; segmentIndex++) + { + int packed = bandAssignments[segmentIndex]; + if (packed < 0) + { + continue; + } + + int firstBandIndex = packed & 0xFFFF; + int lastBandIndex = (packed >> 16) & 0xFFFF; + for (int bandIndex = firstBandIndex; bandIndex <= lastBandIndex; bandIndex++) + { + bandSegmentIndices[bandCursors[bandIndex]++] = segmentIndex; + } + } + } + + /// + /// Computes the inclusive row-band span touched by a segment with the given Y extents. + /// + private static bool TryGetLocalBandSpan( + float segmentMinY, + float segmentMaxY, + int translateY, + int interestTop, + int interestHeight, + float samplingOffsetY, + int bandTopStart, + int rowHeight, + int bandCount, + out int firstBandIndex, + out int lastBandIndex) + { + float localMinY = ((segmentMinY + translateY) - interestTop) + samplingOffsetY; + float localMaxY = ((segmentMaxY + translateY) - interestTop) + samplingOffsetY; + float clipMinY = MathF.Max(0F, localMinY); + float clipMaxY = MathF.Min(interestHeight, localMaxY); + + if (clipMaxY <= clipMinY) + { + firstBandIndex = 0; + lastBandIndex = -1; + return false; + } + + firstBandIndex = FloorDiv((int)MathF.Floor(clipMinY - bandTopStart), rowHeight); + lastBandIndex = FloorDiv((int)MathF.Ceiling(clipMaxY - bandTopStart) - 1, rowHeight); + if (lastBandIndex < 0 || firstBandIndex >= bandCount) + { + return false; + } + + firstBandIndex = Math.Max(0, firstBandIndex); + lastBandIndex = Math.Min(bandCount - 1, lastBandIndex); + return lastBandIndex >= firstBandIndex; + } + + /// + /// Converts a pixel width to the number of machine-word bit vectors required per row. + /// + private static int BitVectorsForMaxBitCount(int maxBitCount) + => (maxBitCount + (nint.Size * 8) - 1) / (nint.Size * 8); + + /// + /// Performs mathematical floor division for potentially negative coordinates. + /// + private static int FloorDiv(int value, int divisor) + { + int quotient = value / divisor; + int remainder = value % divisor; + if (remainder != 0 && ((remainder < 0) != (divisor < 0))) + { + quotient--; + } + + return quotient; + } + + private readonly struct BandRowOperation + where TPixel : unmanaged, IPixel + { + private readonly BrushRenderer renderer; + private readonly BandTarget target; + private readonly BrushWorkspace workspace; + private readonly int interestLeft; + + /// + /// Initializes a new instance of the struct. + /// + public BandRowOperation( + BrushRenderer renderer, + BandTarget target, + int interestLeft, + BrushWorkspace workspace) + { + this.renderer = renderer; + this.target = target; + this.interestLeft = interestLeft; + this.workspace = workspace; + } + + /// + /// Applies one emitted coverage row to the current band target. + /// + public void InvokeCoverageRow(int y, int startX, Span coverage) + { + int destinationX = this.interestLeft + startX; + Span destinationRow = this.target.GetRow(y, destinationX, coverage.Length); + this.renderer.Apply(destinationRow, coverage, destinationX, y, this.workspace); + } + } + + private readonly struct BandTarget + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the struct. + /// + public BandTarget(Buffer2DRegion region, int originX, int originY, Rectangle activeBounds) + { + this.Region = region; + this.OriginX = originX; + this.OriginY = originY; + this.ActiveBounds = activeBounds; + } + + /// + /// Gets the backing destination region for this band target. + /// + public Buffer2DRegion Region { get; } + + /// + /// Gets the absolute X origin of . + /// + public int OriginX { get; } + + /// + /// Gets the absolute Y origin of . + /// + public int OriginY { get; } + + /// + /// Gets the active absolute bounds within for the current band. + /// + public Rectangle ActiveBounds { get; } + + /// + /// Creates the root band target for the destination frame. + /// + public static BandTarget CreateRoot(Buffer2DRegion destinationFrame) + => new( + destinationFrame, + destinationFrame.Rectangle.X, + destinationFrame.Rectangle.Y, + destinationFrame.Rectangle); + + /// + /// Creates a child band target clipped to the current layer bounds and band span. + /// + public static BandTarget CreateLayer( + BandTarget parent, + Rectangle layerBounds, + int bandTop, + int bandHeight) + { + Rectangle activeBounds = Rectangle.Intersect( + parent.ActiveBounds, + new Rectangle(layerBounds.X, bandTop, layerBounds.Width, bandHeight)); + activeBounds = Rectangle.Intersect(activeBounds, layerBounds); + return new(parent.Region, parent.OriginX, parent.OriginY, activeBounds); + } + + /// + /// Gets a writable row slice in absolute destination coordinates. + /// + public Span GetRow(int y, int x, int length) + { + int localY = y - this.OriginY; + int localX = x - this.OriginX; + return this.Region.DangerousGetRowSpan(localY).Slice(localX, length); + } + } + + /// + /// Row-major execution record for one prebuilt rasterizable band. + /// + private readonly struct RowItem + { + /// + /// Initializes a new instance of the struct. + /// + public RowItem( + int itemIndex, + int lineStart, + int startCoverStart, + Rectangle destinationRegion) + { + this.ItemIndex = itemIndex; + this.LineStart = lineStart; + this.StartCoverStart = startCoverStart; + this.DestinationRegion = destinationRegion; + } + + /// + /// Gets the visible command index referenced by this row item. + /// + public int ItemIndex { get; } + + /// + /// Gets the starting line-data index for this row item. + /// + public int LineStart { get; } + + /// + /// Gets the starting start-cover index for this row item. + /// + public int StartCoverStart { get; } + + /// + /// Gets the target-local destination region covered by this row item. + /// + public Rectangle DestinationRegion { get; } + } + + /// + /// Intermediate row item used while the scene is being built. + /// + private readonly struct PendingRowItem + { + /// + /// Initializes a new instance of the struct. + /// + public PendingRowItem( + int itemIndex, + int localBandIndex, + int segmentStart, + int segmentCount, + Rectangle destinationRegion) + { + this.ItemIndex = itemIndex; + this.LocalBandIndex = localBandIndex; + this.SegmentStart = segmentStart; + this.SegmentCount = segmentCount; + this.DestinationRegion = destinationRegion; + } + + /// + /// Gets the visible command index referenced by this pending row item. + /// + public int ItemIndex { get; } + + /// + /// Gets the local band index within the owning item. + /// + public int LocalBandIndex { get; } + + /// + /// Gets the starting segment-reference index for this pending row item. + /// + public int SegmentStart { get; } + + /// + /// Gets the number of segment references for this pending row item. + /// + public int SegmentCount { get; } + + /// + /// Gets the target-local destination region covered by this pending row item. + /// + public Rectangle DestinationRegion { get; } + } + + /// + /// CPU-only compositing state associated with one active layer depth. + /// + private readonly struct LayerCompositeState + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the struct. + /// + public LayerCompositeState(Buffer2DRegion backdrop, GraphicsOptions options, bool hasBackdrop) + { + this.Backdrop = backdrop; + this.Options = options; + this.HasBackdrop = hasBackdrop; + } + + /// + /// Gets the saved backdrop for this layer scope. + /// + public Buffer2DRegion Backdrop { get; } + + /// + /// Gets the compositing options used when this layer closes. + /// + public GraphicsOptions Options { get; } + + /// + /// Gets a value indicating whether this layer intersects the current band and owns a backdrop. + /// + public bool HasBackdrop { get; } + } + + /// + /// Base raster worker state shared by execution workers. + /// + private class RasterWorkerState : IDisposable + { + private DefaultRasterizer.WorkerScratch? scratch; + + /// + /// Initializes a new instance of the class. + /// + public RasterWorkerState( + MemoryAllocator allocator, + int maxWordsPerRow, + int maxCoverStride, + int maxWidth, + int maxBandCapacity) => + this.scratch = DefaultRasterizer.WorkerScratch.Create( + allocator, + maxWordsPerRow, + maxCoverStride, + maxWidth, + maxBandCapacity); + + /// + /// Gets the reusable raster scratch for this worker. + /// + public ref DefaultRasterizer.WorkerScratch? Scratch => ref this.scratch; + + /// + public void Dispose() + { + this.scratch?.Dispose(); + this.scratch = null; + } + } + + /// + /// Execution worker state that combines raster scratch with brush workspace scratch. + /// + private sealed class ExecuteWorkerState : RasterWorkerState + where TPixel : unmanaged, IPixel + { + private readonly MemoryAllocator allocator; + private Buffer2D?[] layerBackdropBuffers = []; + private LayerCompositeState[] layerCompositeStates = []; + private Rectangle[] layerStack = []; + + /// + /// Initializes a new instance of the class. + /// + public ExecuteWorkerState( + MemoryAllocator allocator, + int maxWordsPerRow, + int maxCoverStride, + int maxWidth, + int maxBandCapacity) + : base(allocator, maxWordsPerRow, maxCoverStride, maxWidth, maxBandCapacity) + { + this.allocator = allocator; + this.BrushWorkspace = new BrushWorkspace(allocator, maxWidth); + } + + /// + /// Gets the reusable brush workspace for this worker. + /// + public BrushWorkspace BrushWorkspace { get; } + + /// + /// Gets writable layer-stack storage sized for the current execution depth. + /// + public Span GetLayerStack() => this.layerStack; + + /// + /// Ensures the structural layer stack can address the requested depth. + /// + public Span EnsureLayerDepth(int depth) + { + int requiredLength = depth + 1; + if (requiredLength > this.layerStack.Length) + { + Array.Resize(ref this.layerStack, Math.Max(requiredLength, this.layerStack.Length == 0 ? 4 : this.layerStack.Length * 2)); + } + + return this.layerStack; + } + + /// + /// Stores the CPU-only compositing state for one active layer depth. + /// + public void SetLayerCompositeState(int depth, Buffer2DRegion backdrop, GraphicsOptions options, bool hasBackdrop) + { + int requiredLength = depth + 1; + if (requiredLength > this.layerCompositeStates.Length) + { + Array.Resize(ref this.layerCompositeStates, Math.Max(requiredLength, this.layerCompositeStates.Length == 0 ? 4 : this.layerCompositeStates.Length * 2)); + } + + this.layerCompositeStates[depth] = new(backdrop, options, hasBackdrop); + } + + /// + /// Gets the CPU-only compositing state for one active layer depth. + /// + public LayerCompositeState GetLayerCompositeState(int depth) + => this.layerCompositeStates[depth]; + + /// + /// Ensures the backdrop buffer table can address the requested depth. + /// + private void EnsureBackdropBufferDepth(int depth) + { + int requiredLength = depth + 1; + if (requiredLength > this.layerBackdropBuffers.Length) + { + Array.Resize(ref this.layerBackdropBuffers, Math.Max(requiredLength, this.layerBackdropBuffers.Length == 0 ? 4 : this.layerBackdropBuffers.Length * 2)); + } + } + + /// + /// Gets scratch storage for the backdrop covered by one active layer depth. + /// The buffer is reused across bands and only grows when a later band needs more space. + /// + /// The current nested layer depth within this worker. + /// The active band width that must be preserved. + /// The active band height that must be preserved. + /// A temporary backdrop region sized for the current layer band. + public Buffer2DRegion GetLayerBackdropRegion(int depth, int width, int height) + { + this.EnsureBackdropBufferDepth(depth); + + Buffer2D? buffer = this.layerBackdropBuffers[depth]; + if (buffer is null || buffer.Width < width || buffer.Height < height) + { + buffer?.Dispose(); + buffer = this.allocator.Allocate2D(width, height, AllocationOptions.None); + this.layerBackdropBuffers[depth] = buffer; + } + + return new Buffer2DRegion(buffer, new Rectangle(0, 0, width, height)); + } + + /// + /// Releases the worker-local brush workspace and raster scratch owned by this state. + /// + public new void Dispose() + { + for (int i = 0; i < this.layerBackdropBuffers.Length; i++) + { + this.layerBackdropBuffers[i]?.Dispose(); + } + + this.BrushWorkspace.Dispose(); + base.Dispose(); + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame.cs b/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame.cs new file mode 100644 index 000000000..f3cb2e795 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Per-frame destination for . +/// +/// The pixel format. +public interface ICanvasFrame + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the frame bounds in root target coordinates. + /// + public Rectangle Bounds { get; } + + /// + /// Attempts to get a CPU-accessible destination region. + /// + /// The CPU region when available. + /// when a CPU region is available. + public bool TryGetCpuRegion(out Buffer2DRegion region); + + /// + /// Attempts to get an opaque native destination surface. + /// + /// The native surface when available. + /// when a native surface is available. + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface); +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 92f6f56e4..105418a58 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -1,62 +1,49 @@ // 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. +/// 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 +public interface IDrawingBackend { /// - /// Fills a path into the destination image using the given brush and drawing options. + /// Gets a value indicating whether this backend is available on the current system. + /// + public bool IsSupported => true; + + /// + /// Flushes queued composition operations for the target. /// - /// - /// 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( + /// Destination frame. + /// Scene commands in submission order. + public void FlushCompositions( Configuration configuration, - ImageFrame source, - IPath path, - Brush brush, - in GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - Rectangle brushBounds, - MemoryAllocator allocator) + ICanvasFrame target, + CompositionScene compositionScene) where TPixel : unmanaged, IPixel; /// - /// Rasterizes path coverage into a floating-point destination map. + /// Attempts to read source pixels from the target into a caller-provided buffer. /// - /// - /// 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); + /// The pixel format. + /// The active processing configuration. + /// The target frame. + /// Source rectangle in target-local coordinates. + /// + /// The caller-allocated region to receive the pixel data. + /// Must be at least as large as (clamped to target bounds). + /// + /// when readback succeeds; otherwise . + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/MaterializedPath.cs b/src/ImageSharp.Drawing/Processing/Backends/MaterializedPath.cs new file mode 100644 index 000000000..64f2c3f5b --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/MaterializedPath.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Lightweight indexed view over flattened path data. References existing +/// point buffers without copying vertex data — only small metadata arrays are allocated. +/// +internal readonly struct MaterializedPath +{ + private readonly SubPathInfo[] subPaths; + + private MaterializedPath(SubPathInfo[] subPaths, int totalSegmentCount) + { + this.subPaths = subPaths; + this.TotalSegmentCount = totalSegmentCount; + } + + /// + /// Gets the total number of line segments across all subpaths. + /// + public int TotalSegmentCount { get; } + + /// + /// Creates a materialized path from a prepared . + /// + public static MaterializedPath Create(IPath path) + { + List? list = null; + int totalSegments = 0; + + foreach (ISimplePath sp in path.Flatten()) + { + ReadOnlyMemory points = sp.Points; + if (points.Length < 2) + { + continue; + } + + int segCount = sp.IsClosed ? points.Length : points.Length - 1; + list ??= []; + list.Add(new SubPathInfo(points, sp.IsClosed, totalSegments)); + totalSegments += segCount; + } + + if (list is null) + { + return default; + } + + return new MaterializedPath([.. list], totalSegments); + } + + /// + /// Gets the segment (P0, P1) at the given flat index. + /// The is used to avoid searching from the start; + /// pass 0 initially and the method updates it to the current subpath index. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void GetSegment(int flatIndex, out PointF p0, out PointF p1, ref int subPathHint) + { + SubPathInfo info = this.FindSubPath(flatIndex, ref subPathHint); + int localIndex = flatIndex - info.SegmentOffset; + ReadOnlySpan points = info.Points.Span; + p0 = points[localIndex]; + p1 = points[(localIndex + 1) % points.Length]; + } + + /// + /// Returns an enumerator that yields (P0, P1, MinY, MaxY) for every segment. + /// + public SegmentEnumerator GetSegmentEnumerator() => new(this.subPaths); + + private SubPathInfo FindSubPath(int flatIndex, ref int hint) + { + SubPathInfo[] sp = this.subPaths; + + // Start from the hint and scan forward/backward. Band segment indices + // are sorted so the next lookup is almost always at the same or next subpath. + int i = Math.Min(hint, sp.Length - 1); + + // If we overshot, scan backward. + while (i > 0 && flatIndex < sp[i].SegmentOffset) + { + i--; + } + + // Scan forward to the right subpath. + while (i + 1 < sp.Length && flatIndex >= sp[i + 1].SegmentOffset) + { + i++; + } + + hint = i; + return sp[i]; + } + + internal readonly struct SubPathInfo(ReadOnlyMemory points, bool isClosed, int segmentOffset) + { + public readonly ReadOnlyMemory Points = points; + public readonly bool IsClosed = isClosed; + public readonly int SegmentOffset = segmentOffset; + } + + /// + /// Enumerates all segments in a materialized path, yielding endpoints and Y extents. + /// + internal ref struct SegmentEnumerator + { + private readonly SubPathInfo[] subPaths; + private int subPathIndex; + private int localIndex; + private int segmentCount; + private ReadOnlySpan currentPoints; + + internal SegmentEnumerator(SubPathInfo[] subPaths) + { + this.subPaths = subPaths; + this.subPathIndex = 0; + this.localIndex = -1; + this.segmentCount = 0; + this.currentPoints = default; + this.CurrentP0 = default; + this.CurrentP1 = default; + this.CurrentMinY = 0; + this.CurrentMaxY = 0; + + if (subPaths is { Length: > 0 }) + { + SubPathInfo first = subPaths[0]; + this.currentPoints = first.Points.Span; + this.segmentCount = first.IsClosed ? first.Points.Length : first.Points.Length - 1; + } + } + + public PointF CurrentP0 { get; private set; } + + public PointF CurrentP1 { get; private set; } + + public float CurrentMinY { get; private set; } + + public float CurrentMaxY { get; private set; } + + public bool MoveNext() + { + while (true) + { + this.localIndex++; + if (this.localIndex < this.segmentCount) + { + this.CurrentP0 = this.currentPoints[this.localIndex]; + this.CurrentP1 = this.currentPoints[(this.localIndex + 1) % this.currentPoints.Length]; + this.CurrentMinY = Math.Min(this.CurrentP0.Y, this.CurrentP1.Y); + this.CurrentMaxY = Math.Max(this.CurrentP0.Y, this.CurrentP1.Y); + return true; + } + + this.subPathIndex++; + if (this.subPathIndex >= this.subPaths.Length) + { + return false; + } + + SubPathInfo info = this.subPaths[this.subPathIndex]; + this.currentPoints = info.Points.Span; + this.segmentCount = info.IsClosed ? info.Points.Length : info.Points.Length - 1; + this.localIndex = -1; + } + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs new file mode 100644 index 000000000..6105eca8e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/MemoryCanvasFrame{TPixel}.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Canvas frame adapter over a . +/// +/// The pixel format. +public sealed class MemoryCanvasFrame : ICanvasFrame + where TPixel : unmanaged, IPixel +{ + private readonly Buffer2DRegion region; + + /// + /// Initializes a new instance of the class. + /// + /// The pixel buffer region backing this frame. + public MemoryCanvasFrame(Buffer2DRegion region) + { + Guard.NotNull(region.Buffer, nameof(region)); + this.region = region; + } + + /// + public Rectangle Bounds => this.region.Rectangle; + + /// + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = this.region; + return true; + } + + /// + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + { + surface = null; + return false; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs new file mode 100644 index 000000000..b09d39b5a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/NativeCanvasFrame{TPixel}.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Canvas frame adapter over a . +/// +/// The pixel format. +public sealed class NativeCanvasFrame : ICanvasFrame + where TPixel : unmanaged, IPixel +{ + private readonly NativeSurface surface; + + /// + /// Initializes a new instance of the class. + /// + /// The frame bounds. + /// The native surface backing this frame. + public NativeCanvasFrame(Rectangle bounds, NativeSurface surface) + { + Guard.NotNull(surface, nameof(surface)); + this.Bounds = bounds; + this.surface = surface; + } + + /// + public Rectangle Bounds { get; } + + /// + public bool TryGetCpuRegion(out Buffer2DRegion region) + { + region = default; + return false; + } + + /// + public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) + { + surface = this.surface; + return true; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs b/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs new file mode 100644 index 000000000..5791ae03a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Opaque native destination with backend capability attachments. +/// +public sealed class NativeSurface +{ + private readonly ConcurrentDictionary capabilities = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Pixel format information for the destination surface. + public NativeSurface(PixelTypeInfo pixelType) + => this.PixelType = pixelType; + + /// + /// Gets pixel format information for this destination surface. + /// + public PixelTypeInfo PixelType { get; } + + /// + /// Sets or replaces a capability object. + /// + /// Capability type. + /// Capability instance. + public void SetCapability(TCapability capability) + where TCapability : class + { + Guard.NotNull(capability, nameof(capability)); + this.capabilities[typeof(TCapability)] = capability; + } + + /// + /// Attempts to get a capability object by type. + /// + /// Capability type. + /// Capability instance when available. + /// when found. + public bool TryGetCapability([NotNullWhen(true)] out TCapability? capability) + where TCapability : class + { + if (this.capabilities.TryGetValue(typeof(TCapability), out object? value) && value is TCapability typed) + { + capability = typed; + return true; + } + + capability = null; + return false; + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD new file mode 100644 index 000000000..16e6b5555 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/PolygonScanning.MD @@ -0,0 +1,304 @@ +# DefaultRasterizer (Fixed-Point, Tiled + Banded Fallback) + +This document describes the current `DefaultRasterizer` implementation in +`src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs`. + +The scanner is a fixed-point, area/cover rasterizer inspired by Blaze-style +scan conversion. + +https://github.com/aurimasg/blaze (MIT-Licensed) + +## Goals + +- 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 + +### Fill Path (`RasterizeRows`) + +``` +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 +``` + +### Stroke Path (`RasterizeStrokeRows`) + +Stroke rasterization fuses stroke expansion with coverage rasterization so that +each parallel band only expands the centerline edges that overlap it. This avoids +the cost of a serial full-path `GenerateOutline()` call and eliminates the +intermediate `IPath` allocation for the expanded outline. + +For dashed strokes, `SplitPathExtensions` splits the centerline into dash segments +on the CPU before passing the result through the same per-band stroke expansion +pipeline. + +``` +IPath (centerline) + | + +--> [if dashed] SplitPathExtensions.GenerateDashes(path, strokeWidth, pattern) + | + v +path.Flatten() -> List (preserving open/closed state) + | + v +BuildStrokeEdgeTable(contours) -> StrokeEdgeData[] + | For each contour: + | - Closed: N side edges + N join descriptors = 2N descriptors + | - Open: (N-1) side edges + (N-2) joins + 2 caps = 2N-1 descriptors + | Each descriptor carries StrokeEdgeFlags (None/Join/CapStart/CapEnd) + | + v +TryBuildBandSortedStrokeEdges(edges, expansion) + | Band-sort with Y expansion = halfWidth * max(miterLimit, 1) + | to ensure join/cap geometry reaches all overlapping bands + | + v +Choose execution mode: + | + +--> Parallel row-tiles + | | + | +--> Per tile: ExpandStrokeEdges -> EmitOutlineEdge -> RasterizeLine + | +--> EmitCoverageRows -> ordered emit via output buffer + | + +--> Sequential band loop + | + +--> Per band: ExpandStrokeEdges -> EmitOutlineEdge -> RasterizeLine + +--> EmitCoverageRows -> direct callback +``` + +#### Stroke Edge Expansion (`ExpandStrokeEdges`) + +Each `StrokeEdgeData` descriptor is expanded into outline edges based on its +`StrokeEdgeFlags`, mirroring the GPU `StrokeExpandComputeShader`: + +| Flag | Expansion | Outline edges | +|------|-----------|---------------| +| `None` (side) | Two edges offset by stroke normal | 2 | +| `Join` | Inner bevel + outer join (miter/round/bevel) | 2-N (round scales with width) | +| `CapStart`/`CapEnd` | Cap geometry (butt/square/round) | 1-N (round scales with width) | + +Outline edges are converted from float to 24.8 fixed-point, clipped to band +bounds, and fed directly to `RasterizeLine` — no intermediate edge buffer. + +## Coordinate System and Precision + +- Geometry is transformed to scanner-local coordinates: + - `xLocal = (x - interest.Left) + samplingOffsetX` + - `yLocal = (y - interest.Top) + samplingOffsetY` (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 +``` + +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)`. + +### Aliased Thresholding + +When `RasterizationMode` is `Aliased`, the continuous coverage value computed +above is snapped to binary using `AntialiasThreshold`: + +``` +if coverage >= antialiasThreshold: + coverage = 1.0 +else: + coverage = 0.0 +``` + +Lower threshold values (e.g. 0.1) preserve more edge pixels and thin features; +higher values (e.g. 0.9) produce a tighter, more conservative fill. The default +threshold is 0.5. + +## 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 using `AntialiasThreshold` +- `RasterizerOptions.AntialiasThreshold` (0–1, default 0.5): the coverage cutoff + used in `Aliased` mode. Pixels with coverage at or above this value become fully + opaque; pixels below are discarded. Ignored in `Antialiased` mode. +- `RasterizerSamplingOrigin` affects both X and Y sample 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 + +- 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/Processing/Backends/PreparedCompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs new file mode 100644 index 000000000..366782163 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/PreparedCompositionCommand.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// One normalized composition command that applies a brush to the active coverage map. +/// +public struct PreparedCompositionCommand +{ + /// + /// Initializes a new instance of the struct. + /// + /// The destination region in target-local coordinates. + /// The source offset into the pre-rasterized coverage map. + /// The brush used during composition. + /// Brush bounds used for applicator creation. + /// Graphics options used during composition. + /// The absolute destination offset from the original composition command. + public PreparedCompositionCommand( + Rectangle destinationRegion, + Point sourceOffset, + Brush brush, + Rectangle brushBounds, + GraphicsOptions graphicsOptions, + Point destinationOffset) + { + this.DestinationRegion = destinationRegion; + this.SourceOffset = sourceOffset; + this.Brush = brush; + this.BrushBounds = brushBounds; + this.GraphicsOptions = graphicsOptions; + this.DestinationOffset = destinationOffset; + } + + /// + /// Gets or sets the destination region in target-local coordinates. + /// + public Rectangle DestinationRegion { get; set; } + + /// + /// Gets or sets the source offset into the pre-rasterized coverage map. + /// + public Point SourceOffset { get; set; } + + /// + /// Gets the brush used during composition. + /// + public Brush Brush { get; } + + /// + /// Gets brush bounds used for applicator creation. + /// + public Rectangle BrushBounds { get; } + + /// + /// Gets graphics options used during composition. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the absolute destination offset from the original composition command. + /// + public Point DestinationOffset { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/RasterizerCoverageRowHandler.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerCoverageRowHandler.cs new file mode 100644 index 000000000..405c2cd76 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerCoverageRowHandler.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Delegate invoked for each emitted non-zero coverage span. +/// +/// The destination y coordinate. +/// The first x coordinate represented by . +/// Non-zero coverage values starting at . +internal delegate void RasterizerCoverageRowHandler(int y, int startX, Span coverage); diff --git a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs similarity index 75% rename from src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs rename to src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs index 66a8cfbb7..9f1639047 100644 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/RasterizerOptions.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Describes whether rasterizers should emit continuous coverage or binary aliased coverage. /// -internal enum RasterizationMode +public enum RasterizationMode { /// /// Emit continuous coverage in the range [0, 1]. @@ -22,7 +22,7 @@ internal enum RasterizationMode /// /// Describes where sample coverage is aligned relative to destination pixels. /// -internal enum RasterizerSamplingOrigin +public enum RasterizerSamplingOrigin { /// /// Samples are aligned to pixel boundaries. @@ -38,7 +38,7 @@ internal enum RasterizerSamplingOrigin /// /// Immutable options used by rasterizers when scan-converting vector geometry. /// -internal readonly struct RasterizerOptions +public readonly struct RasterizerOptions { /// /// Initializes a new instance of the struct. @@ -47,16 +47,19 @@ internal readonly struct RasterizerOptions /// Polygon intersection rule. /// Rasterization coverage mode. /// Sampling origin alignment. + /// Coverage threshold for aliased mode (0–1). public RasterizerOptions( Rectangle interest, IntersectionRule intersectionRule, - RasterizationMode rasterizationMode = RasterizationMode.Antialiased, - RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary) + RasterizationMode rasterizationMode, + RasterizerSamplingOrigin samplingOrigin, + float antialiasThreshold) { this.Interest = interest; this.IntersectionRule = intersectionRule; this.RasterizationMode = rasterizationMode; this.SamplingOrigin = samplingOrigin; + this.AntialiasThreshold = antialiasThreshold; } /// @@ -79,11 +82,17 @@ public RasterizerOptions( /// public RasterizerSamplingOrigin SamplingOrigin { get; } + /// + /// Gets the coverage threshold used when is . + /// Pixels with coverage above this value are rendered as fully opaque; pixels below are discarded. + /// + public float AntialiasThreshold { 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); + => new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin, this.AntialiasThreshold); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/StrokeEdgeFlags.cs b/src/ImageSharp.Drawing/Processing/Backends/StrokeEdgeFlags.cs new file mode 100644 index 000000000..27156fde8 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/StrokeEdgeFlags.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Bit flags identifying the type of a stroke edge descriptor. +/// Values match the WGSL shader constants in StrokeExpandComputeShader. +/// +[Flags] +public enum StrokeEdgeFlags +{ + /// + /// Side edge: (X0,Y0)→(X1,Y1) is a centerline segment. + /// + None = 0, + + /// + /// Join at a contour vertex. + /// (X0,Y0) is the vertex, (X1,Y1) is the previous endpoint, + /// (AdjX,AdjY) is the next endpoint. + /// + Join = 32, + + /// + /// Start cap on an open contour. + /// (X0,Y0) is the cap vertex, (X1,Y1) is the adjacent endpoint. + /// + CapStart = 64, + + /// + /// End cap on an open contour. + /// (X0,Y0) is the cap vertex, (X1,Y1) is the adjacent endpoint. + /// + CapEnd = 128, +} diff --git a/src/ImageSharp.Drawing/Processing/Brush.cs b/src/ImageSharp.Drawing/Processing/Brush.cs index 6b13530f5..4862efe28 100644 --- a/src/ImageSharp.Drawing/Processing/Brush.cs +++ b/src/ImageSharp.Drawing/Processing/Brush.cs @@ -1,39 +1,48 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Represents a logical configuration of a brush which can be used to source pixel colors. /// /// -/// A brush is a simple class that will return an that will perform the -/// logic for retrieving pixel values for specific locations. +/// A brush creates a that performs the logic for retrieving +/// pixel values for specific locations. /// public abstract class Brush : IEquatable { /// - /// Creates the applicator for this brush. + /// Creates the prepared execution object for this brush. /// /// The pixel type. /// The configuration instance to use when performing operations. /// The graphic options. - /// The source image. + /// The canvas width for the current render pass. /// The region the brush will be applied to. /// - /// The for this brush. + /// The for this brush. /// /// /// The when being applied to things like shapes would usually be the /// bounding box of the shape not necessarily the bounds of the whole image. /// - public abstract BrushApplicator CreateApplicator( + public abstract BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, RectangleF region) where TPixel : unmanaged, IPixel; + /// + /// Returns a new brush with its defining geometry transformed by the given matrix. + /// + /// The transformation matrix to apply. + /// A transformed brush, or this if the brush has no spatial parameters. + public virtual Brush Transform(Matrix4x4 matrix) => this; + /// public abstract bool Equals(Brush? other); diff --git a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs b/src/ImageSharp.Drawing/Processing/BrushApplicator.cs deleted file mode 100644 index 99b16023e..000000000 --- a/src/ImageSharp.Drawing/Processing/BrushApplicator.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Performs the application of an implementation against individual scanlines. -/// -/// The pixel format. -/// -public abstract class BrushApplicator : IDisposable - where TPixel : unmanaged, IPixel -{ - /// - /// Initializes a new instance of the class. - /// - /// The configuration instance to use when performing operations. - /// The graphics options. - /// The target image frame. - protected BrushApplicator(Configuration configuration, GraphicsOptions options, ImageFrame target) - { - this.Configuration = configuration; - this.Target = target; - this.Options = options; - this.Blender = PixelOperations.Instance.GetPixelBlender(options); - } - - /// - /// Gets the configuration instance to use when performing operations. - /// - protected Configuration Configuration { get; } - - /// - /// Gets the pixel blender. - /// - internal PixelBlender Blender { get; } - - /// - /// Gets the target image frame. - /// - protected ImageFrame Target { get; } - - /// - /// Gets the graphics options - /// - protected GraphicsOptions Options { get; } - - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the object and frees resources for the Garbage Collector. - /// - /// Whether to dispose managed and unmanaged objects. - protected virtual void Dispose(bool disposing) - { - } - - /// - /// Applies the opacity weighting for each pixel in a scanline to the target based on the - /// pattern contained in the brush. - /// - /// - /// A collection of opacity values between 0 and 1 to be merged with the brushed color value - /// before being applied to the - /// target. - /// - /// The x-position in the target pixel space that the start of the scanline data corresponds to. - /// The y-position in the target pixel space that whole scanline corresponds to. - public abstract void Apply(Span scanline, int x, int y); -} diff --git a/src/ImageSharp.Drawing/Processing/BrushRenderer.cs b/src/ImageSharp.Drawing/Processing/BrushRenderer.cs new file mode 100644 index 000000000..6308b095e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/BrushRenderer.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Renders a against individual coverage scanlines. +/// +/// The pixel format. +public abstract class BrushRenderer : IDisposable + where TPixel : unmanaged, IPixel +{ + /// + /// Initializes a new instance of the class. + /// + /// The configuration instance to use when performing operations. + /// The graphics options. + /// The canvas width for the current render pass. + protected BrushRenderer( + Configuration configuration, + GraphicsOptions options, + int canvasWidth) + { + this.Configuration = configuration; + this.Options = options; + this.CanvasWidth = canvasWidth; + this.Blender = PixelOperations.Instance.GetPixelBlender(options); + } + + /// + /// Gets the configuration instance to use when performing operations. + /// + protected Configuration Configuration { get; } + + /// + /// Gets the pixel blender. + /// + internal PixelBlender Blender { get; } + + /// + /// Gets the graphics options. + /// + protected GraphicsOptions Options { get; } + + /// + /// Gets the canvas width for the current render pass. + /// + protected int CanvasWidth { get; } + + /// + /// Gets a value indicating whether this renderer owns disposable resources. + /// + internal virtual bool RequiresDispose => false; + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Applies the opacity weighting for each pixel in a scanline to the target based on the + /// pattern contained in the brush. + /// + /// The destination row slice to shade. + /// The coverage values for the current destination scanline. + /// The x-position in the target pixel space that the start of the scanline data corresponds to. + /// The y-position in the target pixel space that the scanline corresponds to. + /// The worker-local scratch workspace for temporary blending buffers. + public abstract void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace); + + /// + /// Disposes the object and frees resources for the Garbage Collector. + /// + /// Whether to dispose managed and unmanaged objects. + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/ImageSharp.Drawing/Processing/BrushWorkspace.cs b/src/ImageSharp.Drawing/Processing/BrushWorkspace.cs new file mode 100644 index 000000000..eda40c8f5 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/BrushWorkspace.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Worker-local scratch workspace used by prepared brushes during row composition. +/// +/// The target pixel format. +public sealed class BrushWorkspace : IDisposable + where TPixel : unmanaged, IPixel +{ + private readonly IMemoryOwner amountsOwner; + private readonly IMemoryOwner overlaysOwner; + + internal BrushWorkspace(MemoryAllocator allocator, int rowWidth) + { + int capacity = Math.Max(1, rowWidth); + this.amountsOwner = allocator.Allocate(capacity); + this.overlaysOwner = allocator.Allocate(capacity); + } + + /// + /// Gets the shared amount buffer for the requested length. + /// + /// The number of elements required. + /// A slice of the worker-local pooled amount buffer. + public Span GetAmounts(int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + return this.amountsOwner.Memory.Span[..length]; + } + + /// + /// Gets the shared overlay buffer for the requested length. + /// + /// The number of elements required. + /// A slice of the worker-local pooled overlay buffer. + public Span GetOverlays(int length) + { + ArgumentOutOfRangeException.ThrowIfNegative(length); + return this.overlaysOwner.Memory.Span[..length]; + } + + /// + public void Dispose() + { + this.amountsOwner.Dispose(); + this.overlaysOwner.Dispose(); + } +} diff --git a/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md new file mode 100644 index 000000000..c2b20861e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md @@ -0,0 +1,348 @@ +# DrawingCanvas + +This document describes how `DrawingCanvas` records drawing commands, manages state, handles layers, and hands work off to the drawing backend. + +`DrawingCanvas` is a deferred renderer. Most public drawing calls do not rasterize immediately. They capture intent as `CompositionCommand` instances in `DrawingCanvasBatcher`. The real normalization work happens later, during flush, when each command is prepared and turned into backend-ready geometry. + +## Overview + +At a high level the canvas owns: + +- a target frame +- a drawing backend +- one active batcher +- a stack of immutable drawing-state snapshots +- a stack of active layer records +- a list of temporary image resources that must stay alive until flush + +The canvas itself is the public API surface. The batcher is the deferred command queue. The backend is the execution engine. + +## Core Responsibilities + +`DrawingCanvas` is responsible for: + +- state-stack management (`Save`, `Restore`, `SaveLayer`) +- creating and queuing `CompositionCommand` instances +- routing drawing to the current batcher +- coordinating temporary image lifetimes for `DrawImage` and `Process` +- flushing queued work and releasing temporary resources + +It is not responsible for: + +- flattening paths +- stroke expansion +- clip application +- prepared geometry caching +- rasterization +- brush composition + +Those all happen downstream during flush. + +## State Model + +The active state lives in `DrawingCanvasState`: + +- `Options` +- `ClipPaths` +- `IsLayer` +- `LayerOptions` +- `LayerBounds` + +`DrawingCanvasState` is an immutable snapshot. `ResolveState()` returns the top of `savedStates`, and every drawing call reads from that active state. + +### Save And Restore + +`Save()` pushes a non-layer copy of the current state. + +`Save(options, clipPaths)` pushes a new snapshot with replacement options and clip paths. + +`Restore()` pops one state. If the popped state represents a layer, the canvas composites that layer before returning to the parent batcher. + +`RestoreTo(saveCount)` keeps popping until the requested save depth is reached. + +## Layer Lifecycle + +`SaveLayer` is the mechanism for isolated group rendering. + +The canvas no longer flushes or switches to a temporary backend frame. +Instead it records inline layer boundaries into the shared command stream and +updates the active command target bounds for commands issued inside the layer. + +### SaveLayer Flow + +```mermaid +flowchart TD + A[Save layer request] --> B[Clamp bounds to canvas] + B --> C[Convert layer bounds to absolute target bounds] + C --> D[Queue BeginLayer command] + D --> E[Push layer drawing state with layer target bounds] +``` + +### Restore Layer Flow + +```mermaid +flowchart TD + A[Restore or RestoreTo] --> B{Popped state is a layer?} + B -- No --> C[Return] + B -- Yes --> D[Queue EndLayer command] +``` + +### Important Layer Details + +- Layer isolation now lives in the command stream as `BeginLayer` / `EndLayer`. +- Commands recorded inside a bounded layer use that layer's absolute target bounds. +- The CPU backend lowers layers inline inside `FlushScene`. +- The WebGPU backend lowers layers inline through staged clip/layer commands. +- Explicit `Restore()` and implicit disposal both close layers by recording `EndLayer`. + +## The Batcher + +`DrawingCanvasBatcher` owns the pending command list and flush boundary. + +Its job is narrow: + +- accept queued `CompositionCommand` instances +- prepare them during flush +- package them into a `CompositionScene` +- call `backend.FlushCompositions(...)` +- always clear the command queue afterward + +### Flush Pipeline + +```mermaid +flowchart TD + A[Canvas flush] --> B[Batcher flush] + B --> C{Any commands?} + C -- No --> D[Return] + C -- Yes --> E[PrepareCommands in parallel] + E --> F[Create CompositionScene] + F --> G[Backend flush compositions] + G --> H[Clear command list in finally] + H --> I[DisposePendingImageResources] +``` + +### What Preparation Does + +`CompositionCommand.Prepare(...)` is where command normalization happens: + +- apply the queued transform to the source path +- expand strokes to fill geometry when a `Pen` is present +- apply clip paths +- build or reuse `PreparedGeometry` +- transform the brush into the prepared geometry space +- recompute raster interest +- recompute brush bounds +- recompute the coverage definition key + + +## Command Creation + +Public canvas methods do not do heavy geometry work. They capture inputs and queue commands. + +### Fill + +`Fill(brush, path)`: + +- resolves the active state +- closes the path +- computes placeholder rasterizer options from the current bounds +- queues a `CompositionCommand` + +The queued command still contains: + +- the original path reference +- the original brush +- the active transform +- clip paths +- graphics options +- placeholder rasterizer metadata + +### Draw + +`Draw(pen, path)` follows the same pattern, but: + +- forces `IntersectionRule.NonZero` for stroke semantics +- uses `RasterizerSamplingOrigin.PixelCenter` +- stores the `Pen` so preparation can expand stroke geometry later + +### Clear + +`Clear(...)` is implemented as a fill under a temporary state with `GraphicsOptions` modified for clear semantics. + +### DrawText + +`DrawText(...)`: + +- resolves the active state +- uses `RichTextGlyphRenderer` to build drawing operations +- sorts operations by render pass where needed +- converts operations to `CompositionCommand` instances +- queues them in submission order + +Glyph-local paths can stay shared up to command preparation, which matters for repeated glyph bodies. + +### DrawImage + +`DrawImage(...)` is the main exception to the "queue raw intent" rule. It performs eager image work before queuing the final fill: + +1. crop and/or scale the source image +2. if a canvas transform is active, bake that transform into the image pixels +3. align the transformed bitmap to integer canvas bounds +4. create an `ImageBrush` +5. queue a fill for the final destination path + +After eager image transformation, the queued image command uses: + +- an identity command transform +- clip paths already transformed into the same space as the baked image + +That avoids applying the canvas transform twice. + +### Process + +`Process(path, operation)` is read-modify-write: + +1. flush queued work first +2. compute a conservative source rectangle from the path bounds +3. ask the backend for pixel readback +4. mutate the readback image through `IImageProcessingContext` +5. create an `ImageBrush` over the result +6. queue a fill back into the canvas + +## Frame Model + +Every batcher targets an `ICanvasFrame`. + +`ICanvasFrame` exposes: + +- `Bounds` +- `TryGetCpuRegion(...)` +- `TryGetNativeSurface(...)` + +Implementations: + +- `MemoryCanvasFrame`: CPU-backed frame +- `NativeCanvasFrame`: native or GPU-backed frame +- `CanvasRegionFrame`: clipped view over another frame + +The backend decides how to execute against the frame it receives. + +## Transform Handling + +Transforms live in `DrawingOptions.Transform`. + +For path-based drawing, the transform is queued on the command and applied during `CompositionCommand.Prepare()`. + +For image drawing, the transform is baked into pixels before the command is queued. + +### Transform Responsibilities + +- paths: transformed during command preparation +- brushes: transformed during command preparation +- images: transformed eagerly in `DrawImageCore(...)` + +## Clipping + +Clip paths are stored on the active `DrawingCanvasState` and attached to queued commands. + +During preparation, clipping happens after transform and after stroke expansion: + +```csharp +path = path.Clip(shapeOptions, clipPaths); +``` + +For strokes, the ordering is: + +1. transform path +2. expand stroke to outline +3. apply clip +4. build prepared fill geometry + +## CreateRegion + +`CreateRegion(region)` creates a child canvas over a clipped sub-region. + +```mermaid +flowchart TD + A[Create region request] --> B[Clamp to canvas bounds] + B --> C[Wrap target frame in CanvasRegionFrame] + C --> D[Clone current drawing state with child target bounds] + D --> E[Create child DrawingCanvas] + E --> F[Reuse backend] + E --> G[Reuse batcher] + E --> H[Reuse deferred image resource list] +``` + +The child canvas gets local bounds `(0, 0, clipped.Width, clipped.Height)`. +Commands recorded through that child use the child frame's absolute bounds as +their target bounds and derive destination offsets from that same origin. + +## Disposal + +`Dispose()` is structured, not just a final flush. + +```mermaid +flowchart TD + A[Dispose] --> B[Restore saved state stack to depth 1] + B --> C{Owns shared batcher?} + C -- No --> D[Mark canvas disposed] + C -- Yes --> E[Final batcher flush] + E --> F[DisposePendingImageResources] + F --> G[Mark canvas disposed] +``` + +Important detail: + +- `Dispose()` always unwinds active saved states through the same `EndLayer` recording path used by `Restore()` and `RestoreTo(...)` +- only the owning canvas flushes and releases the shared deferred image resources +- child region canvases therefore do not force an early flush when they are disposed + +## Backend Touchpoints + +`DrawingCanvas` talks to the backend through these operations: + +- `FlushCompositions(...)` +- `TryReadRegion(...)` + +`DefaultDrawingBackend` executes the CPU path. + +`WebGPUDrawingBackend` can supply native surfaces and GPU execution while still fitting the same canvas contract. + +## End-To-End Command Flow + +```mermaid +flowchart TD + A[Public drawing call] --> B[Resolve active DrawingCanvasState] + B --> C[Create CompositionCommand] + C --> D[Queue command in batcher] + D --> E[More draw calls] + E --> F[Flush or Dispose] + F --> G[Batcher flush] + G --> H[PrepareCommands in parallel] + H --> I[Create CompositionScene] + I --> J[Backend flush compositions] + J --> K[CPU row first FlushScene execution] + J --> L[WebGPU batch planning and GPU execution] + K --> M[Clear command list] + L --> M + M --> N[DisposePendingImageResources] +``` + +## Practical Reading Guide + +If you are tracing behavior in code, these are the most useful entry points: + +- `DrawingCanvas{TPixel}.cs` +- `DrawingCanvasBatcher{TPixel}.cs` +- `CompositionCommand.cs` +- `DefaultDrawingBackend.cs` +- `FlushScene.cs` + +Start at the canvas public method, then follow: + +1. command creation +2. batcher flush +3. command preparation +4. backend execution + +That path matches the real runtime behavior much more closely than the public API surface alone. diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs new file mode 100644 index 000000000..acd16ac59 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Queues normalized composition commands emitted by +/// and flushes them to in deterministic draw order. +/// +/// +/// The batcher owns command buffering and normalization only; it does not rasterize or composite. +/// During flush it emits a so each backend can plan execution +/// (for example: CPU batching or GPU tiling) without changing the canvas call surface. +/// +internal sealed class DrawingCanvasBatcher + where TPixel : unmanaged, IPixel +{ + private readonly Configuration configuration; + private readonly IDrawingBackend backend; + private readonly List commands = []; + private bool hasLayers; + + internal DrawingCanvasBatcher( + Configuration configuration, + IDrawingBackend backend, + ICanvasFrame targetFrame) + { + this.configuration = configuration; + this.backend = backend; + this.TargetFrame = targetFrame; + } + + /// + /// Gets the target frame that this batcher flushes to. + /// + public ICanvasFrame TargetFrame { get; } + + /// + /// Appends one normalized composition command to the pending queue. + /// + /// The command to queue. + public void AddComposition(in CompositionCommand composition) + { + this.commands.Add(composition); + this.hasLayers |= composition.Kind is not CompositionCommandKind.FillLayer; + } + + /// + /// Flushes queued commands to the backend as one scene packet, preserving submission order. + /// + /// + /// Backends are responsible for planning execution (for example: grouping by coverage, caching, + /// or GPU binning). The batcher only records scene commands and forwards them on flush. + /// + public void FlushCompositions() + { + if (this.commands.Count == 0) + { + return; + } + + try + { + // Expand stroke commands to fills and clip to target bounds in parallel. + // After this, every command has an immutable prepared path and visibility state. + this.PrepareCommands(); + + CompositionScene scene = new(this.commands, this.hasLayers); + this.backend.FlushCompositions(this.configuration, this.TargetFrame, scene); + } + finally + { + // Always clear the queue, even if backend flush throws. + this.commands.Clear(); + this.hasLayers = false; + } + } + + /// + /// Prepares all queued commands in parallel. Each command expands strokes to fills, + /// applies transforms, clips, flattens its path, and clips to target bounds. + /// After this call every command is a fill with an immutable prepared path + /// and pre-computed visibility. + /// + private void PrepareCommands() + => _ = Parallel.ForEach(Partitioner.Create(0, this.commands.Count), range => + { + Span span = CollectionsMarshal.AsSpan(this.commands); + for (int i = range.Item1; i < range.Item2; i++) + { + span[i].Prepare(); + } + }); +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs new file mode 100644 index 000000000..3854cf9cc --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Immutable drawing state snapshot used by . +/// +internal sealed class DrawingCanvasState +{ + /// + /// Initializes a new instance of the class. + /// + /// Drawing options for this state. + /// Clip paths for this state. + /// Absolute target bounds used for commands recorded in this state. + public DrawingCanvasState( + DrawingOptions options, + IReadOnlyList clipPaths, + Rectangle targetBounds) + { + this.Options = options; + this.ClipPaths = clipPaths; + this.TargetBounds = targetBounds; + } + + /// + /// Gets drawing options associated with this state. + /// + /// + /// This is the original reference supplied to the state. + /// It is not deep-cloned. + /// + public DrawingOptions Options { get; } + + /// + /// Gets clip paths associated with this state. + /// + public IReadOnlyList ClipPaths { get; } + + /// + /// Gets the absolute target bounds used for commands recorded in this state. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets a value indicating whether this state represents a compositing layer. + /// + public bool IsLayer { get; init; } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs new file mode 100644 index 000000000..aafb603ff --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -0,0 +1,1471 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable CA1000 // Do not declare static members on generic types + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// A drawing canvas over a frame target. +/// +/// The pixel format. +public sealed partial class DrawingCanvas : IDrawingCanvas + where TPixel : unmanaged, IPixel +{ + /// + /// Processing configuration used by operations executed through this canvas. + /// + private readonly Configuration configuration; + + /// + /// Backend responsible for rasterizing and composing draw commands. + /// + private readonly IDrawingBackend backend; + + /// + /// Destination frame receiving rendered output. + /// + private readonly ICanvasFrame targetFrame; + + /// + /// Command batcher used to defer and submit composition commands. + /// + private readonly DrawingCanvasBatcher batcher; + + /// + /// Temporary image resources that must stay alive until queued commands are flushed. + /// + private readonly List> pendingImageResources = []; + + /// + /// Indicates whether this canvas owns final disposal of the shared batcher. + /// + private readonly bool ownsBatcher; + + /// + /// Tracks whether this instance has already been disposed. + /// + private bool isDisposed; + + /// + /// Stack of saved drawing states for Save/Restore operations. + /// + private readonly Stack savedStates = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// The destination target region. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + Buffer2DRegion targetRegion, + DrawingOptions options, + params IPath[] clipPaths) + : this(configuration, new MemoryCanvasFrame(targetRegion), options, clipPaths) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// The destination frame. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + ICanvasFrame targetFrame, + DrawingOptions options, + params IPath[] clipPaths) + : this(configuration, configuration.GetDrawingBackend(), targetFrame, options, clipPaths) + { + } + + /// + /// Initializes a new instance of the class with an explicit backend and initial state. + /// + /// The active processing configuration. + /// The drawing backend implementation. + /// The destination frame. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + internal DrawingCanvas( + Configuration configuration, + IDrawingBackend backend, + ICanvasFrame targetFrame, + DrawingOptions options, + params IPath[] clipPaths) + : this( + configuration, + backend, + targetFrame, + new DrawingCanvasBatcher(configuration, backend, targetFrame), + new DrawingCanvasState(options, clipPaths, targetFrame.Bounds), + true) + { + } + + /// + /// Initializes a new instance of the class + /// with explicit backend and batcher instances. + /// + /// The active processing configuration. + /// The drawing backend implementation. + /// The destination frame. + /// The command batcher used for deferred composition. + /// The default state used when no scoped state is active. + /// Whether this canvas owns final disposal of the shared batcher. + private DrawingCanvas( + Configuration configuration, + IDrawingBackend backend, + ICanvasFrame targetFrame, + DrawingCanvasBatcher batcher, + DrawingCanvasState defaultState, + bool ownsBatcher) + { + Guard.NotNull(configuration, nameof(configuration)); + Guard.NotNull(backend, nameof(backend)); + Guard.NotNull(targetFrame, nameof(targetFrame)); + Guard.NotNull(batcher, nameof(batcher)); + Guard.NotNull(defaultState, nameof(defaultState)); + + if (!targetFrame.TryGetCpuRegion(out _) && !targetFrame.TryGetNativeSurface(out _)) + { + throw new NotSupportedException("Canvas frame must expose either a CPU region or a native surface."); + } + + this.configuration = configuration; + this.backend = backend; + this.targetFrame = targetFrame; + this.batcher = batcher; + this.ownsBatcher = ownsBatcher; + + // Canvas coordinates are local to the current frame; origin stays at (0,0). + this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); + this.savedStates.Push(defaultState); + } + + /// + public Rectangle Bounds { get; } + + /// + public int SaveCount => this.savedStates.Count; + + /// + /// Creates a drawing canvas over an existing frame. + /// + /// The frame backing the canvas. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting . + public static DrawingCanvas FromFrame( + ImageFrame frame, + DrawingOptions options, + params IPath[] clipPaths) + { + Guard.NotNull(frame, nameof(frame)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + return new DrawingCanvas( + frame.Configuration, + new Buffer2DRegion(frame.PixelBuffer, frame.Bounds), + options, + clipPaths); + } + + /// + /// Creates a drawing canvas over a specific frame of an image. + /// + /// The image containing the frame. + /// The zero-based frame index to target. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting the selected frame. + public static DrawingCanvas FromImage( + Image image, + int frameIndex, + DrawingOptions options, + params IPath[] clipPaths) + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + Guard.MustBeBetweenOrEqualTo(frameIndex, 0, image.Frames.Count - 1, nameof(frameIndex)); + + return FromFrame(image.Frames[frameIndex], options, clipPaths); + } + + /// + /// Creates a drawing canvas over the root frame of an image. + /// + /// The image whose root frame should be targeted. + /// Initial drawing options for this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting the root frame. + public static DrawingCanvas FromRootFrame( + Image image, + DrawingOptions options, + params IPath[] clipPaths) + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + return FromFrame(image.Frames.RootFrame, options, clipPaths); + } + + /// + public int Save() + { + this.EnsureNotDisposed(); + DrawingCanvasState current = this.ResolveState(); + + // Push a non-layer copy of the current state. + // Only states pushed by SaveLayer() should trigger layer compositing on restore. + this.savedStates.Push(new DrawingCanvasState(current.Options, current.ClipPaths, current.TargetBounds)); + return this.savedStates.Count; + } + + /// + public int Save(DrawingOptions options, params IPath[] clipPaths) + => this.SaveCore(options, clipPaths); + + private int SaveCore(DrawingOptions options, IReadOnlyList clipPaths) + { + this.EnsureNotDisposed(); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + _ = this.Save(); + DrawingCanvasState current = this.ResolveState(); + DrawingCanvasState state = new(options, clipPaths, current.TargetBounds); + _ = this.savedStates.Pop(); + this.savedStates.Push(state); + return this.savedStates.Count; + } + + /// + public int SaveLayer() + => this.SaveLayer(new GraphicsOptions()); + + /// + public int SaveLayer(GraphicsOptions layerOptions) + => this.SaveLayer(layerOptions, this.Bounds); + + /// + public int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds) + { + this.EnsureNotDisposed(); + Guard.NotNull(layerOptions, nameof(layerOptions)); + + Rectangle layerBounds = Rectangle.Intersect(this.Bounds, bounds); + Rectangle absoluteLayerBounds = new( + this.targetFrame.Bounds.X + layerBounds.X, + this.targetFrame.Bounds.Y + layerBounds.Y, + layerBounds.Width, + layerBounds.Height); + + // Keep layer boundaries in the shared command stream so the backend can lower them inline. + this.batcher.AddComposition(CompositionCommand.CreateBeginLayer(absoluteLayerBounds, layerOptions)); + + DrawingCanvasState currentState = this.ResolveState(); + DrawingCanvasState layerState = new(currentState.Options, currentState.ClipPaths, absoluteLayerBounds) + { + IsLayer = true, + }; + + this.savedStates.Push(layerState); + return this.savedStates.Count; + } + + /// + public void Restore() + { + this.EnsureNotDisposed(); + if (this.savedStates.Count <= 1) + { + return; + } + + DrawingCanvasState popped = this.savedStates.Pop(); + if (popped.IsLayer) + { + this.batcher.AddComposition(CompositionCommand.CreateEndLayer()); + } + } + + /// + public void RestoreTo(int saveCount) + { + this.EnsureNotDisposed(); + Guard.MustBeBetweenOrEqualTo(saveCount, 1, this.savedStates.Count, nameof(saveCount)); + + this.RestoreToCore(saveCount); + } + + /// + public DrawingCanvas CreateRegion(Rectangle region) + { + this.EnsureNotDisposed(); + + Rectangle clipped = Rectangle.Intersect(this.Bounds, region); + CanvasRegionFrame childFrame = new(this.targetFrame, clipped); + DrawingCanvasState currentState = this.ResolveState(); + + // Regions share the same batcher and deferred image resources. Only the root canvas owns flushing. + return new DrawingCanvas( + this.configuration, + this.backend, + childFrame, + this.batcher, + new DrawingCanvasState(currentState.Options, currentState.ClipPaths, childFrame.Bounds) + { + IsLayer = currentState.IsLayer, + }, + false); + } + + /// + IDrawingCanvas IDrawingCanvas.CreateRegion(Rectangle region) + => this.CreateRegion(region); + + /// + public void Clear(Brush brush) + { + DrawingCanvasState state = this.ResolveState(); + DrawingOptions options = state.Options.CloneForClearOperation(); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush)); + } + + /// + public void Clear(Brush brush, Rectangle region) + { + DrawingCanvasState state = this.ResolveState(); + DrawingOptions options = state.Options.CloneForClearOperation(); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush, region)); + } + + /// + public void Clear(Brush brush, IPath path) + { + DrawingCanvasState state = this.ResolveState(); + DrawingOptions options = state.Options.CloneForClearOperation(); + this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush, path)); + } + + /// + public void Fill(Brush brush) + => this.Fill(brush, this.Bounds); + + /// + public void Fill(Brush brush, Rectangle region) + => this.Fill(brush, new RectangularPolygon(region.X, region.Y, region.Width, region.Height)); + + /// + public void Fill(Brush brush, IPathCollection paths) + { + Guard.NotNull(paths, nameof(paths)); + foreach (IPath path in paths) + { + this.EnqueueFillPath(brush, path); + } + } + + /// + public void Fill(Brush brush, PathBuilder pathBuilder) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + this.Fill(brush, pathBuilder.Build()); + } + + /// + public void Fill(Brush brush, IPath path) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + this.EnqueueFillPath(brush, path); + } + + /// + public void Process(Rectangle region, Action operation) + => this.Process(new RectangularPolygon(region.X, region.Y, region.Width, region.Height), operation); + + /// + public void Process(PathBuilder pathBuilder, Action operation) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + this.Process(pathBuilder.Build(), operation); + } + + /// + public void Process(IPath path, Action operation) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(operation, nameof(operation)); + + // This operation samples the current destination state. Flush queued commands first + // so readback observes strict draw-order semantics. + this.Flush(); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + IPath closed = path.AsClosedPath(); + + RectangleF rawBounds = RectangleF.Transform(closed.Bounds, effectiveOptions.Transform); + + Rectangle sourceRect = ToConservativeBounds(rawBounds); + sourceRect = Rectangle.Intersect(this.Bounds, sourceRect); + if (sourceRect.Width <= 0 || sourceRect.Height <= 0) + { + return; + } + + // Defensive guard: built-in backends should provide either direct readback (CPU/backed surface) + // or shadow fallback. If this fails, it indicates a backend implementation bug or an unsupported custom backend. + if (!this.TryCreateProcessSourceImage(sourceRect, out Image? sourceImage)) + { + throw new NotSupportedException("Canvas process operations require either CPU pixels, backend readback support, or shadow fallback."); + } + + sourceImage.Mutate(operation); + + Point brushOffset = new( + sourceRect.X - (int)MathF.Floor(rawBounds.Left), + sourceRect.Y - (int)MathF.Floor(rawBounds.Top)); + ImageBrush brush = new(sourceImage, sourceImage.Bounds, brushOffset); + + this.pendingImageResources.Add(sourceImage); + this.PrepareCompositionCore( + closed, + brush, + effectiveOptions, + RasterizerSamplingOrigin.PixelBoundary, + state.ClipPaths); + } + + /// + public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) + => this.Draw(pen, new Path(new ArcLineSegment(center, radius, rotation, startAngle, sweepAngle))); + + /// + public void DrawBezier(Pen pen, params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + this.Draw(pen, new Path(new CubicBezierLineSegment(points))); + } + + /// + public void DrawEllipse(Pen pen, PointF center, SizeF size) + => this.Draw(pen, new EllipsePolygon(center, size)); + + /// + public void DrawLine(Pen pen, params PointF[] points) + { + Guard.NotNull(points, nameof(points)); + this.Draw(pen, new Path(points)); + } + + /// + public void Draw(Pen pen, Rectangle region) + => this.Draw(pen, new RectangularPolygon(region.X, region.Y, region.Width, region.Height)); + + /// + public void Draw(Pen pen, IPathCollection paths) + { + Guard.NotNull(paths, nameof(paths)); + foreach (IPath path in paths) + { + this.Draw(pen, path); + } + } + + /// + public void Draw(Pen pen, PathBuilder pathBuilder) + { + Guard.NotNull(pathBuilder, nameof(pathBuilder)); + this.Draw(pen, pathBuilder.Build()); + } + + /// + public void Draw(Pen pen, IPath path) + { + this.EnsureNotDisposed(); + Guard.NotNull(pen, nameof(pen)); + Guard.NotNull(path, nameof(path)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. + if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) + { + ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); + shapeOptions.IntersectionRule = IntersectionRule.NonZero; + effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); + } + + this.PrepareCompositionCore( + path, + pen.StrokeFill, + effectiveOptions, + RasterizerSamplingOrigin.PixelCenter, + state.ClipPaths, + pen); + } + + /// + public void DrawText( + RichTextOptions textOptions, + ReadOnlySpan text, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + return; + } + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + if (brush is null && pen is null) + { + throw new ArgumentException($"Expected a {nameof(brush)} or {nameof(pen)}. Both were null"); + } + + RichTextOptions configuredOptions = ConfigureTextOptions(textOptions); + using RichTextGlyphRenderer glyphRenderer = new(configuredOptions, effectiveOptions, pen, brush); + TextRenderer renderer = new(glyphRenderer); + renderer.RenderText(text, configuredOptions); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); + } + + /// + public void DrawGlyphs( + Brush brush, + Pen pen, + IReadOnlyList glyphs) + { + this.EnsureNotDisposed(); + Guard.NotNull(brush, nameof(brush)); + Guard.NotNull(pen, nameof(pen)); + Guard.NotNull(glyphs, nameof(glyphs)); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions baseOptions = state.Options; + IReadOnlyList clipPaths = state.ClipPaths; + + for (int glyphIndex = 0; glyphIndex < glyphs.Count; glyphIndex++) + { + GlyphPathCollection glyph = glyphs[glyphIndex]; + if (glyph.LayerCount == 0) + { + continue; + } + + if (glyph.LayerCount == 1) + { + this.Fill(brush, glyph.Paths); + continue; + } + + float glyphArea = glyph.Bounds.Width * glyph.Bounds.Height; + for (int layerIndex = 0; layerIndex < glyph.LayerCount; layerIndex++) + { + GlyphLayerInfo layer = glyph.Layers[layerIndex]; + if (layer.Count == 0) + { + continue; + } + + PathCollection layerPaths = glyph.GetLayerPaths(layerIndex); + DrawingOptions layerOptions = baseOptions.CloneOrReturnForRules( + layer.IntersectionRule, + layer.PixelAlphaCompositionMode, + layer.PixelColorBlendingMode); + + bool shouldFill; + if (layer.Kind is GlyphLayerKind.Decoration or GlyphLayerKind.Glyph) + { + shouldFill = true; + } + else + { + float layerArea = layerPaths.ComputeArea(); + shouldFill = layerArea > 0F && glyphArea > 0F && (layerArea / glyphArea) < 0.50F; + } + + this.ExecuteWithTemporaryState(layerOptions, clipPaths, () => + { + if (shouldFill) + { + this.Fill(brush, layerPaths); + } + else + { + this.Draw(pen, layerPaths); + } + }); + } + } + } + + /// + public RectangleF MeasureTextAdvance(RichTextOptions textOptions, ReadOnlySpan text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } + + FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); + return RectangleF.FromLTRB(advance.Left, advance.Top, advance.Right, advance.Bottom); + } + + /// + public RectangleF MeasureTextBounds(RichTextOptions textOptions, ReadOnlySpan text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } + + FontRectangle bounds = TextMeasurer.MeasureBounds(text, textOptions); + return RectangleF.FromLTRB(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom); + } + + /// + public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } + + FontRectangle renderableBounds = TextMeasurer.MeasureRenderableBounds(text, textOptions); + return RectangleF.FromLTRB(renderableBounds.Left, renderableBounds.Top, renderableBounds.Right, renderableBounds.Bottom); + } + + /// + public RectangleF MeasureTextSize(RichTextOptions textOptions, ReadOnlySpan text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + return RectangleF.Empty; + } + + FontRectangle size = TextMeasurer.MeasureSize(text, textOptions); + return RectangleF.FromLTRB(size.Left, size.Top, size.Right, size.Bottom); + } + + /// + public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan advances) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + advances = []; + return false; + } + + return TextMeasurer.TryMeasureCharacterAdvances(text, textOptions, out advances); + } + + /// + public bool TryMeasureCharacterBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + bounds = []; + return false; + } + + return TextMeasurer.TryMeasureCharacterBounds(text, textOptions, out bounds); + } + + /// + public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + bounds = []; + return false; + } + + return TextMeasurer.TryMeasureCharacterRenderableBounds(text, textOptions, out bounds); + } + + /// + public bool TryMeasureCharacterSizes(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan sizes) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + sizes = []; + return false; + } + + return TextMeasurer.TryMeasureCharacterSizes(text, textOptions, out sizes); + } + + /// + public int CountTextLines(RichTextOptions textOptions, ReadOnlySpan text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + return 0; + } + + return TextMeasurer.CountLines(text, textOptions); + } + + /// + public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, ReadOnlySpan text) + { + this.EnsureNotDisposed(); + Guard.NotNull(textOptions, nameof(textOptions)); + + if (text.IsEmpty) + { + return []; + } + + return TextMeasurer.GetLineMetrics(text, textOptions); + } + + /// + void IDrawingCanvas.DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler) + { + this.EnsureNotDisposed(); + Guard.NotNull(image, nameof(image)); + + if (image is Image specificImage) + { + this.DrawImageCore(specificImage, sourceRect, destinationRect, sampler, ownsSourceImage: false); + return; + } + + Image convertedImage = image.CloneAs(); + this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, ownsSourceImage: true); + } + + /// + public void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler = null) + => this.DrawImageCore(image, sourceRect, destinationRect, sampler, ownsSourceImage: false); + + private void DrawImageCore( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler, + bool ownsSourceImage) + { + this.EnsureNotDisposed(); + Guard.NotNull(image, nameof(image)); + bool disposeSourceImage = ownsSourceImage; + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + DrawingOptions commandOptions = effectiveOptions; + IReadOnlyList commandClipPaths = state.ClipPaths; + + if (sourceRect.Width <= 0 || + sourceRect.Height <= 0 || + destinationRect.Width <= 0 || + destinationRect.Height <= 0) + { + return; + } + + Rectangle clippedSourceRect = Rectangle.Intersect(sourceRect, image.Bounds); + if (clippedSourceRect.Width <= 0 || clippedSourceRect.Height <= 0) + { + return; + } + + RectangleF clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect); + if (clippedDestinationRect.Width <= 0 || clippedDestinationRect.Height <= 0) + { + return; + } + + Size scaledSize = new( + Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)), + Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height))); + + bool requiresScaling = + clippedSourceRect.Width != scaledSize.Width || + clippedSourceRect.Height != scaledSize.Height; + + Image brushImage = image; + RectangleF brushImageRegion = clippedSourceRect; + RectangleF renderDestinationRect = clippedDestinationRect; + Image? ownedImage = null; + + try + { + // Phase 1: Prepare source pixels (crop/scale) in image-local space. + if (requiresScaling) + { + ownedImage = CreateScaledDrawImage(image, clippedSourceRect, scaledSize, sampler); + brushImage = ownedImage; + brushImageRegion = ownedImage.Bounds; + } + else if (clippedSourceRect != image.Bounds) + { + ownedImage = image.Clone(ctx => ctx.Crop(clippedSourceRect)); + brushImage = ownedImage; + brushImageRegion = ownedImage.Bounds; + } + + // Phase 2: Apply canvas transform to image content when requested. + if (effectiveOptions.Transform != Matrix4x4.Identity) + { + Image transformed = CreateTransformedDrawImage( + brushImage, + clippedDestinationRect, + effectiveOptions.Transform, + sampler, + out renderDestinationRect); + + ownedImage?.Dispose(); + ownedImage = transformed; + brushImage = transformed; + brushImageRegion = transformed.Bounds; + + // The image pixels and destination rect are already in transformed canvas space, + // so the queued fill must not apply the canvas transform a second time. + commandOptions = new DrawingOptions( + effectiveOptions.GraphicsOptions, + effectiveOptions.ShapeOptions, + Matrix4x4.Identity); + commandClipPaths = TransformClipPaths(state.ClipPaths, effectiveOptions.Transform); + } + + if (renderDestinationRect.Width <= 0 || renderDestinationRect.Height <= 0) + { + return; + } + + // Phase 3: Transfer temp-image ownership to deferred batch execution. + if (!ReferenceEquals(brushImage, image)) + { + if (disposeSourceImage) + { + image.Dispose(); + disposeSourceImage = false; + } + + this.pendingImageResources.Add(brushImage); + ownedImage = null; + } + else if (disposeSourceImage) + { + this.pendingImageResources.Add(image); + disposeSourceImage = false; + } + + ImageBrush brush = new(brushImage, brushImageRegion); + IPath destinationPath = new RectangularPolygon( + renderDestinationRect.X, + renderDestinationRect.Y, + renderDestinationRect.Width, + renderDestinationRect.Height); + + this.PrepareCompositionCore( + destinationPath, + brush, + commandOptions, + RasterizerSamplingOrigin.PixelBoundary, + commandClipPaths); + } + finally + { + ownedImage?.Dispose(); + if (disposeSourceImage) + { + image.Dispose(); + } + } + } + + /// + /// Prepares a path fill composition command and enqueues it in the batcher. + /// + /// Path to fill. + /// Brush used for shading. + /// Effective drawing options. + /// Rasterizer sampling origin. + /// Optional clip paths to apply during preparation. + /// Optional pen for stroke commands. + private void PrepareCompositionCore( + IPath path, + Brush brush, + DrawingOptions options, + RasterizerSamplingOrigin samplingOrigin, + IReadOnlyList? clipPaths = null, + Pen? pen = null) + { + brush = this.NormalizeBrush(brush); + + GraphicsOptions graphicsOptions = options.GraphicsOptions; + ShapeOptions shapeOptions = options.ShapeOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; + + RectangleF bounds = path.Bounds; + if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) + { + bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); + } + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + RasterizerOptions rasterizerOptions = new( + interest, + shapeOptions.IntersectionRule, + rasterizationMode, + samplingOrigin, + graphicsOptions.AntialiasThreshold); + + DrawingCanvasState state = this.ResolveState(); + + // Commands carry their absolute target bounds and destination origin explicitly. + // Region canvases therefore only need to update state.TargetBounds. + this.batcher.AddComposition( + CompositionCommand.Create( + path, + brush, + graphicsOptions, + in rasterizerOptions, + shapeOptions, + options.Transform, + state.TargetBounds, + state.TargetBounds.Location, + pen, + clipPaths)); + } + + /// + /// Normalizes brushes that carry image sources containing the wrong pixel format exactly once. + /// + /// The logical brush supplied by the caller. + /// The brush to queue for this canvas flush. + private Brush NormalizeBrush(Brush brush) + { + if (brush is not ImageBrush imageBrush) + { + return brush; + } + + if (brush is ImageBrush typedBrush) + { + return typedBrush; + } + + // This brush references an image with a pixel format that doesn't match the canvas target. + // Clone the image as TPixel + Image convertedImage = imageBrush.UntypedImage.CloneAs(); + this.pendingImageResources.Add(convertedImage); + return new ImageBrush(convertedImage, imageBrush.SourceRegion, imageBrush.Offset); + } + + /// + /// Enqueues a fill command for one path using the current canvas state. + /// + /// Brush used for shading. + /// Path to fill. + private void EnqueueFillPath(Brush brush, IPath path) + { + this.EnsureNotDisposed(); + Guard.NotNull(path, nameof(path)); + Guard.NotNull(brush, nameof(brush)); + + DrawingCanvasState state = this.ResolveState(); + IPath closed = path.AsClosedPath(); + + this.PrepareCompositionCore( + closed, + brush, + state.Options, + RasterizerSamplingOrigin.PixelBoundary, + state.ClipPaths); + } + + /// + /// Converts rendered text operations to composition commands and submits them to the batcher. + /// + /// Text drawing operations produced by glyph layout/rendering. + /// Drawing options applied to each operation. + /// Clip paths resolved from effective canvas state. + private void DrawTextOperations( + List operations, + DrawingOptions drawingOptions, + IReadOnlyList clipPaths) + { + this.EnsureNotDisposed(); + + // Build composition commands and enforce render-pass ordering while preserving + // original emission order inside each pass. This preserves overlapping color-font + // layer compositing semantics (for example emoji mouth/teeth layers). + List<(byte RenderPass, int Sequence, CompositionCommand Command)> entries = new(operations.Count); + for (int i = 0; i < operations.Count; i++) + { + DrawingOperation operation = operations[i]; + entries.Add((operation.RenderPass, i, this.CreateCompositionCommand(operation, drawingOptions, clipPaths))); + } + + entries.Sort(static (a, b) => + { + int cmp = a.RenderPass.CompareTo(b.RenderPass); + return cmp != 0 ? cmp : a.Sequence.CompareTo(b.Sequence); + }); + + for (int i = 0; i < entries.Count; i++) + { + this.batcher.AddComposition(entries[i].Command); + } + } + + /// + /// Resolves the currently active drawing state. + /// + /// The current state. + private DrawingCanvasState ResolveState() => this.savedStates.Peek(); + + /// + /// Executes an action with a temporary scoped state, restoring the previous scoped state afterwards. + /// + /// Temporary drawing options. + /// Temporary clip paths. + /// Action to execute. + private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList clipPaths, Action action) + { + int saveCount = this.savedStates.Count; + _ = this.SaveCore(options, clipPaths); + try + { + action(); + } + finally + { + this.RestoreTo(saveCount); + } + } + + /// + /// Attempts to create a source image for process-in-path operations. + /// The backend copies pixels directly into the image's pixel buffer — single copy. + /// + /// Source rectangle in local canvas coordinates. + /// The readback image when available. + /// when source pixels were resolved. + private bool TryCreateProcessSourceImage(Rectangle sourceRect, [NotNullWhen(true)] out Image? sourceImage) + { + sourceImage = new Image(this.configuration, sourceRect.Width, sourceRect.Height); + if (!this.backend.TryReadRegion( + this.configuration, + this.targetFrame, + sourceRect, + new Buffer2DRegion(sourceImage.Frames.RootFrame.PixelBuffer))) + { + sourceImage.Dispose(); + sourceImage = null; + return false; + } + + return true; + } + + /// + public void Flush() + { + this.EnsureNotDisposed(); + try + { + this.batcher.FlushCompositions(); + } + finally + { + this.DisposePendingImageResources(); + } + } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + try + { + // Dispose should finalize the same drawing state transitions as RestoreTo(1), + // otherwise active layers can composite with different options than an explicit restore. + this.RestoreToCore(1); + if (this.ownsBatcher) + { + this.batcher.FlushCompositions(); + } + } + finally + { + if (this.ownsBatcher) + { + this.DisposePendingImageResources(); + } + + this.isDisposed = true; + } + } + + /// + /// Ensures this instance is not disposed. + /// + private void EnsureNotDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); + + /// + /// Restores the saved-state stack to without public guard checks. + /// Layer states are unwound through the normal compositing path so restore and disposal + /// preserve identical layer semantics. + /// + /// The target stack depth to restore to. + private void RestoreToCore(int saveCount) + { + while (this.savedStates.Count > saveCount) + { + DrawingCanvasState popped = this.savedStates.Pop(); + if (popped.IsLayer) + { + // Restore and Dispose unwind layers through the same command stream path. + this.batcher.AddComposition(CompositionCommand.CreateEndLayer()); + } + } + } + + /// + /// Normalizes text options to avoid applying origin translation twice when path-based text is used. + /// + /// Input text options. + /// Normalized text options for rendering. + private static RichTextOptions ConfigureTextOptions(RichTextOptions options) + { + if (options.Path is not null && options.Origin != Vector2.Zero) + { + // Path-based text uses the path itself as positioning source; fold origin into the path + // to avoid applying both path layout and origin translation. + return new RichTextOptions(options) + { + Origin = Vector2.Zero, + Path = options.Path.Translate(options.Origin) + }; + } + + return options; + } + + /// + /// Builds a normalized composition command for a text drawing operation. + /// + /// The source drawing operation. + /// Drawing options applied to the operation. + /// Optional clip paths to apply during preparation. + /// A composition command ready for batching. + private CompositionCommand CreateCompositionCommand( + DrawingOperation operation, + DrawingOptions drawingOptions, + IReadOnlyList? clipPaths = null) + { + Brush compositeBrush = operation.Kind == DrawingOperationKind.Fill + ? operation.Brush! + : operation.Pen!.StrokeFill; + + GraphicsOptions graphicsOptions = + drawingOptions.GraphicsOptions.CloneOrReturnForRules( + operation.PixelAlphaCompositionMode, + operation.PixelColorBlendingMode); + + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + ShapeOptions shapeOptions = drawingOptions.ShapeOptions; + + DrawingCanvasState state = this.ResolveState(); + Point destinationOffset = new( + state.TargetBounds.X + operation.RenderLocation.X, + state.TargetBounds.Y + operation.RenderLocation.Y); + + Pen? pen = operation.Kind == DrawingOperationKind.Draw ? operation.Pen : null; + + // Interest, sampling origin, and intersection rule are computed by Prepare(). + // Placeholder rasterizer options carry only the fields Prepare() preserves. + IntersectionRule intersectionRule = pen is not null && operation.IntersectionRule != IntersectionRule.NonZero + ? IntersectionRule.NonZero + : operation.IntersectionRule; + + RasterizerSamplingOrigin samplingOrigin = pen is not null + ? RasterizerSamplingOrigin.PixelCenter + : RasterizerSamplingOrigin.PixelBoundary; + + RasterizerOptions rasterizerOptions = new( + default, + intersectionRule, + rasterizationMode, + samplingOrigin, + graphicsOptions.AntialiasThreshold); + + return CompositionCommand.Create( + operation.Path, + compositeBrush, + graphicsOptions, + in rasterizerOptions, + shapeOptions, + Matrix4x4.Identity, + state.TargetBounds, + destinationOffset, + pen, + clipPaths); + } + + /// + /// Converts floating bounds to a conservative integer rectangle using floor/ceiling. + /// + /// The floating bounds to convert. + /// A rectangle covering the full floating bounds extent. + private static Rectangle ToConservativeBounds(RectangleF bounds) + => Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + /// + /// Creates resize options used for image drawing operations. + /// + /// Requested output size. + /// Optional resampler. Defaults to bicubic. + /// A resize options instance configured for stretch behavior. + private static ResizeOptions CreateDrawImageResizeOptions(Size size, IResampler? sampler) + => new() + { + Size = size, + Mode = ResizeMode.Stretch, + Sampler = sampler ?? KnownResamplers.Bicubic + }; + + /// + /// Creates a scaled image for drawing, optionally cropping to a source region first. + /// + /// The source image. + /// The clipped source rectangle. + /// The target scaled size. + /// Optional resampler used for scaling. + /// A new image containing the scaled pixels. + private static Image CreateScaledDrawImage( + Image image, + Rectangle clippedSourceRect, + Size scaledSize, + IResampler? sampler) + { + ResizeOptions effectiveResizeOptions = CreateDrawImageResizeOptions(scaledSize, sampler); + if (clippedSourceRect == image.Bounds) + { + return image.Clone(ctx => ctx.Resize(effectiveResizeOptions)); + } + + Image result = image.Clone(ctx => ctx.Crop(clippedSourceRect)); + result.Mutate(ctx => ctx.Resize(effectiveResizeOptions)); + return result; + } + + /// + /// Applies a transform to image content and returns the transformed image. + /// + /// The source image. + /// Destination rectangle in canvas coordinates. + /// Canvas transform to apply. + /// Optional resampler used during transform. + /// Receives the transformed destination bounds. + /// A new image containing transformed pixels. + private static Image CreateTransformedDrawImage( + Image image, + RectangleF destinationRect, + Matrix4x4 transform, + IResampler? sampler, + out RectangleF transformedDestinationRect) + { + // Source space: pixel coordinates in the untransformed source image (0..Width, 0..Height). + // Destination space: where that image would land on the canvas without any extra transform. + // This matrix maps source -> destination by scaling to destination size then translating to destination origin. + Matrix4x4 sourceToDestination = Matrix4x4.CreateScale( + destinationRect.Width / image.Width, + destinationRect.Height / image.Height, + 1) + * Matrix4x4.CreateTranslation(destinationRect.X, destinationRect.Y, 0); + + // Apply the canvas transform after source->destination placement: + // source -> destination -> transformed-canvas. + Matrix4x4 sourceToTransformedCanvas = sourceToDestination * transform; + + // Compute the transformed axis-aligned bounds in canvas space. + RectangleF transformedBounds = RectangleF.Transform( + new RectangleF(0, 0, image.Width, image.Height), + sourceToTransformedCanvas); + + // ImageBrush samples against integer pixel locations. Align the baked bitmap to integer + // canvas bounds so the bitmap origin and brush sampling origin agree exactly. + int alignedLeft = (int)MathF.Floor(transformedBounds.Left); + int alignedTop = (int)MathF.Floor(transformedBounds.Top); + int alignedRight = (int)MathF.Ceiling(transformedBounds.Right); + int alignedBottom = (int)MathF.Ceiling(transformedBounds.Bottom); + + transformedDestinationRect = RectangleF.FromLTRB( + alignedLeft, + alignedTop, + alignedRight, + alignedBottom); + + Size targetSize = new( + Math.Max(1, alignedRight - alignedLeft), + Math.Max(1, alignedBottom - alignedTop)); + + // ImageSharp.Transform expects output coordinates relative to the output bitmap origin (0,0). + // Shift transformed-canvas coordinates so the aligned integer canvas bounds become 0,0. + Matrix4x4 sourceToTarget = sourceToTransformedCanvas + * Matrix4x4.CreateTranslation(-alignedLeft, -alignedTop, 0); + + // Resample source pixels into the target bitmap using the computed source->target mapping. + return image.Clone(ctx => ctx.Transform( + image.Bounds, + sourceToTarget, + targetSize, + sampler ?? KnownResamplers.Bicubic)); + } + + /// + /// Maps a clipped source rectangle back to the corresponding destination rectangle. + /// + /// Original source rectangle. + /// Original destination rectangle. + /// Source rectangle clipped to image bounds. + /// The destination rectangle corresponding to the clipped source region. + private static RectangleF MapSourceClipToDestination( + Rectangle sourceRect, + RectangleF destinationRect, + Rectangle clippedSourceRect) + { + float scaleX = destinationRect.Width / sourceRect.Width; + float scaleY = destinationRect.Height / sourceRect.Height; + + float left = destinationRect.Left + ((clippedSourceRect.Left - sourceRect.Left) * scaleX); + float top = destinationRect.Top + ((clippedSourceRect.Top - sourceRect.Top) * scaleY); + float width = clippedSourceRect.Width * scaleX; + float height = clippedSourceRect.Height * scaleY; + + return new RectangleF(left, top, width, height); + } + + /// + /// Transforms clip paths into the same coordinate space as an eagerly-transformed draw-image command. + /// + /// Clip paths from the current canvas state. + /// Canvas transform already applied to the image content. + /// The transformed clip path list. + private static IReadOnlyList TransformClipPaths(IReadOnlyList clipPaths, Matrix4x4 transform) + { + if (clipPaths.Count == 0 || transform.IsIdentity) + { + return clipPaths; + } + + IPath[] transformed = new IPath[clipPaths.Count]; + for (int i = 0; i < transformed.Length; i++) + { + transformed[i] = clipPaths[i].Transform(transform); + } + + return transformed; + } + + /// + /// Computes the axis-aligned bounding rectangle of a transformed rectangle. + /// + /// + /// Disposes image resources retained for deferred draw execution. + /// + private void DisposePendingImageResources() + { + if (this.pendingImageResources.Count == 0) + { + return; + } + + // Release deferred image resources once queued operations have executed. + for (int i = 0; i < this.pendingImageResources.Count; i++) + { + this.pendingImageResources[i].Dispose(); + } + + this.pendingImageResources.Clear(); + } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingOperation.cs b/src/ImageSharp.Drawing/Processing/DrawingOperation.cs new file mode 100644 index 000000000..d98eac94f --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingOperation.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +internal enum DrawingOperationKind : byte +{ + Fill = 0, + Draw = 1 +} + +internal struct DrawingOperation +{ + public DrawingOperationKind Kind { get; set; } + + public IPath Path { get; set; } + + public Point RenderLocation { get; set; } + + public IntersectionRule IntersectionRule { get; set; } + + public byte RenderPass { get; set; } + + public Brush? Brush { get; set; } + + public Pen? Pen { get; set; } + + public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; set; } + + public PixelColorBlendingMode PixelColorBlendingMode { get; set; } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs index a0f4f959b..8924f629c 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs @@ -6,7 +6,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Options for influencing the drawing functions. +/// Provides options for influencing drawing operations, combining graphics rendering settings, +/// shape fill-rule behavior, and an optional coordinate transform. /// public class DrawingOptions { @@ -20,13 +21,13 @@ public DrawingOptions() { this.graphicsOptions = new GraphicsOptions(); this.shapeOptions = new ShapeOptions(); - this.Transform = Matrix3x2.Identity; + this.Transform = Matrix4x4.Identity; } internal DrawingOptions( GraphicsOptions graphicsOptions, ShapeOptions shapeOptions, - Matrix3x2 transform) + Matrix4x4 transform) { DebugGuard.NotNull(graphicsOptions, nameof(graphicsOptions)); DebugGuard.NotNull(shapeOptions, nameof(shapeOptions)); @@ -37,7 +38,8 @@ internal DrawingOptions( } /// - /// Gets or sets the Graphics Options. + /// Gets or sets the graphics rendering options that control antialiasing, blending, alpha composition, + /// and coverage thresholding for the drawing operation. /// public GraphicsOptions GraphicsOptions { @@ -50,7 +52,7 @@ public GraphicsOptions GraphicsOptions } /// - /// Gets or sets the Shape Options. + /// Gets or sets the shape options that control fill-rule intersection mode and boolean clipping behavior. /// public ShapeOptions ShapeOptions { @@ -63,7 +65,9 @@ public ShapeOptions ShapeOptions } /// - /// Gets or sets the Transform to apply during rasterization. + /// Gets or sets the affine transform matrix applied to vector geometry before rasterization. + /// Can be used to translate, rotate, scale, or skew shapes. + /// Defaults to . /// - public Matrix3x2 Transform { get; set; } + public Matrix4x4 Transform { get; set; } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs index 4d4c66737..942ae2318 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public static class DrawingOptionsDefaultsExtensions { - private const string DrawingTransformMatrixKey = "DrawingTransformMatrix3x2"; + private const string DrawingTransformMatrixKey = "DrawingTransformMatrix4x4"; /// /// Gets the default shape processing options against The source image processing context. @@ -26,7 +26,7 @@ public static DrawingOptions GetDrawingOptions(this IImageProcessingContext cont /// The image processing context to store default against. /// The matrix to use. /// The passed in to allow chaining. - public static IImageProcessingContext SetDrawingTransform(this IImageProcessingContext context, Matrix3x2 matrix) + public static IImageProcessingContext SetDrawingTransform(this IImageProcessingContext context, Matrix4x4 matrix) { context.Properties[DrawingTransformMatrixKey] = matrix; return context; @@ -37,7 +37,7 @@ public static IImageProcessingContext SetDrawingTransform(this IImageProcessingC /// /// The configuration to store default against. /// The default matrix to use. - public static void SetDrawingTransform(this Configuration configuration, Matrix3x2 matrix) + public static void SetDrawingTransform(this Configuration configuration, Matrix4x4 matrix) => configuration.Properties[DrawingTransformMatrixKey] = matrix; /// @@ -45,9 +45,9 @@ public static void SetDrawingTransform(this Configuration configuration, Matrix3 /// /// The image processing context to retrieve defaults from. /// The matrix. - public static Matrix3x2 GetDrawingTransform(this IImageProcessingContext context) + public static Matrix4x4 GetDrawingTransform(this IImageProcessingContext context) { - if (context.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix3x2 go) + if (context.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix4x4 go) { return go; } @@ -62,13 +62,28 @@ public static Matrix3x2 GetDrawingTransform(this IImageProcessingContext context /// /// The configuration to retrieve defaults from. /// The globally configured default matrix. - public static Matrix3x2 GetDrawingTransform(this Configuration configuration) + public static Matrix4x4 GetDrawingTransform(this Configuration configuration) { - if (configuration.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix3x2 go) + if (configuration.Properties.TryGetValue(DrawingTransformMatrixKey, out object? options) && options is Matrix4x4 go) { return go; } - return Matrix3x2.Identity; + return Matrix4x4.Identity; + } + + /// + /// Clones the path graphic options and applies changes required to force clearing. + /// + /// The drawing options to clone + /// A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set + public static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions) + { + GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone(); + options.ColorBlendingMode = PixelColorBlendingMode.Normal; + options.AlphaCompositionMode = PixelAlphaCompositionMode.Src; + options.BlendPercentage = 1F; + + return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform); } } diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index fbe4233f0..278448f6d 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -13,12 +14,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class EllipticGradientBrush : GradientBrush { - private readonly PointF center; - - private readonly PointF referenceAxisEnd; - - private readonly float axisRatio; - /// /// The center of the elliptical gradient and 0 for the color stops. /// The end point of the reference axis of the ellipse. @@ -37,90 +32,112 @@ public EllipticGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.center = center; - this.referenceAxisEnd = referenceAxisEnd; - this.axisRatio = axisRatio; + this.Center = center; + this.ReferenceAxisEnd = referenceAxisEnd; + this.AxisRatio = axisRatio; + } + + /// + /// Gets the center of the ellipse. + /// + public PointF Center { get; } + + /// + /// Gets the end point of the reference axis. + /// + public PointF ReferenceAxisEnd { get; } + + /// + /// Gets the ratio of the secondary axis to the primary axis. + /// + public float AxisRatio { get; } + + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc = PointF.Transform(this.Center, matrix); + PointF tRef = PointF.Transform(this.ReferenceAxisEnd, matrix); + + // Compute a point on the perpendicular (secondary) axis and transform it. + float refDx = this.ReferenceAxisEnd.X - this.Center.X; + float refDy = this.ReferenceAxisEnd.Y - this.Center.Y; + float refLen = MathF.Sqrt((refDx * refDx) + (refDy * refDy)); + float secondLen = refLen * this.AxisRatio; + + // Perpendicular direction (rotated 90 degrees). + PointF secondEnd = new( + this.Center.X + (-refDy / refLen * secondLen), + this.Center.Y + (refDx / refLen * secondLen)); + PointF tSec = PointF.Transform(secondEnd, matrix); + + // Derive new ratio from transformed lengths. + float newRefLen = MathF.Sqrt( + ((tRef.X - tc.X) * (tRef.X - tc.X)) + ((tRef.Y - tc.Y) * (tRef.Y - tc.Y))); + float newSecLen = MathF.Sqrt( + ((tSec.X - tc.X) * (tSec.X - tc.X)) + ((tSec.Y - tc.Y) * (tSec.Y - tc.Y))); + float newRatio = newRefLen > 0f ? newSecLen / newRefLen : this.AxisRatio; + + return new EllipticGradientBrush(tc, tRef, newRatio, this.RepetitionMode, this.ColorStopsArray); } /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, RectangleF region) => - new RadialGradientBrushApplicator( + new EllipticGradientBrushRenderer( configuration, options, - source, - this.center, - this.referenceAxisEnd, - this.axisRatio, - this.ColorStops, + canvasWidth, + this, + this.ColorStopsArray, this.RepetitionMode); /// - private sealed class RadialGradientBrushApplicator : GradientBrushApplicator + private sealed class EllipticGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel { private readonly PointF center; - private readonly PointF referenceAxisEnd; - - private readonly float axisRatio; - - private readonly double rotation; - - private readonly float referenceRadius; - - private readonly float secondRadius; - private readonly float cosRotation; private readonly float sinRotation; - private readonly float secondRadiusSquared; - private readonly float referenceRadiusSquared; + private readonly float secondRadiusSquared; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. - /// Center of the ellipse. - /// Point on one angular points of the ellipse. - /// - /// Ratio of the axis length's. Used to determine the length of the second axis, - /// the first is defined by and . + /// The canvas width for the current render pass. + /// The elliptic gradient brush. /// Definition of colors. /// Defines how the gradient colors are repeated. - public RadialGradientBrushApplicator( + public EllipticGradientBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame target, - PointF center, - PointF referenceAxisEnd, - float axisRatio, + int canvasWidth, + EllipticGradientBrush brush, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, target, colorStops, repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) { - this.center = center; - this.referenceAxisEnd = referenceAxisEnd; - this.axisRatio = axisRatio; - this.rotation = AngleBetween( - this.center, - new PointF(this.center.X + 1, this.center.Y), - this.referenceAxisEnd); - this.referenceRadius = DistanceBetween(this.center, this.referenceAxisEnd); - this.secondRadius = this.referenceRadius * this.axisRatio; - - this.referenceRadiusSquared = this.referenceRadius * this.referenceRadius; - this.secondRadiusSquared = this.secondRadius * this.secondRadius; - - this.sinRotation = (float)Math.Sin(this.rotation); - this.cosRotation = (float)Math.Cos(this.rotation); + this.center = brush.Center; + + float refDx = brush.ReferenceAxisEnd.X - brush.Center.X; + float refDy = brush.ReferenceAxisEnd.Y - brush.Center.Y; + float rotation = MathF.Atan2(refDy, refDx); + float referenceRadius = MathF.Sqrt((refDx * refDx) + (refDy * refDy)); + float secondRadius = referenceRadius * brush.AxisRatio; + + this.referenceRadiusSquared = referenceRadius * referenceRadius; + this.secondRadiusSquared = secondRadius * secondRadius; + this.sinRotation = MathF.Sin(rotation); + this.cosRotation = MathF.Cos(rotation); } /// @@ -135,16 +152,7 @@ protected override float PositionOnGradient(float x, float y) float xSquared = xR * xR; float ySquared = yR * yR; - return (xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared); + return MathF.Sqrt((xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared)); } - - private static float AngleBetween(PointF junction, PointF a, PointF b) - { - PointF vA = a - junction; - PointF vB = b - junction; - return MathF.Atan2(vB.Y, vB.X) - MathF.Atan2(vA.Y, vA.X); - } - - private static float DistanceBetween(PointF p1, PointF p2) => Vector2.Distance(p1, p2); } } diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs deleted file mode 100644 index f60e0f211..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of images without blending. -/// -public static class ClearExtensions -{ - /// - /// Flood fills the image with the specified color without any blending. - /// - /// The source image processing context. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, Color color) - => source.Clear(new SolidBrush(color)); - - /// - /// Flood fills the image with the specified color without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, DrawingOptions options, Color color) - => source.Clear(options, new SolidBrush(color)); - - /// - /// Flood fills the image with the specified brush without any blending. - /// - /// The source image processing context. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, Brush brush) => - source.Clear(source.GetDrawingOptions(), brush); - - /// - /// Flood fills the image with the specified brush without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, DrawingOptions options, Brush brush) - => source.Fill(options.CloneForClearOperation(), brush); - - /// - /// Clones the path graphic options and applies changes required to force clearing. - /// - /// The drawing options to clone - /// A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set - internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions) - { - GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone(); - options.ColorBlendingMode = PixelColorBlendingMode.Normal; - options.AlphaCompositionMode = PixelAlphaCompositionMode.Src; - options.BlendPercentage = 1F; - - return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs deleted file mode 100644 index f5ea9a1f9..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearPathExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of polygon outlines without blending. -/// -public static class ClearPathExtensions -{ - /// - /// Flood fills the image within the provided region defined by an using the specified - /// color without any blending. - /// - /// The source image processing context. - /// The color. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - Color color, - IPath region) - => source.Clear(new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an using the specified color - /// without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - IPath region) - => source.Clear(options, new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an using the specified brush - /// without any blending. - /// - /// The source image processing context. - /// The brush. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - Brush brush, - IPath region) - => source.Clear(source.GetDrawingOptions(), brush, region); - - /// - /// Flood fills the image within the provided region defined by an using the specified brush - /// without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - IPath region) - => source.Fill(options.CloneForClearOperation(), brush, region); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClearRectangleExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClearRectangleExtensions.cs deleted file mode 100644 index 0654942ac..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClearRectangleExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of rectangle outlines without blending. -/// -public static class ClearRectangleExtensions -{ - /// - /// Flood fills the image in the rectangle of the provided rectangle with the specified color without any blending. - /// - /// The source image processing context. - /// The color. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear(this IImageProcessingContext source, Color color, RectangleF rectangle) - => source.Clear(new SolidBrush(color), rectangle); - - /// - /// Flood fills the image in the rectangle of the provided rectangle with the specified color without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - RectangleF rectangle) - => source.Clear(options, new SolidBrush(color), rectangle); - - /// - /// Flood fills the image in the rectangle of the provided rectangle with the specified brush without any blending. - /// - /// The source image processing context. - /// The brush. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - Brush brush, - RectangleF rectangle) - => source.Clear(brush, new RectangularPolygon(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)); - - /// - /// Flood fills the image at the given rectangle bounds with the specified brush without any blending. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The rectangle defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Clear( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - RectangleF rectangle) - => source.Clear(options, brush, new RectangularPolygon(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs deleted file mode 100644 index 10ac9ba3a..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/ClipPathExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the application of processors within a clipped path. -/// -public static class ClipPathExtensions -{ - /// - /// Applies the processing operation within the region defined by an . - /// - /// The source image processing context. - /// - /// The defining the clip region. Only pixels inside the clip are affected. - /// - /// - /// The operation to perform. This executes in the clipped context so results are constrained to the - /// clip bounds. - /// - /// The to allow chaining of operations. - public static IImageProcessingContext Clip( - this IImageProcessingContext source, - IPath region, - Action operation) - => source.ApplyProcessor(new ClipPathProcessor(source.GetDrawingOptions(), region, operation)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawBezierExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawBezierExtensions.cs deleted file mode 100644 index 707e53ac1..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawBezierExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of Bezier paths. -/// -public static class DrawBezierExtensions -{ - /// - /// Draws the provided points as an open Bezier path with the supplied pen - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - params PointF[] points) => - source.Draw(options, pen, new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path with the supplied pen - /// - /// The source image processing context. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - Pen pen, - params PointF[] points) => - source.Draw(pen, new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(options, new SolidPen(brush, thickness), new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(new SolidPen(brush, thickness), new Path(new CubicBezierLineSegment(points))); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - Color color, - float thickness, - params PointF[] points) => - source.DrawBeziers(new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as an open Bezier path at the provided thickness with the supplied brush - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawBeziers( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - params PointF[] points) => - source.DrawBeziers(options, new SolidBrush(color), thickness, points); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawLineExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawLineExtensions.cs deleted file mode 100644 index d4fb0bef3..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawLineExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of lines. -/// -public static class DrawLineExtensions -{ - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The line thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(options, new SolidPen(brush, thickness), new Path(points)); - - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The brush. - /// The line thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - Brush brush, - float thickness, - params PointF[] points) => - source.Draw(new SolidPen(brush, thickness), new Path(points)); - - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The color. - /// The line thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - Color color, - float thickness, - params PointF[] points) => - source.DrawLine(new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as an open linear path at the provided thickness with the supplied brush. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The line thickness. - /// The points. - /// The to allow chaining of operations.> - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - params PointF[] points) => - source.DrawLine(options, new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as an open linear path with the supplied pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - params PointF[] points) => - source.Draw(options, pen, new Path(points)); - - /// - /// Draws the provided points as an open linear path with the supplied pen. - /// - /// The source image processing context. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawLine( - this IImageProcessingContext source, - Pen pen, - params PointF[] points) => - source.Draw(pen, new Path(points)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs deleted file mode 100644 index 6726e2bfc..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of collections of polygon outlines. -/// -public static class DrawPathCollectionExtensions -{ - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - IPathCollection paths) - { - foreach (IPath path in paths) - { - source.Draw(options, pen, path); - } - - return source; - } - - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext - Draw(this IImageProcessingContext source, Pen pen, IPathCollection paths) - => source.Draw(source.GetDrawingOptions(), pen, paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The shapes. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - IPathCollection paths) => - source.Draw(options, new SolidPen(brush, thickness), paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Brush brush, - float thickness, - IPathCollection paths) => - source.Draw(new SolidPen(brush, thickness), paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - IPathCollection paths) => - source.Draw(options, new SolidBrush(color), thickness, paths); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Color color, - float thickness, - IPathCollection paths) => - source.Draw(new SolidBrush(color), thickness, paths); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs deleted file mode 100644 index fd0ed2aa3..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPathExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of polygon outlines. -/// -public static class DrawPathExtensions -{ - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - IPath path) => - source.ApplyProcessor(new DrawPathProcessor(options, pen, path)); - - /// - /// Draws the outline of the polygon with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw(this IImageProcessingContext source, Pen pen, IPath path) => - source.Draw(source.GetDrawingOptions(), pen, path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - IPath path) => - source.Draw(options, new SolidPen(brush, thickness), path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Brush brush, - float thickness, - IPath path) => - source.Draw(new SolidPen(brush, thickness), path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - IPath path) => - source.Draw(options, new SolidBrush(color), thickness, path); - - /// - /// Draws the outline of the polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The path. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Color color, - float thickness, - IPath path) => - source.Draw(new SolidBrush(color), thickness, path); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs deleted file mode 100644 index 6ebad1e06..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawPolygonExtensions.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of closed linear polygons. -/// -public static class DrawPolygonExtensions -{ - /// - /// Draws the provided points as a closed linear polygon with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - Pen pen, - params PointF[] points) => - source.Draw(source.GetDrawingOptions(), pen, new Polygon(points)); - - /// - /// Draws the provided points as a closed linear polygon with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - params PointF[] points) => - source.Draw(options, pen, new Polygon(points)); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - params PointF[] points) => - source.DrawPolygon(options, new SolidPen(brush, thickness), points); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - Brush brush, - float thickness, - params PointF[] points) => - source.DrawPolygon(new SolidPen(brush, thickness), points); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - Color color, - float thickness, - params PointF[] points) => - source.DrawPolygon(new SolidBrush(color), thickness, points); - - /// - /// Draws the provided points as a closed linear polygon with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - params PointF[] points) => - source.DrawPolygon(options, new SolidBrush(color), thickness, points); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs deleted file mode 100644 index 0971db357..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawRectangleExtensions.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of rectangles. -/// -public static class DrawRectangleExtensions -{ - /// - /// Draws the outline of the rectangle with the provided pen. - /// - /// The source image processing context. - /// The options. - /// The pen. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Pen pen, - RectangleF shape) => - source.Draw(options, pen, new RectangularPolygon(shape.X, shape.Y, shape.Width, shape.Height)); - - /// - /// Draws the outline of the rectangle with the provided pen. - /// - /// The source image processing context. - /// The pen. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw(this IImageProcessingContext source, Pen pen, RectangleF shape) => - source.Draw(source.GetDrawingOptions(), pen, shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - float thickness, - RectangleF shape) => - source.Draw(options, new SolidPen(brush, thickness), shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The brush. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Brush brush, - float thickness, - RectangleF shape) => - source.Draw(new SolidPen(brush, thickness), shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - float thickness, - RectangleF shape) => - source.Draw(options, new SolidBrush(color), thickness, shape); - - /// - /// Draws the outline of the rectangle with the provided brush at the provided thickness. - /// - /// The source image processing context. - /// The color. - /// The thickness. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Draw( - this IImageProcessingContext source, - Color color, - float thickness, - RectangleF shape) => - source.Draw(new SolidBrush(color), thickness, shape); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs deleted file mode 100644 index 9ad68315a..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/DrawTextExtensions.cs +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the drawing of text. -/// -public static class DrawTextExtensions -{ - /// - /// Draws the text onto the image filled with the given color. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The color. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Color color, - PointF location) => - source.DrawText(source.GetDrawingOptions(), text, font, color, location); - - /// - /// Draws the text using the supplied drawing options onto the image filled with the given color. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The color. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Color color, - PointF location) => - source.DrawText(drawingOptions, text, font, Brushes.Solid(color), null, location); - - /// - /// Draws the text using the supplied text options onto the image filled via the brush. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Color color) => - source.DrawText(textOptions, text, Brushes.Solid(color), null); - - /// - /// Draws the text onto the image filled via the brush. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Brush brush, - PointF location) => - source.DrawText(source.GetDrawingOptions(), text, font, brush, location); - - /// - /// Draws the text onto the image outlined via the pen. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Pen pen, - PointF location) => - source.DrawText(source.GetDrawingOptions(), text, font, pen, location); - - /// - /// Draws the text onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - string text, - Font font, - Brush brush, - Pen pen, - PointF location) - { - RichTextOptions textOptions = new(font) { Origin = location }; - return source.DrawText(textOptions, text, brush, pen); - } - - /// - /// Draws the text using the given options onto the image filled via the brush. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The brush used to fill the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Brush brush) => - source.DrawText(source.GetDrawingOptions(), textOptions, text, brush, null); - - /// - /// Draws the text using the given options onto the image outlined via the pen. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The pen used to outline the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Pen pen) => - source.DrawText(source.GetDrawingOptions(), textOptions, text, null, pen); - - /// - /// Draws the text using the given options onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The text rendering options. - /// The text to draw. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - RichTextOptions textOptions, - string text, - Brush? brush, - Pen? pen) => - source.DrawText(source.GetDrawingOptions(), textOptions, text, brush, pen); - - /// - /// Draws the text onto the image outlined via the pen. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Pen pen, - PointF location) - => source.DrawText(drawingOptions, text, font, null, pen, location); - - /// - /// Draws the text onto the image filled via the brush. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Brush brush, - PointF location) - => source.DrawText(drawingOptions, text, font, brush, null, location); - - /// - /// Draws the text using the given drawing options onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The drawing options. - /// The text to draw. - /// The font. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The location. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - string text, - Font font, - Brush? brush, - Pen? pen, - PointF location) - { - RichTextOptions textOptions = new(font) { Origin = location }; - return source.ApplyProcessor(new DrawTextProcessor(drawingOptions, textOptions, text, brush, pen)); - } - - /// - /// Draws the text using the given options onto the image filled via the brush then outlined via the pen. - /// - /// The source image processing context. - /// The drawing options. - /// The text rendering options. - /// The text to draw. - /// The brush used to fill the text. - /// The pen used to outline the text. - /// The to allow chaining of operations. - public static IImageProcessingContext DrawText( - this IImageProcessingContext source, - DrawingOptions drawingOptions, - RichTextOptions textOptions, - string text, - Brush? brush, - Pen? pen) - => source.ApplyProcessor(new DrawTextProcessor(drawingOptions, textOptions, text, brush, pen)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillExtensions.cs deleted file mode 100644 index 86bb20c23..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of images. -/// -public static class FillExtensions -{ - /// - /// Flood fills the image with the specified color. - /// - /// The source image processing context. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, Color color) - => source.Fill(new SolidBrush(color)); - - /// - /// Flood fills the image with the specified color. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, DrawingOptions options, Color color) - => source.Fill(options, new SolidBrush(color)); - - /// - /// Flood fills the image with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, Brush brush) - => source.Fill(source.GetDrawingOptions(), brush); - - /// - /// Flood fills the image with the specified brush. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill(this IImageProcessingContext source, DrawingOptions options, Brush brush) - => source.ApplyProcessor(new FillProcessor(options, brush)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs deleted file mode 100644 index 1629bdfa8..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathBuilderExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the flood filling of polygon outlines. -/// -public static class FillPathBuilderExtensions -{ - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified color. - /// - /// The source image processing context. - /// The color. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - Action region) - => source.Fill(new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified color. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - Action region) - => source.Fill(options, new SolidBrush(color), region); - - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - Action region) - => source.Fill(source.GetDrawingOptions(), brush, region); - - /// - /// Flood fills the image within the provided region defined by an method - /// using the specified brush. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The method defining the region to fill. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - Action region) - { - PathBuilder pb = new(); - region(pb); - - return source.Fill(options, brush, pb.Build()); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs deleted file mode 100644 index 3b8cb4d8b..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Text; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of collections of polygon outlines. -/// -public static class FillPathCollectionExtensions -{ - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The collection of paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - IPathCollection paths) - { - foreach (IPath s in paths) - { - source.Fill(options, brush, s); - } - - return source; - } - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified brush and pen. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The pen. - /// The collection of glyph paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - Pen pen, - IReadOnlyList paths) - => source.Fill(options, brush, pen, paths, static (gp, layer, path) => - { - if (layer.Kind == GlyphLayerKind.Decoration) - { - // Decorations (underlines, strikethroughs, etc) are always filled. - return true; - } - - if (layer.Kind == GlyphLayerKind.Glyph) - { - // Standard glyph layers are filled by default. - return true; - } - - // Default heuristic: stroke "background-like" layers (large coverage), fill others. - // Use the bounding box area as an approximation of the glyph area as it is cheaper to compute. - float glyphArea = gp.Bounds.Width * gp.Bounds.Height; - float layerArea = path.ComputeArea(); - - if (layerArea <= 0 || glyphArea <= 0) - { - return false; // degenerate glyph, don't fill - } - - float coverage = layerArea / glyphArea; - - // <50% coverage, fill. Otherwise, stroke. - return coverage < 0.50F; - }); - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified brush and pen. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The graphics options. - /// The brush. - /// The pen. - /// The collection of glyph paths. - /// - /// A function that decides whether to fill or stroke a given layer within a multi-layer (painted) glyph. - /// - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - Pen pen, - IReadOnlyList paths, - Func shouldFillLayer) - { - foreach (GlyphPathCollection gp in paths) - { - if (gp.LayerCount == 0) - { - continue; - } - - if (gp.LayerCount == 1) - { - // Single-layer glyph: just fill with the supplied brush. - source.Fill(options, brush, gp.Paths); - continue; - } - - // Multi-layer: decide per layer whether to fill or stroke. - for (int i = 0; i < gp.Layers.Count; i++) - { - GlyphLayerInfo layer = gp.Layers[i]; - IPath path = gp.PathList[i]; - - if (shouldFillLayer(gp, layer, path)) - { - // Respect the layer's fill rule if different to the drawing options. - DrawingOptions o = options.CloneOrReturnForRules( - layer.IntersectionRule, - layer.PixelAlphaCompositionMode, - layer.PixelColorBlendingMode); - - source.Fill(o, brush, path); - } - else - { - // Outline only to preserve interior detail. - source.Draw(options, pen, path); - } - } - } - - return source; - } - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The collection of paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - IPathCollection paths) => - source.Fill(source.GetDrawingOptions(), brush, paths); - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified brush and pen. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The brush. - /// The pen. - /// The collection of glyph paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - Pen pen, - IReadOnlyList paths) => - source.Fill(source.GetDrawingOptions(), brush, pen, paths); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified color. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - IPathCollection paths) => - source.Fill(options, new SolidBrush(color), paths); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified color. - /// - /// The source image processing context. - /// The color. - /// The collection of paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - IPathCollection paths) => - source.Fill(new SolidBrush(color), paths); - - /// - /// Flood fills the image in the shape of the provided glyphs with the specified color. - /// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer. - /// - /// The source image processing context. - /// The color. - /// The collection of glyph paths. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - IReadOnlyList paths) => - source.Fill(new SolidBrush(color), new SolidPen(color), paths); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs deleted file mode 100644 index ef26eb107..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPathExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of polygon outlines. -/// -public static class FillPathExtensions -{ - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The color. - /// The logic path. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Color color, - IPath path) => - source.Fill(new SolidBrush(color), path); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The drawing options. - /// The color. - /// The logic path. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - IPath path) => - source.Fill(options, new SolidBrush(color), path); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The logic path. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - Brush brush, - IPath path) => - source.Fill(source.GetDrawingOptions(), brush, path); - - /// - /// Flood fills the image in the shape of the provided polygon with the specified brush. - /// - /// The source image processing context. - /// The drawing options. - /// The brush. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - IPath path) => - source.ApplyProcessor(new FillPathProcessor(options, brush, path)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillPolygonExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillPolygonExtensions.cs deleted file mode 100644 index 6c87e509e..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillPolygonExtensions.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of closed linear polygons. -/// -public static class FillPolygonExtensions -{ - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - params PointF[] points) => - source.Fill(options, brush, new Polygon(points)); - - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The brush. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - Brush brush, - params PointF[] points) => - source.Fill(brush, new Polygon(points)); - - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The options. - /// The color. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - params PointF[] points) => - source.Fill(options, new SolidBrush(color), new Polygon(points)); - - /// - /// Flood fills the image in the shape of a linear polygon described by the points - /// - /// The source image processing context. - /// The color. - /// The points. - /// The to allow chaining of operations. - public static IImageProcessingContext FillPolygon( - this IImageProcessingContext source, - Color color, - params PointF[] points) => - source.Fill(new SolidBrush(color), new Polygon(points)); -} diff --git a/src/ImageSharp.Drawing/Processing/Extensions/FillRectangleExtensions.cs b/src/ImageSharp.Drawing/Processing/Extensions/FillRectangleExtensions.cs deleted file mode 100644 index f8d8c7632..000000000 --- a/src/ImageSharp.Drawing/Processing/Extensions/FillRectangleExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Adds extensions that allow the filling of rectangles. -/// -public static class FillRectangleExtensions -{ - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The options. - /// The brush. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Brush brush, - RectangleF shape) => - source.Fill(options, brush, new RectangularPolygon(shape.X, shape.Y, shape.Width, shape.Height)); - - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The brush. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext - Fill(this IImageProcessingContext source, Brush brush, RectangleF shape) => - source.Fill(brush, new RectangularPolygon(shape.X, shape.Y, shape.Width, shape.Height)); - - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The options. - /// The color. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext Fill( - this IImageProcessingContext source, - DrawingOptions options, - Color color, - RectangleF shape) => - source.Fill(options, new SolidBrush(color), shape); - - /// - /// Flood fills the image in the shape of the provided rectangle with the specified brush. - /// - /// The source image processing context. - /// The color. - /// The shape. - /// The to allow chaining of operations. - public static IImageProcessingContext - Fill(this IImageProcessingContext source, Color color, RectangleF shape) => - source.Fill(new SolidBrush(color), shape); -} diff --git a/src/ImageSharp.Drawing/Processing/FlattenedPath.cs b/src/ImageSharp.Drawing/Processing/FlattenedPath.cs new file mode 100644 index 000000000..5d2df6136 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/FlattenedPath.cs @@ -0,0 +1,226 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// A lightweight wrapper around pre-flattened points. +/// returns this directly, avoiding redundant curve subdivision. +/// Points are mutated in place on ; no buffers are copied. +/// +internal sealed class FlattenedPath : IPath, ISimplePath +{ + private readonly PointF[] points; + private readonly bool isClosed; + private RectangleF bounds; + private ISimplePath[]? flattenResult; + + public FlattenedPath(PointF[] points, bool isClosed, RectangleF bounds) + { + this.points = points; + this.isClosed = isClosed; + this.bounds = bounds; + } + + /// + public RectangleF Bounds => this.bounds; + + /// + public PathTypes PathType => this.isClosed ? PathTypes.Closed : PathTypes.Open; + + /// + bool ISimplePath.IsClosed => this.isClosed; + + /// + ReadOnlyMemory ISimplePath.Points => this.points; + + /// + public IEnumerable Flatten() => this.flattenResult ??= [this]; + + /// + /// Transforms all points in place and updates the bounds. + /// This mutates the current instance — the point buffer is not copied. + /// + /// The transform matrix. + /// This instance, with points and bounds updated. + public IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + for (int i = 0; i < this.points.Length; i++) + { + ref PointF p = ref this.points[i]; + p = PointF.Transform(p, matrix); + + if (p.X < minX) + { + minX = p.X; + } + + if (p.Y < minY) + { + minY = p.Y; + } + + if (p.X > maxX) + { + maxX = p.X; + } + + if (p.Y > maxY) + { + maxY = p.Y; + } + } + + this.bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); + return this; + } + + /// + public IPath AsClosedPath() + { + if (this.isClosed) + { + return this; + } + + PointF[] closedPoints = new PointF[this.points.Length + 1]; + for (int i = 0; i < this.points.Length; i++) + { + closedPoints[i] = this.points[i]; + } + + closedPoints[^1] = this.points[0]; + return new FlattenedPath(closedPoints, true, this.bounds); + } +} + +/// +/// A lightweight wrapper around multiple pre-flattened sub-paths. +/// yields each sub-path directly, avoiding redundant curve subdivision. +/// Sub-path points are mutated in place on ; no buffers are copied. +/// +internal sealed class FlattenedCompositePath : IPath +{ + private readonly FlattenedPath[] subPaths; + private RectangleF bounds; + private PathTypes? pathType; + + public FlattenedCompositePath(FlattenedPath[] subPaths, RectangleF bounds) + { + this.subPaths = subPaths; + this.bounds = bounds; + } + + /// + public RectangleF Bounds => this.bounds; + + /// + public PathTypes PathType + { + get + { + if (this.pathType.HasValue) + { + return this.pathType.Value; + } + + bool hasOpen = false; + bool hasClosed = false; + foreach (FlattenedPath sp in this.subPaths) + { + if (sp.PathType == PathTypes.Open) + { + hasOpen = true; + } + else + { + hasClosed = true; + } + + if (hasOpen && hasClosed) + { + return PathTypes.Mixed; + } + } + + this.pathType = hasClosed ? PathTypes.Closed : PathTypes.Open; + return this.pathType.Value; + } + } + + /// + public IEnumerable Flatten() => this.subPaths; + + /// + /// Transforms all sub-path points in place and updates the bounds. + /// This mutates the current instance — no buffers are copied. + /// + /// The transform matrix. + /// This instance, with all sub-paths and bounds updated. + public IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + float minX = float.MaxValue, minY = float.MaxValue; + float maxX = float.MinValue, maxY = float.MinValue; + + for (int i = 0; i < this.subPaths.Length; i++) + { + _ = this.subPaths[i].Transform(matrix); + RectangleF spBounds = this.subPaths[i].Bounds; + + if (spBounds.Left < minX) + { + minX = spBounds.Left; + } + + if (spBounds.Top < minY) + { + minY = spBounds.Top; + } + + if (spBounds.Right > maxX) + { + maxX = spBounds.Right; + } + + if (spBounds.Bottom > maxY) + { + maxY = spBounds.Bottom; + } + } + + this.bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); + return this; + } + + /// + public IPath AsClosedPath() + { + if (this.PathType == PathTypes.Closed) + { + return this; + } + + FlattenedPath[] closed = new FlattenedPath[this.subPaths.Length]; + for (int i = 0; i < this.subPaths.Length; i++) + { + closed[i] = (FlattenedPath)this.subPaths[i].AsClosedPath(); + } + + return new FlattenedCompositePath(closed, this.bounds); + } +} diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 065125c6a..2b451f658 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -2,8 +2,6 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -18,18 +16,25 @@ public abstract class GradientBrush : Brush protected GradientBrush(GradientRepetitionMode repetitionMode, params ColorStop[] colorStops) { this.RepetitionMode = repetitionMode; - this.ColorStops = colorStops; + + InsertionSort(colorStops, (a, b) => a.Ratio.CompareTo(b.Ratio)); + this.ColorStopsArray = colorStops; } /// /// Gets how the colors are repeated beyond the interval [0..1]. /// - protected GradientRepetitionMode RepetitionMode { get; } + public GradientRepetitionMode RepetitionMode { get; } + + /// + /// Gets the color stops for this gradient. + /// + public ReadOnlySpan ColorStops => this.ColorStopsArray; /// - /// Gets the list of color stops for this gradient. + /// Gets the color stops array for use by derived applicators. /// - protected ColorStop[] ColorStops { get; } + protected ColorStop[] ColorStopsArray { get; } /// public override bool Equals(Brush? other) @@ -37,7 +42,7 @@ public override bool Equals(Brush? other) if (other is GradientBrush brush) { return this.RepetitionMode == brush.RepetitionMode - && this.ColorStops?.SequenceEqual(brush.ColorStops) == true; + && this.ColorStopsArray?.SequenceEqual(brush.ColorStopsArray) == true; } return false; @@ -45,13 +50,35 @@ public override bool Equals(Brush? other) /// public override int GetHashCode() - => HashCode.Combine(this.RepetitionMode, this.ColorStops); + => HashCode.Combine(this.RepetitionMode, this.ColorStopsArray); + + /// + /// Sorts the collection in place using a stable insertion sort. + /// is not stable and can reorder + /// equal-ratio color stops, producing non-deterministic gradient results. + /// + private static void InsertionSort(T[] collection, Comparison comparison) + { + int count = collection.Length; + for (int j = 1; j < count; j++) + { + T key = collection[j]; + + int i = j - 1; + for (; i >= 0 && comparison(collection[i], key) > 0; i--) + { + collection[i + 1] = collection[i]; + } + + collection[i + 1] = key; + } + } /// /// Base class for gradient brush applicators /// /// The pixel format. - internal abstract class GradientBrushApplicator : BrushApplicator + internal abstract class GradientBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel { private static readonly TPixel Transparent = Color.Transparent.ToPixel(); @@ -60,45 +87,38 @@ internal abstract class GradientBrushApplicator : BrushApplicator blenderBuffers; - - private bool isDisposed; - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. + /// The canvas width for the current render pass. /// An array of color stops sorted by their position. /// Defines if and how the gradient should be repeated. - protected GradientBrushApplicator( + protected GradientBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame target, + int canvasWidth, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, target) + : base(configuration, options, canvasWidth) { this.colorStops = colorStops; - - // Ensure the color-stop order is correct. - InsertionSort(this.colorStops, (x, y) => x.Ratio.CompareTo(y.Ratio)); this.repetitionMode = repetitionMode; - this.scanlineWidth = target.Width; - this.allocator = configuration.MemoryAllocator; - this.blenderBuffers = new ThreadLocalBlenderBuffers(this.allocator, this.scanlineWidth); } internal TPixel this[int x, int y] { get { - float positionOnCompleteGradient = this.PositionOnGradient(x + 0.5f, y + 0.5f); + float fx = x + 0.5f; + float fy = y + 0.5f; + + float positionOnCompleteGradient = this.PositionOnGradient(fx, fy); + if (float.IsNaN(positionOnCompleteGradient)) + { + return Transparent; + } switch (this.repetitionMode) { @@ -146,10 +166,15 @@ protected GradientBrushApplicator( } /// - public override void Apply(Span scanline, int x, int y) + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) { - Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; - Span overlays = this.blenderBuffers.OverlaySpan[..scanline.Length]; + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); float blendPercentage = this.Options.BlendPercentage; // TODO: Remove bounds checks. @@ -170,7 +195,6 @@ public override void Apply(Span scanline, int x, int y) } } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend(this.Configuration, destinationRow, destinationRow, overlays, amounts); } @@ -188,24 +212,6 @@ public override void Apply(Span scanline, int x, int y) /// protected abstract float PositionOnGradient(float x, float y); - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - base.Dispose(disposing); - - if (disposing) - { - this.blenderBuffers.Dispose(); - } - - this.isDisposed = true; - } - private (ColorStop From, ColorStop To) GetGradientSegment(float positionOnCompleteGradient) { ColorStop localGradientFrom = this.colorStops[0]; @@ -227,29 +233,5 @@ protected override void Dispose(bool disposing) return (localGradientFrom, localGradientTo); } - - /// - /// Provides a stable sorting algorithm for the given array. - /// is not stable. - /// - /// The type of element to sort. - /// The array to sort. - /// The comparison delegate. - private static void InsertionSort(T[] collection, Comparison comparison) - { - int count = collection.Length; - for (int j = 1; j < count; j++) - { - T key = collection[j]; - - int i = j - 1; - for (; i >= 0 && comparison(collection[i], key) > 0; i--) - { - collection[i + 1] = collection[i]; - } - - collection[i + 1] = key; - } - } } } diff --git a/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs new file mode 100644 index 000000000..4caf7868d --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/IDrawingCanvas.cs @@ -0,0 +1,445 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Represents a drawing canvas over a frame target. +/// +public interface IDrawingCanvas : IDisposable +{ + /// + /// Gets the local bounds of this canvas. + /// + public Rectangle Bounds { get; } + + /// + /// Gets the number of saved states currently on the canvas stack. + /// + public int SaveCount { get; } + + /// + /// Saves the current drawing state on the state stack. + /// + /// + /// This operation stores the current canvas state by reference. + /// If the same instance is mutated after + /// , those mutations are visible when restoring. + /// + /// The save count after the state has been pushed. + public int Save(); + + /// + /// Saves the current drawing state and replaces the active state with the provided options and clip paths. + /// + /// + /// The provided instance is stored by reference. + /// Mutating it after this call mutates the active/restored state behavior. + /// + /// Drawing options for the new active state. + /// Clip paths for the new active state. + /// The save count after the previous state has been pushed. + public int Save(DrawingOptions options, params IPath[] clipPaths); + + /// + /// Saves the current drawing state and begins an isolated compositing layer. + /// Subsequent draw commands are recorded into an isolated logical layer. When + /// closes the layer, that layer becomes eligible for + /// composition during the next or . + /// + /// The save count after the layer state has been pushed. + public int SaveLayer(); + + /// + /// Saves the current drawing state and begins an isolated compositing layer. + /// Subsequent draw commands are recorded into an isolated logical layer. When + /// closes the layer, that layer is composed during the next + /// or using the specified + /// (blend mode, alpha composition, opacity). + /// + /// + /// Graphics options controlling how the layer is composited on restore. + /// + /// The save count after the layer state has been pushed. + public int SaveLayer(GraphicsOptions layerOptions); + + /// + /// Saves the current drawing state and begins an isolated compositing layer + /// bounded to a subregion. Subsequent draw commands are recorded into that isolated + /// logical layer. When closes the layer, it is composed during + /// the next or using the specified + /// . + /// + /// + /// Graphics options controlling how the layer is composited on restore. + /// + /// + /// The local bounds of the layer. Only this region is allocated and composited. + /// + /// The save count after the layer state has been pushed. + public int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds); + + /// + /// Restores the most recently saved state. + /// + /// + /// If the most recently saved state was created by a SaveLayer overload, + /// the layer is closed in the deferred scene. Actual composition happens during the + /// next or . + /// + public void Restore(); + + /// + /// Restores to a specific save count. + /// + /// + /// State frames above are discarded, + /// and the last discarded frame becomes the current state. + /// If any discarded state was created by a SaveLayer overload, + /// those layers are closed in the deferred scene and are composed during the next + /// or . + /// + /// The save count to restore to. + public void RestoreTo(int saveCount); + + /// + /// Creates a child canvas over a subregion in local coordinates. + /// + /// The child region in local coordinates. + /// A child canvas with local origin at (0,0). + public IDrawingCanvas CreateRegion(Rectangle region); + + /// + /// Clears the whole canvas using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + public void Clear(Brush brush); + + /// + /// Clears a local region using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + /// Region to clear in local coordinates. + public void Clear(Brush brush, Rectangle region); + + /// + /// Clears a path region using the given brush and clear-style composition options. + /// + /// Brush used to shade destination pixels during clear. + /// The path region to clear. + public void Clear(Brush brush, IPath path); + + /// + /// Fills the whole canvas using the given brush. + /// + /// Brush used to shade destination pixels. + public void Fill(Brush brush); + + /// + /// Fills a local region using the given brush. + /// + /// Brush used to shade destination pixels. + /// Region to fill in local coordinates. + public void Fill(Brush brush, Rectangle region); + + /// + /// Fills all paths in a collection using the given brush and drawing options. + /// + /// Brush used to shade covered pixels. + /// Path collection to fill. + public void Fill(Brush brush, IPathCollection paths); + + /// + /// Fills a path built by the provided builder using the given brush. + /// + /// Brush used to shade covered pixels. + /// The path builder describing the fill region. + public void Fill(Brush brush, PathBuilder pathBuilder); + + /// + /// Fills a path in local coordinates using the given brush. + /// + /// Brush used to shade covered pixels. + /// The path to fill. + public void Fill(Brush brush, IPath path); + + /// + /// Applies an image-processing operation to a local region. + /// + /// The local region to process. + /// The image-processing operation to apply to the region. + public void Process(Rectangle region, Action operation); + + /// + /// Applies an image-processing operation to a region described by a path builder. + /// + /// The path builder describing the region to process. + /// The image-processing operation to apply to the region. + public void Process(PathBuilder pathBuilder, Action operation); + + /// + /// Applies an image-processing operation to a path region. + /// + /// + /// The operation is constrained to the path bounds and then composited back using an image brush. + /// + /// The path region to process. + /// The image-processing operation to apply to the region. + public void Process(IPath path, Action operation); + + /// + /// Draws an arc outline using the provided pen and drawing options. + /// + /// Pen used to generate the arc outline. + /// Arc center point in local coordinates. + /// Arc radii in local coordinates. + /// Ellipse rotation in degrees. + /// Arc start angle in degrees. + /// Arc sweep angle in degrees. + public void DrawArc(Pen pen, PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle); + + /// + /// Draws a cubic bezier outline using the provided pen and drawing options. + /// + /// Pen used to generate the bezier outline. + /// Bezier control points. + public void DrawBezier(Pen pen, params PointF[] points); + + /// + /// Draws an ellipse outline using the provided pen and drawing options. + /// + /// Pen used to generate the ellipse outline. + /// Ellipse center point in local coordinates. + /// Ellipse width and height in local coordinates. + public void DrawEllipse(Pen pen, PointF center, SizeF size); + + /// + /// Draws a polyline outline using the provided pen and drawing options. + /// + /// Pen used to generate the line outline. + /// Polyline points. + public void DrawLine(Pen pen, params PointF[] points); + + /// + /// Draws a rectangular outline using the provided pen and drawing options. + /// + /// Pen used to generate the rectangle outline. + /// Rectangle region to stroke. + public void Draw(Pen pen, Rectangle region); + + /// + /// Draws all paths in a collection using the provided pen and drawing options. + /// + /// Pen used to generate outlines. + /// Path collection to stroke. + public void Draw(Pen pen, IPathCollection paths); + + /// + /// Draws a path outline built by the provided builder using the given pen. + /// + /// Pen used to generate the outline fill path. + /// The path builder describing the path to stroke. + public void Draw(Pen pen, PathBuilder pathBuilder); + + /// + /// Draws a path outline in local coordinates using the given pen. + /// + /// Pen used to generate the outline fill path. + /// The path to stroke. + public void Draw(Pen pen, IPath path); + + /// + /// Draws text onto this canvas. + /// + /// The text rendering options. + /// The text to draw. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. + public void DrawText( + RichTextOptions textOptions, + ReadOnlySpan text, + Brush? brush, + Pen? pen); + + /// + /// Draws layered glyph geometry using a monochrome projection. + /// + /// + /// For painted glyph layers, the implementation uses a coverage/compactness heuristic + /// to keep one dominant background-like layer as outline-only to preserve interior definition. + /// All non-painted layers are filled. + /// + /// Brush used to fill glyph layers. + /// Pen used to outline dominant painted layers. + /// Layered glyph geometry to draw. + public void DrawGlyphs( + Brush brush, + Pen pen, + IReadOnlyList glyphs); + + /// + /// Measures the logical advance of the text in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The logical advance rectangle of the text if it was to be rendered. + /// + /// This measurement reflects line-box height and horizontal or vertical text advance from the layout model. + /// It does not guarantee that all rendered glyph pixels fit within the returned rectangle. + /// Use for glyph ink bounds or + /// for the union of logical advance and rendered bounds. + /// + public RectangleF MeasureTextAdvance(RichTextOptions textOptions, ReadOnlySpan text); + + /// + /// Measures the rendered glyph bounds of the text in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The rendered glyph bounds of the text if it was to be rendered. + /// + /// This measures the tight ink bounds enclosing all rendered glyphs. The returned rectangle + /// may be smaller or larger than the logical advance and may have a non-zero origin. + /// Use for the logical layout box or + /// for the union of both. + /// + public RectangleF MeasureTextBounds(RichTextOptions textOptions, ReadOnlySpan text); + + /// + /// Measures the full renderable bounds of the text in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// + /// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered. + /// + /// + /// The returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds. + /// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle. + /// + public RectangleF MeasureTextRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text); + + /// + /// Measures the normalized rendered size of the text in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The rendered size of the text with the origin normalized to (0, 0). + /// + /// This is equivalent to measuring the rendered bounds and returning only the width and height. + /// Use when the returned X and Y offset are also required. + /// + public RectangleF MeasureTextSize(RichTextOptions textOptions, ReadOnlySpan text); + + /// + /// Measures the logical advance of each laid-out character entry in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The list of per-entry logical advances of the text if it was to be rendered. + /// Whether any of the entries had non-empty advances. + /// + /// Each entry reflects the typographic advance width and height for one character. + /// Use for per-character ink bounds or + /// for the union of both. + /// + public bool TryMeasureCharacterAdvances(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan advances); + + /// + /// Measures the rendered glyph bounds of each laid-out character entry in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The list of per-entry rendered glyph bounds of the text if it was to be rendered. + /// Whether any of the entries had non-empty bounds. + /// + /// Each entry reflects the tight ink bounds of one rendered glyph. + /// Use for per-character logical advances or + /// for the union of both. + /// + public bool TryMeasureCharacterBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds); + + /// + /// Measures the full renderable bounds of each laid-out character entry in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The list of per-entry renderable bounds of the text if it was to be rendered. + /// Whether any of the entries had non-empty bounds. + /// + /// Each returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds for the corresponding laid-out entry. + /// Use this when both typographic advance and rendered glyph overshoot must fit within the same rectangle. + /// + public bool TryMeasureCharacterRenderableBounds(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan bounds); + + /// + /// Measures the normalized rendered size of each laid-out character entry in pixel units. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The list of per-entry rendered sizes with the origin normalized to (0, 0). + /// Whether any of the entries had non-empty dimensions. + /// + /// This is equivalent to measuring per-character bounds and returning only the width and height. + /// Use when the returned X and Y offset are also required. + /// + public bool TryMeasureCharacterSizes(RichTextOptions textOptions, ReadOnlySpan text, out ReadOnlySpan sizes); + + /// + /// Gets the number of laid-out lines contained within the text. + /// + /// The text shaping and layout options. + /// The text to measure. + /// The laid-out line count. + public int CountTextLines(RichTextOptions textOptions, ReadOnlySpan text); + + /// + /// Gets per-line layout metrics for the supplied text. + /// + /// The text shaping and layout options. + /// The text to measure. + /// + /// An array of in pixel units, one entry per laid-out line. + /// + /// + /// + /// The returned and are expressed + /// in the primary flow direction for the active layout mode. + /// + /// + /// , , and + /// are line-box positions relative to the current line origin and are suitable for drawing guide lines. + /// + /// + /// Horizontal layouts: Start = X position, Extent = width. + /// Vertical layouts: Start = Y position, Extent = height. + /// + /// + public LineMetrics[] GetTextLineMetrics(RichTextOptions textOptions, ReadOnlySpan text); + + /// + /// Draws an image source region into a destination rectangle. + /// + /// The source image. + /// The source rectangle within . + /// The destination rectangle in local canvas coordinates. + /// + /// Optional resampler used when scaling or transforming the image. Defaults to . + /// + public void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + IResampler? sampler = null); + + /// + /// Flushes queued drawing commands to the target in submission order. + /// + public void Flush(); +} diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index 5a8062e30..91cc87310 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -1,36 +1,71 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; -using SixLabors.ImageSharp.Memory; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Provides an implementation of an image brush for painting images within areas. /// -public class ImageBrush : Brush +/// The pixel format of the source image. +public sealed class ImageBrush : ImageBrush + where TPixel : unmanaged, IPixel { /// - /// The image to paint. + /// Initializes a new instance of the class. /// - private readonly Image image; + /// The source image to draw. + public ImageBrush(Image image) + : base(image) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// An offset to apply to the image while drawing the texture. + public ImageBrush(Image image, Point offset) + : base(image, offset) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// The region of interest within the source image. + public ImageBrush(Image image, RectangleF region) + : base(image, region) + => this.SourceImage = image; /// - /// The region of the source image we will be using to paint. + /// Initializes a new instance of the class. /// - private readonly RectangleF region; + /// The source image to draw. + /// The region of interest within the source image. + /// An offset to apply to the image while drawing the texture. + public ImageBrush(Image image, RectangleF region, Point offset) + : base(image, region, offset) + => this.SourceImage = image; /// - /// The offet to apply to the source image while applying the imagebrush + /// Gets the typed source image used by this brush. /// - private readonly Point offset; + public Image SourceImage { get; } +} +/// +/// The untyped base class for image brushes, used to support non-generic brush references in drawing contexts. +/// +public abstract class ImageBrush : Brush +{ /// /// Initializes a new instance of the class. /// /// The source image to draw. - public ImageBrush(Image image) + protected ImageBrush(Image image) : this(image, image.Bounds) { } @@ -42,7 +77,7 @@ public ImageBrush(Image image) /// /// An offset to apply the to image image while drawing apply the texture. /// - public ImageBrush(Image image, Point offset) + protected ImageBrush(Image image, Point offset) : this(image, image.Bounds, offset) { } @@ -55,7 +90,7 @@ public ImageBrush(Image image, Point offset) /// The region of interest. /// This overrides any region used to initialize the brush applicator. /// - public ImageBrush(Image image, RectangleF region) + protected ImageBrush(Image image, RectangleF region) : this(image, region, Point.Empty) { } @@ -71,53 +106,73 @@ public ImageBrush(Image image, RectangleF region) /// /// An offset to apply the to image image while drawing apply the texture. /// - public ImageBrush(Image image, RectangleF region, Point offset) + protected ImageBrush(Image image, RectangleF region, Point offset) { - this.image = image; - this.region = RectangleF.Intersect(image.Bounds, region); - this.offset = offset; + this.UntypedImage = image; + this.SourceRegion = RectangleF.Intersect(image.Bounds, region); + this.Offset = offset; } + /// + /// Gets the source image used by this brush. + /// + public Image UntypedImage { get; } + + /// + /// Gets the source region within the image. + /// + public RectangleF SourceRegion { get; } + + /// + /// Gets the offset applied to the brush origin. + /// + public Point Offset { get; } + /// public override bool Equals(Brush? other) { if (other is ImageBrush ib) { - return ib.image == this.image && ib.region == this.region; + return ib.UntypedImage == this.UntypedImage && ib.SourceRegion == this.SourceRegion; } return false; } /// - public override int GetHashCode() => HashCode.Combine(this.image, this.region); + public override int GetHashCode() => HashCode.Combine(this.UntypedImage, this.SourceRegion); /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, RectangleF region) { - if (this.image is Image specificImage) + if (this.UntypedImage is Image image) { - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offset, false); + return new ImageBrushRenderer(configuration, options, canvasWidth, image, region, this.SourceRegion, this.Offset); } - specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offset, true); + // This will never be hit as the brush is always normalized by the drawing canvas + // but we do it to satisfy the type system. + ThrowIfInvalidImagePixelFormat(); + return null; } + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowIfInvalidImagePixelFormat() + => throw new UnreachableException("The pixel format of the image is not supported by this brush renderer"); + /// /// The image brush applicator. /// /// The pixel format. - private class ImageBrushApplicator : BrushApplicator + private class ImageBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel { private readonly ImageFrame sourceFrame; - private readonly Image sourceImage; - private readonly bool shouldDisposeImage; /// /// The region of the source image we will be using to draw from. @@ -136,31 +191,26 @@ private class ImageBrushApplicator : BrushApplicator private bool isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. + /// The canvas width for the current render pass. /// The image. /// The region of the target image we will be drawing to. /// The region of the source image we will be using to source pixels to draw from. /// An offset to apply to the texture while drawing. - /// Whether to dispose the image on disposal of the applicator. - public ImageBrushApplicator( + public ImageBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame target, + int canvasWidth, Image image, RectangleF targetRegion, RectangleF sourceRegion, - Point offset, - bool shouldDisposeImage) - : base(configuration, options, target) + Point offset) + : base(configuration, options, canvasWidth) { - this.sourceImage = image; this.sourceFrame = image.Frames.RootFrame; - this.shouldDisposeImage = shouldDisposeImage; - this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion); this.offsetY = (int)MathF.Floor(targetRegion.Top) + offset.Y; @@ -185,24 +235,20 @@ protected override void Dispose(bool disposing) return; } - if (disposing && this.shouldDisposeImage) - { - this.sourceImage?.Dispose(); - } - this.isDisposed = true; base.Dispose(disposing); } /// - public override void Apply(Span scanline, int x, int y) + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) { - // Create a span for colors - MemoryAllocator allocator = this.Configuration.MemoryAllocator; - using IMemoryOwner amountBuffer = allocator.Allocate(scanline.Length); - using IMemoryOwner overlay = allocator.Allocate(scanline.Length); - Span amountSpan = amountBuffer.Memory.Span; - Span overlaySpan = overlay.Memory.Span; + Span amountSpan = workspace.GetAmounts(scanline.Length); + Span overlaySpan = workspace.GetOverlays(scanline.Length); int offsetX = x - this.offsetX; int sourceY = ((((y - this.offsetY) % this.sourceRegion.Height) // clamp the number between -height and +height @@ -221,7 +267,6 @@ public override void Apply(Span scanline, int x, int y) overlaySpan[i] = sourceRow[sourceX]; } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend( this.Configuration, destinationRow, diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index 22692dc0d..5debe3320 100644 --- a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Memory; + namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -9,10 +12,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class LinearGradientBrush : GradientBrush { - private readonly PointF startPoint; - private readonly PointF endPoint; - private readonly PointF? rotationPoint; - /// /// Initializes a new instance of the class using /// a start and end point. @@ -28,9 +27,8 @@ public LinearGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.startPoint = p0; - this.endPoint = p1; - this.rotationPoint = null; + this.StartPoint = p0; + this.EndPoint = p1; } /// @@ -52,20 +50,37 @@ public LinearGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.startPoint = p0; - this.endPoint = p1; - this.rotationPoint = rotationPoint; + ResolveAxis(p0, p1, rotationPoint, out PointF start, out PointF end); + this.StartPoint = start; + this.EndPoint = end; } + /// + /// Gets the start point of the gradient axis. + /// + public PointF StartPoint { get; } + + /// + /// Gets the end point of the gradient axis. + /// + public PointF EndPoint { get; } + + /// + public override Brush Transform(Matrix4x4 matrix) + => new LinearGradientBrush( + PointF.Transform(this.StartPoint, matrix), + PointF.Transform(this.EndPoint, matrix), + this.RepetitionMode, + this.ColorStopsArray); + /// public override bool Equals(Brush? other) { if (other is LinearGradientBrush brush) { return base.Equals(other) - && this.startPoint.Equals(brush.startPoint) - && this.endPoint.Equals(brush.endPoint) - && Nullable.Equals(this.rotationPoint, brush.rotationPoint); + && this.StartPoint.Equals(brush.StartPoint) + && this.EndPoint.Equals(brush.EndPoint); } return false; @@ -73,155 +88,111 @@ public override bool Equals(Brush? other) /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), this.startPoint, this.endPoint, this.rotationPoint); + => HashCode.Combine(base.GetHashCode(), this.StartPoint, this.EndPoint); + + /// + /// Resolves a three-point gradient axis into a two-point axis by projecting + /// the gradient vector (p0→p1) onto the perpendicular of the rotation vector (p0→rotationPoint). + /// This follows the COLRv1 font specification for rotated linear gradients. + /// + /// The gradient start point. + /// The gradient vector endpoint. + /// The rotation reference point. + /// The resolved start point of the gradient axis. + /// The resolved end point of the gradient axis. + private static void ResolveAxis(PointF p0, PointF p1, PointF rotationPoint, out PointF start, out PointF end) + { + // Gradient vector from p0 to p1. + float vx = p1.X - p0.X; + float vy = p1.Y - p0.Y; + + // Rotation vector from p0 to rotation point. + float rx = rotationPoint.X - p0.X; + float ry = rotationPoint.Y - p0.Y; + + // Perpendicular to the rotation vector. + float nx = ry; + float ny = -rx; + + float ndotn = (nx * nx) + (ny * ny); + if (ndotn == 0f) + { + // Degenerate: p0 == rotationPoint, fall back to original axis. + start = p0; + end = p1; + } + else + { + // Project the gradient vector onto the perpendicular direction. + float vdotn = (vx * nx) + (vy * ny); + float scale = vdotn / ndotn; + start = p0; + end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny)); + } + } /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, RectangleF region) - => new LinearGradientBrushApplicator( + => new LinearGradientBrushRenderer( configuration, options, - source, - this.startPoint, - this.endPoint, - this.rotationPoint, - this.ColorStops, + canvasWidth, + this, + this.ColorStopsArray, this.RepetitionMode); /// /// Implements the gradient application logic for . /// /// The pixel format. - private sealed class LinearGradientBrushApplicator : GradientBrushApplicator + private sealed class LinearGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel { private readonly PointF start; - private readonly PointF end; private readonly float alongX; private readonly float alongY; - private readonly float acrossX; - private readonly float acrossY; private readonly float alongsSquared; - private readonly float length; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The ImageSharp configuration. /// The graphics options. - /// The target image frame. - /// The gradient start point. - /// The gradient end point. - /// The optional rotation point. + /// The canvas width for the current render pass. + /// The linear gradient brush. /// The gradient color stops. /// Defines how the gradient repeats. - public LinearGradientBrushApplicator( + public LinearGradientBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, - PointF p0, - PointF p1, - PointF? p2, + int canvasWidth, + LinearGradientBrush brush, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, source, colorStops, repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) { - // Determine whether this is a simple linear gradient (2 points) - // or a rotated one (3 points). - if (p2 is null) - { - // Classic SVG-style gradient from start -> end. - this.start = p0; - this.end = p1; - } - else - { - // Compute the rotated gradient axis per COLRv1 rules. - // p0 = start, p1 = gradient vector, p2 = rotation reference. - float vx = p1.X - p0.X; - float vy = p1.Y - p0.Y; - float rx = p2.Value.X - p0.X; - float ry = p2.Value.Y - p0.Y; - - // n = perpendicular to rotation vector - float nx = ry; - float ny = -rx; - - // Avoid divide-by-zero if p0 == p2. - float ndotn = (nx * nx) + (ny * ny); - if (ndotn == 0f) - { - this.start = p0; - this.end = p1; - } - else - { - // Project p1 - p0 onto perpendicular direction. - float vdotn = (vx * nx) + (vy * ny); - float scale = vdotn / ndotn; - - // The derived endpoint after rotation. - this.start = p0; - this.end = new PointF(p0.X + (scale * nx), p0.Y + (scale * ny)); - } - } - - // Calculate axis vectors. - this.alongX = this.end.X - this.start.X; - this.alongY = this.end.Y - this.start.Y; + this.start = brush.StartPoint; - // Perpendicular axis vector. - this.acrossX = this.alongY; - this.acrossY = -this.alongX; - - // Precompute squared length and actual length for later use. + this.alongX = brush.EndPoint.X - this.start.X; + this.alongY = brush.EndPoint.Y - this.start.Y; this.alongsSquared = (this.alongX * this.alongX) + (this.alongY * this.alongY); - this.length = MathF.Sqrt(this.alongsSquared); } /// protected override float PositionOnGradient(float x, float y) { - // Degenerate case: gradient length == 0, use final stop color. if (this.alongsSquared == 0f) { return 1f; } - // Fast path for horizontal gradients. - if (this.acrossX == 0f) - { - float denom = this.end.X - this.start.X; - return denom != 0f ? (x - this.start.X) / denom : 1f; - } - - // Fast path for vertical gradients. - if (this.acrossY == 0f) - { - float denom = this.end.Y - this.start.Y; - return denom != 0f ? (y - this.start.Y) / denom : 1f; - } - - // General case: project sample point onto the gradient axis. float deltaX = x - this.start.X; float deltaY = y - this.start.Y; - - // Compute perpendicular projection scalar. - float k = ((this.alongY * deltaX) - (this.alongX * deltaY)) / this.alongsSquared; - - // Determine projected point on the gradient line. - float projX = x - (k * this.alongY); - float projY = y + (k * this.alongX); - - // Compute distance from gradient start to projected point. - float dx = projX - this.start.X; - float dy = projY - this.start.Y; - - // Normalize to [0,1] range along the gradient length. - return this.length > 0f ? MathF.Sqrt((dx * dx) + (dy * dy)) / this.length : 1f; + return ((deltaX * this.alongX) + (deltaY * this.alongY)) / this.alongsSquared; } } } diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index ef3154273..7710ae0ce 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -2,7 +2,8 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Drawing.Helpers; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -85,15 +86,15 @@ public override int GetHashCode() => HashCode.Combine(this.edges, this.centerColor, this.hasSpecialCenterColor); /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, RectangleF region) - => new PathGradientBrushApplicator( + => new PathGradientBrushRenderer( configuration, options, - source, + canvasWidth, this.edges, this.centerColor, this.hasSpecialCenterColor); @@ -156,7 +157,7 @@ public bool Intersect( Vector2 start, Vector2 end, ref Vector2 ip) => - Utilities.Intersect.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip); + PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip); public Vector4 ColorAt(float distance) { @@ -184,7 +185,7 @@ public override int GetHashCode() /// The path gradient brush applicator. /// /// The pixel format. - private sealed class PathGradientBrushApplicator : BrushApplicator + private sealed class PathGradientBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel { private readonly Vector2 center; @@ -201,27 +202,23 @@ private sealed class PathGradientBrushApplicator : BrushApplicator blenderBuffers; - - private bool isDisposed; - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. + /// The canvas width for the current render pass. /// Edges of the polygon. /// Color at the center of the gradient area to which the other colors converge. /// Whether the center color is different from a smooth gradient between the edges. - public PathGradientBrushApplicator( + public PathGradientBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, IList edges, Color centerColor, bool hasSpecialCenterColor) - : base(configuration, options, source) + : base(configuration, options, canvasWidth) { this.edges = edges; Vector2[] points = [.. edges.Select(s => s.Start)]; @@ -232,14 +229,14 @@ public PathGradientBrushApplicator( this.centerPixel = centerColor.ToPixel(); this.maxDistance = points.Select(p => p - this.center).Max(d => d.Length()); this.transparentPixel = Color.Transparent.ToPixel(); - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width); } internal TPixel this[int x, int y] { get { - Vector2 point = new(x, y); + // Match other gradient brushes by evaluating at pixel centers. + Vector2 point = new(x + 0.5F, y + 0.5F); if (point == this.center) { @@ -289,10 +286,15 @@ public PathGradientBrushApplicator( } /// - public override void Apply(Span scanline, int x, int y) + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) { - Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; - Span overlays = this.blenderBuffers.OverlaySpan[..scanline.Length]; + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); float blendPercentage = this.Options.BlendPercentage; // TODO: Remove bounds checks. @@ -313,28 +315,9 @@ public override void Apply(Span scanline, int x, int y) } } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend(this.Configuration, destinationRow, destinationRow, overlays, amounts); } - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - base.Dispose(disposing); - - if (disposing) - { - this.blenderBuffers.Dispose(); - } - - this.isDisposed = true; - } - private (Edge Edge, Vector2 Point)? FindIntersection( PointF start, PointF end) @@ -342,7 +325,7 @@ protected override void Dispose(bool disposing) Vector2 ip = default; Vector2 closestIntersection = default; Edge? closestEdge = null; - const float minDistance = float.MaxValue; + float minDistance = float.MaxValue; foreach (Edge edge in this.edges) { if (!edge.Intersect(start, end, ref ip)) @@ -350,9 +333,10 @@ protected override void Dispose(bool disposing) continue; } - float d = Vector2.DistanceSquared(start, end); + float d = Vector2.DistanceSquared(start, ip); if (d < minDistance) { + minDistance = d; closestEdge = edge; closestIntersection = ip; } diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index 92bf3db83..f58dda79c 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -83,6 +83,11 @@ internal PatternBrush(PatternBrush brush) this.patternVector = brush.patternVector; } + /// + /// Gets the pattern color matrix. + /// + public DenseMatrix Pattern => this.pattern; + /// public override bool Equals(Brush? other) { @@ -100,45 +105,41 @@ public override int GetHashCode() => HashCode.Combine(this.pattern, this.patternVector); /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, - RectangleF region) => - new PatternBrushApplicator( + int canvasWidth, + RectangleF region) + => + new PatternBrushRenderer( configuration, options, - source, + canvasWidth, this.pattern.ToPixelMatrix()); /// /// The pattern brush applicator. /// /// The pixel format. - private sealed class PatternBrushApplicator : BrushApplicator + private sealed class PatternBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel { private readonly DenseMatrix pattern; - private readonly ThreadLocalBlenderBuffers blenderBuffers; - private bool isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. + /// The canvas width for the current render pass. /// The pattern. - public PatternBrushApplicator( + public PatternBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, in DenseMatrix pattern) - : base(configuration, options, source) - { - this.pattern = pattern; - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width); - } + : base(configuration, options, canvasWidth) + => this.pattern = pattern; internal TPixel this[int x, int y] { @@ -153,21 +154,25 @@ public PatternBrushApplicator( } /// - public override void Apply(Span scanline, int x, int y) + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) { int patternY = y % this.pattern.Rows; - Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; - Span overlays = this.blenderBuffers.OverlaySpan[..scanline.Length]; + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); for (int i = 0; i < scanline.Length; i++) { - amounts[i] = NumericUtilities.ClampFloat(scanline[i] * this.Options.BlendPercentage, 0, 1F); + amounts[i] = Math.Clamp(scanline[i] * this.Options.BlendPercentage, 0, 1F); int patternX = (x + i) % this.pattern.Columns; overlays[i] = this.pattern[patternY, patternX]; } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend( this.Configuration, destinationRow, @@ -175,23 +180,5 @@ public override void Apply(Span scanline, int x, int y) overlays, amounts); } - - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - base.Dispose(disposing); - - if (disposing) - { - this.blenderBuffers.Dispose(); - } - - this.isDisposed = true; - } } } diff --git a/src/ImageSharp.Drawing/Processing/PatternPen.cs b/src/ImageSharp.Drawing/Processing/PatternPen.cs index f6da8ee04..75b7e32bb 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.StrokeOptions); + => path.GenerateOutline(strokeWidth, this.StrokePattern.Span, this.StrokeOptions); } diff --git a/src/ImageSharp.Drawing/Processing/Pen.cs b/src/ImageSharp.Drawing/Processing/Pen.cs index e3fbd3094..0ad0b0b39 100644 --- a/src/ImageSharp.Drawing/Processing/Pen.cs +++ b/src/ImageSharp.Drawing/Processing/Pen.cs @@ -80,7 +80,7 @@ protected Pen(PenOptions options) public float StrokeWidth { get; } /// - public ReadOnlySpan StrokePattern => this.pattern; + public ReadOnlyMemory StrokePattern => this.pattern; /// public StrokeOptions StrokeOptions { get; } @@ -107,7 +107,7 @@ public virtual bool Equals(Pen? other) && this.StrokeWidth == other.StrokeWidth && this.StrokeFill.Equals(other.StrokeFill) && this.StrokeOptions.Equals(other.StrokeOptions) - && this.StrokePattern.SequenceEqual(other.StrokePattern); + && this.StrokePattern.Span.SequenceEqual(other.StrokePattern.Span); /// public override bool Equals(object? obj) => this.Equals(obj as Pen); diff --git a/src/ImageSharp.Drawing/Processing/ProcessWithCanvasExtensions.cs b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasExtensions.cs new file mode 100644 index 000000000..c27880c9e --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasExtensions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Represents a drawing callback executed against a . +/// +/// The drawing canvas for the current frame. +public delegate void CanvasAction(IDrawingCanvas canvas); + +/// +/// Adds extensions that execute drawing callbacks against all frames through . +/// +public static class ProcessWithCanvasExtensions +{ + /// + /// Executes for each image frame using drawing options from the current context. + /// + /// The source image processing context. + /// The drawing callback to execute for each frame. + /// The to allow chaining of operations. + public static IImageProcessingContext ProcessWithCanvas( + this IImageProcessingContext source, + CanvasAction action) + => source.ProcessWithCanvas(source.GetDrawingOptions(), action); + + /// + /// Executes for each image frame using the supplied drawing options. + /// + /// The source image processing context. + /// The drawing options. + /// The drawing callback to execute for each frame. + /// The to allow chaining of operations. + public static IImageProcessingContext ProcessWithCanvas( + this IImageProcessingContext source, + DrawingOptions options, + CanvasAction action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(action, nameof(action)); + + return source.ApplyProcessor(new ProcessWithCanvasProcessor(options, action)); + } +} diff --git a/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor.cs b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor.cs new file mode 100644 index 000000000..25b105f6c --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Defines a processor that executes a canvas callback for each image frame. +/// +public sealed class ProcessWithCanvasProcessor : IImageProcessor +{ + /// + /// Initializes a new instance of the class. + /// + /// The drawing options. + /// The per-frame canvas callback. + public ProcessWithCanvasProcessor(DrawingOptions options, CanvasAction action) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(action, nameof(action)); + + this.Options = options; + this.Action = action; + } + + /// + /// Gets the drawing options. + /// + public DrawingOptions Options { get; } + + internal CanvasAction Action { get; } + + /// + public IImageProcessor CreatePixelSpecificProcessor( + Configuration configuration, + Image source, + Rectangle sourceRectangle) + where TPixel : unmanaged, IPixel + => new ProcessWithCanvasProcessor(configuration, this, source, sourceRectangle); +} diff --git a/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor{TPixel}.cs new file mode 100644 index 000000000..130f8796b --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/ProcessWithCanvasProcessor{TPixel}.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Executes a per-frame canvas callback for a specific pixel type. +/// +/// The pixel format. +internal sealed class ProcessWithCanvasProcessor : ImageProcessor + where TPixel : unmanaged, IPixel +{ + private readonly ProcessWithCanvasProcessor definition; + private readonly CanvasAction action; + + /// + /// Initializes a new instance of the class. + /// + /// The processing configuration. + /// The processor definition. + /// The source image. + /// The source bounds. + public ProcessWithCanvasProcessor( + Configuration configuration, + ProcessWithCanvasProcessor definition, + Image source, + Rectangle sourceRectangle) + : base(configuration, source, sourceRectangle) + { + this.definition = definition; + this.action = definition.Action; + } + + /// + protected override void OnFrameApply(ImageFrame source) + { + using DrawingCanvas canvas = DrawingCanvas.FromFrame(source, this.definition.Options); + this.action(canvas); + } +} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor.cs deleted file mode 100644 index a6a9bb475..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Allows the recursive application of processing operations against an image within a given region. -/// -public class ClipPathProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The drawing options. - /// The defining the region to operate within. - /// The operation to perform on the source. - public ClipPathProcessor(DrawingOptions options, IPath path, Action operation) - { - this.Options = options; - this.Region = path; - this.Operation = operation; - } - - /// - /// Gets the drawing options. - /// - public DrawingOptions Options { get; } - - /// - /// Gets the defining the region to operate within. - /// - public IPath Region { get; } - - /// - /// Gets the operation to perform on the source. - /// - public Action Operation { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor( - Configuration configuration, - Image source, - Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new ClipPathProcessor(this, source, configuration, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs deleted file mode 100644 index 5e4089c68..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Applies a processing operation to a clipped path region by constraining the operation's input domain -/// to the bounds of the path, then using the processed result as an image brush to fill the path. -/// -/// The type of pixel. -internal class ClipPathProcessor : IImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly ClipPathProcessor definition; - private readonly Image source; - private readonly Configuration configuration; - private readonly Rectangle sourceRectangle; - - public ClipPathProcessor(ClipPathProcessor definition, Image source, Configuration configuration, Rectangle sourceRectangle) - { - this.definition = definition; - this.source = source; - this.configuration = configuration; - this.sourceRectangle = sourceRectangle; - } - - public void Dispose() - { - } - - public void Execute() - { - // Bounds in drawing are floating point. We must conservatively cover the entire shape bounds. - RectangleF boundsF = this.definition.Region.Bounds; - - int left = (int)MathF.Floor(boundsF.Left); - int top = (int)MathF.Floor(boundsF.Top); - int right = (int)MathF.Ceiling(boundsF.Right); - int bottom = (int)MathF.Ceiling(boundsF.Bottom); - - Rectangle crop = Rectangle.FromLTRB(left, top, right, bottom); - - // Constrain the operation to the intersection of the requested bounds and source region. - Rectangle clipped = Rectangle.Intersect(this.sourceRectangle, crop); - - if (clipped.Width <= 0 || clipped.Height <= 0) - { - return; - } - - Action operation = this.definition.Operation; - - // Run the operation on the clipped context so only pixels inside the clip are affected, - // matching the expected semantics of clipping in other graphics APIs. - using Image clone = this.source.Clone(ctx => operation(ctx.Crop(clipped))); - - // Use the clone as a brush source so only the clipped result contributes to the fill, - // keeping the effect confined to the clipped region. - Point brushOffset = new( - clipped.X - (int)MathF.Floor(boundsF.Left), - clipped.Y - (int)MathF.Floor(boundsF.Top)); - - ImageBrush brush = new(clone, clone.Bounds, brushOffset); - - // Fill the shape using the image brush. - FillPathProcessor processor = new(this.definition.Options, brush, this.definition.Region); - using IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(this.configuration, this.source, this.sourceRectangle); - pixelProcessor.Execute(); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs deleted file mode 100644 index 5b3a5cc88..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/DrawPathProcessor.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Defines a processor to fill pixels withing a given -/// with the given and blending defined by the given . -/// -public class DrawPathProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The graphics options. - /// The details how to outline the region of interest. - /// The path to be filled. - public DrawPathProcessor(DrawingOptions options, Pen pen, IPath path) - { - this.Path = path; - this.Pen = pen; - this.Options = options; - } - - /// - /// Gets the used for filling the destination image. - /// - public Pen Pen { get; } - - /// - /// Gets the path that this processor applies to. - /// - public IPath Path { get; } - - /// - /// Gets the defining how to blend the brush pixels over the image pixels. - /// - public DrawingOptions Options { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - { - // Offset drawlines to align drawing outlines to pixel centers. - // The global transform is applied in the FillPathProcessor. - IPath outline = this.Pen.GeneratePath(this.Path.Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); - - 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 deleted file mode 100644 index 5dfadb974..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Defines a processor to fill pixels withing a given -/// with the given and blending defined by the given . -/// -public class FillPathProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The graphics options. - /// The details how to fill the region of interest. - /// The logic path to be filled. - public FillPathProcessor(DrawingOptions options, Brush brush, IPath path) - { - this.Region = path; - this.Brush = brush; - this.Options = options; - } - - /// - /// Gets the used for filling the destination image. - /// - public Brush Brush { get; } - - /// - /// Gets the logic path that this processor applies to. - /// - public IPath Region { get; } - - /// - /// Gets the defining how to blend the brush pixels over the image pixels. - /// - public DrawingOptions Options { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - { - IPath shape = this.Region.Transform(this.Options.Transform); - - if (shape is RectangularPolygon rectPoly) - { - RectangleF rectF = new(rectPoly.Location, rectPoly.Size); - Rectangle rect = (Rectangle)rectF; - if (!this.Options.GraphicsOptions.Antialias || rectF == rect) - { - // Cast as in and back are the same or we are using anti-aliasing - return new FillProcessor(this.Options, this.Brush) - .CreatePixelSpecificProcessor(configuration, source, rect); - } - } - - // Clone the definition so we can pass the transformed path. - FillPathProcessor definition = new(this.Options, this.Brush, shape); - return new FillPathProcessor(configuration, definition, source, sourceRectangle); - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs deleted file mode 100644 index 0788cb6fc..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Uses a brush and a shape to fill the shape with contents of the brush. -/// -/// The type of the color. -/// -internal class FillPathProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly FillPathProcessor definition; - 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, - Image source, - Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - { - IPath path = definition.Region; - int left = (int)MathF.Floor(path.Bounds.Left); - int top = (int)MathF.Floor(path.Bounds.Top); - int right = (int)MathF.Ceiling(path.Bounds.Right); - int bottom = (int)MathF.Ceiling(path.Bounds.Bottom); - - this.bounds = Rectangle.FromLTRB(left, top, right, bottom); - this.path = path.AsClosedPath(); - this.definition = definition; - } - - /// - protected override void OnFrameApply(ImageFrame source) - { - Configuration configuration = this.Configuration; - ShapeOptions shapeOptions = this.definition.Options.ShapeOptions; - GraphicsOptions graphicsOptions = this.definition.Options.GraphicsOptions; - Brush brush = this.definition.Brush; - - // Align start/end positions. - Rectangle interest = Rectangle.Intersect(this.bounds, source.Bounds); - if (interest.Equals(Rectangle.Empty)) - { - return; // No effect inside image; - } - - MemoryAllocator allocator = this.Configuration.MemoryAllocator; - IDrawingBackend drawingBackend = configuration.GetDrawingBackend(); - RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; - RasterizerOptions rasterizerOptions = new( - interest, - shapeOptions.IntersectionRule, - 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/Drawing/FillProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs deleted file mode 100644 index cec760e71..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Defines a processor to fill an with the given -/// using blending defined by the given . -/// -public class FillProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The defining how to blend the brush pixels over the image pixels. - /// The brush to use for filling. - public FillProcessor(DrawingOptions options, Brush brush) - { - this.Brush = brush; - this.Options = options; - } - - /// - /// Gets the used for filling the destination image. - /// - public Brush Brush { get; } - - /// - /// Gets the defining how to blend the brush pixels over the image pixels. - /// - public DrawingOptions Options { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new FillProcessor(configuration, this, source, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs deleted file mode 100644 index 9e1e3c86f..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; - -/// -/// Using the brush as a source of pixels colors blends the brush color with source. -/// -/// The pixel format. -internal class FillProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private readonly FillProcessor definition; - - public FillProcessor(Configuration configuration, FillProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - => this.definition = definition; - - /// - protected override void OnFrameApply(ImageFrame source) - { - Rectangle interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds); - if (interest.Width == 0 || interest.Height == 0) - { - return; - } - - Configuration configuration = this.Configuration; - Brush brush = this.definition.Brush; - GraphicsOptions options = this.definition.Options.GraphicsOptions; - - // If there's no reason for blending, then avoid it. - if (this.IsSolidBrushWithoutBlending(out SolidBrush? solidBrush)) - { - ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(configuration) - .MultiplyMinimumPixelsPerTask(4); - - TPixel colorPixel = solidBrush.Color.ToPixel(); - - FillProcessor.SolidBrushRowIntervalOperation solidOperation = new(interest, source, colorPixel); - ParallelRowIterator.IterateRowIntervals( - interest, - parallelSettings, - in solidOperation); - - return; - } - - using IMemoryOwner amount = configuration.MemoryAllocator.Allocate(interest.Width); - using BrushApplicator applicator = brush.CreateApplicator( - configuration, - options, - source, - this.SourceRectangle); - - amount.Memory.Span.Fill(1F); - - FillProcessor.RowIntervalOperation operation = new(interest, applicator, amount.Memory); - ParallelRowIterator.IterateRowIntervals( - configuration, - interest, - in operation); - } - - private bool IsSolidBrushWithoutBlending([NotNullWhen(true)] out SolidBrush? solidBrush) - { - solidBrush = this.definition.Brush as SolidBrush; - - if (solidBrush is null) - { - return false; - } - - return this.definition.Options.GraphicsOptions.IsOpaqueColorWithoutBlending(solidBrush.Color); - } - - private readonly struct SolidBrushRowIntervalOperation : IRowIntervalOperation - { - private readonly Rectangle bounds; - private readonly ImageFrame source; - private readonly TPixel color; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public SolidBrushRowIntervalOperation(Rectangle bounds, ImageFrame source, TPixel color) - { - this.bounds = bounds; - this.source = source; - this.color = color; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invoke(in RowInterval rows) - { - for (int y = rows.Min; y < rows.Max; y++) - { - this.source.PixelBuffer.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width).Fill(this.color); - } - } - } - - private readonly struct RowIntervalOperation : IRowIntervalOperation - { - private readonly Memory amount; - private readonly Rectangle bounds; - private readonly BrushApplicator applicator; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public RowIntervalOperation(Rectangle bounds, BrushApplicator applicator, Memory amount) - { - this.bounds = bounds; - this.applicator = applicator; - this.amount = amount; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invoke(in RowInterval rows) - { - Span amountSpan = this.amount.Span; - int x = this.bounds.X; - for (int y = rows.Min; y < rows.Max; y++) - { - this.applicator.Apply(amountSpan, x, y); - } - } - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs deleted file mode 100644 index 0eba43ace..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -/// -/// Defines a processor to draw text on an . -/// -public class DrawTextProcessor : IImageProcessor -{ - /// - /// Initializes a new instance of the class. - /// - /// The drawing options. - /// The text rendering options. - /// The text we want to render - /// The brush to source pixel colors from. - /// The pen to outline text with. - public DrawTextProcessor(DrawingOptions drawingOptions, RichTextOptions textOptions, string text, Brush? brush, Pen? pen) - { - Guard.NotNull(text, nameof(text)); - if (brush is null && pen is null) - { - throw new ArgumentException($"Expected a {nameof(brush)} or {nameof(pen)}. Both were null"); - } - - this.DrawingOptions = drawingOptions; - this.TextOptions = textOptions; - this.Text = text; - this.Brush = brush; - this.Pen = pen; - } - - /// - /// Gets the brush used to fill the glyphs. - /// - public Brush? Brush { get; } - - /// - /// Gets the defining blending modes and shape drawing settings. - /// - public DrawingOptions DrawingOptions { get; } - - /// - /// Gets the defining text-specific drawing settings. - /// - public RichTextOptions TextOptions { get; } - - /// - /// Gets the text to draw. - /// - public string Text { get; } - - /// - /// Gets the pen used for outlining the text, if Null then we will not outline - /// - public Pen? Pen { get; } - - /// - /// Gets the location to draw the text at. - /// - public PointF Location { get; } - - /// - public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) - where TPixel : unmanaged, IPixel - => new DrawTextProcessor(configuration, this, source, sourceRectangle); -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs deleted file mode 100644 index a8f60b240..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawTextProcessor{TPixel}.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.Fonts.Rendering; -using SixLabors.ImageSharp.Drawing.Text; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Processing.Processors; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -/// -/// Using the brush as a source of pixels colors blends the brush color with source. -/// -/// The pixel format. -internal class DrawTextProcessor : ImageProcessor - where TPixel : unmanaged, IPixel -{ - private RichTextGlyphRenderer? textRenderer; - private readonly DrawTextProcessor definition; - - public DrawTextProcessor(Configuration configuration, DrawTextProcessor definition, Image source, Rectangle sourceRectangle) - : base(configuration, source, sourceRectangle) - => this.definition = definition; - - protected override void BeforeImageApply() - { - base.BeforeImageApply(); - - // Do everything at the image level as we are delegating - // the processing down to other processors - RichTextOptions textOptions = ConfigureOptions(this.definition.TextOptions); - - this.textRenderer = new RichTextGlyphRenderer( - textOptions, - this.definition.DrawingOptions, - this.Configuration.MemoryAllocator, - this.Configuration.GetDrawingBackend(), - this.definition.Pen, - this.definition.Brush); - - TextRenderer renderer = new(this.textRenderer); - renderer.RenderText(this.definition.Text, textOptions); - } - - protected override void AfterImageApply() - { - base.AfterImageApply(); - this.textRenderer?.Dispose(); - this.textRenderer = null; - } - - /// - protected override void OnFrameApply(ImageFrame source) - { - void Draw(IEnumerable operations) - { - foreach (DrawingOperation operation in operations) - { - GraphicsOptions graphicsOptions = - this.definition.DrawingOptions.GraphicsOptions.CloneOrReturnForRules( - operation.PixelAlphaCompositionMode, - operation.PixelColorBlendingMode); - - using BrushApplicator app = operation.Brush.CreateApplicator( - this.Configuration, - graphicsOptions, - source, - this.SourceRectangle); - - Buffer2D buffer = operation.Map; - int startY = operation.RenderLocation.Y; - int startX = operation.RenderLocation.X; - int offsetSpan = 0; - - if (startY + buffer.Height < 0) - { - continue; - } - - if (startX + buffer.Width < 0) - { - continue; - } - - if (startX < 0) - { - offsetSpan = -startX; - startX = 0; - } - - if (startX >= source.Width) - { - continue; - } - - int firstRow = 0; - if (startY < 0) - { - firstRow = -startY; - } - - int maxWidth = source.Width - startX; - int maxHeight = source.Height - startY; - int end = Math.Min(operation.Map.Height, maxHeight); - - for (int row = firstRow; row < end; row++) - { - int y = startY + row; - Span span = buffer.DangerousGetRowSpan(row).Slice(offsetSpan, Math.Min(buffer.Width - offsetSpan, maxWidth)); - app.Apply(span, startX, y); - } - } - } - - // Not null, initialized in earlier event. - if (this.textRenderer!.DrawingOperations.Count > 0) - { - Draw(this.textRenderer.DrawingOperations.OrderBy(x => x.RenderPass)); - } - } - - private static RichTextOptions ConfigureOptions(RichTextOptions options) - { - // When a path is specified we should explicitly follow that path - // and not adjust the origin. Any translation should be applied to the path. - if (options.Path is not null && options.Origin != Vector2.Zero) - { - return new RichTextOptions(options) - { - Origin = Vector2.Zero, - Path = options.Path.Translate(options.Origin) - }; - } - - return options; - } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs deleted file mode 100644 index 1914f4bb5..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/DrawingOperation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -internal struct DrawingOperation -{ - public Buffer2D Map { get; set; } - - public IPath Path { get; set; } - - public byte RenderPass { get; set; } - - public Point RenderLocation { get; set; } - - public Brush Brush { get; internal set; } - - public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; set; } - - public PixelColorBlendingMode PixelColorBlendingMode { get; set; } -} diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs deleted file mode 100644 index 6436d67ef..000000000 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs +++ /dev/null @@ -1,704 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.Fonts; -using SixLabors.Fonts.Rendering; -using SixLabors.Fonts.Unicode; -using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; -using SixLabors.ImageSharp.Drawing.Text; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; - -/// -/// Allows the rendering of rich text configured via . -/// -internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposable -{ - private const byte RenderOrderFill = 0; - private const byte RenderOrderOutline = 1; - private const byte RenderOrderDecoration = 2; - - private readonly DrawingOptions drawingOptions; - private readonly MemoryAllocator memoryAllocator; - private readonly IDrawingBackend drawingBackend; - private readonly Pen? defaultPen; - private readonly Brush? defaultBrush; - private readonly IPathInternals? path; - private bool isDisposed; - - private TextRun? currentTextRun; - private Brush? currentBrush; - private Pen? currentPen; - private FillRule currentFillRule; - private PixelAlphaCompositionMode currentCompositionMode; - private PixelColorBlendingMode currentBlendingMode; - private bool currentDecorationIsVertical; - private bool hasLayer; - - // Just enough accuracy to allow for 1/8 px differences which later are accumulated while rendering, - // but do not grow into full px offsets. - // The value 8 is benchmarked to: - // - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant) - // - Cache hit ratio above 60% - private const float AccuracyMultiple = 8; - private readonly Dictionary> glyphCache = []; - private int cacheReadIndex; - - private bool rasterizationRequired; - private readonly bool noCache; - private CacheKey currentCacheKey; - - 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 = []; - this.currentCompositionMode = drawingOptions.GraphicsOptions.AlphaCompositionMode; - this.currentBlendingMode = drawingOptions.GraphicsOptions.ColorBlendingMode; - - IPath? path = textOptions.Path; - if (path is not null) - { - // Turn off caching. The chances of a hit are near-zero. - this.rasterizationRequired = true; - this.noCache = true; - if (path is IPathInternals internals) - { - this.path = internals; - } - else - { - this.path = new ComplexPolygon(path); - } - } - } - - public List DrawingOperations { get; } - - /// - protected override void BeginText(in FontRectangle bounds) - { - foreach (DrawingOperation operation in this.DrawingOperations) - { - operation.Map.Dispose(); - } - - this.DrawingOperations.Clear(); - } - - /// - protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) - { - // Reset state. - this.cacheReadIndex = 0; - this.currentDecorationIsVertical = parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; - this.currentTextRun = parameters.TextRun; - if (parameters.TextRun is RichTextRun drawingRun) - { - this.currentBrush = drawingRun.Brush; - this.currentPen = drawingRun.Pen; - } - else - { - this.currentBrush = null; - this.currentPen = null; - } - - if (!this.noCache) - { - // Create a cache entry for the glyph. - // We need to apply the default transform to the bounds to get the correct size - // for comparison with future glyphs. We can use this cached glyph anywhere in the text block. - RectangleF currentBounds = RectangleF.Transform( - new RectangleF(bounds.Location, new SizeF(bounds.Width, bounds.Height)), - this.drawingOptions.Transform); - - PointF currentBoundsDelta = currentBounds.Location - ClampToPixel(currentBounds.Location); - PointF subPixelLocation = new( - MathF.Round(currentBoundsDelta.X * AccuracyMultiple) / AccuracyMultiple, - MathF.Round(currentBoundsDelta.Y * AccuracyMultiple) / AccuracyMultiple); - - SizeF subPixelSize = new( - MathF.Round(currentBounds.Width * AccuracyMultiple) / AccuracyMultiple, - MathF.Round(currentBounds.Height * AccuracyMultiple) / AccuracyMultiple); - - this.currentCacheKey = CacheKey.FromParameters(parameters, new RectangleF(subPixelLocation, subPixelSize)); - if (this.glyphCache.ContainsKey(this.currentCacheKey)) - { - // We have already drawn the glyph vectors. - this.rasterizationRequired = false; - return; - } - } - - // Transform the glyph vectors using the original bounds - // The default transform will automatically be applied. - this.TransformGlyph(in bounds); - this.rasterizationRequired = true; - } - - protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) - { - this.hasLayer = true; - if (TryCreateBrush(paint, this.Builder.Transform, out Brush? brush)) - { - this.currentBrush = brush; - this.currentCompositionMode = TextUtilities.MapCompositionMode(paint.CompositeMode); - this.currentBlendingMode = TextUtilities.MapBlendingMode(paint.CompositeMode); - } - } - - protected override void EndLayer() - { - GlyphRenderData renderData = default; - - // Fix up the text runs colors. - // Only if both brush and pen is null do we fallback to the default value. - if (this.currentBrush == null && this.currentPen == null) - { - this.currentBrush = this.defaultBrush; - this.currentPen = this.defaultPen; - } - - // When rendering layers we only fill them. - // Any drawing of outlines is ignored as that doesn't really make sense. - bool renderFill = this.currentBrush != null; - - // Path has already been added to the collection via the base class. - IPath path = this.CurrentPaths[^1]; - Point renderLocation = ClampToPixel(path.Bounds.Location); - if (this.noCache || this.rasterizationRequired) - { - if (path.Bounds.Equals(RectangleF.Empty)) - { - return; - } - - if (renderFill) - { - renderData.FillMap = this.Render(path); - } - - // Capture the delta between the location and the truncated render location. - // We can use this to offset the render location on the next instance of this glyph. - renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); - - if (!this.noCache) - { - this.UpdateCache(renderData); - } - } - else - { - renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; - - // Offset the render location by the delta from the cached glyph and this one. - Vector2 previousDelta = renderData.LocationDelta; - Vector2 currentLocation = path.Bounds.Location; - Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); - - if (previousDelta.Y > currentDelta.Y) - { - // Move the location down to match the previous location offset. - currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); - } - else if (previousDelta.Y < currentDelta.Y) - { - // Move the location up to match the previous location offset. - currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); - } - else if (previousDelta.X > currentDelta.X) - { - // Move the location right to match the previous location offset. - currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); - } - else if (previousDelta.X < currentDelta.X) - { - // Move the location left to match the previous location offset. - currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); - } - - renderLocation = ClampToPixel(currentLocation); - } - - if (renderData.FillMap != null) - { - this.DrawingOperations.Add(new DrawingOperation - { - RenderLocation = renderLocation, - Map = renderData.FillMap, - Brush = this.currentBrush!, - RenderPass = RenderOrderFill, - PixelAlphaCompositionMode = this.currentCompositionMode, - PixelColorBlendingMode = this.currentBlendingMode - }); - } - - this.currentFillRule = FillRule.NonZero; - this.currentCompositionMode = this.drawingOptions.GraphicsOptions.AlphaCompositionMode; - this.currentBlendingMode = this.drawingOptions.GraphicsOptions.ColorBlendingMode; - } - - public override TextDecorations EnabledDecorations() - { - TextRun? run = this.currentTextRun; - TextDecorations decorations = run?.TextDecorations ?? TextDecorations.None; - - if (this.currentTextRun is RichTextRun drawingRun) - { - if (drawingRun.UnderlinePen != null) - { - decorations |= TextDecorations.Underline; - } - - if (drawingRun.StrikeoutPen != null) - { - decorations |= TextDecorations.Strikeout; - } - - if (drawingRun.OverlinePen != null) - { - decorations |= TextDecorations.Overline; - } - } - - return decorations; - } - - public override void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) - { - if (thickness == 0) - { - return; - } - - Brush? brush = null; - Pen? pen = null; - if (this.currentTextRun is RichTextRun drawingRun) - { - brush = drawingRun.Brush; - - if (textDecorations == TextDecorations.Strikeout) - { - pen = drawingRun.StrikeoutPen ?? pen; - } - else if (textDecorations == TextDecorations.Underline) - { - pen = drawingRun.UnderlinePen ?? pen; - } - else if (textDecorations == TextDecorations.Overline) - { - pen = drawingRun.OverlinePen; - } - } - - // Always respect the pen stroke width if explicitly set. - float originalThickness = thickness; - if (pen is not null) - { - // Clamp the thickness to whole pixels. - thickness = MathF.Max(1F, (float)Math.Round(pen.StrokeWidth)); - } - else - { - // The thickness of the line has already been clamped in the base class. - pen = new SolidPen((brush ?? this.defaultBrush)!, thickness); - } - - // Path has already been added to the collection via the base class. - IPath path = this.CurrentPaths[^1]; - IPath outline = path; - - if (originalThickness != thickness) - { - // Respect edge anchoring per decoration type: - // - Overline: keep the base edge fixed (bottom in horizontal; left in vertical) - // - Underline: keep the top edge fixed (top in horizontal; right in vertical) - // - Strikeout: keep the center fixed (default behavior) - float ratio = thickness / originalThickness; - if (ratio != 1f) - { - Vector2 scale = this.currentDecorationIsVertical - ? new Vector2(ratio, 1f) - : new Vector2(1f, ratio); - - RectangleF b = path.Bounds; - Vector2 center = new(b.Left + (b.Width * 0.5f), b.Top + (b.Height * 0.5f)); - Vector2 anchor = center; - - if (textDecorations == TextDecorations.Overline) - { - anchor = this.currentDecorationIsVertical - ? new Vector2(b.Left, center.Y) // vertical: anchor left edge - : new Vector2(center.X, b.Bottom); // horizontal: anchor bottom edge - } - else if (textDecorations == TextDecorations.Underline) - { - anchor = this.currentDecorationIsVertical - ? new Vector2(b.Right, center.Y) // vertical: anchor right edge - : new Vector2(center.X, b.Top); // horizontal: anchor top edge - } - - // Scale about the chosen anchor so the fixed edge stays in place. - outline = outline.Transform(Matrix3x2.CreateScale(scale, anchor)); - } - } - - // Render the path here. Decorations are un-cached. - this.DrawingOperations.Add(new DrawingOperation - { - Brush = pen.StrokeFill, - RenderLocation = ClampToPixel(outline.Bounds.Location), - Map = this.Render(outline), - RenderPass = RenderOrderDecoration - }); - } - - protected override void EndGlyph() - { - if (this.hasLayer) - { - // The layer has already been rendered. - this.hasLayer = false; - return; - } - - GlyphRenderData renderData = default; - - // Fix up the text runs colors. - // Only if both brush and pen is null do we fallback to the default value. - if (this.currentBrush == null && this.currentPen == null) - { - this.currentBrush = this.defaultBrush; - this.currentPen = this.defaultPen; - } - - bool renderFill = false; - bool renderOutline = false; - - // If we are using the fonts color layers we ignore the request to draw an outline only - // because that won't really work. Instead we force drawing using fill with the requested color. - if (this.currentBrush != null) - { - renderFill = true; - } - - if (this.currentPen != null) - { - renderOutline = true; - } - - // Path has already been added to the collection via the base class. - IPath path = this.CurrentPaths[^1]; - Point renderLocation = ClampToPixel(path.Bounds.Location); - if (this.noCache || this.rasterizationRequired) - { - if (path.Bounds.Equals(RectangleF.Empty)) - { - return; - } - - if (renderFill) - { - renderData.FillMap = this.Render(path); - } - - // Capture the delta between the location and the truncated render location. - // We can use this to offset the render location on the next instance of this glyph. - renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); - - if (renderOutline) - { - path = this.currentPen!.GeneratePath(path); - renderData.OutlineMap = this.Render(path); - } - - if (!this.noCache) - { - this.UpdateCache(renderData); - } - } - else - { - renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; - - // Offset the render location by the delta from the cached glyph and this one. - Vector2 previousDelta = renderData.LocationDelta; - Vector2 currentLocation = path.Bounds.Location; - Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); - - if (previousDelta.Y > currentDelta.Y) - { - // Move the location down to match the previous location offset. - currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); - } - else if (previousDelta.Y < currentDelta.Y) - { - // Move the location up to match the previous location offset. - currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); - } - else if (previousDelta.X > currentDelta.X) - { - // Move the location right to match the previous location offset. - currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); - } - else if (previousDelta.X < currentDelta.X) - { - // Move the location left to match the previous location offset. - currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); - } - - renderLocation = ClampToPixel(currentLocation); - } - - if (renderData.FillMap != null) - { - this.DrawingOperations.Add(new DrawingOperation - { - RenderLocation = renderLocation, - Map = renderData.FillMap, - Brush = this.currentBrush!, - RenderPass = RenderOrderFill, - PixelAlphaCompositionMode = this.currentCompositionMode, - PixelColorBlendingMode = this.currentBlendingMode - }); - } - - if (renderData.OutlineMap != null) - { - int offset = (int)((this.currentPen?.StrokeWidth ?? 0) / 2); - this.DrawingOperations.Add(new DrawingOperation - { - RenderLocation = renderLocation - new Size(offset, offset), - Map = renderData.OutlineMap, - Brush = this.currentPen?.StrokeFill ?? this.currentBrush!, - RenderPass = RenderOrderOutline, - PixelAlphaCompositionMode = this.currentCompositionMode, - PixelColorBlendingMode = this.currentBlendingMode - }); - } - } - - private void UpdateCache(GlyphRenderData renderData) - { - if (!this.glyphCache.TryGetValue(this.currentCacheKey, out List? _)) - { - this.glyphCache[this.currentCacheKey] = []; - } - - this.glyphCache[this.currentCacheKey].Add(renderData); - } - - public void Dispose() => this.Dispose(true); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Point ClampToPixel(PointF point) => Point.Truncate(point); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void TransformGlyph(in FontRectangle bounds) - => this.Builder.SetTransform(this.ComputeTransform(in bounds)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Matrix3x2 ComputeTransform(in FontRectangle bounds) - { - if (this.path is null) - { - return Matrix3x2.Identity; - } - - // Find the point of this intersection along the given path. - // We want to find the point on the path that is closest to the center-bottom side of the glyph. - Vector2 half = new(bounds.Width * .5F, 0); - SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); - - // Now offset to our target point since we're aligning the top-left location of our glyph against the path. - Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); - 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 - // path location. - IPath offsetPath = path.Translate(-ClampToPixel(path.Bounds.Location)); - Size size = Rectangle.Ceiling(offsetPath.Bounds).Size; - - // Pad to prevent edge clipping. - size += new Size(2, 2); - - RasterizerSamplingOrigin samplingOrigin = RasterizerSamplingOrigin.PixelBoundary; - GraphicsOptions graphicsOptions = this.drawingOptions.GraphicsOptions; - RasterizationMode rasterizationMode = graphicsOptions.Antialias - ? RasterizationMode.Antialiased - : RasterizationMode.Aliased; - - // Take the path inside the path builder, scan thing and generate a Buffer2D representing the glyph. - Buffer2D buffer = this.memoryAllocator.Allocate2D(size.Width, size.Height, AllocationOptions.Clean); - RasterizerOptions rasterizerOptions = new( - new Rectangle(0, 0, size.Width, size.Height), - TextUtilities.MapFillRule(this.currentFillRule), - rasterizationMode, - samplingOrigin); - - // 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; - } - - private void Dispose(bool disposing) - { - if (!this.isDisposed) - { - if (disposing) - { - foreach (KeyValuePair> kv in this.glyphCache) - { - foreach (GlyphRenderData data in kv.Value) - { - data.Dispose(); - } - } - - this.glyphCache.Clear(); - - foreach (DrawingOperation operation in this.DrawingOperations) - { - operation.Map.Dispose(); - } - - this.DrawingOperations.Clear(); - } - - this.isDisposed = true; - } - } - - private struct GlyphRenderData : IDisposable - { - public Vector2 LocationDelta; - - public Buffer2D FillMap; - - public Buffer2D OutlineMap; - - public readonly void Dispose() - { - this.FillMap?.Dispose(); - this.OutlineMap?.Dispose(); - } - } - - private readonly struct CacheKey : IEquatable - { - public string Font { get; init; } - - public GlyphColor GlyphColor { get; init; } - - public GlyphType GlyphType { get; init; } - - public FontStyle FontStyle { get; init; } - - public ushort GlyphId { get; init; } - - public ushort CompositeGlyphId { get; init; } - - public CodePoint CodePoint { get; init; } - - public float PointSize { get; init; } - - public float Dpi { get; init; } - - public GlyphLayoutMode LayoutMode { get; init; } - - public TextAttributes TextAttributes { get; init; } - - public TextDecorations TextDecorations { get; init; } - - public RectangleF Bounds { get; init; } - - public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right); - - public static bool operator !=(CacheKey left, CacheKey right) => !(left == right); - - public static CacheKey FromParameters(in GlyphRendererParameters parameters, RectangleF bounds) - => new() - { - // Do not include the grapheme index as that will - // always vary per glyph instance. - Font = parameters.Font, - GlyphType = parameters.GlyphType, - FontStyle = parameters.FontStyle, - GlyphId = parameters.GlyphId, - CompositeGlyphId = parameters.CompositeGlyphId, - CodePoint = parameters.CodePoint, - PointSize = parameters.PointSize, - Dpi = parameters.Dpi, - LayoutMode = parameters.LayoutMode, - TextAttributes = parameters.TextRun.TextAttributes, - TextDecorations = parameters.TextRun.TextDecorations, - Bounds = bounds - }; - - public override bool Equals(object? obj) - => obj is CacheKey key && this.Equals(key); - - public bool Equals(CacheKey other) - => this.Font == other.Font && - this.GlyphColor.Equals(other.GlyphColor) && - this.GlyphType == other.GlyphType && - this.FontStyle == other.FontStyle && - this.GlyphId == other.GlyphId && - this.CompositeGlyphId == other.CompositeGlyphId && - this.CodePoint.Equals(other.CodePoint) && - this.PointSize == other.PointSize && - this.Dpi == other.Dpi && - this.LayoutMode == other.LayoutMode && - this.TextAttributes == other.TextAttributes && - this.TextDecorations == other.TextDecorations && - this.Bounds.Equals(other.Bounds); - - public override int GetHashCode() - { - HashCode hash = default; - hash.Add(this.Font); - hash.Add(this.GlyphColor); - hash.Add(this.GlyphType); - hash.Add(this.FontStyle); - hash.Add(this.GlyphId); - hash.Add(this.CompositeGlyphId); - hash.Add(this.CodePoint); - hash.Add(this.PointSize); - hash.Add(this.Dpi); - hash.Add(this.LayoutMode); - hash.Add(this.TextAttributes); - hash.Add(this.TextDecorations); - hash.Add(this.Bounds); - return hash.ToHashCode(); - } - } -} diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index 6e13391bb..e74d788ae 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; + namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -11,11 +14,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class RadialGradientBrush : GradientBrush { - private readonly PointF center0; - private readonly float radius0; - private readonly PointF? center1; // null means single-circle form - private readonly float? radius1; - /// /// Initializes a new instance of the class using a single circle. /// @@ -30,10 +28,10 @@ public RadialGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.center0 = center; - this.radius0 = radius; - this.center1 = null; - this.radius1 = null; + this.Center0 = center; + this.Radius0 = radius; + this.Center1 = null; + this.Radius1 = null; } /// @@ -54,10 +52,49 @@ public RadialGradientBrush( params ColorStop[] colorStops) : base(repetitionMode, colorStops) { - this.center0 = startCenter; - this.radius0 = startRadius; - this.center1 = endCenter; - this.radius1 = endRadius; + this.Center0 = startCenter; + this.Radius0 = startRadius; + this.Center1 = endCenter; + this.Radius1 = endRadius; + } + + /// + /// Gets the center of the starting circle. + /// + public PointF Center0 { get; } + + /// + /// Gets the radius of the starting circle. + /// + public float Radius0 { get; } + + /// + /// Gets the center of the ending circle, or for single-circle form. + /// + public PointF? Center1 { get; } + + /// + /// Gets the radius of the ending circle, or for single-circle form. + /// + public float? Radius1 { get; } + + /// + /// Gets a value indicating whether this is a two-circle radial gradient. + /// + public bool IsTwoCircle => this.Center1.HasValue && this.Radius1.HasValue; + + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc0 = PointF.Transform(this.Center0, matrix); + float scale = MatrixUtilities.GetAverageScale(in matrix); + if (this.IsTwoCircle) + { + PointF tc1 = PointF.Transform(this.Center1!.Value, matrix); + return new RadialGradientBrush(tc0, this.Radius0 * scale, tc1, this.Radius1!.Value * scale, this.RepetitionMode, this.ColorStopsArray); + } + + return new RadialGradientBrush(tc0, this.Radius0 * scale, this.RepetitionMode, this.ColorStopsArray); } /// @@ -66,10 +103,10 @@ public override bool Equals(Brush? other) if (other is RadialGradientBrush b) { return base.Equals(other) - && this.center0.Equals(b.center0) - && this.radius0.Equals(b.radius0) - && Nullable.Equals(this.center1, b.center1) - && Nullable.Equals(this.radius1, b.radius1); + && this.Center0.Equals(b.Center0) + && this.Radius0.Equals(b.Radius0) + && Nullable.Equals(this.Center1, b.Center1) + && Nullable.Equals(this.Radius1, b.Radius1); } return false; @@ -77,72 +114,73 @@ public override bool Equals(Brush? other) /// public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), this.center0, this.radius0, this.center1, this.radius1); + => HashCode.Combine(base.GetHashCode(), this.Center0, this.Radius0, this.Center1, this.Radius1); /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, RectangleF region) - => new RadialGradientBrushApplicator( + => new RadialGradientBrushRenderer( configuration, options, - source, - this.center0, - this.radius0, - this.center1, - this.radius1, - this.ColorStops, + canvasWidth, + this.Center0, + this.Radius0, + this.Center1, + this.Radius1, + this.ColorStopsArray, this.RepetitionMode); /// /// The radial gradient brush applicator. /// - private sealed class RadialGradientBrushApplicator : GradientBrushApplicator + private sealed class RadialGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel { + private const float GradientEpsilon = 1F / (1 << 12); + // Single-circle fields private readonly bool isTwoCircle; private readonly float c0x; private readonly float c0y; private readonly float r0; - // Two-circle fields - private readonly float c1x; - private readonly float c1y; - private readonly float r1; - - // Precomputed for two-circle solve - private readonly float dx; - private readonly float dy; - private readonly float dd; // d·d - private readonly float dr; // r1 - r0 - private readonly float denom; // dd - dr^2 + // Two-circle gradient fields. + // The transform changes coordinates so the gradient can be evaluated + // with simple formulas around a canonical line/circle configuration. + private readonly Matrix3x2 radialTransform; + private readonly float focalX; + private readonly float radius; + private readonly bool isStrip; + private readonly bool isCircular; + private readonly bool isFocalOnCircle; + private readonly bool isSwapped; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The target image. + /// The canvas width for the current render pass. /// Center of the starting circle. /// Radius of the starting circle. /// Center of the ending circle, or null to use single-circle form. /// Radius of the ending circle, or null to use single-circle form. /// Definition of colors. /// How the colors are repeated beyond the first gradient. - public RadialGradientBrushApplicator( + public RadialGradientBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame target, + int canvasWidth, PointF center0, float radius0, PointF? center1, float? radius1, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, target, colorStops, repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) { this.c0x = center0.X; this.c0y = center0.Y; @@ -152,28 +190,29 @@ public RadialGradientBrushApplicator( if (this.isTwoCircle) { - this.c1x = center1!.Value.X; - this.c1y = center1.Value.Y; - this.r1 = radius1!.Value; + ConicalGradientParameters parameters = CreateConicalGradientParameters( + center0, + radius0, + center1!.Value, + radius1!.Value); - this.dx = this.c1x - this.c0x; - this.dy = this.c1y - this.c0y; - this.dd = (this.dx * this.dx) + (this.dy * this.dy); - this.dr = this.r1 - this.r0; - - // A = |d|^2 - dr^2 - this.denom = this.dd - (this.dr * this.dr); + this.radialTransform = parameters.Transform; + this.focalX = parameters.FocalX; + this.radius = parameters.Radius; + this.isStrip = parameters.IsStrip; + this.isCircular = parameters.IsCircular; + this.isFocalOnCircle = parameters.IsFocalOnCircle; + this.isSwapped = parameters.IsSwapped; } else { - this.c1x = 0F; - this.c1y = 0F; - this.r1 = 0F; - this.dx = 0F; - this.dy = 0F; - this.dd = 0F; - this.dr = 0F; - this.denom = 0F; + this.radialTransform = Matrix3x2.Identity; + this.focalX = 0F; + this.radius = 0F; + this.isStrip = false; + this.isCircular = false; + this.isFocalOnCircle = false; + this.isSwapped = false; } } @@ -186,29 +225,203 @@ protected override float PositionOnGradient(float x, float y) return MathF.Sqrt((ux * ux) + (uy * uy)) / this.r0; } - float qx = x - this.c0x, qy = y - this.c0y; + // Move the sample into the canonical coordinate system where the + // end circle lies on the x-axis and the conic can be solved using + // closed-form expressions. + Vector2 local = Vector2.Transform(new Vector2(x, y), this.radialTransform); + float localX = local.X; + float localY = local.Y; + float xx = localX * localX; + float yy = localY * localY; + float t; - // Concentric case: centers equal -> dd == 0 - if (this.dd == 0f) + if (this.isStrip) { - // t = (|p-c0| - r0) / (r1 - r0) - float dist = MathF.Sqrt((qx * qx) + (qy * qy)); - float invDr = 1f / MathF.Max(MathF.Abs(this.dr), 1e-20f); - return (dist - this.r0) * invDr; + // Strip gradients are bounded by a band around the axis. + // radius stores the squared half-width in normalized space, + // so points outside the band are invalid. + float a = this.radius - yy; + if (a < 0F) + { + return float.NaN; + } + + // Once inside the band, the parameter advances along the axis. + t = MathF.Sqrt(a) + localX; } + else if (this.isFocalOnCircle) + { + // This degenerate case reduces to a rational expression where + // the focal point sits exactly on the limiting circle. + if (localX == 0F) + { + return float.NaN; + } - // General two-circle fast form: - // t = ((q·d) - r0*dr) / (|d|^2 - dr^2) - if (this.denom == 0f) + t = (xx + yy) / localX; + if (t < 0F) + { + return float.NaN; + } + } + else if (this.radius > 1F) { - // Near-singular; fall back to concentric-like ratio - float dist = MathF.Sqrt((qx * qx) + (qy * qy)); - float invDr = 1f / MathF.Max(MathF.Abs(this.dr), 1e-20f); - return (dist - this.r0) * invDr; + // Wide cones use a circular norm. The x term shifts the root + // back into the original gradient parameterization. + float radiusReciprocal = this.isCircular ? 0F : 1F / this.radius; + t = MathF.Sqrt(xx + yy) - (localX * radiusReciprocal); + } + else + { + // Narrow cones use a hyperbolic form. Points with x^2 < y^2 + // lie outside the valid branch and must not contribute. + float a = xx - yy; + if (a < 0F) + { + return float.NaN; + } + + // lessScale picks the correct branch of the hyperbola after + // swaps and orientation changes. + float lessScale = (this.isSwapped || (1F - this.focalX) < 0F) ? -1F : 1F; + t = (lessScale * MathF.Sqrt(a)) - (localX / this.radius); + if (t < 0F) + { + return float.NaN; + } } - float num = (qx * this.dx) + (qy * this.dy) - (this.r0 * this.dr); - return num / this.denom; + // Convert back from the normalized local solution into the brush's + // gradient parameter, then undo the earlier swap if required. + t = this.focalX + (MathF.Sign(1F - this.focalX) * t); + return this.isSwapped ? 1F - t : t; } + + private static ConicalGradientParameters CreateConicalGradientParameters( + PointF center0, + float radius0, + PointF center1, + float radius1) + { + PointF p0 = center0; + PointF p1 = center1; + float r0 = radius0; + float r1 = radius1; + + if (MathF.Abs(r0 - r1) <= GradientEpsilon) + { + // When both circles have the same radius, the locus becomes a + // strip: solve along the axis between the centers, with the + // radius contributing only a perpendicular cutoff. + float scaled = r0 / Distance(p0, p1); + return new ConicalGradientParameters( + TwoPointToUnitLine(p0, p1), + 0F, + scaled * scaled, + IsStrip: true, + IsCircular: false, + IsFocalOnCircle: false, + IsSwapped: false); + } + + bool isCircular = false; + if (p0 == p1) + { + isCircular = true; + + // Equal centers make the conic circular. Nudge slightly so the + // line construction below stays invertible. + p0 = new PointF(p0.X + GradientEpsilon, p0.Y + GradientEpsilon); + } + + bool isSwapped = false; + if (r1 == 0F) + { + isSwapped = true; + + // Put the zero-radius focus on the start side so the later + // formulas keep one orientation. + (p0, p1) = (p1, p0); + (r0, r1) = (r1, r0); + } + + // focalX describes where the focal point lies along the line from + // the start circle to the end circle. Values outside [0, 1] are + // valid and correspond to cones whose focus lies beyond an endpoint. + float focalX = r0 / (r0 - r1); + PointF cf = new( + ((1F - focalX) * p0.X) + (focalX * p1.X), + ((1F - focalX) * p0.Y) + (focalX * p1.Y)); + + // radius is the end-circle radius expressed in the normalized frame + // built from the focal point and the end center. + float radius = r1 / Distance(cf, p1); + Matrix3x2 userToUnitLine = TwoPointToUnitLine(cf, p1); + Matrix3x2 transform; + bool isFocalOnCircle = false; + + if (MathF.Abs(radius - 1F) <= GradientEpsilon) + { + isFocalOnCircle = true; + + // When the focal point lies on the circle, the quadratic terms + // collapse to a simpler rational form. + float scale = 0.5F * MathF.Abs(1F - focalX); + transform = userToUnitLine * Matrix3x2.CreateScale(scale); + } + else + { + // Otherwise scale the unit-line frame so the gradient can be + // tested with either x^2 + y^2 or x^2 - y^2, depending on + // whether the cone opens wider or narrower than the unit case. + float a = (radius * radius) - 1F; + float scaleRatio = MathF.Abs(1F - focalX) / a; + float scaleX = radius * scaleRatio; + float scaleY = MathF.Sqrt(MathF.Abs(a)) * scaleRatio; + transform = userToUnitLine * Matrix3x2.CreateScale(scaleX, scaleY); + } + + return new ConicalGradientParameters( + transform, + focalX, + radius, + IsStrip: false, + IsCircular: isCircular, + IsFocalOnCircle: isFocalOnCircle, + IsSwapped: isSwapped); + } + + private static float Distance(Vector2 p0, Vector2 p1) => Vector2.Distance(p0, p1); + + private static Matrix3x2 TwoPointToUnitLine(PointF p0, PointF p1) + { + // Build a change-of-basis that sends the segment p0->p1 to the + // unit line. That lets the gradient math work in one fixed frame + // instead of re-deriving equations for every brush. + Matrix3x2 source = FromPoly2(p0, p1); + Matrix3x2.Invert(source, out Matrix3x2 inverse); + return inverse * FromPoly2(new PointF(0F, 0F), new PointF(1F, 0F)); + } + + private static Matrix3x2 FromPoly2(PointF p0, PointF p1) + + // This affine frame uses p0 as the origin and p0->p1 as one axis. + // Its inverse is the basis change we need for normalization. + => new( + p1.Y - p0.Y, + p0.X - p1.X, + p1.X - p0.X, + p1.Y - p0.Y, + p0.X, + p0.Y); + + private readonly record struct ConicalGradientParameters( + Matrix3x2 Transform, + float FocalX, + float Radius, + bool IsStrip, + bool IsCircular, + bool IsFocalOnCircle, + bool IsSwapped); } } diff --git a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs index db2361cfc..c600a40de 100644 --- a/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/RasterizerDefaultsExtensions.cs @@ -2,14 +2,13 @@ // 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. +/// Adds extensions that allow configuring the drawing backend implementation. /// -internal static class RasterizerDefaultsExtensions +public static class RasterizerDefaultsExtensions { /// /// Sets the drawing backend against the source image processing context. @@ -22,11 +21,6 @@ internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingC Guard.NotNull(backend, nameof(backend)); context.Properties[typeof(IDrawingBackend)] = backend; - if (backend is CpuDrawingBackend cpuBackend) - { - context.Properties[typeof(IRasterizer)] = cpuBackend.PrimaryRasterizer; - } - return context; } @@ -35,15 +29,10 @@ internal static IImageProcessingContext SetDrawingBackend(this IImageProcessingC /// /// The configuration to store the backend against. /// The backend to use. - internal static void SetDrawingBackend(this Configuration configuration, IDrawingBackend backend) + public 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; - } } /// @@ -54,17 +43,12 @@ internal static void SetDrawingBackend(this Configuration configuration, IDrawin internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext context) { if (context.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is IDrawingBackend configured) + backend is IDrawingBackend configured && + configured.IsSupported) { return configured; } - if (context.Properties.TryGetValue(typeof(IRasterizer), out object? rasterizer) && - rasterizer is IRasterizer configuredRasterizer) - { - return CpuDrawingBackend.Create(configuredRasterizer); - } - return context.Configuration.GetDrawingBackend(); } @@ -76,95 +60,14 @@ internal static IDrawingBackend GetDrawingBackend(this IImageProcessingContext c internal static IDrawingBackend GetDrawingBackend(this Configuration configuration) { if (configuration.Properties.TryGetValue(typeof(IDrawingBackend), out object? backend) && - backend is IDrawingBackend configured) + backend is IDrawingBackend configured && + configured.IsSupported) { 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; + IDrawingBackend defaultBackend = DefaultDrawingBackend.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/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index c9369761e..1a7646b35 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -40,14 +40,15 @@ public RecolorBrush(Color sourceColor, Color targetColor, float threshold) public Color TargetColor { get; } /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, - RectangleF region) => new RecolorBrushApplicator( + int canvasWidth, + RectangleF region) + => new RecolorBrushRenderer( configuration, options, - source, + canvasWidth, this.SourceColor.ToPixel(), this.TargetColor.ToPixel(), this.Threshold); @@ -73,32 +74,30 @@ public override int GetHashCode() /// The recolor brush applicator. /// /// The pixel format. - private class RecolorBrushApplicator : BrushApplicator + private class RecolorBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel { private readonly Vector4 sourceColor; private readonly float threshold; private readonly TPixel targetColorPixel; - private readonly ThreadLocalBlenderBuffers blenderBuffers; - private bool isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The options - /// The source image. + /// The canvas width for the current render pass. /// Color of the source. /// Color of the target. /// The threshold . - public RecolorBrushApplicator( + public RecolorBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, TPixel sourceColor, TPixel targetColor, float threshold) - : base(configuration, options, source) + : base(configuration, options, canvasWidth) { this.sourceColor = sourceColor.ToScaledVector4(); this.targetColorPixel = targetColor; @@ -108,55 +107,30 @@ public RecolorBrushApplicator( TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue)); TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue)); this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold; - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width); - } - - internal TPixel this[int x, int y] - { - get - { - // Offset the requested pixel by the value in the rectangle (the shapes position) - TPixel result = this.Target[x, y]; - Vector4 background = result.ToVector4(); - float distance = Vector4.DistanceSquared(background, this.sourceColor); - if (distance <= this.threshold) - { - float lerpAmount = (this.threshold - distance) / this.threshold; - return this.Blender.Blend( - result, - this.targetColorPixel, - lerpAmount); - } - - return result; - } } /// - public override void Apply(Span scanline, int x, int y) + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) { - if (x < 0 || y < 0 || x >= this.Target.Width || y >= this.Target.Height) - { - return; - } - - // Limit the scanline to the bounds of the image relative to x. - scanline = scanline[..Math.Min(this.Target.Width - x, scanline.Length)]; - Span amounts = this.blenderBuffers.AmountSpan[..scanline.Length]; - Span overlays = this.blenderBuffers.OverlaySpan[..scanline.Length]; + Span amounts = workspace.GetAmounts(scanline.Length); + Span overlays = workspace.GetOverlays(scanline.Length); for (int i = 0; i < scanline.Length; i++) { amounts[i] = scanline[i] * this.Options.BlendPercentage; - - int offsetX = x + i; - - // No doubt this one can be optimized further but I can't imagine its - // actually being used and can probably be removed/internalized for now - overlays[i] = this[offsetX, y]; + TPixel result = destinationRow[i]; + Vector4 background = result.ToVector4(); + float distance = Vector4.DistanceSquared(background, this.sourceColor); + overlays[i] = distance <= this.threshold + ? this.Blender.Blend(result, this.targetColorPixel, (this.threshold - distance) / this.threshold) + : result; } - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x, scanline.Length); this.Blender.Blend( this.Configuration, destinationRow, @@ -164,23 +138,5 @@ public override void Apply(Span scanline, int x, int y) overlays, amounts); } - - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - base.Dispose(disposing); - - if (disposing) - { - this.blenderBuffers.Dispose(); - } - - this.isDisposed = true; - } } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.Brushes.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs similarity index 88% rename from src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.Brushes.cs rename to src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs index f9e044838..c35c40856 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.Brushes.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs @@ -5,6 +5,7 @@ using System.Numerics; using SixLabors.Fonts; using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; @@ -20,7 +21,7 @@ internal sealed partial class RichTextGlyphRenderer /// A transform to apply to the brush coordinates. /// The resulting brush, or if the paint is unsupported. /// if a brush could be created; otherwise, . - public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix3x2 transform, [NotNullWhen(true)] out Brush? brush) + public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix4x4 transform, [NotNullWhen(true)] out Brush? brush) { brush = null; @@ -53,7 +54,7 @@ public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix3x2 tr /// The transform to apply to the gradient points. /// The resulting brush. /// if created; otherwise, . - private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix3x2 transform, out Brush? brush) + private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); @@ -68,12 +69,12 @@ private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matr // Apply any transform defined on the paint. if (!transform.IsIdentity) { - p0 = Vector2.Transform(p0, transform); - p1 = Vector2.Transform(p1, transform); + p0 = PointF.Transform(p0, transform); + p1 = PointF.Transform(p1, transform); if (p2.HasValue) { - p2 = Vector2.Transform(p2.Value, transform); + p2 = PointF.Transform(p2.Value, transform); } } @@ -94,7 +95,7 @@ private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matr /// The transform to apply to the gradient center point. /// The resulting brush. /// if created; otherwise, . - private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix3x2 transform, out Brush? brush) + private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); @@ -105,13 +106,18 @@ private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matr // Apply any transform defined on the paint. PointF center0 = paint.Center0; PointF center1 = paint.Center1; + float radius0 = paint.Radius0; + float radius1 = paint.Radius1; if (!transform.IsIdentity) { - center0 = Vector2.Transform(center0, transform); - center1 = Vector2.Transform(center1, transform); + center0 = PointF.Transform(center0, transform); + center1 = PointF.Transform(center1, transform); + float scale = MatrixUtilities.GetAverageScale(in transform); + radius0 *= scale; + radius1 *= scale; } - brush = new RadialGradientBrush(center0, paint.Radius0, center1, paint.Radius1, mode, stops); + brush = new RadialGradientBrush(center0, radius0, center1, radius1, mode, stops); return true; } @@ -122,7 +128,7 @@ private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matr /// The transform to apply to the gradient center point. /// The resulting brush. /// if created; otherwise, . - private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix3x2 transform, out Brush? brush) + private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity); @@ -134,7 +140,7 @@ private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix PointF center = paint.Center; if (!transform.IsIdentity) { - center = Vector2.Transform(center, transform); + center = PointF.Transform(center, transform); } brush = new SweepGradientBrush(center, paint.StartAngle, paint.EndAngle, mode, stops); diff --git a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs new file mode 100644 index 000000000..b917811b4 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs @@ -0,0 +1,961 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp.Drawing.Text; + +namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; + +/// +/// Allows the rendering of rich text configured via . +/// +internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposable +{ + // --- Render-pass ordering constants --- + // Within DrawTextOperations, operations are sorted first by RenderPass so that + // fills paint beneath outlines, and outlines beneath decorations. + private const byte RenderOrderFill = 0; + private const byte RenderOrderOutline = 1; + private const byte RenderOrderDecoration = 2; + + private readonly DrawingOptions drawingOptions; + + /// The default pen supplied by the caller (e.g. from DrawText(..., pen)). + private readonly Pen? defaultPen; + + /// The default brush supplied by the caller (e.g. from DrawText(..., brush)). + private readonly Brush? defaultBrush; + + /// + /// When the text is laid out along a path, this holds the path internals + /// for point-along-path queries. for normal (linear) text. + /// + private readonly IPathInternals? path; + private bool isDisposed; + + // --- Per-glyph mutable state reset in BeginGlyph --- + + /// The (or ) governing the current glyph. + private TextRun? currentTextRun; + + /// Brush resolved from the current , or . + private Brush? currentBrush; + + /// Pen resolved from the current , or . + private Pen? currentPen; + + /// The fill rule for the current color layer (COLR). + private FillRule currentFillRule; + + /// Alpha composition mode active for the current glyph/layer. + private PixelAlphaCompositionMode currentCompositionMode; + + /// Color blending mode active for the current glyph/layer. + private PixelColorBlendingMode currentBlendingMode; + + /// Whether the current glyph uses vertical layout (affects decoration orientation). + private bool currentDecorationIsVertical; + + /// Set to when is called, cleared in . + private bool hasLayer; + + // --- Glyph outline cache --- + // Glyphs that share the same CacheKey (same glyph id, sub-pixel position quantized + // to 1/AccuracyMultiple, pen reference, etc.) reuse the translated IPath from the + // first occurrence. This avoids re-building the full outline for repeated characters. + // + // AccuracyMultiple = 8 means sub-pixel positions are quantized to 1/8 px steps. + // Benchmarked to give <0.2% image difference vs. uncached, with >60% cache hit ratio. + private const float AccuracyMultiple = 8; + + /// Maps cache keys to their list of entries (one per layer). + private readonly Dictionary> glyphCache = []; + + /// Read cursor into the cached layer list for layered cache hits. + private int cacheReadIndex; + + /// + /// when the current glyph is a cache miss and its outline + /// must be fully rasterized; on a cache hit (reuse path). + /// + private bool rasterizationRequired; + + /// + /// to disable the glyph cache entirely (e.g. path-based text + /// where every glyph has a unique transform). + /// + private readonly bool noCache; + + /// The cache key computed for the current glyph in . + private CacheKey currentCacheKey; + + /// + /// The transformed (post-) bounding-box location + /// of the current glyph. Stored so can compute + /// for future cache-hit render location estimation. + /// + private PointF currentTransformedBoundsLocation; + + /// + /// Initializes a new instance of the class. + /// + /// Rich text options that may include a layout path and text runs. + /// Drawing options (transform, graphics options) for the text block. + /// Default pen for outlined text, or for fill-only. + /// Default brush for filled text, or for outline-only. + public RichTextGlyphRenderer( + RichTextOptions textOptions, + DrawingOptions drawingOptions, + Pen? pen, + Brush? brush) + : base(drawingOptions.Transform) + { + this.drawingOptions = drawingOptions; + this.defaultPen = pen; + this.defaultBrush = brush; + this.DrawingOperations = []; + this.currentCompositionMode = drawingOptions.GraphicsOptions.AlphaCompositionMode; + this.currentBlendingMode = drawingOptions.GraphicsOptions.ColorBlendingMode; + + IPath? path = textOptions.Path; + if (path is not null) + { + // Path-based text: each glyph gets a unique per-position transform, + // so cache hits are near-impossible — disable caching entirely. + this.rasterizationRequired = true; + this.noCache = true; + if (path is IPathInternals internals) + { + this.path = internals; + } + else + { + this.path = new ComplexPolygon(path); + } + } + } + + /// + /// Gets the list of instances accumulated during text rendering. + /// After RenderText completes, this list is consumed by + /// to build composition commands. + /// + public List DrawingOperations { get; } + + /// + protected override void BeginText(in FontRectangle bounds) => this.DrawingOperations.Clear(); + + /// + protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + // Resolves the active brush/pen from the text run, computes the cache key, + // and takes one of three paths: + // 1. Non-layered cache hit without decorations → emit cached ops, return false (fast path). + // 2. Layered or decorated cache hit → reuse cached path, return true for EndGlyph/SetDecoration. + // 3. Cache miss → rasterize from scratch. + this.cacheReadIndex = 0; + this.currentDecorationIsVertical = parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + this.currentTextRun = parameters.TextRun; + if (parameters.TextRun is RichTextRun drawingRun) + { + this.currentBrush = drawingRun.Brush; + this.currentPen = drawingRun.Pen; + } + else + { + this.currentBrush = null; + this.currentPen = null; + } + + if (!this.noCache) + { + // Transform the font-metric bounds by the drawing transform so that the + // sub-pixel position and size reflect the final screen coordinates. + // Quantize to 1/AccuracyMultiple px steps for cache key comparison. + RectangleF currentBounds = RectangleF.Transform( + new RectangleF(bounds.Location, new SizeF(bounds.Width, bounds.Height)), + this.drawingOptions.Transform); + + this.currentTransformedBoundsLocation = currentBounds.Location; + + PointF currentBoundsDelta = currentBounds.Location - ClampToPixel(currentBounds.Location); + PointF subPixelLocation = new( + MathF.Round(currentBoundsDelta.X * AccuracyMultiple) / AccuracyMultiple, + MathF.Round(currentBoundsDelta.Y * AccuracyMultiple) / AccuracyMultiple); + + SizeF subPixelSize = new( + MathF.Round(currentBounds.Width * AccuracyMultiple) / AccuracyMultiple, + MathF.Round(currentBounds.Height * AccuracyMultiple) / AccuracyMultiple); + + this.currentCacheKey = CacheKey.FromParameters( + parameters, + new RectangleF(subPixelLocation, subPixelSize), + this.currentPen ?? this.defaultPen); + + if (this.glyphCache.TryGetValue(this.currentCacheKey, out List? cachedEntries)) + { + if (cachedEntries.Count > 0 && !cachedEntries[0].IsLayered + && this.EnabledDecorations() == TextDecorations.None) + { + // Non-layered cache hit without decorations: emit operations directly + // and tell the font engine to skip the outline entirely + // (no MoveTo/LineTo/SetDecoration/EndGlyph). + this.EmitCachedGlyphOperations(cachedEntries[0], currentBounds.Location); + return false; + } + + // Layered or decorated cache hit: let the normal flow handle + // per-layer state and decoration callbacks. + this.rasterizationRequired = false; + return true; + } + } + + // Transform the glyph vectors using the original bounds + // The default transform will automatically be applied. + this.TransformGlyph(in bounds); + this.rasterizationRequired = true; + return true; + } + + /// + protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + // Capture the color-layer paint, fill rule, and composite mode. + // Setting hasLayer tells EndGlyph to skip its default single-layer path emission. + this.hasLayer = true; + this.currentFillRule = fillRule; + if (TryCreateBrush(paint, this.Builder.Transform, out Brush? brush)) + { + this.currentBrush = brush; + this.currentCompositionMode = TextUtilities.MapCompositionMode(paint.CompositeMode); + this.currentBlendingMode = TextUtilities.MapBlendingMode(paint.CompositeMode); + } + } + + /// + protected override void EndLayer() + { + // Finalizes a color layer. On a cache miss, translates the built path to local + // coordinates and stores it for future hits. On a cache hit, reads the stored + // path and adjusts the render location using sub-pixel delta compensation. + GlyphRenderData renderData = default; + IPath? fillPath = null; + + // Fix up the text runs colors. + // Only if both brush and pen is null do we fallback to the default value. + if (this.currentBrush == null && this.currentPen == null) + { + this.currentBrush = this.defaultBrush; + this.currentPen = this.defaultPen; + } + + // When rendering layers we only fill them. + // Any drawing of outlines is ignored as that doesn't really make sense. + bool renderFill = this.currentBrush != null; + + // Path has already been added to the collection via the base class. + IPath path = this.CurrentPaths[^1]; + Point renderLocation = ClampToPixel(path.Bounds.Location); + if (this.noCache || this.rasterizationRequired) + { + if (path.Bounds.Equals(RectangleF.Empty)) + { + return; + } + + if (renderFill) + { + renderData.FillPath = path.Translate(-renderLocation); + fillPath = renderData.FillPath; + } + + // Capture the delta between the location and the truncated render location. + // We can use this to offset the render location on the next instance of this glyph. + renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); + renderData.IsLayered = true; + + if (!this.noCache) + { + this.UpdateCache(renderData); + } + } + else + { + renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; + + // Offset the render location by the delta from the cached glyph and this one. + Vector2 previousDelta = renderData.LocationDelta; + Vector2 currentLocation = path.Bounds.Location; + Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); + + if (previousDelta.Y > currentDelta.Y) + { + // Move the location down to match the previous location offset. + currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); + } + else if (previousDelta.Y < currentDelta.Y) + { + // Move the location up to match the previous location offset. + currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); + } + else if (previousDelta.X > currentDelta.X) + { + // Move the location right to match the previous location offset. + currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); + } + else if (previousDelta.X < currentDelta.X) + { + // Move the location left to match the previous location offset. + currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); + } + + renderLocation = ClampToPixel(currentLocation); + + if (renderFill && renderData.FillPath is not null) + { + fillPath = renderData.FillPath; + } + } + + if (fillPath is not null) + { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = fillPath, + RenderLocation = renderLocation, + IntersectionRule = fillRule, + Brush = this.currentBrush, + RenderPass = RenderOrderFill, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + + this.currentFillRule = FillRule.NonZero; + this.currentCompositionMode = this.drawingOptions.GraphicsOptions.AlphaCompositionMode; + this.currentBlendingMode = this.drawingOptions.GraphicsOptions.ColorBlendingMode; + } + + /// + public override TextDecorations EnabledDecorations() + { + // Returns the union of decorations from TextRun.TextDecorations and any + // decoration pens set on the current RichTextRun. The font engine uses + // this result to decide which SetDecoration calls to emit. + TextRun? run = this.currentTextRun; + TextDecorations decorations = run?.TextDecorations ?? TextDecorations.None; + + if (this.currentTextRun is RichTextRun drawingRun) + { + if (drawingRun.UnderlinePen != null) + { + decorations |= TextDecorations.Underline; + } + + if (drawingRun.StrikeoutPen != null) + { + decorations |= TextDecorations.Strikeout; + } + + if (drawingRun.OverlinePen != null) + { + decorations |= TextDecorations.Overline; + } + } + + return decorations; + } + + /// + public override void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + // Emits a DrawingOperation for a text decoration. Resolves the decoration pen + // from the current RichTextRun, re-scales the base-class path when the pen's + // stroke width differs from the font-metric thickness, and anchors the scaling + // per decoration type (overline→bottom edge, underline→top edge, strikeout→center). + // Decorations are not cached. + if (thickness == 0) + { + return; + } + + Brush? brush = null; + Pen? pen = null; + if (this.currentTextRun is RichTextRun drawingRun) + { + brush = drawingRun.Brush; + + if (textDecorations == TextDecorations.Strikeout) + { + pen = drawingRun.StrikeoutPen ?? pen; + } + else if (textDecorations == TextDecorations.Underline) + { + pen = drawingRun.UnderlinePen ?? pen; + } + else if (textDecorations == TextDecorations.Overline) + { + pen = drawingRun.OverlinePen; + } + } + + // Always respect the pen stroke width if explicitly set. + float originalThickness = thickness; + if (pen is not null) + { + // Clamp the thickness to whole pixels. + thickness = MathF.Max(1F, (float)Math.Round(pen.StrokeWidth)); + } + else + { + // The thickness of the line has already been clamped in the base class. + pen = new SolidPen((brush ?? this.defaultBrush)!, thickness); + } + + // Path has already been added to the collection via the base class. + IPath path = this.CurrentPaths[^1]; + IPath outline = path; + + if (originalThickness != thickness) + { + // Respect edge anchoring per decoration type: + // - Overline: keep the base edge fixed (bottom in horizontal; left in vertical) + // - Underline: keep the top edge fixed (top in horizontal; right in vertical) + // - Strikeout: keep the center fixed (default behavior) + float ratio = thickness / originalThickness; + if (ratio != 1f) + { + Vector2 scale = this.currentDecorationIsVertical + ? new Vector2(ratio, 1f) + : new Vector2(1f, ratio); + + RectangleF b = path.Bounds; + Vector2 center = new(b.Left + (b.Width * 0.5f), b.Top + (b.Height * 0.5f)); + Vector2 anchor = center; + + if (textDecorations == TextDecorations.Overline) + { + anchor = this.currentDecorationIsVertical + ? new Vector2(b.Left, center.Y) // vertical: anchor left edge + : new Vector2(center.X, b.Bottom); // horizontal: anchor bottom edge + } + else if (textDecorations == TextDecorations.Underline) + { + anchor = this.currentDecorationIsVertical + ? new Vector2(b.Right, center.Y) // vertical: anchor right edge + : new Vector2(center.X, b.Top); // horizontal: anchor top edge + } + + // Scale about the chosen anchor so the fixed edge stays in place. + outline = outline.Transform(Matrix4x4.CreateScale(scale.X, scale.Y, 1, new Vector3(anchor, 0))); + } + } + + // Render the path here. Decorations are un-cached. + Point renderLocation = ClampToPixel(outline.Bounds.Location); + IPath decorationPath = outline.Translate(-renderLocation); + Brush decorationBrush = pen.StrokeFill; + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = decorationPath, + RenderLocation = renderLocation, + IntersectionRule = IntersectionRule.NonZero, + Brush = decorationBrush, + RenderPass = RenderOrderDecoration + }); + } + + /// + protected override void EndGlyph() + { + // If hasLayer is set, layers were already handled by EndLayer — skip. + // Otherwise, on a cache miss the built path is translated to local coordinates, + // stored for future hits, and emitted as fill and/or outline DrawingOperations. + // On a cache hit the stored path is reused with sub-pixel delta compensation. + if (this.hasLayer) + { + // The layer has already been rendered. + this.hasLayer = false; + return; + } + + GlyphRenderData renderData = default; + IPath? glyphPath = null; + + // Fix up the text runs colors. + // Only if both brush and pen is null do we fallback to the default value. + if (this.currentBrush == null && this.currentPen == null) + { + this.currentBrush = this.defaultBrush; + this.currentPen = this.defaultPen; + } + + bool renderFill = false; + bool renderOutline = false; + + // If we are using the fonts color layers we ignore the request to draw an outline only + // because that won't really work. Instead we force drawing using fill with the requested color. + if (this.currentBrush != null) + { + renderFill = true; + } + + if (this.currentPen != null) + { + renderOutline = true; + } + + // Path has already been added to the collection via the base class. + IPath path = this.CurrentPaths[^1]; + Point renderLocation = ClampToPixel(path.Bounds.Location); + if (this.noCache || this.rasterizationRequired) + { + if (path.Bounds.Equals(RectangleF.Empty)) + { + return; + } + + IPath localPath = path.Translate(-renderLocation); + if (renderFill || renderOutline) + { + renderData.FillPath = localPath; + glyphPath = renderData.FillPath; + } + + // Capture the delta between the location and the truncated render location. + // We can use this to offset the render location on the next instance of this glyph. + renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); + + // Store the offset between outline bounds and font metric bounds so that + // cache hits in BeginGlyph can accurately estimate the path location. + renderData.BoundsOffset = (Vector2)(path.Bounds.Location - this.currentTransformedBoundsLocation); + + if (!this.noCache) + { + this.UpdateCache(renderData); + } + } + else + { + renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; + + // Offset the render location by the delta from the cached glyph and this one. + Vector2 previousDelta = renderData.LocationDelta; + Vector2 currentLocation = path.Bounds.Location; + Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); + + if (previousDelta.Y > currentDelta.Y) + { + // Move the location down to match the previous location offset. + currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); + } + else if (previousDelta.Y < currentDelta.Y) + { + // Move the location up to match the previous location offset. + currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); + } + else if (previousDelta.X > currentDelta.X) + { + // Move the location right to match the previous location offset. + currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); + } + else if (previousDelta.X < currentDelta.X) + { + // Move the location left to match the previous location offset. + currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); + } + + renderLocation = ClampToPixel(currentLocation); + + if (renderFill && renderData.FillPath is not null) + { + glyphPath = renderData.FillPath; + } + + if (renderOutline && renderData.FillPath is not null) + { + glyphPath = renderData.FillPath; + } + } + + if (renderFill && glyphPath is not null) + { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = fillRule, + Brush = this.currentBrush, + RenderPass = RenderOrderFill, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + + if (renderOutline && glyphPath is not null) + { + IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Draw, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = outlineRule, + Pen = this.currentPen, + RenderPass = RenderOrderOutline, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + } + + /// + /// Emits fill and/or outline s from a cached + /// entry. Called from on a + /// non-layered, decoration-free cache hit when the font engine is told to skip + /// the outline entirely (returns ). + /// + /// The cached render data containing the translated path and location delta. + /// The transformed bounding-box origin for the current glyph instance. + private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF currentBoundsLocation) + { + // Estimate the outline bounds location using the stored offset between + // the outline bounds and the font metric bounds from the original glyph. + PointF estimatedPathLocation = new( + currentBoundsLocation.X + renderData.BoundsOffset.X, + currentBoundsLocation.Y + renderData.BoundsOffset.Y); + Point renderLocation = ComputeCacheHitRenderLocation(estimatedPathLocation, renderData.LocationDelta); + + // Fix up the text runs colors. + Brush? brush = this.currentBrush; + Pen? pen = this.currentPen; + if (brush == null && pen == null) + { + brush = this.defaultBrush; + pen = this.defaultPen; + } + + IPath? glyphPath = renderData.FillPath; + if (glyphPath is null) + { + return; + } + + if (brush != null) + { + IntersectionRule fillRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Fill, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = fillRule, + Brush = brush, + RenderPass = RenderOrderFill, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + + if (pen != null) + { + IntersectionRule outlineRule = TextUtilities.MapFillRule(this.currentFillRule); + this.DrawingOperations.Add(new DrawingOperation + { + Kind = DrawingOperationKind.Draw, + Path = glyphPath, + RenderLocation = renderLocation, + IntersectionRule = outlineRule, + Pen = pen, + RenderPass = RenderOrderOutline, + PixelAlphaCompositionMode = this.currentCompositionMode, + PixelColorBlendingMode = this.currentBlendingMode + }); + } + } + + /// + /// Computes the pixel-snapped render location for a cache-hit glyph by compensating + /// for the sub-pixel delta difference between the original cached glyph and the + /// current instance. This keeps glyphs visually aligned even when their sub-pixel + /// positions differ slightly. + /// + /// The estimated outline bounds origin for the current glyph. + /// The sub-pixel delta recorded when the path was first cached. + /// A pixel-snapped render location. + private static Point ComputeCacheHitRenderLocation(PointF pathLocation, Vector2 previousDelta) + { + Vector2 currentLocation = (Vector2)pathLocation; + Vector2 currentDelta = currentLocation - (Vector2)ClampToPixel(pathLocation); + + if (previousDelta.Y > currentDelta.Y) + { + currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); + } + else if (previousDelta.Y < currentDelta.Y) + { + currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); + } + else if (previousDelta.X > currentDelta.X) + { + currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); + } + else if (previousDelta.X < currentDelta.X) + { + currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); + } + + return ClampToPixel(currentLocation); + } + + /// + /// Stores a entry in the glyph cache under the + /// current key. Creates the cache list on first insertion for a given key. + /// + private void UpdateCache(GlyphRenderData renderData) + { + if (!this.glyphCache.TryGetValue(this.currentCacheKey, out List? _)) + { + this.glyphCache[this.currentCacheKey] = []; + } + + this.glyphCache[this.currentCacheKey].Add(renderData); + } + + /// + public void Dispose() => this.Dispose(true); + + /// + /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Point ClampToPixel(PointF point) => Point.Truncate(point); + + /// + /// Applies the path-based transform to the + /// for the current glyph, positioning it along the text path (if any) or + /// leaving the identity transform for linear text. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void TransformGlyph(in FontRectangle bounds) + => this.Builder.SetTransform(this.ComputeTransform(in bounds)); + + /// + /// Computes the combined translation + rotation matrix that places a glyph + /// along the text path. For linear text (no path), returns . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Matrix4x4 ComputeTransform(in FontRectangle bounds) + { + if (this.path is null) + { + return Matrix4x4.Identity; + } + + // Find the point of this intersection along the given path. + // We want to find the point on the path that is closest to the center-bottom side of the glyph. + Vector2 half = new(bounds.Width * .5F, 0); + SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); + + // Now offset to our target point since we're aligning the top-left location of our glyph against the path. + Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); + return Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); + } + + /// + /// Releases managed resources (glyph cache and drawing operations list). + /// + /// to release managed resources. + private void Dispose(bool disposing) + { + if (!this.isDisposed) + { + if (disposing) + { + this.glyphCache.Clear(); + this.DrawingOperations.Clear(); + } + + this.isDisposed = true; + } + } + + /// + /// Per-layer cached data for a rasterized glyph. Stores the locally-translated + /// path and the sub-pixel deltas needed to reposition the path at a different + /// screen location on a cache hit. + /// + private struct GlyphRenderData + { + /// + /// The fractional-pixel offset between the path's bounding-box origin + /// and the truncated (pixel-snapped) render location. Used to compensate + /// for sub-pixel position differences between cache hits. + /// + public Vector2 LocationDelta; + + /// + /// The offset between the outline path's bounding-box origin and the + /// font-metric bounds origin. Stored on first rasterization so that + /// can estimate the path location + /// from only the font-metric bounds (which are available without outline data). + /// + public Vector2 BoundsOffset; + + /// + /// The glyph outline path translated to local coordinates (origin at 0,0). + /// Shared across all cache hits for the same . + /// + public IPath? FillPath; + + /// + /// if this entry belongs to a multi-layer (COLR) glyph. + /// Non-layered cache hits with no decorations can skip the outline entirely + /// (return from ); layered hits + /// still need the per-layer BeginLayer/EndLayer callbacks. + /// + public bool IsLayered; + } + + /// + /// Identifies a unique glyph variant for caching purposes. Two glyphs with the same + /// share identical outline geometry and can reuse the same + /// . The key includes the glyph id, font metrics, + /// sub-pixel position (quantized to ), and the pen reference + /// (since stroke width affects the outline path). + /// + private readonly struct CacheKey : IEquatable + { + /// Gets the font family name. + public string Font { get; init; } + + /// Gets the glyph color variant (normal, COLR, etc.). + public GlyphColor GlyphColor { get; init; } + + /// Gets the glyph type (simple, composite, etc.). + public GlyphType GlyphType { get; init; } + + /// Gets the font style (regular, bold, italic, etc.). + public FontStyle FontStyle { get; init; } + + /// Gets the glyph index within the font. + public ushort GlyphId { get; init; } + + /// Gets the composite glyph parent index (0 for non-composite). + public ushort CompositeGlyphId { get; init; } + + /// Gets the Unicode code point this glyph represents. + public CodePoint CodePoint { get; init; } + + /// Gets the em-size at which the glyph is rendered. + public float PointSize { get; init; } + + /// Gets the DPI used for rendering. + public float Dpi { get; init; } + + /// Gets the layout mode (horizontal, vertical, vertical-rotated). + public GlyphLayoutMode LayoutMode { get; init; } + + /// Gets any text attributes (e.g. superscript/subscript) that affect rendering. + public TextAttributes TextAttributes { get; init; } + + /// Gets text decorations that may influence outline geometry. + public TextDecorations TextDecorations { get; init; } + + /// Gets the quantized sub-pixel bounds used for position-sensitive cache lookup. + public RectangleF Bounds { get; init; } + + /// + /// Gets the pen reference used for outlined text. Compared by reference equality + /// so that different pen instances (even with the same stroke width) produce + /// separate cache entries — this is correct because pen identity affects stroke + /// pattern and dash style. + /// + public Pen? PenReference { get; init; } + + public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right); + + public static bool operator !=(CacheKey left, CacheKey right) => !(left == right); + + /// + /// Creates a from glyph renderer parameters and quantized bounds. + /// The grapheme index is intentionally excluded because it varies per glyph instance + /// while the outline geometry remains the same for matching glyph+position. + /// + /// The glyph renderer parameters from the font engine. + /// Quantized sub-pixel bounds for position-sensitive lookup. + /// The pen reference for outlined text, or . + /// A new cache key. + public static CacheKey FromParameters( + in GlyphRendererParameters parameters, + RectangleF bounds, + Pen? penReference) + => new() + { + // Do not include the grapheme index as that will + // always vary per glyph instance. + Font = parameters.Font, + GlyphType = parameters.GlyphType, + FontStyle = parameters.FontStyle, + GlyphId = parameters.GlyphId, + CompositeGlyphId = parameters.CompositeGlyphId, + CodePoint = parameters.CodePoint, + PointSize = parameters.PointSize, + Dpi = parameters.Dpi, + LayoutMode = parameters.LayoutMode, + TextAttributes = parameters.TextRun.TextAttributes, + TextDecorations = parameters.TextRun.TextDecorations, + Bounds = bounds, + PenReference = penReference + }; + + public override bool Equals(object? obj) + => obj is CacheKey key && this.Equals(key); + + public bool Equals(CacheKey other) + => this.Font == other.Font && + this.GlyphColor.Equals(other.GlyphColor) && + this.GlyphType == other.GlyphType && + this.FontStyle == other.FontStyle && + this.GlyphId == other.GlyphId && + this.CompositeGlyphId == other.CompositeGlyphId && + this.CodePoint.Equals(other.CodePoint) && + this.PointSize == other.PointSize && + this.Dpi == other.Dpi && + this.LayoutMode == other.LayoutMode && + this.TextAttributes == other.TextAttributes && + this.TextDecorations == other.TextDecorations && + this.Bounds.Equals(other.Bounds) && + ReferenceEquals(this.PenReference, other.PenReference); + + public override int GetHashCode() + { + HashCode hash = default; + hash.Add(this.Font); + hash.Add(this.GlyphColor); + hash.Add(this.GlyphType); + hash.Add(this.FontStyle); + hash.Add(this.GlyphId); + hash.Add(this.CompositeGlyphId); + hash.Add(this.CodePoint); + hash.Add(this.PointSize); + hash.Add(this.Dpi); + hash.Add(this.LayoutMode); + hash.Add(this.TextAttributes); + hash.Add(this.TextDecorations); + hash.Add(this.Bounds); + hash.Add(this.PenReference is null ? 0 : RuntimeHelpers.GetHashCode(this.PenReference)); + return hash.ToHashCode(); + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/RichTextOptions.cs b/src/ImageSharp.Drawing/Processing/RichTextOptions.cs index 268592a69..383a06772 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextOptions.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextOptions.cs @@ -25,7 +25,25 @@ public RichTextOptions(Font font) /// The options whose properties are copied into this instance. public RichTextOptions(RichTextOptions options) : base(options) - => this.Path = options.Path; + { + this.Path = options.Path; + List runs = new(options.TextRuns.Count); + foreach (RichTextRun run in options.TextRuns) + { + runs.Add(new RichTextRun() + { + Brush = run.Brush, + Pen = run.Pen, + StrikeoutPen = run.StrikeoutPen, + UnderlinePen = run.UnderlinePen, + OverlinePen = run.OverlinePen, + Start = run.Start, + End = run.End + }); + } + + this.TextRuns = runs; + } /// /// Gets or sets an optional collection of text runs to apply to the body of text. diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs index bba986c04..79e0b6dc2 100644 --- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs @@ -4,7 +4,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Options for influencing the drawing functions. +/// Provides options for controlling how vector shapes are interpreted during rasterization, +/// including the fill-rule intersection mode and boolean clipping operations. /// public class ShapeOptions : IDeepCloneable { @@ -22,14 +23,18 @@ private ShapeOptions(ShapeOptions source) } /// - /// Gets or sets the clipping operation. + /// Gets or sets the boolean clipping operation used when a clipping path is applied. + /// Determines how the clip shape interacts with the target region + /// (e.g. subtracts the clip shape). /// /// Defaults to . /// public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference; /// - /// Gets or sets the rule for calculating intersection points. + /// Gets or sets the fill rule that determines how overlapping or nested contours affect coverage. + /// fills any region with a non-zero winding number; + /// alternates fill/hole for each contour crossing. /// /// Defaults to . /// diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index 41c3e0717..27781e14f 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -1,9 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Buffers; -using SixLabors.ImageSharp.Drawing.Utilities; - namespace SixLabors.ImageSharp.Drawing.Processing; /// @@ -23,11 +20,12 @@ public sealed class SolidBrush : Brush public Color Color { get; } /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, - RectangleF region) => new SolidBrushApplicator(configuration, options, source, this.Color.ToPixel()); + int canvasWidth, + RectangleF region) + => new SolidBrushRenderer(configuration, options, canvasWidth, this.Color.ToPixel()); /// public override bool Equals(Brush? other) @@ -47,90 +45,60 @@ public override bool Equals(Brush? other) /// The solid brush applicator. /// /// The pixel format. - private sealed class SolidBrushApplicator : BrushApplicator + private sealed class SolidBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel { - private readonly IMemoryOwner colors; - private readonly ThreadLocalBlenderBuffers blenderBuffers; - private bool isDisposed; + private readonly TPixel color; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. + /// The canvas width for the current render pass. /// The color. - public SolidBrushApplicator( + public SolidBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, TPixel color) - : base(configuration, options, source) - { - this.colors = configuration.MemoryAllocator.Allocate(source.Width); - this.colors.Memory.Span.Fill(color); - - // The threadlocal value is lazily invoked so there is no need to optionally create the type. - this.blenderBuffers = new ThreadLocalBlenderBuffers(configuration.MemoryAllocator, source.Width, true); - } + : base(configuration, options, canvasWidth) + => this.color = color; /// - public override void Apply(Span scanline, int x, int y) + public override void Apply( + Span destinationRow, + ReadOnlySpan scanline, + int x, + int y, + BrushWorkspace workspace) { - Span destinationRow = this.Target.PixelBuffer.DangerousGetRowSpan(y).Slice(x); - // Constrain the spans to each other if (destinationRow.Length > scanline.Length) { - destinationRow = destinationRow.Slice(0, scanline.Length); + destinationRow = destinationRow[..scanline.Length]; } else { - scanline = scanline.Slice(0, destinationRow.Length); + scanline = scanline[..destinationRow.Length]; } Configuration configuration = this.Configuration; if (this.Options.BlendPercentage == 1F) { - // TODO: refactor the BlendPercentage == 1 logic to a separate, simpler BrushApplicator class. - this.Blender.Blend(configuration, destinationRow, destinationRow, this.colors.Memory.Span, scanline); + this.Blender.Blend(configuration, destinationRow, destinationRow, this.color, scanline); } else { - Span amounts = this.blenderBuffers.AmountSpan.Slice(0, scanline.Length); + Span amounts = workspace.GetAmounts(scanline.Length); for (int i = 0; i < scanline.Length; i++) { amounts[i] = scanline[i] * this.Options.BlendPercentage; } - this.Blender.Blend( - configuration, - destinationRow, - destinationRow, - this.colors.Memory.Span, - amounts); - } - } - - /// - protected override void Dispose(bool disposing) - { - if (this.isDisposed) - { - return; - } - - if (disposing) - { - this.colors.Dispose(); - this.blenderBuffers.Dispose(); + this.Blender.Blend(configuration, destinationRow, destinationRow, this.color, amounts); } - - base.Dispose(disposing); - - this.isDisposed = true; } } } diff --git a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs index 51886f915..44e73969b 100644 --- a/src/ImageSharp.Drawing/Processing/StrokeOptions.cs +++ b/src/ImageSharp.Drawing/Processing/StrokeOptions.cs @@ -8,16 +8,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class StrokeOptions : IEquatable { - /// - /// Gets or sets a value indicating whether stroked contours should be normalized by - /// resolving self-intersections and overlaps before returning. - /// - /// - /// Defaults to for maximum throughput. - /// When disabled, callers should rasterize with a non-zero winding fill rule. - /// - public bool NormalizeOutput { get; set; } - /// /// Gets or sets the miter limit used to clamp outer miter joins. /// @@ -50,28 +40,38 @@ public sealed class StrokeOptions : IEquatable /// public InnerJoin InnerJoin { get; set; } = InnerJoin.Miter; + /// + /// Gets or sets a value indicating whether stroked contours should be normalized + /// by resolving self-intersections and overlaps before returning. + /// + /// + /// Defaults to false for maximum throughput. When disabled, callers should rasterize + /// with a non-zero winding fill rule. + /// + public bool NormalizeOutput { get; set; } + /// 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; + this.InnerJoin == other.InnerJoin && + this.NormalizeOutput == other.NormalizeOutput; /// public override int GetHashCode() => HashCode.Combine( - this.NormalizeOutput, this.MiterLimit, this.InnerMiterLimit, this.ArcDetailScale, this.LineJoin, this.LineCap, - this.InnerJoin); + this.InnerJoin, + this.NormalizeOutput); } diff --git a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs index 5aed66787..2988cfdd1 100644 --- a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs @@ -1,11 +1,16 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; + namespace SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Memory; + /// /// Provides an implementation of a brush for painting sweep (conic) gradients within areas. -/// Angles increase clockwise (y-down coordinate system) with 0° pointing to the +X direction. +/// Angles increase counter-clockwise from +X on the design grid. /// public sealed class SweepGradientBrush : GradientBrush { @@ -19,8 +24,15 @@ public sealed class SweepGradientBrush : GradientBrush /// Initializes a new instance of the class. /// /// The center point of the sweep gradient in device space. - /// The starting angle, in degrees (clockwise, 0° is +X). - /// The ending angle, in degrees (clockwise, 0° is +X). If equal to , the gradient is treated as a full 360° sweep. + /// + /// The starting angle, in degrees, measured counter-clockwise from +X on the design grid. + /// This value is stored as provided so the sign and magnitude of the sweep remain intact. + /// + /// + /// The ending angle, in degrees, measured counter-clockwise from +X on the design grid. + /// If equal to , the gradient is treated as a full 360 degree sweep. + /// Otherwise, the signed difference between start and end determines the sweep direction. + /// /// Defines how the gradient colors are repeated beyond the interval [0..1]. /// The gradient color stops. Ratios must be in [0..1] and are interpreted along the angular sweep. public SweepGradientBrush( @@ -36,9 +48,74 @@ public SweepGradientBrush( this.endAngleDegrees = endAngleDegrees; } + /// + /// Gets the center point of the sweep gradient. + /// + public PointF Center => this.center; + + /// + /// Gets the starting angle in degrees. + /// + public float StartAngleDegrees => this.startAngleDegrees; + + /// + /// Gets the ending angle in degrees. + /// + public float EndAngleDegrees => this.endAngleDegrees; + + /// + public override Brush Transform(Matrix4x4 matrix) + { + PointF tc = PointF.Transform(this.center, matrix); + + // Treat the brush as two rays starting at the center: + // one ray for the start angle and one ray for the end angle. + // The important value is the signed angular distance between those rays. + // We keep that sign so a reflected transform can turn a counter-clockwise + // sweep into a clockwise sweep instead of silently "fixing" it. + float sweepDegrees = GetEffectiveSweepDegrees(this.startAngleDegrees, this.endAngleDegrees); + float startRad = GeometryUtilities.DegreeToRadian(this.startAngleDegrees); + float endRad = GeometryUtilities.DegreeToRadian(this.startAngleDegrees + sweepDegrees); + + // The public API uses the design-grid convention, which is y-up. + // Screen pixels are y-down, so a positive mathematical rotation uses + // `center.Y - sin(theta)` rather than `center.Y + sin(theta)`. + PointF startDir = PointF.Transform(new PointF(this.center.X + MathF.Cos(startRad), this.center.Y - MathF.Sin(startRad)), matrix); + PointF endDir = PointF.Transform(new PointF(this.center.X + MathF.Cos(endRad), this.center.Y - MathF.Sin(endRad)), matrix); + + // Convert the transformed rays back into brush angles in the same public convention: + // counter-clockwise from +X on the design grid. + float newStart = NormalizeDirectionDegrees(MathF.Atan2(-(startDir.Y - tc.Y), startDir.X - tc.X) * (180f / MathF.PI)); + float newEnd = NormalizeDirectionDegrees(MathF.Atan2(-(endDir.Y - tc.Y), endDir.X - tc.X) * (180f / MathF.PI)); + + // A negative determinant means the transform flips orientation. + // That flips the direction of the sweep, so we use it to decide whether + // the end angle should unwrap forwards or backwards from the new start. + float determinant = (matrix.M11 * matrix.M22) - (matrix.M12 * matrix.M21); + float directionHint = MathF.Sign(sweepDegrees); + if (directionHint == 0F) + { + directionHint = 1F; + } + + if (determinant < 0F) + { + directionHint = -directionHint; + } + + return new SweepGradientBrush( + tc, + newStart, + UnwrapSweepEndDegrees(newStart, newEnd, directionHint, MathF.Abs(sweepDegrees)), + this.RepetitionMode, + this.ColorStopsArray); + } + /// public override bool Equals(Brush? other) { + // Sweep brushes are equal only when they describe the same center, + // the same signed angular interval, and the same inherited stop data. if (other is SweepGradientBrush brush) { return base.Equals(other) @@ -58,27 +135,114 @@ public override int GetHashCode() this.startAngleDegrees, this.endAngleDegrees); + /// + /// Converts the stored start/end angles into the signed sweep interval that the brush should render. + /// + /// The starting angle in degrees. + /// The ending angle in degrees. + /// + /// The signed angular interval in degrees. Equal endpoints are treated as a full turn. + /// + // Sweep gradients interpret equal endpoints as "full turn". + // All other cases keep the caller-provided signed angular span. + private static float GetEffectiveSweepDegrees(float startAngleDegrees, float endAngleDegrees) + { + float sweepDegrees = endAngleDegrees - startAngleDegrees; + if (MathF.Abs(sweepDegrees) < 1e-6F) + { + // Equal endpoints mean "full circle", not an empty span. + return 360F; + } + + return sweepDegrees; + } + + /// + /// Normalizes an angle to the canonical [0, 360) direction range. + /// + /// The angle to normalize. + /// The equivalent direction in the canonical degree range. + // Convert any equivalent direction into the canonical [0, 360) representation + // so transformed brushes remain stable when compared or reused. + private static float NormalizeDirectionDegrees(float degrees) + { + float normalized = degrees % 360F; + if (normalized < 0F) + { + normalized += 360F; + } + + return normalized; + } + + /// + /// Reconstructs the signed end angle after independently transforming the start and end rays. + /// + /// The transformed starting angle in normalized degrees. + /// The transformed ending angle in normalized degrees. + /// + /// The expected sweep direction. Positive means unwrap forwards, negative means unwrap backwards. + /// + /// The minimum magnitude the restored interval must preserve. + /// The unwrapped ending angle measured relative to . + // After transforming the start and end rays separately, both directions land in [0, 360). + // This method restores the intended signed sweep by unwrapping the end angle relative to + // the start angle, using the desired direction as the constraint. + private static float UnwrapSweepEndDegrees(float startDegrees, float endDegrees, float directionHint, float minimumMagnitude) + { + float delta = endDegrees - startDegrees; + if (directionHint >= 0F) + { + // Keep the end angle ahead of the start angle for a positive sweep. + while (delta < 0F) + { + delta += 360F; + } + + if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F) + { + delta = 360F; + } + } + else + { + // Keep the end angle behind the start angle for a negative sweep. + while (delta > 0F) + { + delta -= 360F; + } + + if (MathF.Abs(delta) < 1e-6F && minimumMagnitude >= 360F - 1e-6F) + { + delta = -360F; + } + } + + return startDegrees + delta; + } + /// - public override BrushApplicator CreateApplicator( + public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, + int canvasWidth, RectangleF region) - => new SweepGradientBrushApplicator( + { + // The renderer precomputes the angular interval once and then samples it per pixel. + return new SweepGradientBrushRenderer( configuration, options, - source, - this.center, - this.startAngleDegrees, - this.endAngleDegrees, - this.ColorStops, + canvasWidth, + this, + this.ColorStopsArray, this.RepetitionMode); + } /// /// The sweep (conic) gradient brush applicator. /// /// The pixel format. - private sealed class SweepGradientBrushApplicator : GradientBrushApplicator + private sealed class SweepGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel { private const float Tau = MathF.Tau; @@ -89,121 +253,61 @@ private sealed class SweepGradientBrushApplicator : GradientBrushApplica private readonly float startRad; - private readonly float invSweep; - - private readonly bool isFullCircle; + private readonly float endRad; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. - /// The source image. - /// The center of the sweep gradient. - /// The start angle in degrees (clockwise). - /// The end angle in degrees (clockwise). + /// The canvas width for the current render pass. + /// The sweep gradient brush. /// The gradient color stops (ratios in [0..1]). /// Defines how gradient colors are repeated outside [0..1]. - public SweepGradientBrushApplicator( + public SweepGradientBrushRenderer( Configuration configuration, GraphicsOptions options, - ImageFrame source, - PointF center, - float startAngleDegrees, - float endAngleDegrees, + int canvasWidth, + SweepGradientBrush brush, ColorStop[] colorStops, GradientRepetitionMode repetitionMode) - : base(configuration, options, source, colorStops, repetitionMode) + : base(configuration, options, canvasWidth, colorStops, repetitionMode) { - this.cx = center.X; - this.cy = center.Y; - - float start = GeometryUtilities.DegreeToRadian(NormalizeDegrees(startAngleDegrees)); - float end = GeometryUtilities.DegreeToRadian(NormalizeDegrees(endAngleDegrees)); - - float sweep = NormalizeDeltaRadians(end - start); - - // If sweep collapses numerically to ~0, treat as full circle. - if (MathF.Abs(sweep) < 1e-6f) - { - sweep = Tau; - } + this.cx = brush.Center.X; + this.cy = brush.Center.Y; - this.startRad = start; - this.invSweep = 1f / sweep; - this.isFullCircle = MathF.Abs(sweep - Tau) < 1e-6f; + // Store the interval as radians once so sampling only needs one subtraction and one divide. + float sweepDegrees = GetEffectiveSweepDegrees(brush.StartAngleDegrees, brush.EndAngleDegrees); + this.startRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees); + this.endRad = GeometryUtilities.DegreeToRadian(brush.StartAngleDegrees + sweepDegrees); } - /// - /// Calculates the position parameter along the sweep gradient for the given device-space point. - /// The returned value is not clamped to [0..1]; repetition semantics are applied by the base class. - /// - /// The x-coordinate of the point (device space). - /// The y-coordinate of the point (device space). - /// The unbounded position on the gradient. + /// protected override float PositionOnGradient(float x, float y) { - // Vector from center to sample point. Y is inverted to maintain clockwise angles in y-down space. + // Move the sample into center-relative coordinates. float dx = x - this.cx; float dy = y - this.cy; if (dx == 0f && dy == 0f) { - // Arbitrary but stable choice for the center. + // The center has no unique angle, so pick a stable value on the gradient. return 0f; } - float angle = MathF.Atan2(-dy, dx); // (-π, π] + // Convert from y-down image space back into the brush's y-up angle convention, + // then normalize to [0, 2π) so subtraction against the stored start angle is stable. + float angle = MathF.Atan2(-dy, dx); if (angle < 0f) { - angle += Tau; // [0, 2π) - } - - // Rotate basis by 180° so that 0.75 (270°) maps to "up"/top. - // This shifts the canonical directions: right->left, up->down, etc. - angle += MathF.PI; - if (angle >= Tau) - { - angle -= Tau; - } - - // Phase measured clockwise from start. - float phase = angle - this.startRad; - if (phase < 0f) - { - phase += Tau; - } - - if (this.isFullCircle) - { - // Map full circle to [0..1). - return phase / Tau; - } - - // Partial sweep: phase beyond sweep -> t > 1 (lets repetition mode handle clipping). - return phase * this.invSweep; - } - - private static float NormalizeDegrees(float deg) - { - float d = deg % 360f; - if (d < 0f) - { - d += 360f; - } - - return d; - } - - private static float NormalizeDeltaRadians(float delta) - { - float d = delta % Tau; - if (d <= 0f) - { - d += Tau; + angle += Tau; } - return d; + // Divide by the signed angular span. + // A positive denominator produces a counter-clockwise sweep and a negative + // denominator produces a clockwise sweep. The base gradient code then applies + // the repetition mode to this unbounded parameter. + return (angle - this.startRad) / (this.endRad - this.startRad); } } } diff --git a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs b/src/ImageSharp.Drawing/RectangularPolygon.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs rename to src/ImageSharp.Drawing/RectangularPolygon.cs index aec7e31e5..f23514602 100644 --- a/src/ImageSharp.Drawing/Shapes/RectangularPolygon.cs +++ b/src/ImageSharp.Drawing/RectangularPolygon.cs @@ -156,7 +156,7 @@ public static explicit operator RectangularPolygon(Polygon polygon) => new(polygon.Bounds.X, polygon.Bounds.Y, polygon.Bounds.Width, polygon.Bounds.Height); /// - public IPath Transform(Matrix3x2 matrix) + public IPath Transform(Matrix4x4 matrix) { if (matrix.IsIdentity) { diff --git a/src/ImageSharp.Drawing/Shapes/RegularPolygon.cs b/src/ImageSharp.Drawing/RegularPolygon.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/RegularPolygon.cs rename to src/ImageSharp.Drawing/RegularPolygon.cs index b914d5389..44d92d3db 100644 --- a/src/ImageSharp.Drawing/Shapes/RegularPolygon.cs +++ b/src/ImageSharp.Drawing/RegularPolygon.cs @@ -70,7 +70,7 @@ private static LinearLineSegment CreateSegment(PointF location, float radius, in PointF[] points = new PointF[vertices]; for (int i = 0; i < vertices; i++) { - PointF rotated = PointF.Transform(distanceVector, Matrix3x2.CreateRotation(current)); + PointF rotated = PointF.Transform(distanceVector, Matrix4x4.CreateRotationZ(current)); points[i] = rotated + location; diff --git a/src/ImageSharp.Drawing/Shapes/SegmentInfo.cs b/src/ImageSharp.Drawing/SegmentInfo.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/SegmentInfo.cs rename to src/ImageSharp.Drawing/SegmentInfo.cs diff --git a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs b/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs deleted file mode 100644 index b9b3ccde9..000000000 --- a/src/ImageSharp.Drawing/Shapes/ClipPathExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Provides extension methods to that allow the clipping of shapes. -/// -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 . - public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths) - => subjectPath.Clip(DefaultOptions, clipPaths); - - /// - /// Clips the specified subject path with the provided clipping paths. - /// - /// The subject path. - /// The shape options. - /// The clipping paths. - /// The clipped . - public static IPath Clip( - this IPath subjectPath, - ShapeOptions options, - params IPath[] clipPaths) - => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); - - /// - /// Clips the specified subject path with the provided clipping paths. - /// - /// The subject path. - /// The clipping paths. - /// The clipped . - public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) - => subjectPath.Clip(DefaultOptions, clipPaths); - - /// - /// Clips the specified subject path with the provided clipping paths. - /// - /// The subject path. - /// The shape options. - /// The clipping paths. - /// The clipped . - public static IPath Clip( - this IPath subjectPath, - ShapeOptions options, - IEnumerable clipPaths) - => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); -} diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs deleted file mode 100644 index c8e7cc26e..000000000 --- a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayBuilder{T}.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Helpers; - -/// -/// A helper type for avoiding allocations while building arrays. -/// -/// The type of item contained in the array. -internal struct ArrayBuilder - where T : struct -{ - private const int DefaultCapacity = 4; - - // Starts out null, initialized on first Add. - private T[]? data; - private int size; - - /// - /// Initializes a new instance of the struct. - /// - /// The initial capacity of the array. - public ArrayBuilder(int capacity) - : this() - { - if (capacity > 0) - { - this.data = new T[capacity]; - } - } - - /// - /// Gets or sets the number of items in the array. - /// - public int Length - { - readonly get => this.size; - - set - { - if (value > 0) - { - this.EnsureCapacity(value); - this.size = value; - } - else - { - this.size = 0; - } - } - } - - /// - /// Returns a reference to specified element of the array. - /// - /// The index of the element to return. - /// The . - /// - /// Thrown when index less than 0 or index greater than or equal to . - /// - public readonly ref T this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - DebugGuard.MustBeBetweenOrEqualTo(index, 0, this.size, nameof(index)); - return ref this.data![index]; - } - } - - /// - /// Adds the given item to the array. - /// - /// The item to add. - public void Add(T item) - { - int position = this.size; - T[]? array = this.data; - - if (array != null && (uint)position < (uint)array.Length) - { - this.size = position + 1; - array[position] = item; - } - else - { - this.AddWithResize(item); - } - } - - // Non-inline from Add to improve its code quality as uncommon path - [MethodImpl(MethodImplOptions.NoInlining)] - private void AddWithResize(T item) - { - int size = this.size; - this.Grow(size + 1); - this.size = size + 1; - this.data[size] = item; - } - - /// - /// Remove the last item from the array. - /// - public void RemoveLast() - { - DebugGuard.MustBeGreaterThan(this.size, 0, nameof(this.size)); - this.size--; - } - - /// - /// Clears the array. - /// Allocated memory is left intact for future usage. - /// - public void Clear() => - - // No need to actually clear since we're not allowing reference types. - this.size = 0; - - private void EnsureCapacity(int min) - { - int length = this.data?.Length ?? 0; - if (length < min) - { - this.Grow(min); - } - } - - [MemberNotNull(nameof(this.data))] - private void Grow(int capacity) - { - // Same expansion algorithm as List. - int length = this.data?.Length ?? 0; - int newCapacity = length == 0 ? DefaultCapacity : length * 2; - if ((uint)newCapacity > Array.MaxLength) - { - newCapacity = Array.MaxLength; - } - - if (newCapacity < capacity) - { - newCapacity = capacity; - } - - T[] array = new T[newCapacity]; - - if (this.size > 0) - { - Array.Copy(this.data!, array, this.size); - } - - this.data = array; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs b/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs deleted file mode 100644 index 1e2e9c103..000000000 --- a/src/ImageSharp.Drawing/Shapes/Helpers/ArrayExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Extensions on arrays. -/// -internal static class ArrayExtensions -{ - /// - /// Merges the specified source2. - /// - /// the type of the array - /// The source1. - /// The source2. - /// the Merged arrays - public static T[] Merge(this T[] source1, T[] source2) - { - if (source2 is null || source2.Length == 0) - { - return source1; - } - - T[] target = new T[source1.Length + source2.Length]; - - for (int i = 0; i < source1.Length; i++) - { - target[i] = source1[i]; - } - - for (int i = 0; i < source2.Length; i++) - { - target[i + source1.Length] = source2[i]; - } - - return target; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs b/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs deleted file mode 100644 index 6f5c2f401..000000000 --- a/src/ImageSharp.Drawing/Shapes/Helpers/TopologyUtilities.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.Helpers; - -/// -/// Implements some basic algorithms on raw data structures. -/// Polygons are represented with a span of points, -/// where first point should be repeated at the end. -/// -/// -/// Positive orientation means Clockwise in world coordinates (positive direction goes UP on paper). -/// Since the Drawing library deals mostly with Screen coordinates where this is opposite, -/// we use different terminology here to avoid confusion. -/// -internal static class TopologyUtilities -{ - /// - /// Positive: CCW in world coords (CW on screen) - /// Negative: CW in world coords (CCW on screen) - /// - public static void EnsureOrientation(Span polygon, int expectedOrientation) - { - if (GetPolygonOrientation(polygon) * expectedOrientation < 0) - { - polygon.Reverse(); - } - } - - /// - /// Zero: area is 0 - /// Positive: CCW in world coords (CW on screen) - /// Negative: CW in world coords (CCW on screen) - /// - private static int GetPolygonOrientation(ReadOnlySpan polygon) - { - float sum = 0f; - for (int i = 0; i < polygon.Length - 1; ++i) - { - PointF curr = polygon[i]; - PointF next = polygon[i + 1]; - sum += (curr.X * next.Y) - (next.X * curr.Y); - } - - // Normally, this should be a tolerant comparison, we don't have a special path for zero-area - // (or for self-intersecting, semi-zero-area) polygons in edge scanning. - return Math.Sign(sum); - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs b/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs deleted file mode 100644 index 849c93e79..000000000 --- a/src/ImageSharp.Drawing/Shapes/Helpers/VectorExtensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Extensions on arrays. -/// -internal static class VectorExtensions -{ - /// - /// Merges the specified source2. - /// - /// The source1. - /// The source2. - /// The threshold. - /// - /// the Merged arrays - /// - public static bool Equivalent(this PointF source1, PointF source2, float threshold) - { - Vector2 abs = Vector2.Abs(source1 - source2); - - return abs.X < threshold && abs.Y < threshold; - } - - /// - /// Merges the specified source2. - /// - /// The source1. - /// The source2. - /// The threshold. - /// - /// the Merged arrays - /// - public static bool Equivalent(this Vector2 source1, Vector2 source2, float threshold) - { - Vector2 abs = Vector2.Abs(source1 - source2); - - return abs.X < threshold && abs.Y < threshold; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/InternalPath.cs b/src/ImageSharp.Drawing/Shapes/InternalPath.cs deleted file mode 100644 index cc6a53ea0..000000000 --- a/src/ImageSharp.Drawing/Shapes/InternalPath.cs +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Internal logic for integrating linear paths. -/// -internal class InternalPath -{ - /// - /// The epsilon for float comparison - /// - private const float Epsilon = 0.003f; - private const float Epsilon2 = 0.2f; - - /// - /// The maximum vector - /// - private static readonly Vector2 MaxVector = new(float.MaxValue); - - /// - /// The points. - /// - private readonly PointData[] points; - - /// - /// The closed path. - /// - private readonly bool closedPath; - - /// - /// Initializes a new instance of the class. - /// - /// The segments. - /// if set to true [is closed path]. - /// Whether to remove close and collinear vertices - internal InternalPath(IReadOnlyList segments, bool isClosedPath, bool removeCloseAndCollinear = true) - : this(Simplify(segments, isClosedPath, removeCloseAndCollinear), isClosedPath) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The segment. - /// if set to true [is closed path]. - internal InternalPath(ILineSegment segment, bool isClosedPath) - : this(segment?.Flatten() ?? Array.Empty(), isClosedPath) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The points. - /// if set to true [is closed path]. - internal InternalPath(ReadOnlyMemory points, bool isClosedPath) - : this(Simplify(points.Span, isClosedPath, true), isClosedPath) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The points. - /// if set to true [is closed path]. - private InternalPath(PointData[] points, bool isClosedPath) - { - this.points = points; - this.closedPath = isClosedPath; - - if (this.points.Length > 0) - { - float minX, minY, maxX, maxY, length; - length = 0; - minX = minY = float.MaxValue; - maxX = maxY = float.MinValue; - - foreach (PointData point in this.points) - { - length += point.Length; - minX = Math.Min(point.Point.X, minX); - minY = Math.Min(point.Point.Y, minY); - maxX = Math.Max(point.Point.X, maxX); - maxY = Math.Max(point.Point.Y, maxY); - } - - this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY); - this.Length = length; - } - else - { - this.Bounds = RectangleF.Empty; - this.Length = 0; - } - } - - /// - /// Gets the bounds. - /// - /// - /// The bounds. - /// - public RectangleF Bounds { get; } - - /// - /// Gets the length. - /// - /// - /// The length. - /// - public float Length { get; } - - /// - /// Gets the length. - /// - public int PointCount => this.points.Length; - - /// - /// Gets the points. - /// - /// The - internal ReadOnlyMemory Points() => this.points.Select(x => x.Point).ToArray(); - - /// - /// Calculates the point a certain distance a path. - /// - /// The distance along the path to find details of. - /// - /// Returns details about a point along a path. - /// - /// Thrown if no points found. - internal SegmentInfo PointAlongPath(float distanceAlongPath) - { - int pointCount = this.PointCount; - if (this.closedPath) - { - // Move the distance back to the beginning since this is a closed polygon. - distanceAlongPath %= this.Length; - pointCount--; - } - - for (int i = 0; i < pointCount; i++) - { - int next = WrapArrayIndex(i + 1, this.PointCount); - if (distanceAlongPath < this.points[next].Length) - { - float t = distanceAlongPath / this.points[next].Length; - Vector2 point = Vector2.Lerp(this.points[i].Point, this.points[next].Point, t); - Vector2 diff = this.points[i].Point - this.points[next].Point; - - return new SegmentInfo - { - Point = point, - Angle = (float)(Math.Atan2(diff.Y, diff.X) % (Math.PI * 2)) - }; - } - - distanceAlongPath -= this.points[next].Length; - } - - // Closed paths will never reach this point. - // For open paths we're going to create a new virtual point that extends past the path. - // The position and angle for that point are calculated based upon the last two points. - PointF a = this.points[Math.Max(this.points.Length - 2, 0)].Point; - PointF b = this.points[this.points.Length - 1].Point; - Vector2 delta = a - b; - float angle = (float)(Math.Atan2(delta.Y, delta.X) % (Math.PI * 2)); - - Matrix3x2 transform = Matrix3x2.CreateRotation(angle - MathF.PI) * Matrix3x2.CreateTranslation(b.X, b.Y); - - return new SegmentInfo - { - Point = Vector2.Transform(new Vector2(distanceAlongPath, 0), transform), - Angle = angle - }; - } - - internal IMemoryOwner ExtractVertices(MemoryAllocator allocator) - { - IMemoryOwner buffer = allocator.Allocate(this.points.Length + 1); - Span span = buffer.Memory.Span; - - for (int i = 0; i < this.points.Length; i++) - { - span[i] = this.points[i].Point; - } - - return buffer; - } - - // Modulo is a very slow operation. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vector2 r) - { - // See http://www.geeksforgeeks.org/orientation-3-ordered-points/ - // for details of below formula. - Vector2 qp = q - p; - Vector2 rq = r - q; - float val = (qp.Y * rq.X) - (qp.X * rq.Y); - - if (val is > -Epsilon and < Epsilon) - { - return PointOrientation.Collinear; // colinear - } - - return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointOrientation CalculateOrientation(Vector2 qp, Vector2 rq) - { - // See http://www.geeksforgeeks.org/orientation-3-ordered-points/ - // for details of below formula. - float val = (qp.Y * rq.X) - (qp.X * rq.Y); - - if (val > -Epsilon && val < Epsilon) - { - return PointOrientation.Collinear; // colinear - } - - return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise - } - - /// - /// Simplifies the collection of segments. - /// - /// The segments. - /// Weather the path is closed or open. - /// Whether to remove close and collinear vertices - /// - /// The . - /// - private static PointData[] Simplify(IReadOnlyList segments, bool isClosed, bool removeCloseAndCollinear) - { - List simplified = new(segments.Count); - - foreach (ILineSegment seg in segments) - { - ReadOnlyMemory points = seg.Flatten(); - simplified.AddRange(points.Span); - } - - return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear); - } - - private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, bool removeCloseAndCollinear) - { - int polyCorners = points.Length; - if (polyCorners == 0) - { - return []; - } - - List results = new(polyCorners); - Vector2 lastPoint = points[0]; - - if (!isClosed) - { - results.Add(new PointData - { - Point = points[0], - Orientation = PointOrientation.Collinear, - Length = 0 - }); - } - else - { - int prev = polyCorners; - do - { - prev--; - if (prev == 0) - { - // All points are common, shouldn't match anything - results.Add( - new PointData - { - Point = points[0], - Orientation = PointOrientation.Collinear, - Length = 0, - }); - - return [.. results]; - } - } - while (removeCloseAndCollinear && points[0].Equivalent(points[prev], Epsilon2)); // skip points too close together - - polyCorners = prev + 1; - lastPoint = points[prev]; - - results.Add( - new PointData - { - Point = points[0], - Orientation = CalculateOrientation(lastPoint, points[0], points[1]), - Length = Vector2.Distance(lastPoint, points[0]), - }); - - lastPoint = points[0]; - } - - for (int i = 1; i < polyCorners; i++) - { - int next = WrapArrayIndex(i + 1, polyCorners); - PointOrientation or = CalculateOrientation(lastPoint, points[i], points[next]); - if (or == PointOrientation.Collinear && next != 0) - { - continue; - } - - results.Add( - new PointData - { - Point = points[i], - Orientation = or, - Length = Vector2.Distance(lastPoint, points[i]), - }); - lastPoint = points[i]; - } - - if (isClosed && removeCloseAndCollinear) - { - // walk back removing collinear points - while (results.Count > 2 && results[^1].Orientation == PointOrientation.Collinear) - { - results.RemoveAt(results.Count - 1); - } - } - - return [.. results]; - } - - private struct PointData - { - public PointF Point; - public PointOrientation Orientation; - public float Length; - } -} diff --git a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs deleted file mode 100644 index 78ff4fc15..000000000 --- a/src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Extensions to that allow the generation of outlines. -/// -public static class OutlinePathExtensions -{ - private static readonly StrokeOptions DefaultOptions = new(); - - /// - /// Generates an outline of the path. - /// - /// The path to outline - /// The outline width. - /// A new representing the outline. - public static IPath GenerateOutline(this IPath path, float width) - => GenerateOutline(path, width, DefaultOptions); - - /// - /// Generates an outline of the path. - /// - /// The path to outline - /// The outline width. - /// The stroke geometry options. - /// A new representing the outline. - public static IPath GenerateOutline(this IPath path, float width, StrokeOptions strokeOptions) - { - if (width <= 0) - { - return Path.Empty; - } - - return StrokedShapeGenerator.GenerateStrokedShapes(path, width, strokeOptions); - } - - /// - /// Generates an outline of the path with alternating on and off segments based on the pattern. - /// - /// The path to outline - /// The outline width. - /// The pattern made of multiples of the width. - /// A new representing the outline. - public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern) - => path.GenerateOutline(width, pattern, false); - - /// - /// Generates an outline of the path with alternating on and off segments based on the pattern. - /// - /// The path to outline - /// The outline width. - /// The pattern made of multiples of the width. - /// The stroke geometry options. - /// A new representing the outline. - 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. - /// - /// The 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. - /// A new representing the outline. - 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. - /// - /// The path to outline - /// The outline width. - /// The pattern made of multiples of the width. - /// Whether the first item in the pattern is on or off. - /// The stroke geometry options. - /// A new representing the outline. - public static IPath GenerateOutline( - this IPath path, - float width, - ReadOnlySpan pattern, - bool startOff, - StrokeOptions strokeOptions) - { - if (width <= 0) - { - return Path.Empty; - } - - if (pattern.Length < 2) - { - return path.GenerateOutline(width, strokeOptions); - } - - const float eps = 1e-6f; - const int maxPatternSegments = 10000; - - // Compute the absolute pattern length in path units to detect degenerate patterns. - float patternLength = 0f; - for (int i = 0; i < pattern.Length; i++) - { - patternLength += MathF.Abs(pattern[i]) * width; - } - - // Fallback to a solid outline when the dash pattern is too small to be meaningful. - if (patternLength <= eps) - { - return path.GenerateOutline(width, strokeOptions); - } - - IEnumerable paths = path.Flatten(); - - List outlines = []; - List buffer = new(64); // arbitrary initial capacity hint. - - foreach (ISimplePath p in paths) - { - bool online = !startOff; - int patternPos = 0; - 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]); - } - - if (totalLength > eps) - { - // Avoid runaway segmentation by falling back when the dash count explodes. - float estimatedSegments = (totalLength / patternLength) * pattern.Length; - if (estimatedSegments > maxPatternSegments) - { - return path.GenerateOutline(width, strokeOptions); - } - } - - int i = 0; - Vector2 current = pts[0]; - - while (i < edgeCount) - { - 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) - { - buffer.Add(current); - current = next; - i++; - targetLength -= segLen; - continue; - } - - // 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); - - if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) - { - outlines.Add([.. buffer]); - } - - buffer.Clear(); - online = !online; - - current = next; - i++; - patternPos = (patternPos + 1) % pattern.Length; - targetLength = pattern[patternPos] * width; - continue; - } - - // 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]) - { - 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) - { - buffer.Add(current); // terminate at the true end position - - if (online && buffer.Count >= 2 && buffer[0] != buffer[^1]) - { - outlines.Add([.. buffer]); - } - - buffer.Clear(); - } - } - - // 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/PathExtensions.Internal.cs b/src/ImageSharp.Drawing/Shapes/PathExtensions.Internal.cs deleted file mode 100644 index 8a3eba57b..000000000 --- a/src/ImageSharp.Drawing/Shapes/PathExtensions.Internal.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Convenience methods that can be applied to shapes and paths. -/// -public static partial class PathExtensions -{ - /// - /// Create a path with the segment order reversed. - /// - /// The path to reverse. - /// The reversed . - internal static IPath Reverse(this IPath path) - { - IEnumerable segments = path.Flatten().Select(p => new LinearLineSegment(p.Points.ToArray().Reverse().ToArray())); - bool closed = false; - if (path is ISimplePath sp) - { - closed = sp.IsClosed; - } - - return closed ? new Polygon(segments) : new Path(segments); - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs deleted file mode 100644 index d22aff793..000000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/ClipperException.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; - -/// -/// The exception that is thrown when an error occurs clipping a polygon. -/// -public class ClipperException : Exception -{ - /// - /// Initializes a new instance of the class. - /// - public ClipperException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public ClipperException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class with a specified error message and a - /// reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a - /// reference if no inner exception is specified. - public ClipperException(string message, Exception innerException) - : base(message, innerException) - { - } -} diff --git a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs deleted file mode 100644 index a3dc75836..000000000 --- a/src/ImageSharp.Drawing/Shapes/PolygonGeometry/StrokedShapeGenerator.cs +++ /dev/null @@ -1,177 +0,0 @@ -// 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/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs deleted file mode 100644 index 2544ff5d9..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/DefaultRasterizer.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index af642517a..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/IRasterizer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index 150b2612c..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs +++ /dev/null @@ -1,2275 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes.Rasterization; - -/// -/// Fixed-point polygon scanner that converts polygon edges into per-row coverage runs. -/// -/// -/// The scanner has two execution modes: -/// 1. Parallel tiled execution (default): build an edge table once, bucket edges by tile rows, -/// rasterize tiles in parallel with worker-local scratch, then emit in deterministic Y order. -/// 2. Sequential execution: reuse the same edge table and process band buckets on one thread. -/// -/// Both modes share the same coverage math and fill-rule handling, ensuring predictable output -/// regardless of scheduling strategy. -/// -internal static class PolygonScanner -{ - // Upper bound for temporary scanner buffers (bit vectors + cover/area + start-cover rows). - // Keeping this bounded prevents pathological full-image allocations on very large interests. - private const long BandMemoryBudgetBytes = 64L * 1024L * 1024L; - - // Blaze-style tile height used by the parallel row-tiling pipeline. - private const int DefaultTileHeight = 16; - - // Cap for buffered output coverage in the parallel path. We buffer one float per destination - // pixel plus one dirty-row byte per tile row before deterministic ordered emission. - private const long ParallelOutputPixelBudget = 16L * 1024L * 1024L; // 4096 x 4096 - - private const int FixedShift = 8; - private const int FixedOne = 1 << FixedShift; - private static readonly int WordBitCount = nint.Size * 8; - 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 - { - Rectangle interest = options.Interest; - int width = interest.Width; - int height = interest.Height; - if (width <= 0 || height <= 0) - { - return; - } - - int wordsPerRow = BitVectorsForMaxBitCount(width); - int maxBandRows = 0; - long coverStride = (long)width * 2; - if (coverStride > int.MaxValue || - !TryGetBandHeight(width, height, wordsPerRow, coverStride, out maxBandRows)) - { - ThrowInterestBoundsTooLarge(); - } - - int coverStrideInt = (int)coverStride; - float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - - // Create tessellated rings once. Both sequential and parallel paths consume this single - // canonical representation so path flattening/orientation work is never repeated. - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(path, allocator); - using IMemoryOwner edgeDataOwner = allocator.Allocate(multipolygon.TotalVertexCount); - int edgeCount = BuildEdgeTable(multipolygon, interest.Left, interest.Top, height, samplingOffsetX, edgeDataOwner.Memory.Span); - if (edgeCount <= 0) - { - return; - } - - if (allowParallel && - TryRasterizeParallel( - edgeDataOwner.Memory, - edgeCount, - width, - height, - interest.Top, - wordsPerRow, - coverStrideInt, - maxBandRows, - options.IntersectionRule, - options.RasterizationMode, - allocator, - ref state, - scanlineHandler)) - { - return; - } - - RasterizeSequentialBands( - edgeDataOwner.Memory.Span[..edgeCount], - width, - height, - interest.Top, - wordsPerRow, - coverStrideInt, - maxBandRows, - options.IntersectionRule, - options.RasterizationMode, - allocator, - ref state, - scanlineHandler); - } - - /// - /// Sequential implementation using band buckets over the prebuilt edge table. - /// - /// The caller-owned mutable state type. - /// Prebuilt edges in scanner-local coordinates. - /// Destination width in pixels. - /// Destination height in pixels. - /// Absolute top Y of the interest rectangle. - /// Bit-vector words per row. - /// Cover-area stride in ints. - /// Maximum rows per reusable scratch band. - /// Fill rule. - /// Coverage mode (AA or aliased). - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - private static void RasterizeSequentialBands( - ReadOnlySpan edges, - int width, - int height, - int interestTop, - int wordsPerRow, - int coverStrideInt, - int maxBandRows, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - int bandHeight = maxBandRows; - int bandCount = (height + bandHeight - 1) / bandHeight; - if (bandCount < 1) - { - return; - } - - 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(); - } - } - - /// - /// 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 - { - 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]++; - } - } - - int totalReferences = (int)totalTileEdgeReferences; - using IMemoryOwner tileOffsetsOwner = allocator.Allocate(tileCount + 1); - Memory tileOffsetsMemory = tileOffsetsOwner.Memory; - Span tileOffsets = tileOffsetsMemory.Span; - - int offset = 0; - for (int t = 0; t < tileCount; t++) - { - // Prefix sum over tile counts so each tile gets one contiguous slice. - tileOffsets[t] = offset; - offset += tileCounts[t]; - } - - tileOffsets[tileCount] = offset; - using IMemoryOwner tileWriteCursorOwner = allocator.Allocate(tileCount); - Span tileWriteCursor = tileWriteCursorOwner.Memory.Span; - tileOffsets[..tileCount].CopyTo(tileWriteCursor); - - using IMemoryOwner tileEdgeReferencesOwner = allocator.Allocate(totalReferences); - Memory tileEdgeReferencesMemory = tileEdgeReferencesOwner.Memory; - Span tileEdgeReferences = tileEdgeReferencesMemory.Span; - - for (int edgeIndex = 0; edgeIndex < edgeCount; edgeIndex++) - { - int startTile = edgeBuffer[edgeIndex].MinRow / tileHeight; - int endTile = edgeBuffer[edgeIndex].MaxRow / tileHeight; - for (int t = startTile; t <= endTile; t++) - { - // Scatter edge indices into each tile's contiguous bucket. - tileEdgeReferences[tileWriteCursor[t]++] = edgeIndex; - } - } - - TileOutput[] tileOutputs = new TileOutput[tileCount]; - ParallelOptions parallelOptions = new() - { - MaxDegreeOfParallelism = Math.Min(Environment.ProcessorCount, tileCount) - }; - - try - { - _ = Parallel.For( - 0, - tileCount, - parallelOptions, - () => WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, tileHeight), - (tileIndex, _, scratch) => - { - ReadOnlySpan edges = edgeMemory.Span[..edgeCount]; - Span tileOffsets = tileOffsetsMemory.Span; - Span tileEdgeReferences = tileEdgeReferencesMemory.Span; - int bandTop = tileIndex * tileHeight; - int bandHeight = Math.Min(tileHeight, height - bandTop); - int start = tileOffsets[tileIndex]; - int length = tileOffsets[tileIndex + 1] - start; - ReadOnlySpan tileEdges = tileEdgeReferences.Slice(start, length); - - // Each tile rasterizes fully independently into worker-local scratch. - RasterizeTile( - scratch, - edges, - tileEdges, - bandTop, - bandHeight, - width, - intersectionRule, - rasterizationMode, - allocator, - tileOutputs, - tileIndex); - - return scratch; - }, - static scratch => scratch.Dispose()); - - EmitTileOutputs(tileOutputs, width, interestTop, ref state, scanlineHandler); - return true; - } - finally - { - foreach (TileOutput output in tileOutputs) - { - output?.Dispose(); - } - } - } - - /// - /// Rasterizes a single tile directly into the caller callback. - /// - /// - /// This avoids parallel setup and tile-output buffering for tiny workloads while preserving - /// the same scan-conversion math and callback ordering as the general tiled path. - /// - /// The caller-owned mutable state type. - /// Prebuilt edge table. - /// Destination width in pixels. - /// Destination height in pixels. - /// Absolute top Y of the interest rectangle. - /// Bit-vector words per row. - /// Cover-area stride in ints. - /// Fill rule. - /// Coverage mode (AA or aliased). - /// Temporary buffer allocator. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - private static void RasterizeSingleTileDirect( - ReadOnlySpan edges, - int width, - int height, - int interestTop, - int wordsPerRow, - int coverStride, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - MemoryAllocator allocator, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - using WorkerScratch scratch = WorkerScratch.Create(allocator, wordsPerRow, coverStride, width, height); - Context context = scratch.CreateContext(height, intersectionRule, rasterizationMode); - context.RasterizeEdgeTable(edges, bandTop: 0); - context.EmitScanlines(interestTop, scratch.Scanline, ref state, scanlineHandler); - context.ResetTouchedRows(); - } - - /// - /// Rasterizes one tile/band edge subset into temporary coverage buffers. - /// - /// Worker-local scratch buffers. - /// Shared edge table. - /// Indices of edges intersecting this tile. - /// Tile top row in scanner-local coordinates. - /// Tile height in rows. - /// Destination width in pixels. - /// Fill rule. - /// Coverage mode (AA or aliased). - /// Temporary buffer allocator. - /// Output slot array indexed by tile ID. - /// Current tile index. - private static void RasterizeTile( - WorkerScratch scratch, - ReadOnlySpan edges, - ReadOnlySpan tileEdgeIndices, - int bandTop, - int bandHeight, - int width, - IntersectionRule intersectionRule, - RasterizationMode rasterizationMode, - MemoryAllocator allocator, - TileOutput[] outputs, - int tileIndex) - { - if (tileEdgeIndices.Length == 0) - { - return; - } - - Context context = scratch.CreateContext(bandHeight, intersectionRule, rasterizationMode); - context.RasterizeEdgeTable(edges, tileEdgeIndices, bandTop); - - int coverageLength = checked(width * bandHeight); - IMemoryOwner coverageOwner = allocator.Allocate(coverageLength, AllocationOptions.Clean); - IMemoryOwner dirtyRowsOwner = allocator.Allocate(bandHeight, AllocationOptions.Clean); - bool committed = false; - - try - { - TileCaptureState captureState = new(width, coverageOwner.Memory, dirtyRowsOwner.Memory); - - // Emit with destinationTop=0 into tile-local storage; global Y is restored later. - context.EmitScanlines(0, scratch.Scanline, ref captureState, CaptureTileScanline); - outputs[tileIndex] = new TileOutput(bandTop, bandHeight, coverageOwner, dirtyRowsOwner); - committed = true; - } - finally - { - context.ResetTouchedRows(); - - if (!committed) - { - coverageOwner.Dispose(); - dirtyRowsOwner.Dispose(); - } - } - } - - /// - /// Emits buffered tile outputs in deterministic top-to-bottom order. - /// - /// The caller-owned mutable state type. - /// Tile outputs captured by workers. - /// Destination width in pixels. - /// Absolute top Y of the interest rectangle. - /// Caller-owned mutable state. - /// Scanline callback invoked in ascending Y order. - private static void EmitTileOutputs( - TileOutput[] outputs, - int width, - int destinationTop, - ref TState state, - RasterizerScanlineHandler scanlineHandler) - where TState : struct - { - foreach (TileOutput output in outputs) - { - if (output is null) - { - continue; - } - - Span coverage = output.CoverageOwner.Memory.Span; - Span dirtyRows = output.DirtyRowsOwner.Memory.Span; - for (int row = 0; row < output.Height; row++) - { - if (dirtyRows[row] == 0) - { - // Rows are sparse; untouched rows were never emitted by the tile worker. - continue; - } - - // Stable top-to-bottom emission keeps observable callback order deterministic. - Span scanline = coverage.Slice(row * width, width); - scanlineHandler(destinationTop + output.Top + row, scanline, ref state); - } - } - } - - /// - /// Captures one emitted scanline into a tile-local output buffer. - /// - /// Row index relative to tile-local coordinates. - /// Coverage scanline. - /// Tile capture state. - private static void CaptureTileScanline(int y, Span scanline, ref TileCaptureState state) - { - // y is tile-local (destinationTop was 0 in RasterizeTile). - int row = y - state.Top; - scanline.CopyTo(state.Coverage.Span.Slice(row * state.Width, state.Width)); - state.DirtyRows.Span[row] = 1; - } - - /// - /// Builds 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++) - { - 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; - } - - /// - /// 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) - { - int y0Row = y0 >> FixedShift; - int y1Row = y1 >> FixedShift; - - // First touched row is floor(min(y0, y1)). - minRow = y0Row < y1Row ? y0Row : y1Row; - - int y0Fraction = y0 & (FixedOne - 1); - int y1Fraction = y1 & (FixedOne - 1); - - // Last touched row is ceil(max(y)) - 1: - // - when fractional part is non-zero, row is unchanged; - // - when exactly on a row boundary, subtract 1 (edge ownership rule). - int y0Candidate = y0Row - (((y0Fraction - 1) >> 31) & 1); - int y1Candidate = y1Row - (((y1Fraction - 1) >> 31) & 1); - maxRow = y0Candidate > y1Candidate ? y0Candidate : y1Candidate; - } - - /// - /// Clips a fixed-point segment against vertical bounds. - /// - /// Segment start X in 24.8 fixed-point (updated in place). - /// Segment start Y in 24.8 fixed-point (updated in place). - /// Segment end X in 24.8 fixed-point (updated in place). - /// Segment end Y in 24.8 fixed-point (updated in place). - /// Minimum Y bound in 24.8 fixed-point. - /// Maximum Y bound in 24.8 fixed-point. - /// when a non-horizontal clipped segment remains. - private static bool ClipToVerticalBoundsFixed(ref int x0, ref int y0, ref int x1, ref int y1, int minY, int maxY) - { - double t0 = 0D; - double t1 = 1D; - int originX0 = x0; - int originY0 = y0; - long dx = (long)x1 - originX0; - long dy = (long)y1 - originY0; - if (!ClipTestFixed(-(double)dy, originY0 - (double)minY, ref t0, ref t1)) - { - return false; - } - - if (!ClipTestFixed(dy, maxY - (double)originY0, ref t0, ref t1)) - { - return false; - } - - if (t1 < 1D) - { - x1 = originX0 + (int)Math.Round(dx * t1); - y1 = originY0 + (int)Math.Round(dy * t1); - } - - if (t0 > 0D) - { - x0 = originX0 + (int)Math.Round(dx * t0); - y0 = originY0 + (int)Math.Round(dy * t0); - } - - return y0 != y1; - } - - /// - /// Clips a segment against vertical bounds using Liang-Barsky style parametric tests. - /// - /// 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; - } - - /// - /// One Liang-Barsky clip test step. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ClipTest(float p, float q, ref float t0, ref float t1) - { - if (p == 0F) - { - return q >= 0F; - } - - float r = q / p; - if (p < 0F) - { - if (r > t1) - { - return false; - } - - if (r > t0) - { - t0 = r; - } - } - else - { - if (r < t0) - { - return false; - } - - if (r < t1) - { - t1 = r; - } - } - - return true; - } - - /// - /// One Liang-Barsky clip test step for fixed-point clipping. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool ClipTestFixed(double p, double q, ref double t0, ref double t1) - { - if (p == 0D) - { - return q >= 0D; - } - - double r = q / p; - if (p < 0D) - { - if (r > t1) - { - return false; - } - - if (r > t0) - { - t0 = r; - } - } - else - { - if (r < t0) - { - return false; - } - - if (r < t1) - { - t1 = r; - } - } - - return true; - } - - /// - /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. - /// This is used to keep edge ownership consistent for vertical lines. - /// - [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; - } - - /// - /// Machine-word trailing zero count used for sparse bitset iteration. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int TrailingZeroCount(nuint value) - => nint.Size == sizeof(ulong) - ? BitOperations.TrailingZeroCount((ulong)value) - : BitOperations.TrailingZeroCount((uint)value); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowInterestBoundsTooLarge() - => throw new ImageProcessingException("The rasterizer interest bounds are too large for PolygonScanner buffers."); - - [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowBandHeightExceedsScratchCapacity() - => throw new ImageProcessingException("Requested band height exceeds worker scratch capacity."); - - /// - /// Band/tile-local scanner context that owns mutable coverage accumulation state. - /// - /// - /// Instances are intentionally stack-bound to keep hot-path data in spans and avoid heap churn. - /// - private ref struct Context - { - private readonly Span bitVectors; - 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 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) - { - hasCoverage |= FlushSpan(scanline, spanEnd, this.width, this.AreaToCoverage(cover << AreaToCoverageShift)); - } - - 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 deleted file mode 100644 index e4fb24455..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanning.MD +++ /dev/null @@ -1,224 +0,0 @@ -# Polygon Scanner (Fixed-Point, Tiled + Banded Fallback) - -This document describes the current `PolygonScanner` implementation in -`src/ImageSharp.Drawing/Shapes/Rasterization/PolygonScanner.cs`. - -The scanner is a fixed-point, area/cover rasterizer inspired by Blaze-style -scan conversion. - -https://github.com/aurimasg/blaze (MIT-Licensed) - -## Goals - -- 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 -``` - -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 - -- 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/ScanlineRasterizer.cs b/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs deleted file mode 100644 index 6a2183c0b..000000000 --- a/src/ImageSharp.Drawing/Shapes/Rasterization/ScanlineRasterizer.cs +++ /dev/null @@ -1,44 +0,0 @@ -// 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 deleted file mode 100644 index eade34439..000000000 --- a/src/ImageSharp.Drawing/Shapes/TessellatedMultipolygon.cs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Collections; -using SixLabors.ImageSharp.Drawing.Shapes.Helpers; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Shapes; - -/// -/// Compact representation of a multipolygon. -/// Applies some rules which are optimal to implement geometric algorithms: -/// - Outer contour is oriented "Positive" (CCW in world coords, CW on screen) -/// - Holes are oriented "Negative" (CW in world, CCW on screen) -/// - First vertex is always repeated at the end of the span in each ring -/// -internal sealed class TessellatedMultipolygon : IDisposable, IReadOnlyList -{ - private readonly Ring[] rings; - - private TessellatedMultipolygon(Ring[] rings) - { - this.rings = rings; - this.TotalVertexCount = rings.Sum(r => r.VertexCount); - } - - public int TotalVertexCount { get; } - - public int Count => this.rings.Length; - - public Ring this[int index] => this.rings[index]; - - public static TessellatedMultipolygon Create(IPath path, MemoryAllocator memoryAllocator) - { - if (path is IInternalPathOwner ipo) - { - IReadOnlyList internalPaths = ipo.GetRingsAsInternalPath(); - - // 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, - // we enforce the orientation for best output quality. - bool enforcePositiveOrientationOnFirstRing = internalPaths.Count == 1; - - Ring[] rings = new Ring[internalPaths.Count]; - IMemoryOwner pointBuffer = internalPaths[0].ExtractVertices(memoryAllocator); - RepeatFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, enforcePositiveOrientationOnFirstRing); - rings[0] = new Ring(pointBuffer); - - for (int i = 1; i < internalPaths.Count; i++) - { - pointBuffer = internalPaths[i].ExtractVertices(memoryAllocator); - RepeatFirstVertexAndEnsureOrientation(pointBuffer.Memory.Span, false); - rings[i] = new Ring(pointBuffer); - } - - return new TessellatedMultipolygon(rings); - } - else - { - 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, - // we enforce the orientation for best output quality. - bool enforcePositiveOrientationOnFirstRing = points.Length == 1; - - Ring[] rings = new Ring[points.Length]; - rings[0] = MakeRing(points[0], enforcePositiveOrientationOnFirstRing, memoryAllocator); - for (int i = 1; i < points.Length; i++) - { - rings[i] = MakeRing(points[i], false, memoryAllocator); - } - - return new TessellatedMultipolygon(rings); - } - - static Ring MakeRing(ReadOnlyMemory points, bool enforcePositiveOrientation, MemoryAllocator allocator) - { - IMemoryOwner buffer = allocator.Allocate(points.Length + 1); - Span span = buffer.Memory.Span; - points.Span.CopyTo(span); - RepeatFirstVertexAndEnsureOrientation(span, enforcePositiveOrientation); - return new Ring(buffer); - } - - static void RepeatFirstVertexAndEnsureOrientation(Span span, bool enforcePositiveOrientation) - { - // Repeat first vertex for perf: - span[^1] = span[0]; - - if (enforcePositiveOrientation) - { - TopologyUtilities.EnsureOrientation(span, 1); - } - } - } - - public void Dispose() - { - foreach (Ring ring in this.rings) - { - ring.Dispose(); - } - } - - public IEnumerator GetEnumerator() => this.rings.AsEnumerable().GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - internal sealed class Ring : IDisposable - { - private readonly IMemoryOwner buffer; - private Memory memory; - - internal Ring(IMemoryOwner buffer) - { - this.buffer = buffer; - this.memory = buffer.Memory; - } - - public ReadOnlySpan Vertices => this.memory.Span; - - public int VertexCount => this.memory.Length - 1; // Last vertex is repeated - - public void Dispose() - { - this.buffer.Dispose(); - this.memory = default; - } - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs deleted file mode 100644 index 2966e3f7a..000000000 --- a/src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs +++ /dev/null @@ -1,457 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.Fonts; -using SixLabors.Fonts.Rendering; -using SixLabors.ImageSharp.Drawing.Processing; - -namespace SixLabors.ImageSharp.Drawing.Text; - -/// -/// Defines a base rendering surface that Fonts can use to generate shapes. -/// -internal class BaseGlyphBuilder : IGlyphRenderer -{ - private Vector2 currentPoint; - private GlyphRendererParameters parameters; - - // Tracks whether geometry was emitted inside BeginLayer/EndLayer pairs for this glyph. - private bool usedLayers; - - // Tracks whether we are currently inside a layer block. - private bool inLayer; - - // Per-GRAPHEME layered capture (aggregate multiple glyphs of the same grapheme, e.g. COLR v0 layers): - private GlyphPathCollection.Builder? graphemeBuilder; - private int graphemePathCount; - private int currentGraphemeIndex = -1; - private readonly List currentGlyphs = []; - private TextDecorationDetails? previousUnderlineTextDecoration; - private TextDecorationDetails? previousOverlineTextDecoration; - private TextDecorationDetails? previousStrikeoutTextDecoration; - - // Per-layer (within current grapheme) bookkeeping: - private int layerStartIndex; - private Paint? currentLayerPaint; - private FillRule currentLayerFillRule; - private ClipQuad? currentClipBounds; - - public BaseGlyphBuilder() => this.Builder = new PathBuilder(); - - public BaseGlyphBuilder(Matrix3x2 transform) => this.Builder = new PathBuilder(transform); - - /// - /// Gets the flattened paths captured for all glyphs/graphemes. - /// - public IPathCollection Paths => new PathCollection(this.CurrentPaths); - - /// - /// Gets the layer-preserving collections captured per grapheme in rendering order. - /// Each entry aggregates all glyph layers that belong to a single grapheme cluster. - /// - public IReadOnlyList Glyphs => this.currentGlyphs; - - protected PathBuilder Builder { get; } - - /// - /// Gets the paths captured for the current glyph/grapheme. - /// - protected List CurrentPaths { get; } = []; - - void IGlyphRenderer.EndText() - { - // Finalize the last grapheme, if any: - if (this.graphemeBuilder is not null && this.graphemePathCount > 0) - { - this.currentGlyphs.Add(this.graphemeBuilder.Build()); - } - - this.graphemeBuilder = null; - this.graphemePathCount = 0; - this.currentGraphemeIndex = -1; - this.previousUnderlineTextDecoration = null; - this.previousOverlineTextDecoration = null; - this.previousStrikeoutTextDecoration = null; - - this.EndText(); - } - - void IGlyphRenderer.BeginText(in FontRectangle bounds) => this.BeginText(bounds); - - bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) - { - // If grapheme changed, flush previous aggregate and start a new one: - if (this.graphemeBuilder is not null && this.currentGraphemeIndex != parameters.GraphemeIndex) - { - if (this.graphemePathCount > 0) - { - this.currentGlyphs.Add(this.graphemeBuilder.Build()); - } - - this.graphemeBuilder = null; - this.graphemePathCount = 0; - } - - if (this.graphemeBuilder is null) - { - this.graphemeBuilder = new GlyphPathCollection.Builder(); - this.currentGraphemeIndex = parameters.GraphemeIndex; - this.graphemePathCount = 0; - } - - this.parameters = parameters; - this.Builder.Clear(); - this.usedLayers = false; - this.inLayer = false; - - this.layerStartIndex = this.graphemePathCount; - this.currentLayerPaint = null; - this.currentLayerFillRule = FillRule.NonZero; - this.currentClipBounds = null; - this.BeginGlyph(in bounds, in parameters); - return true; - } - - /// - void IGlyphRenderer.BeginFigure() => this.Builder.StartFigure(); - - /// - void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) - { - this.Builder.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); - this.currentPoint = point; - } - - /// - void IGlyphRenderer.EndGlyph() - { - // If the glyph did not open any explicit layer, treat its geometry as a single layer in the current grapheme: - if (!this.usedLayers) - { - IPath path = this.Builder.Build(); - - this.CurrentPaths.Add(path); - - if (this.graphemeBuilder is not null) - { - this.graphemeBuilder.AddPath(path); - this.graphemeBuilder.AddLayer( - startIndex: this.graphemePathCount, - count: 1, - paint: null, - fillRule: FillRule.NonZero, - bounds: path.Bounds, - kind: GlyphLayerKind.Glyph); - - this.graphemePathCount++; - } - } - - this.EndGlyph(); - this.Builder.Clear(); - this.inLayer = false; - this.usedLayers = false; - this.layerStartIndex = this.graphemePathCount; - } - - /// - void IGlyphRenderer.EndFigure() => this.Builder.CloseFigure(); - - /// - void IGlyphRenderer.LineTo(Vector2 point) - { - this.Builder.AddLine(this.currentPoint, point); - this.currentPoint = point; - } - - /// - void IGlyphRenderer.MoveTo(Vector2 point) - { - this.Builder.StartFigure(); - this.currentPoint = point; - } - - /// - void IGlyphRenderer.ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) - { - this.Builder.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point); - this.currentPoint = point; - } - - /// - void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) - { - this.Builder.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); - this.currentPoint = point; - } - - /// - void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) - { - this.usedLayers = true; - this.inLayer = true; - this.layerStartIndex = this.graphemePathCount; - this.currentLayerPaint = paint; - this.currentLayerFillRule = fillRule; - this.currentClipBounds = clipBounds; - - this.Builder.Clear(); - this.BeginLayer(paint, fillRule, clipBounds); - } - - /// - void IGlyphRenderer.EndLayer() - { - if (!this.inLayer) - { - return; - } - - IPath path = this.Builder.Build(); - - if (this.currentClipBounds is not null) - { - ClipQuad clip = this.currentClipBounds.Value; - PointF[] points = [clip.TopLeft, clip.TopRight, clip.BottomRight, clip.BottomLeft]; - LinearLineSegment segment = new(points); - Polygon polygon = new(segment); - - ShapeOptions options = new() - { - BooleanOperation = BooleanOperation.Intersection, - IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule) - }; - - path = path.Clip(options, polygon); - } - - this.CurrentPaths.Add(path); - - if (this.graphemeBuilder is not null) - { - this.graphemeBuilder.AddPath(path); - this.graphemeBuilder.AddLayer( - startIndex: this.layerStartIndex, - count: 1, - paint: this.currentLayerPaint, - fillRule: this.currentLayerFillRule, - bounds: path.Bounds, - kind: GlyphLayerKind.Painted); - - this.graphemePathCount++; - } - - this.Builder.Clear(); - this.inLayer = false; - this.currentLayerPaint = null; - this.currentLayerFillRule = FillRule.NonZero; - this.currentClipBounds = null; - this.EndLayer(); - } - - /// - void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) - { - if (thickness == 0) - { - return; - } - - // Clamp the thickness to whole pixels. - thickness = MathF.Max(1F, (float)Math.Round(thickness)); - IGlyphRenderer renderer = this; - - bool rotated = this.parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; - Vector2 pad = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, thickness * .5F); - - start = ClampToPixel(start, (int)thickness, rotated); - end = ClampToPixel(end, (int)thickness, rotated); - - // Sometimes the start and end points do not align properly leaving pixel sized gaps - // so we need to adjust them. Use any previous decoration to try and continue the line. - TextDecorationDetails? previous = textDecorations switch - { - TextDecorations.Underline => this.previousUnderlineTextDecoration, - TextDecorations.Overline => this.previousOverlineTextDecoration, - TextDecorations.Strikeout => this.previousStrikeoutTextDecoration, - _ => null - }; - - if (previous != null) - { - float prevThickness = previous.Value.Thickness; - Vector2 prevStart = previous.Value.Start; - Vector2 prevEnd = previous.Value.End; - - // If the previous line is identical to the new one ignore it. - // This can happen when multiple glyph layers are used. - if (prevStart == start && prevEnd == end) - { - return; - } - - // Align the new line with the previous one if they are close enough. - // Use a 2 pixel threshold to account for anti-aliasing gaps. - if (rotated) - { - if (thickness == prevThickness - && prevEnd.Y + 2 >= start.Y - && prevEnd.X == start.X) - { - start = prevEnd; - } - } - else if (thickness == prevThickness - && prevEnd.Y == start.Y - && prevEnd.X + 2 >= start.X) - { - start = prevEnd; - } - } - - TextDecorationDetails current = new() - { - Start = start, - End = end, - Thickness = thickness - }; - - switch (textDecorations) - { - case TextDecorations.Underline: - this.previousUnderlineTextDecoration = current; - break; - case TextDecorations.Strikeout: - this.previousStrikeoutTextDecoration = current; - break; - case TextDecorations.Overline: - this.previousOverlineTextDecoration = current; - break; - } - - Vector2 a = start - pad; - Vector2 b = start + pad; - Vector2 c = end + pad; - Vector2 d = end - pad; - - // Drawing is always centered around the point so we need to offset by half. - Vector2 offset = Vector2.Zero; - if (textDecorations == TextDecorations.Overline) - { - // CSS overline is drawn above the position, so we need to move it up. - offset = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, -(thickness * .5F)); - } - else if (textDecorations == TextDecorations.Underline) - { - // CSS underline is drawn below the position, so we need to move it down. - offset = rotated ? new Vector2(-(thickness * .5F), 0) : new Vector2(0, thickness * .5F); - } - - // We clamp the start and end points to the pixel grid to avoid anti-aliasing - // when there is no transform. - renderer.BeginFigure(); - renderer.MoveTo(ClampToPixel(a + offset)); - renderer.LineTo(ClampToPixel(b + offset)); - renderer.LineTo(ClampToPixel(c + offset)); - renderer.LineTo(ClampToPixel(d + offset)); - renderer.EndFigure(); - - IPath path = this.Builder.Build(); - - // If the path is degenerate (e.g. zero width line) we just skip it - // and return. This might happen when clamping moves the points. - if (path.Bounds.IsEmpty) - { - this.Builder.Clear(); - return; - } - - this.CurrentPaths.Add(path); - if (this.graphemeBuilder is not null) - { - this.graphemeBuilder.AddPath(path); - this.graphemeBuilder.AddLayer( - startIndex: this.layerStartIndex, - count: 1, - paint: this.currentLayerPaint, - fillRule: FillRule.NonZero, - bounds: path.Bounds, - kind: GlyphLayerKind.Decoration); - - this.graphemePathCount++; - } - - this.Builder.Clear(); - this.SetDecoration(textDecorations, start, end, thickness); - } - - /// - protected virtual void BeginText(in FontRectangle bounds) - { - } - - /// - protected virtual void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) - { - } - - /// - protected virtual void EndGlyph() - { - } - - /// - protected virtual void EndText() - { - } - - /// - protected virtual void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) - { - } - - /// - protected virtual void EndLayer() - { - } - - public virtual TextDecorations EnabledDecorations() - => this.parameters.TextRun.TextDecorations; - - /// - public virtual void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) - { - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Point ClampToPixel(PointF point) => Point.Truncate(point); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointF ClampToPixel(PointF point, int thickness, bool rotated) - { - // Even. Clamp to whole pixels. - if ((thickness & 1) == 0) - { - return Point.Truncate(point); - } - - // Odd. Clamp to half pixels. - if (rotated) - { - return Point.Truncate(point) + new Vector2(.5F, 0); - } - - return Point.Truncate(point) + new Vector2(0, .5F); - } - - private struct TextDecorationDetails - { - public Vector2 Start { get; set; } - - public Vector2 End { get; set; } - - public float Thickness { get; internal set; } - } -} diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs deleted file mode 100644 index 5f317f2ed..000000000 --- a/src/ImageSharp.Drawing/Shapes/Text/GlyphBuilder.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Drawing.Text; - -/// -/// rendering surface that Fonts can use to generate Shapes. -/// -internal class GlyphBuilder : BaseGlyphBuilder -{ - /// - /// Initializes a new instance of the class. - /// - public GlyphBuilder() - : this(Vector2.Zero) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The origin. - public GlyphBuilder(Vector2 origin) => this.Builder.SetOrigin(origin); -} diff --git a/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs b/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs deleted file mode 100644 index 706b772ac..000000000 --- a/src/ImageSharp.Drawing/Shapes/Text/PathGlyphBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.Fonts; -using SixLabors.Fonts.Rendering; - -namespace SixLabors.ImageSharp.Drawing.Text; - -/// -/// A rendering surface that Fonts can use to generate shapes by following a path. -/// -internal sealed class PathGlyphBuilder : GlyphBuilder -{ - private readonly IPathInternals path; - - /// - /// Initializes a new instance of the class. - /// - /// The path to render the glyphs along. - public PathGlyphBuilder(IPath path) - { - if (path is IPathInternals internals) - { - this.path = internals; - } - else - { - this.path = new ComplexPolygon(path); - } - } - - /// - protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) - => this.TransformGlyph(in bounds); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void TransformGlyph(in FontRectangle bounds) - { - // Find the point of this intersection along the given path. - // We want to find the point on the path that is closest to the center-bottom side of the glyph. - Vector2 half = new(bounds.Width * .5F, 0); - SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); - - // Now offset to our target point since we're aligning the top-left location of our glyph against the path. - Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); - Matrix3x2 matrix = Matrix3x2.CreateTranslation(translation) * Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point); - - this.Builder.SetTransform(matrix); - } -} diff --git a/src/ImageSharp.Drawing/SplitPathExtensions.cs b/src/ImageSharp.Drawing/SplitPathExtensions.cs new file mode 100644 index 000000000..b551121dc --- /dev/null +++ b/src/ImageSharp.Drawing/SplitPathExtensions.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Extensions to for splitting paths into dash segments +/// without performing stroke expansion. +/// +public static class SplitPathExtensions +{ + // Safety limit: if the estimated number of dash segments exceeds this threshold, + // return the original path unsplit to avoid runaway segmentation from very short + // patterns applied to very long paths. + private const int MaxPatternSegments = 10000; + + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// A path containing the "on" dash segments. + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern) + => path.GenerateDashes(strokeWidth, pattern, startOff: false); + + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// Whether the first item in the pattern is off rather than on. + /// A path containing the "on" dash segments. + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern, bool startOff) + { + if (pattern.Length < 2) + { + return path; + } + + const float eps = 1e-6f; + + // Compute the absolute pattern length in path units to detect degenerate patterns. + float patternLength = 0f; + for (int i = 0; i < pattern.Length; i++) + { + patternLength += MathF.Abs(pattern[i]) * strokeWidth; + } + + // Fallback to the original path when the dash pattern is too small to be meaningful. + if (patternLength <= eps) + { + return path; + } + + IEnumerable simplePaths = path.Flatten(); + List segments = []; + List buffer = new(64); + + foreach (ISimplePath p in simplePaths) + { + bool online = !startOff; + int patternPos = 0; + float targetLength = pattern[patternPos] * strokeWidth; + + ReadOnlySpan pts = p.Points.Span; + if (pts.Length < 2) + { + continue; + } + + // Number of edges to traverse (closed paths wrap; open paths stop one short). + int edgeCount = p.IsClosed ? pts.Length : pts.Length - 1; + + // Compute total path length to estimate the number of dash segments. + // This avoids runaway segmentation when a very short pattern is applied + // to a very long path. + float totalLength = 0f; + for (int j = 0; j < edgeCount; j++) + { + int nextIndex = p.IsClosed ? (j + 1) % pts.Length : j + 1; + totalLength += Vector2.Distance(pts[j], pts[nextIndex]); + } + + if (totalLength > eps) + { + float estimatedSegments = (totalLength / patternLength) * pattern.Length; + if (estimatedSegments > MaxPatternSegments) + { + return path; + } + } + + int ei = 0; + Vector2 current = pts[0]; + + while (ei < edgeCount) + { + int nextIndex = p.IsClosed ? (ei + 1) % pts.Length : ei + 1; + Vector2 next = pts[nextIndex]; + float segLen = Vector2.Distance(current, next); + + // Skip degenerate zero-length segments. + if (segLen <= eps) + { + current = next; + ei++; + continue; + } + + // Accumulate into the current dash span when the segment is shorter + // than the remaining target length. + if (segLen + eps < targetLength) + { + if (online) + { + buffer.Add(current); + } + + current = next; + ei++; + targetLength -= segLen; + continue; + } + + // Close out a dash span when the segment length matches the target. + if (MathF.Abs(segLen - targetLength) <= eps) + { + if (online) + { + buffer.Add(current); + buffer.Add(next); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + online = !online; + current = next; + ei++; + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * strokeWidth; + continue; + } + + // Split inside this segment to end the current dash span. + float t = targetLength / segLen; + Vector2 split = current + (t * (next - current)); + + if (online) + { + buffer.Add(current); + buffer.Add(split); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + online = !online; + current = split; // continue along the same geometric segment + patternPos = (patternPos + 1) % pattern.Length; + targetLength = pattern[patternPos] * strokeWidth; + } + + // Flush the tail of the last dash span, if any. + if (buffer.Count > 0) + { + if (online) + { + buffer.Add(current); + FlushBuffer(buffer, segments); + } + + buffer.Clear(); + } + } + + if (segments.Count == 0) + { + return path; + } + + if (segments.Count == 1) + { + return segments[0]; + } + + return new ComplexPolygon(segments); + } + + private static void FlushBuffer(List buffer, List segments) + { + if (buffer.Count >= 2 && buffer[0] != buffer[^1]) + { + segments.Add(new Path(new LinearLineSegment([.. buffer]))); + } + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Star.cs b/src/ImageSharp.Drawing/Star.cs similarity index 97% rename from src/ImageSharp.Drawing/Shapes/Star.cs rename to src/ImageSharp.Drawing/Star.cs index ba5a261ca..241b1bd44 100644 --- a/src/ImageSharp.Drawing/Shapes/Star.cs +++ b/src/ImageSharp.Drawing/Star.cs @@ -87,7 +87,7 @@ private static LinearLineSegment CreateSegment(Vector2 location, float innerRadi distance = distanceVectorInner; } - Vector2 rotated = Vector2.Transform(distance, Matrix3x2.CreateRotation(current)); + Vector2 rotated = PointF.Transform(distance, Matrix4x4.CreateRotationZ(current)); points[i] = rotated + location; diff --git a/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs new file mode 100644 index 000000000..0870a5744 --- /dev/null +++ b/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs @@ -0,0 +1,582 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing.Text; + +/// +/// Defines a base rendering surface that Fonts can use to generate shapes. +/// +internal class BaseGlyphBuilder : IGlyphRenderer +{ + /// + /// The last point emitted by MoveTo / LineTo / curve commands. + /// Used as the implicit start of the next segment. + /// + private Vector2 currentPoint; + + /// + /// Snapshot of the for the glyph currently + /// being processed. Set at the start of each BeginGlyph call and read by + /// SetDecoration to determine layout orientation. + /// + private GlyphRendererParameters parameters; + + // Tracks whether geometry was emitted inside BeginLayer/EndLayer pairs for this glyph. + // When true, EndGlyph skips its default single-layer path capture because layers + // already contributed their paths individually. + private bool usedLayers; + + // Tracks whether we are currently inside a layer block. + // Guards against unbalanced EndLayer calls. + private bool inLayer; + + // --- Per-GRAPHEME layered capture --- + // A grapheme cluster (e.g. a base glyph + COLR v0 color layers) may span + // multiple BeginGlyph/EndGlyph calls. These fields aggregate all layers + // belonging to the same grapheme into a single GlyphPathCollection. + private GlyphPathCollection.Builder? graphemeBuilder; + private int graphemePathCount; + private int currentGraphemeIndex = -1; + private readonly List currentGlyphs = []; + + // Previous decoration details per decoration type, used to stitch adjacent + // decorations together and eliminate sub-pixel gaps between glyphs. + private TextDecorationDetails? previousUnderlineTextDecoration; + private TextDecorationDetails? previousOverlineTextDecoration; + private TextDecorationDetails? previousStrikeoutTextDecoration; + + // Per-layer (within current grapheme) bookkeeping: + private int layerStartIndex; + private Paint? currentLayerPaint; + private FillRule currentLayerFillRule; + private ClipQuad? currentClipBounds; + + /// + /// Initializes a new instance of the class + /// with an identity transform. + /// + public BaseGlyphBuilder() => this.Builder = new PathBuilder(); + + /// + /// Initializes a new instance of the class + /// with the specified transform applied to all incoming glyph geometry. + /// + /// A matrix transform applied to every point received from the font engine. + public BaseGlyphBuilder(Matrix4x4 transform) => this.Builder = new PathBuilder(transform); + + /// + /// Gets the flattened paths captured for all glyphs/graphemes. + /// + public IPathCollection Paths => new PathCollection(this.CurrentPaths); + + /// + /// Gets the layer-preserving collections captured per grapheme in rendering order. + /// Each entry aggregates all glyph layers that belong to a single grapheme cluster. + /// + public IReadOnlyList Glyphs => this.currentGlyphs; + + /// + /// Gets the used to accumulate outline segments + /// (MoveTo, LineTo, curves) for the current glyph or layer. + /// The builder is cleared between glyphs / layers. + /// + protected PathBuilder Builder { get; } + + /// + /// Gets the running list of all instances produced so far + /// (glyph outlines, layer outlines, and decoration rectangles). Subclasses + /// read from the end of this list (e.g. CurrentPaths[^1]) to obtain + /// the most recently built path. + /// + protected List CurrentPaths { get; } = []; + + /// + /// Called by the font engine after all glyphs in the text block have been rendered. + /// Flushes any in-progress grapheme aggregate and resets per-text-block state. + /// + void IGlyphRenderer.EndText() + { + // Finalize the last grapheme, if any: + if (this.graphemeBuilder is not null && this.graphemePathCount > 0) + { + this.currentGlyphs.Add(this.graphemeBuilder.Build()); + } + + this.graphemeBuilder = null; + this.graphemePathCount = 0; + this.currentGraphemeIndex = -1; + this.previousUnderlineTextDecoration = null; + this.previousOverlineTextDecoration = null; + this.previousStrikeoutTextDecoration = null; + + this.EndText(); + } + + void IGlyphRenderer.BeginText(in FontRectangle bounds) => this.BeginText(bounds); + + /// + /// Called by the font engine before emitting outline data for a single glyph. + /// Manages grapheme-cluster transitions and resets per-glyph state. + /// + /// + /// to have the font engine emit the full outline + /// (MoveTo/LineTo/curves/EndGlyph); to skip it entirely, + /// which is used by caching subclasses when the glyph path is already available. + /// + bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + // If grapheme changed, flush previous aggregate and start a new one: + if (this.graphemeBuilder is not null && this.currentGraphemeIndex != parameters.GraphemeIndex) + { + if (this.graphemePathCount > 0) + { + this.currentGlyphs.Add(this.graphemeBuilder.Build()); + } + + this.graphemeBuilder = null; + this.graphemePathCount = 0; + } + + if (this.graphemeBuilder is null) + { + this.graphemeBuilder = new GlyphPathCollection.Builder(); + this.currentGraphemeIndex = parameters.GraphemeIndex; + this.graphemePathCount = 0; + } + + this.parameters = parameters; + this.Builder.Clear(); + this.usedLayers = false; + this.inLayer = false; + + this.layerStartIndex = this.graphemePathCount; + this.currentLayerPaint = null; + this.currentLayerFillRule = FillRule.NonZero; + this.currentClipBounds = null; + return this.BeginGlyph(in bounds, in parameters); + } + + /// + void IGlyphRenderer.BeginFigure() => this.Builder.StartFigure(); + + /// + void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) + { + this.Builder.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); + this.currentPoint = point; + } + + /// + /// Called by the font engine after the outline for a single glyph has been fully emitted. + /// Builds the accumulated path and registers it as a grapheme layer unless explicit + /// BeginLayer/EndLayer pairs already handled layer registration. + /// + void IGlyphRenderer.EndGlyph() + { + // If the glyph did not open any explicit layer, treat its geometry as a single + // implicit layer so that non-color glyphs still produce a GlyphPathCollection entry. + if (!this.usedLayers) + { + IPath path = this.Builder.Build(); + + this.CurrentPaths.Add(path); + + if (this.graphemeBuilder is not null) + { + this.graphemeBuilder.AddPath(path); + this.graphemeBuilder.AddLayer( + startIndex: this.graphemePathCount, + count: 1, + paint: null, + fillRule: FillRule.NonZero, + bounds: path.Bounds, + kind: GlyphLayerKind.Glyph); + + this.graphemePathCount++; + } + } + + this.EndGlyph(); + this.Builder.Clear(); + this.inLayer = false; + this.usedLayers = false; + this.layerStartIndex = this.graphemePathCount; + } + + /// + void IGlyphRenderer.EndFigure() => this.Builder.CloseFigure(); + + /// + void IGlyphRenderer.LineTo(Vector2 point) + { + this.Builder.AddLine(this.currentPoint, point); + this.currentPoint = point; + } + + /// + void IGlyphRenderer.MoveTo(Vector2 point) + { + this.Builder.StartFigure(); + this.currentPoint = point; + } + + /// + void IGlyphRenderer.ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) + { + this.Builder.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point); + this.currentPoint = point; + } + + /// + void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) + { + this.Builder.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); + this.currentPoint = point; + } + + /// + /// Called by the font engine to begin a color layer within a COLR v0/v1 glyph. + /// Each layer receives its own paint, fill rule, and optional clip bounds. + /// + void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + this.usedLayers = true; + this.inLayer = true; + this.layerStartIndex = this.graphemePathCount; + this.currentLayerPaint = paint; + this.currentLayerFillRule = fillRule; + this.currentClipBounds = clipBounds; + + this.Builder.Clear(); + this.BeginLayer(paint, fillRule, clipBounds); + } + + /// + /// Called by the font engine to close a color layer opened by BeginLayer. + /// Builds the layer path, applies any clip quad, and registers the result + /// as a painted layer in the current grapheme aggregate. + /// + void IGlyphRenderer.EndLayer() + { + if (!this.inLayer) + { + return; + } + + IPath path = this.Builder.Build(); + + // If the layer defines a clip quad (e.g. from COLR v1), intersect the + // built path with the quad polygon to constrain rendering. + if (this.currentClipBounds is not null) + { + ClipQuad clip = this.currentClipBounds.Value; + PointF[] points = [clip.TopLeft, clip.TopRight, clip.BottomRight, clip.BottomLeft]; + LinearLineSegment segment = new(points); + Polygon polygon = new(segment); + + ShapeOptions options = new() + { + BooleanOperation = BooleanOperation.Intersection, + IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule) + }; + + path = path.Clip(options, polygon); + } + + this.CurrentPaths.Add(path); + + if (this.graphemeBuilder is not null) + { + this.graphemeBuilder.AddPath(path); + this.graphemeBuilder.AddLayer( + startIndex: this.layerStartIndex, + count: 1, + paint: this.currentLayerPaint, + fillRule: this.currentLayerFillRule, + bounds: path.Bounds, + kind: GlyphLayerKind.Painted); + + this.graphemePathCount++; + } + + this.Builder.Clear(); + this.inLayer = false; + this.currentLayerPaint = null; + this.currentLayerFillRule = FillRule.NonZero; + this.currentClipBounds = null; + this.EndLayer(); + } + + /// + /// Called by the font engine to emit a text decoration (underline, strikeout, or overline) + /// for the current glyph. Builds a filled rectangle path from the start/end positions and + /// thickness, then registers it as a layer. + /// Adjacent decorations are stitched together using the previous decoration details to + /// eliminate sub-pixel gaps caused by font metric rounding. + /// + void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + if (thickness == 0) + { + return; + } + + // Clamp the thickness to whole pixels. + thickness = MathF.Max(1F, (float)Math.Round(thickness)); + IGlyphRenderer renderer = this; + + bool rotated = this.parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + Vector2 pad = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, thickness * .5F); + + start = ClampToPixel(start, (int)thickness, rotated); + end = ClampToPixel(end, (int)thickness, rotated); + + // Sometimes the start and end points do not align properly leaving pixel sized gaps + // so we need to adjust them. Use any previous decoration to try and continue the line. + TextDecorationDetails? previous = textDecorations switch + { + TextDecorations.Underline => this.previousUnderlineTextDecoration, + TextDecorations.Overline => this.previousOverlineTextDecoration, + TextDecorations.Strikeout => this.previousStrikeoutTextDecoration, + _ => null + }; + + if (previous != null) + { + float prevThickness = previous.Value.Thickness; + Vector2 prevStart = previous.Value.Start; + Vector2 prevEnd = previous.Value.End; + + // If the previous line is identical to the new one ignore it. + // This can happen when multiple glyph layers are used. + if (prevStart == start && prevEnd == end) + { + return; + } + + // Align the new line with the previous one if they are close enough. + // Use a 2 pixel threshold to account for anti-aliasing gaps. + if (rotated) + { + if (thickness == prevThickness + && prevEnd.Y + 2 >= start.Y + && prevEnd.X == start.X) + { + start = prevEnd; + } + } + else if (thickness == prevThickness + && prevEnd.Y == start.Y + && prevEnd.X + 2 >= start.X) + { + start = prevEnd; + } + } + + TextDecorationDetails current = new() + { + Start = start, + End = end, + Thickness = thickness + }; + + switch (textDecorations) + { + case TextDecorations.Underline: + this.previousUnderlineTextDecoration = current; + break; + case TextDecorations.Strikeout: + this.previousStrikeoutTextDecoration = current; + break; + case TextDecorations.Overline: + this.previousOverlineTextDecoration = current; + break; + } + + Vector2 a = start - pad; + Vector2 b = start + pad; + Vector2 c = end + pad; + Vector2 d = end - pad; + + // Drawing is always centered around the point so we need to offset by half. + Vector2 offset = Vector2.Zero; + if (textDecorations == TextDecorations.Overline) + { + // CSS overline is drawn above the position, so we need to move it up. + offset = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, -(thickness * .5F)); + } + else if (textDecorations == TextDecorations.Underline) + { + // CSS underline is drawn below the position, so we need to move it down. + offset = rotated ? new Vector2(-(thickness * .5F), 0) : new Vector2(0, thickness * .5F); + } + + // We clamp the start and end points to the pixel grid to avoid anti-aliasing + // when there is no transform. + renderer.BeginFigure(); + renderer.MoveTo(ClampToPixel(a + offset)); + renderer.LineTo(ClampToPixel(b + offset)); + renderer.LineTo(ClampToPixel(c + offset)); + renderer.LineTo(ClampToPixel(d + offset)); + renderer.EndFigure(); + + IPath path = this.Builder.Build(); + + // If the path is degenerate (e.g. zero width line) we just skip it + // and return. This might happen when clamping moves the points. + if (path.Bounds.IsEmpty) + { + this.Builder.Clear(); + return; + } + + this.CurrentPaths.Add(path); + if (this.graphemeBuilder is not null) + { + // Decorations are emitted as independent paths; each layer must point + // at the path index appended for this specific decoration. + this.graphemeBuilder.AddPath(path); + this.graphemeBuilder.AddLayer( + startIndex: this.graphemePathCount, + count: 1, + paint: this.currentLayerPaint, + fillRule: FillRule.NonZero, + bounds: path.Bounds, + kind: GlyphLayerKind.Decoration); + + this.graphemePathCount++; + } + + this.Builder.Clear(); + this.SetDecoration(textDecorations, start, end, thickness); + } + + /// + protected virtual void BeginText(in FontRectangle bounds) + { + } + + /// + /// Called after base-class bookkeeping in IGlyphRenderer.BeginGlyph. + /// Subclasses override this to apply transforms, consult caches, or opt out of + /// outline emission by returning . + /// + /// The font-metric bounding rectangle of the glyph. + /// Identifies the glyph (id, font, layout mode, text run, etc.). + /// + /// to receive outline data and an EndGlyph call; + /// to skip outline emission for this glyph entirely. + /// + protected virtual bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + => true; + + /// + /// Called after the base class has built and registered the glyph path. + /// Subclasses override this to emit drawing operations from the captured path. + /// + protected virtual void EndGlyph() + { + } + + /// + /// Called after the base class has flushed all grapheme aggregates. + /// Subclasses override this for any per-text-block finalization. + /// + protected virtual void EndText() + { + } + + /// + /// Called when a COLR color layer begins. Subclasses override this to + /// capture the layer's paint and composite mode. + /// + /// The paint for this color layer, or for the default foreground. + /// The fill rule to use when rasterizing this layer. + /// Optional clip quad constraining the layer region. + protected virtual void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) + { + } + + /// + /// Called when a COLR color layer ends. Subclasses override this to + /// emit the layer as a drawing operation. + /// + protected virtual void EndLayer() + { + } + + /// + /// Returns the set of text decorations enabled for the current glyph. + /// The font engine calls this to decide which SetDecoration callbacks to emit. + /// Subclasses override this to include decorations implied by rich-text pens + /// (e.g. ). + /// + /// A flags enum of the active text decorations. + public virtual TextDecorations EnabledDecorations() + => this.parameters.TextRun.TextDecorations; + + /// + /// Override point for subclasses to emit decoration drawing operations. + /// Called after the base class has built and registered the decoration path + /// in . + /// + /// The type of decoration (underline, strikeout, or overline). + /// The start position of the decoration line. + /// The end position of the decoration line. + /// The thickness of the decoration line in pixels. + public virtual void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + { + } + + /// + /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Point ClampToPixel(PointF point) => Point.Truncate(point); + + /// + /// Snaps a decoration endpoint to the pixel grid, taking stroke thickness and + /// orientation into account. Even-thickness lines snap to whole pixels; odd-thickness + /// lines snap to half pixels so the stroke center lands on a pixel boundary. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static PointF ClampToPixel(PointF point, int thickness, bool rotated) + { + // Even thickness: snap to whole pixels. + if ((thickness & 1) == 0) + { + return Point.Truncate(point); + } + + // Odd thickness: snap to half pixels along the perpendicular axis + // so the 1px-wide center row/column aligns with physical pixels. + if (rotated) + { + return Point.Truncate(point) + new Vector2(.5F, 0); + } + + return Point.Truncate(point) + new Vector2(0, .5F); + } + + /// + /// Records the start, end, and thickness of a previously emitted decoration line + /// so that the next adjacent decoration can be stitched seamlessly. + /// + private struct TextDecorationDetails + { + /// Gets or sets the start position of the decoration. + public Vector2 Start { get; set; } + + /// Gets or sets the end position of the decoration. + public Vector2 End { get; set; } + + /// Gets or sets the decoration thickness in pixels. + public float Thickness { get; internal set; } + } +} diff --git a/src/ImageSharp.Drawing/Text/GlyphBuilder.cs b/src/ImageSharp.Drawing/Text/GlyphBuilder.cs new file mode 100644 index 000000000..378591ee3 --- /dev/null +++ b/src/ImageSharp.Drawing/Text/GlyphBuilder.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Text; + +/// +/// A rendering surface that Fonts can use to generate shapes. +/// Extends by adding a configurable origin offset +/// so that all captured geometry is translated by the specified amount. +/// +internal class GlyphBuilder : BaseGlyphBuilder +{ + /// + /// Initializes a new instance of the class. + /// + public GlyphBuilder() + : this(Vector2.Zero) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The origin. + public GlyphBuilder(Vector2 origin) => this.Builder.SetOrigin(origin); +} diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphLayerInfo.cs b/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/Text/GlyphLayerInfo.cs rename to src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs index da4a0d4f5..0011f932b 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/GlyphLayerInfo.cs +++ b/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs @@ -100,7 +100,7 @@ private GlyphLayerInfo( /// public GlyphLayerKind Kind { get; } - internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix3x2 matrix) + internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix4x4 matrix) => new( info.StartIndex, info.Count, diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphLayerKind.cs b/src/ImageSharp.Drawing/Text/GlyphLayerKind.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/GlyphLayerKind.cs rename to src/ImageSharp.Drawing/Text/GlyphLayerKind.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/GlyphPathCollection.cs b/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs similarity index 99% rename from src/ImageSharp.Drawing/Shapes/Text/GlyphPathCollection.cs rename to src/ImageSharp.Drawing/Text/GlyphPathCollection.cs index 46d08a9f9..b628e75ea 100644 --- a/src/ImageSharp.Drawing/Shapes/Text/GlyphPathCollection.cs +++ b/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs @@ -68,7 +68,7 @@ internal GlyphPathCollection(List paths, List layers) /// /// A new with the matrix applied to it. /// - public GlyphPathCollection Transform(Matrix3x2 matrix) + public GlyphPathCollection Transform(Matrix4x4 matrix) { List transformed = new(this.paths.Count); diff --git a/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs new file mode 100644 index 000000000..1f6144adb --- /dev/null +++ b/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.ImageSharp.Drawing.Text; + +/// +/// A rendering surface that Fonts can use to generate shapes by following a path. +/// Each glyph is positioned along the path and rotated to match the path tangent +/// at the glyph's horizontal center. +/// +internal sealed class PathGlyphBuilder : GlyphBuilder +{ + /// + /// The path that glyphs are laid out along. Exposed as + /// to access the method for efficient + /// position + tangent queries. + /// + private readonly IPathInternals path; + + /// + /// Initializes a new instance of the class. + /// + /// The path to render the glyphs along. + public PathGlyphBuilder(IPath path) + { + if (path is IPathInternals internals) + { + this.path = internals; + } + else + { + // Wrap in ComplexPolygon to gain IPathInternals. + this.path = new ComplexPolygon(path); + } + } + + /// + protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + // Translate + rotate the glyph to follow the path. Always returns true because + // path-based glyphs are never cached (each has a unique per-position transform). + this.TransformGlyph(in bounds); + return true; + } + + /// + /// Computes the translation + rotation matrix that places a glyph along the path. + /// The glyph's horizontal center is mapped to the path distance, and the glyph + /// is rotated to match the path tangent at that point. + /// + /// The font-metric bounding rectangle of the glyph. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void TransformGlyph(in FontRectangle bounds) + { + // Query the path at the glyph's horizontal center. + Vector2 half = new(bounds.Width * .5F, 0); + SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); + + // Translate so the glyph's top-left aligns with the path point, + // then rotate around the path point to follow the tangent. + Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); + Matrix4x4 matrix = Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); + + this.Builder.SetTransform(matrix); + } +} diff --git a/src/ImageSharp.Drawing/Shapes/Text/TextBuilder.cs b/src/ImageSharp.Drawing/Text/TextBuilder.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/TextBuilder.cs rename to src/ImageSharp.Drawing/Text/TextBuilder.cs diff --git a/src/ImageSharp.Drawing/Shapes/Text/TextUtilities.cs b/src/ImageSharp.Drawing/Text/TextUtilities.cs similarity index 100% rename from src/ImageSharp.Drawing/Shapes/Text/TextUtilities.cs rename to src/ImageSharp.Drawing/Text/TextUtilities.cs diff --git a/src/ImageSharp.Drawing/Utilities/Intersect.cs b/src/ImageSharp.Drawing/Utilities/Intersect.cs deleted file mode 100644 index e20ed9eb9..000000000 --- a/src/ImageSharp.Drawing/Utilities/Intersect.cs +++ /dev/null @@ -1,78 +0,0 @@ -// 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); - - // 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); - intersectionPoint.Y = a0.Y + (t * day); - return true; - } - - return false; - } -} diff --git a/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs b/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs deleted file mode 100644 index b2401ddf9..000000000 --- a/src/ImageSharp.Drawing/Utilities/NumericUtilities.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Drawing.Utilities; - -internal static class NumericUtilities -{ - public static void AddToAllElements(this Span span, float value) - { - ref float current = ref MemoryMarshal.GetReference(span); - ref float max = ref Unsafe.Add(ref current, span.Length); - - if (Vector.IsHardwareAccelerated) - { - int n = span.Length / Vector.Count; - ref Vector currentVec = ref Unsafe.As>(ref current); - ref Vector maxVec = ref Unsafe.Add(ref currentVec, n); - - Vector vecVal = new(value); - while (Unsafe.IsAddressLessThan(ref currentVec, ref maxVec)) - { - currentVec += vecVal; - currentVec = ref Unsafe.Add(ref currentVec, 1); - } - - // current = ref Unsafe.Add(ref current, n * Vector.Count); - current = ref Unsafe.As, float>(ref currentVec); - } - - while (Unsafe.IsAddressLessThan(ref current, ref max)) - { - current += value; - current = ref Unsafe.Add(ref current, 1); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float ClampFloat(float value, float min, float max) - { - if (value >= max) - { - return max; - } - - if (value <= min) - { - return min; - } - - return value; - } -} diff --git a/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs b/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs deleted file mode 100644 index c3a07c111..000000000 --- a/src/ImageSharp.Drawing/Utilities/ThreadLocalBlenderBuffers.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Utilities; - -internal class ThreadLocalBlenderBuffers : IDisposable - where TPixel : unmanaged, IPixel -{ - private readonly ThreadLocal data; - - // amountBufferOnly:true is for SolidBrush, which doesn't need the overlay buffer (it will be dummy) - public ThreadLocalBlenderBuffers(MemoryAllocator allocator, int scanlineWidth, bool amountBufferOnly = false) - => this.data = new ThreadLocal(() => new BufferOwner(allocator, scanlineWidth, amountBufferOnly), true); - - public Span AmountSpan => this.data.Value!.AmountSpan; - - public Span OverlaySpan => this.data.Value!.OverlaySpan; - - /// - public void Dispose() - { - foreach (BufferOwner d in this.data.Values) - { - d.Dispose(); - } - - this.data.Dispose(); - } - - private sealed class BufferOwner : IDisposable - { - private readonly IMemoryOwner amountBuffer; - private readonly IMemoryOwner? overlayBuffer; - - public BufferOwner(MemoryAllocator allocator, int scanlineLength, bool amountBufferOnly) - { - this.amountBuffer = allocator.Allocate(scanlineLength); - this.overlayBuffer = amountBufferOnly ? null : allocator.Allocate(scanlineLength); - } - - public Span AmountSpan => this.amountBuffer.Memory.Span; - - public Span OverlaySpan - { - get - { - if (this.overlayBuffer != null) - { - return this.overlayBuffer.Memory.Span; - } - - return []; - } - } - - public void Dispose() - { - this.amountBuffer.Dispose(); - this.overlayBuffer?.Dispose(); - } - } -} diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 1f2a992f7..9cd3fdadd 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,17 +19,12 @@ - - - + diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs index 6ff494267..7faef9715 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawBeziers.cs @@ -4,7 +4,6 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; -using System.Numerics; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; @@ -19,43 +18,34 @@ public class DrawBeziers [Benchmark(Baseline = true, Description = "System.Drawing Draw Beziers")] public void DrawPathSystemDrawing() { - using (Bitmap destination = new(800, 800)) - using (Graphics graphics = Graphics.FromImage(destination)) - { - graphics.InterpolationMode = InterpolationMode.Default; - graphics.SmoothingMode = SmoothingMode.AntiAlias; - - using (Pen pen = new(System.Drawing.Color.HotPink, 10)) - { - graphics.DrawBeziers( - pen, - [new SDPointF(10, 500), new SDPointF(30, 10), new SDPointF(240, 30), new SDPointF(300, 500)]); - } + using Bitmap destination = new(800, 800); + using Graphics graphics = Graphics.FromImage(destination); + graphics.InterpolationMode = InterpolationMode.Default; + graphics.SmoothingMode = SmoothingMode.AntiAlias; - using (MemoryStream stream = new()) - { - destination.Save(stream, ImageFormat.Bmp); - } + using (Pen pen = new(System.Drawing.Color.HotPink, 10)) + { + graphics.DrawBeziers( + pen, + [new SDPointF(10, 500), new SDPointF(30, 10), new SDPointF(240, 30), new SDPointF(300, 500)]); } + + using MemoryStream stream = new(); + destination.Save(stream, ImageFormat.Bmp); } [Benchmark(Description = "ImageSharp Draw Beziers")] public void DrawLinesCore() { - using (Image image = new(800, 800)) - { - image.Mutate(x => x.DrawBeziers( - Color.HotPink, - 10, - new Vector2(10, 500), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 500))); + using Image image = new(800, 800); + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawBezier( + Processing.Pens.Solid(Color.HotPink, 10), + new PointF(10, 500), + new PointF(30, 10), + new PointF(240, 30), + new PointF(300, 500)))); - using (MemoryStream stream = new()) - { - image.SaveAsBmp(stream); - } - } + using MemoryStream stream = new(); + image.SaveAsBmp(stream); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs index c3080014a..ca32f517b 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawPolygon.cs @@ -8,7 +8,7 @@ using GeoJSON.Net.Feature; using Newtonsoft.Json; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Shapes.Rasterization; +using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Tests; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -38,6 +38,11 @@ public abstract class DrawPolygon private IPath imageSharpPath; private IPath strokedImageSharpPath; + private WebGPUDrawingBackend webGpuBackend; + private Configuration webGpuConfiguration; + private NativeCanvasFrame webGpuNativeFrame; + private nint webGpuNativeTextureHandle; + private nint webGpuNativeTextureViewHandle; protected abstract int Width { get; } @@ -46,7 +51,7 @@ public abstract class DrawPolygon protected abstract float Thickness { get; } protected virtual PointF[][] GetPoints(FeatureCollection features) => - features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))).ToArray(); + [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix4x4.CreateScale(60, 60, 1)))]; [GlobalSetup] public void Setup() @@ -107,6 +112,25 @@ public void Setup() this.image = new Image(this.Width, this.Height); this.isPen = new SolidPen(Color.White, this.Thickness); this.strokedImageSharpPath = this.isPen.GeneratePath(this.imageSharpPath); + this.webGpuBackend = new WebGPUDrawingBackend(); + this.webGpuConfiguration = Configuration.Default.Clone(); + this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); + + if (!WebGPUTestNativeSurfaceAllocator.TryCreate( + this.Width, + this.Height, + out NativeSurface nativeSurface, + out this.webGpuNativeTextureHandle, + out this.webGpuNativeTextureViewHandle, + out string nativeSurfaceError)) + { + throw new InvalidOperationException( + $"Unable to create benchmark native WebGPU target. Error='{nativeSurfaceError}'."); + } + + this.webGpuNativeFrame = new NativeCanvasFrame( + new Rectangle(0, 0, this.Width, this.Height), + nativeSurface); this.sdBitmap = new Bitmap(this.Width, this.Height); this.sdGraphics = Graphics.FromImage(this.sdBitmap); @@ -148,55 +172,33 @@ public void Cleanup() this.skPath.Dispose(); this.image.Dispose(); + WebGPUTestNativeSurfaceAllocator.Release( + this.webGpuNativeTextureHandle, + this.webGpuNativeTextureViewHandle); + this.webGpuNativeTextureHandle = 0; + this.webGpuNativeTextureViewHandle = 0; + this.webGpuBackend.Dispose(); } - [Benchmark] - public void SystemDrawing() - => this.sdGraphics.DrawPath(this.sdPen, this.sdPath); - - // 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 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 => - { - foreach (PointF[] loop in this.points) - { - c.DrawPolygon(Color.White, this.Thickness, loop); - } - }); - [Benchmark(Baseline = true)] public void SkiaSharp() => this.skSurface.Canvas.DrawPath(this.skPath, this.skPaint); [Benchmark] - public IPath ImageSharpStrokeAndClip() => this.isPen.GeneratePath(this.imageSharpPath); + public void SystemDrawing() + => this.sdGraphics.DrawPath(this.sdPen, this.sdPath); + + [Benchmark] + public void ImageSharp() + => this.image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Draw(this.isPen, this.imageSharpPath))); [Benchmark] - public void FillPolygon() => this.image.Mutate(c => c.Fill(Color.White, this.strokedImageSharpPath)); + public void ImageSharpWebGPU() + { + using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, new DrawingOptions()); + canvas.Draw(this.isPen, this.imageSharpPath); + canvas.Flush(); + } } public class DrawPolygonAll : DrawPolygon @@ -220,9 +222,9 @@ protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1); + return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs index 21dedf417..201eeed0f 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawText.cs @@ -84,7 +84,8 @@ public void ImageSharp() Origin = new PointF(10, 10) }; - this.image.Mutate(x => x.DrawText(textOptions, this.TextToRender, Processing.Brushes.Solid(Color.HotPink))); + this.image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.DrawText(textOptions, this.TextToRender, Processing.Brushes.Solid(Color.HotPink), pen: null))); } [Benchmark(Baseline = true)] diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs index 245d7e852..48e9e95fd 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextOutline.cs @@ -60,10 +60,11 @@ public void DrawTextCore() Origin = new PointF(10, 10) }; - image.Mutate(x => x.DrawText( + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText( textOptions, this.TextToRender, - Processing.Pens.Solid(Color.HotPink, 10))); + brush: null, + pen: Processing.Pens.Solid(Color.HotPink, 10)))); } [Benchmark(Description = "ImageSharp Draw Text Outline - Naive")] @@ -79,17 +80,17 @@ public void DrawTextCoreOld() }; image.Mutate( - x => DrawTextOldVersion( - x, + x => x.ProcessWithCanvas(canvas => DrawTextOldVersion( + canvas, new DrawingOptions { GraphicsOptions = { Antialias = true } }, textOptions, this.TextToRender, null, - Processing.Pens.Solid(Color.HotPink, 10))); + Processing.Pens.Solid(Color.HotPink, 10)))); } - static IImageProcessingContext DrawTextOldVersion( - IImageProcessingContext source, + static void DrawTextOldVersion( + IDrawingCanvas canvas, DrawingOptions options, TextOptions textOptions, string text, @@ -97,19 +98,23 @@ static IImageProcessingContext DrawTextOldVersion( Pen pen) { IPathCollection glyphs = TextBuilder.GeneratePaths(text, textOptions); - - DrawingOptions pathOptions = new() { GraphicsOptions = options.GraphicsOptions }; - if (brush != null) + int saveCount = canvas.Save(options); + try { - source.Fill(pathOptions, brush, glyphs); + if (brush != null) + { + canvas.Fill(brush, glyphs); + } + + if (pen != null) + { + canvas.Draw(pen, glyphs); + } } - - if (pen != null) + finally { - source.Draw(pathOptions, pen, glyphs); + canvas.RestoreTo(saveCount); } - - return source; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs new file mode 100644 index 000000000..4f9403a6d --- /dev/null +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; + +[MemoryDiagnoser] +[WarmupCount(5)] +[IterationCount(5)] +public class DrawTextRepeatedGlyphs +{ + public const int Width = 1200; + public const int Height = 280; + + private readonly DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true + } + }; + + private readonly Brush brush = Brushes.Solid(Color.HotPink); + + private Configuration defaultConfiguration; + private Image defaultImage; + private WebGPUDrawingBackend webGpuBackend; + private Configuration webGpuConfiguration; + private NativeCanvasFrame webGpuNativeFrame; + private nint webGpuNativeTextureHandle; + private nint webGpuNativeTextureViewHandle; + private RichTextOptions textOptions; + private string text; + + [Params(200, 1000)] + public int GlyphCount { get; set; } + + [GlobalSetup] + public void Setup() + { + // Tiled rasterization benefits from a warmed worker pool. Doing this once in setup + // reduces first-iteration noise without affecting per-method correctness. + ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads); + int desiredWorkerThreads = Math.Max(minWorkerThreads, Environment.ProcessorCount); + ThreadPool.SetMinThreads(desiredWorkerThreads, minCompletionPortThreads); + Parallel.For(0, desiredWorkerThreads, static _ => { }); + + Font font = SystemFonts.CreateFont("Arial", 48); + this.textOptions = new RichTextOptions(font) + { + Origin = new PointF(8, 8), + WrappingLength = Width - 16 + }; + + this.defaultConfiguration = Configuration.Default; + this.defaultImage = new Image(Width, Height); + this.webGpuBackend = new WebGPUDrawingBackend(); + this.webGpuConfiguration = Configuration.Default.Clone(); + this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); + + if (!WebGPUTestNativeSurfaceAllocator.TryCreate( + Width, + Height, + out NativeSurface nativeSurface, + out this.webGpuNativeTextureHandle, + out this.webGpuNativeTextureViewHandle, + out string nativeSurfaceError)) + { + throw new InvalidOperationException( + $"Unable to create benchmark native WebGPU target. Error='{nativeSurfaceError}'."); + } + + this.webGpuNativeFrame = new NativeCanvasFrame( + new Rectangle(0, 0, Width, Height), + nativeSurface); + + this.text = new string('A', this.GlyphCount); + } + + [GlobalCleanup] + public void Cleanup() + { + this.defaultImage.Dispose(); + WebGPUTestNativeSurfaceAllocator.Release( + this.webGpuNativeTextureHandle, + this.webGpuNativeTextureViewHandle); + this.webGpuNativeTextureHandle = 0; + this.webGpuNativeTextureViewHandle = 0; + this.webGpuBackend.Dispose(); + } + + [Benchmark(Baseline = true, Description = "DrawingCanvas Default Backend")] + public void DrawingCanvasDefaultBackend() + { + MemoryCanvasFrame frame = new(GetFrameRegion(this.defaultImage)); + + using DrawingCanvas canvas = new(this.defaultConfiguration, frame, this.drawingOptions); + canvas.DrawText(this.textOptions, this.text, this.brush, null); + canvas.Flush(); + } + + [Benchmark(Description = "DrawingCanvas WebGPU Backend (NativeSurface)")] + public void DrawingCanvasWebGPUBackendNativeSurface() + { + using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, this.drawingOptions); + canvas.DrawText(this.textOptions, this.text, this.brush, null); + canvas.Flush(); + } + + private static Buffer2DRegion GetFrameRegion(Image image) + => new(image.Frames.RootFrame.PixelBuffer, new Rectangle(0, 0, image.Width, image.Height)); +} diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs index f5c356061..7aed5cd8d 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/EllipseStressTest.cs @@ -23,22 +23,23 @@ public class EllipseStressTest [Benchmark] public void DrawImageSharp() - { - for (int i = 0; i < 20_000; i++) - { - Color brushColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); - Color penColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); - - float r = this.Rand(20f) + 1f; - float x = this.Rand(this.width); - float y = this.Rand(this.height); - EllipsePolygon ellipse = new(new PointF(x, y), r); - this.image.Mutate( - m => - m.Fill(Brushes.Solid(brushColor), ellipse) - .Draw(Pens.Solid(penColor, this.Rand(5)), ellipse)); - } - } + => this.image.Mutate( + m => m.ProcessWithCanvas(canvas => + { + for (int i = 0; i < 20_000; i++) + { + Color brushColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); + Color penColor = Color.FromPixel(new Rgba32((byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255), (byte)this.Rand(255))); + + float r = this.Rand(20f) + 1f; + float x = this.Rand(this.width); + float y = this.Rand(this.height); + EllipsePolygon ellipse = new(new PointF(x, y), r); + + canvas.Fill(Brushes.Solid(brushColor), ellipse); + canvas.Draw(Pens.Solid(penColor, this.Rand(5)), ellipse); + } + })); [GlobalCleanup] public void Cleanup() @@ -49,5 +50,5 @@ public void Cleanup() [MethodImpl(MethodImplOptions.AggressiveInlining)] private float Rand(float x) - => ((float)(((this.random.Next() << 15) | this.random.Next()) & 0x3FFFFFFF) % 1000000) * x / 1000000f; + => Math.Max(0.5f, ((float)(((this.random.Next() << 15) | this.random.Next()) & 0x3FFFFFFF) % 1000000) * x / 1000000f); } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillParis.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillParis.cs new file mode 100644 index 000000000..737499a50 --- /dev/null +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillParis.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Drawing; +using System.Drawing.Drawing2D; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SkiaSharp; +using SDColor = System.Drawing.Color; +using SDPen = System.Drawing.Pen; +using SDSolidBrush = System.Drawing.SolidBrush; + +namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; + +/// +/// Benchmarks rendering a 30k-path Paris map SVG (fill-only, with per-path transforms and opacity) +/// across SkiaSharp, System.Drawing, ImageSharp (CPU), and ImageSharp (WebGPU). +/// +public class FillParis +{ + private const string NeighborhoodPathData = "m652.129 419.702.22 6.474 3.354 3.13 4.571-.215 3.086-3.335-.245-6.464-3.33-3.057-4.585.127z"; + + // The SVG is ~1096x1060 with a Y-flip group transform. + private const float Scale = 1f; + private const int Width = 1096; + private const int Height = 1060; + + private static readonly string SvgFilePath = + TestFile.GetInputFileFullPath(TestImages.Svg.Paris30k); + + private SKSurface skSurface; + private List<(SKPath Path, SKPaint FillPaint, SKPaint StrokePaint)> skElements; + + private Bitmap sdBitmap; + private Graphics sdGraphics; + private List<(GraphicsPath Path, SDSolidBrush Fill, SDPen Stroke)> sdElements; + + private Image image; + private List parsedElements; + private List<(IPath Path, Processing.SolidBrush Fill, SolidPen Stroke)> isElements; + + private WebGPUDrawingBackend webGpuBackend; + private Configuration webGpuConfiguration; + private NativeCanvasFrame webGpuNativeFrame; + private nint webGpuNativeTextureHandle; + private nint webGpuNativeTextureViewHandle; + + [GlobalSetup] + public void Setup() + { + ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads); + int desiredWorkerThreads = Math.Max(minWorkerThreads, Environment.ProcessorCount); + ThreadPool.SetMinThreads(desiredWorkerThreads, minCompletionPortThreads); + Parallel.For(0, desiredWorkerThreads, static _ => { }); + + this.parsedElements = SvgBenchmarkHelper.ParseSvg(SvgFilePath); + + this.skSurface = SKSurface.Create(new SKImageInfo(Width, Height)); + this.skElements = SvgBenchmarkHelper.BuildSkiaElements(this.parsedElements, Scale); + + this.sdBitmap = new Bitmap(Width, Height); + this.sdGraphics = Graphics.FromImage(this.sdBitmap); + this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias; + this.sdElements = SvgBenchmarkHelper.BuildSystemDrawingElements(this.parsedElements, Scale); + + this.image = new Image(Width, Height); + this.isElements = SvgBenchmarkHelper.BuildImageSharpElements(this.parsedElements, Scale); + + this.webGpuBackend = new WebGPUDrawingBackend(); + this.webGpuConfiguration = Configuration.Default.Clone(); + this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); + + if (!WebGPUTestNativeSurfaceAllocator.TryCreate( + Width, + Height, + out NativeSurface nativeSurface, + out this.webGpuNativeTextureHandle, + out this.webGpuNativeTextureViewHandle, + out string nativeSurfaceError)) + { + throw new InvalidOperationException( + $"Unable to create benchmark native WebGPU target. Error='{nativeSurfaceError}'."); + } + + this.webGpuNativeFrame = new NativeCanvasFrame( + new Rectangle(0, 0, Width, Height), + nativeSurface); + } + + [IterationSetup] + public void IterationSetup() + { + this.sdGraphics.Clear(SDColor.Transparent); + this.skSurface.Canvas.Clear(SKColors.Transparent); + } + + [GlobalCleanup] + public void Cleanup() + { + foreach ((SKPath path, SKPaint fill, SKPaint stroke) in this.skElements) + { + path.Dispose(); + fill?.Dispose(); + stroke?.Dispose(); + } + + this.skSurface.Dispose(); + + foreach ((GraphicsPath path, SDSolidBrush fill, SDPen stroke) in this.sdElements) + { + path.Dispose(); + fill?.Dispose(); + stroke?.Dispose(); + } + + this.sdGraphics.Dispose(); + this.sdBitmap.Dispose(); + + this.image.Dispose(); + + WebGPUTestNativeSurfaceAllocator.Release( + this.webGpuNativeTextureHandle, + this.webGpuNativeTextureViewHandle); + this.webGpuNativeTextureHandle = 0; + this.webGpuNativeTextureViewHandle = 0; + this.webGpuBackend.Dispose(); + } + + [Benchmark(Baseline = true)] + public void SkiaSharp() + { + SKCanvas canvas = this.skSurface.Canvas; + foreach ((SKPath path, SKPaint fillPaint, SKPaint strokePaint) in this.skElements) + { + if (fillPaint is not null) + { + canvas.DrawPath(path, fillPaint); + } + + if (strokePaint is not null) + { + canvas.DrawPath(path, strokePaint); + } + } + } + + //[Benchmark] + //public void SystemDrawing() + //{ + // foreach ((GraphicsPath path, SDSolidBrush fill, SDPen stroke) in this.sdElements) + // { + // if (fill is not null) + // { + // this.sdGraphics.FillPath(fill, path); + // } + + // if (stroke is not null) + // { + // this.sdGraphics.DrawPath(stroke, path); + // } + // } + //} + + [Benchmark] + public void ImageSharp() + => this.image.Mutate(c => c.ProcessWithCanvas(canvas => + { + foreach ((IPath path, Processing.SolidBrush fill, SolidPen stroke) in this.isElements) + { + if (fill is not null) + { + canvas.Fill(fill, path); + } + + if (stroke is not null) + { + canvas.Draw(stroke, path); + } + } + })); + + [Benchmark] + public void ImageSharpWebGPU() + { + using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, new DrawingOptions()); + foreach ((IPath path, Processing.SolidBrush fill, SolidPen stroke) in this.isElements) + { + if (fill is not null) + { + canvas.Fill(fill, path); + } + + if (stroke is not null) + { + canvas.Draw(stroke, path); + } + } + + canvas.Flush(); + } + + internal static void VerifyOutput() + { + FillParis bench = new(); + bench.Setup(); + + bench.SkiaSharp(); + // bench.SystemDrawing(); + bench.ImageSharp(); + bench.ImageSharpWebGPU(); + Console.WriteLine($"WebGPU diagnostic last flush used GPU: {bench.webGpuBackend.DiagnosticLastFlushUsedGPU}"); + Console.WriteLine($"WebGPU diagnostic last failure: {bench.webGpuBackend.DiagnosticLastSceneFailure}"); + + SvgBenchmarkHelper.VerifyOutput( + "paris", + Width, + Height, + bench.skSurface, + bench.sdBitmap, + bench.image, + bench.webGpuNativeTextureHandle); + SvgBenchmarkHelper.WriteNeighborhoodSvg( + "paris", + bench.parsedElements, + NeighborhoodPathData, + Width, + Height); + + bench.Cleanup(); + } + + internal static void ProfileCpu(int iterations) + { + FillParis bench = new(); + bench.Setup(); + for (int i = 0; i < iterations; i++) + { + bench.ImageSharp(); + } + + bench.Cleanup(); + } + + internal static void ProfileWebGpu(int iterations) + { + FillParis bench = new(); + bench.Setup(); + for (int i = 0; i < iterations; i++) + { + bench.ImageSharpWebGPU(); + } + + bench.Cleanup(); + } + +} diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs index acac46b09..c83b5880d 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPathGradientBrush.cs @@ -31,6 +31,6 @@ public void FillGradientBrush_ImageSharp() PathGradientBrush brush = new(points, colors, Color.White); - this.image.Mutate(x => x.Fill(brush)); + this.image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.Fill(brush))); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs index 60ba9b199..25b22948c 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillPolygon.cs @@ -36,9 +36,7 @@ public abstract class FillPolygon protected abstract int Height { get; } protected virtual PointF[][] GetPoints(FeatureCollection features) - => features.Features - .SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix3x2.CreateScale(60, 60))) - .ToArray(); + => [.. features.Features.SelectMany(f => PolygonFactory.GetGeoJsonPoints(f, Matrix4x4.CreateScale(60, 60, 1)))]; [GlobalSetup] public void Setup() @@ -48,9 +46,9 @@ public void Setup() FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); this.points = this.GetPoints(featureCollection); - this.polygons = this.points.Select(pts => new Polygon(new LinearLineSegment(pts))).ToArray(); + this.polygons = [.. this.points.Select(pts => new Polygon(new LinearLineSegment(pts)))]; - this.sdPoints = this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray()).ToArray(); + this.sdPoints = [.. this.points.Select(pts => pts.Select(p => new SDPointF(p.X, p.Y)).ToArray())]; this.skPaths = []; foreach (PointF[] ptArr in this.points.Where(pts => pts.Length > 2)) @@ -102,13 +100,13 @@ public void SystemDrawing() [Benchmark] public void ImageSharp() - => this.image.Mutate(c => + => this.image.Mutate(c => c.ProcessWithCanvas(canvas => { foreach (Polygon polygon in this.polygons) { - c.Fill(Color.White, polygon); + canvas.Fill(Processing.Brushes.Solid(Color.White), polygon); } - }); + })); [Benchmark(Baseline = true)] public void SkiaSharp() @@ -144,9 +142,9 @@ protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1); + return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } // ** 11/13/2020 @ Anton's PC *** @@ -174,8 +172,8 @@ protected override PointF[][] GetPoints(FeatureCollection features) { Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Utah"); - Matrix3x2 transform = Matrix3x2.CreateTranslation(-60, -40) - * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); + Matrix4x4 transform = Matrix4x4.CreateTranslation(-60, -40, 0) + * Matrix4x4.CreateScale(60, 60, 1); + return [.. PolygonFactory.GetGeoJsonPoints(state, transform)]; } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs index b5d04ec28..d0be4f328 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillRectangle.cs @@ -3,7 +3,6 @@ using System.Drawing; using System.Drawing.Drawing2D; -using System.Numerics; using BenchmarkDotNet.Attributes; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; @@ -34,7 +33,8 @@ public Size FillRectangleCore() { using (Image image = new(800, 800)) { - image.Mutate(x => x.Fill(Color.HotPink, new Rectangle(10, 10, 190, 140))); + image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.Fill(Processing.Brushes.Solid(Color.HotPink), new Rectangle(10, 10, 190, 140)))); return new Size(image.Width, image.Height); } @@ -45,12 +45,16 @@ public Size FillPolygonCore() { using (Image image = new(800, 800)) { - image.Mutate(x => x.FillPolygon( - Color.HotPink, - new Vector2(10, 10), - new Vector2(200, 10), - new Vector2(200, 150), - new Vector2(10, 150))); + image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.Fill( + Processing.Brushes.Solid(Color.HotPink), + new Polygon( + [ + new PointF(10, 10), + new PointF(200, 10), + new PointF(200, 150), + new PointF(10, 150) + ])))); return new Size(image.Width, image.Height); } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillTiger.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillTiger.cs new file mode 100644 index 000000000..cc5993dc5 --- /dev/null +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillTiger.cs @@ -0,0 +1,223 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Drawing; +using System.Drawing.Drawing2D; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SkiaSharp; +using SDColor = System.Drawing.Color; +using SDPen = System.Drawing.Pen; +using SDSolidBrush = System.Drawing.SolidBrush; + +namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; + +/// +/// Benchmarks rendering the Ghostscript Tiger SVG (~240 path elements with fills and strokes) +/// across SkiaSharp, System.Drawing, ImageSharp (CPU), and ImageSharp (WebGPU). +/// +public class FillTiger +{ + private const float Scale = 4f; + private const int Width = 800; + private const int Height = 800; + + private static readonly string SvgFilePath = + TestFile.GetInputFileFullPath(TestImages.Svg.GhostscriptTiger); + + private SKSurface skSurface; + private List<(SKPath Path, SKPaint FillPaint, SKPaint StrokePaint)> skElements; + + private Bitmap sdBitmap; + private Graphics sdGraphics; + private List<(GraphicsPath Path, SDSolidBrush Fill, SDPen Stroke)> sdElements; + + private Image image; + private List<(IPath Path, Processing.SolidBrush Fill, SolidPen Stroke)> isElements; + + private WebGPUDrawingBackend webGpuBackend; + private Configuration webGpuConfiguration; + private NativeCanvasFrame webGpuNativeFrame; + private nint webGpuNativeTextureHandle; + private nint webGpuNativeTextureViewHandle; + + [GlobalSetup] + public void Setup() + { + ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads); + int desiredWorkerThreads = Math.Max(minWorkerThreads, Environment.ProcessorCount); + ThreadPool.SetMinThreads(desiredWorkerThreads, minCompletionPortThreads); + Parallel.For(0, desiredWorkerThreads, static _ => { }); + + List elements = SvgBenchmarkHelper.ParseSvg(SvgFilePath); + + this.skSurface = SKSurface.Create(new SKImageInfo(Width, Height)); + this.skElements = SvgBenchmarkHelper.BuildSkiaElements(elements, Scale); + + this.sdBitmap = new Bitmap(Width, Height); + this.sdGraphics = Graphics.FromImage(this.sdBitmap); + this.sdGraphics.SmoothingMode = SmoothingMode.AntiAlias; + this.sdElements = SvgBenchmarkHelper.BuildSystemDrawingElements(elements, Scale); + + this.image = new Image(Width, Height); + this.isElements = SvgBenchmarkHelper.BuildImageSharpElements(elements, Scale); + + this.webGpuBackend = new WebGPUDrawingBackend(); + this.webGpuConfiguration = Configuration.Default.Clone(); + this.webGpuConfiguration.SetDrawingBackend(this.webGpuBackend); + + if (!WebGPUTestNativeSurfaceAllocator.TryCreate( + Width, + Height, + out NativeSurface nativeSurface, + out this.webGpuNativeTextureHandle, + out this.webGpuNativeTextureViewHandle, + out string nativeSurfaceError)) + { + throw new InvalidOperationException( + $"Unable to create benchmark native WebGPU target. Error='{nativeSurfaceError}'."); + } + + this.webGpuNativeFrame = new NativeCanvasFrame( + new Rectangle(0, 0, Width, Height), + nativeSurface); + } + + [IterationSetup] + public void IterationSetup() + { + this.sdGraphics.Clear(SDColor.Transparent); + this.skSurface.Canvas.Clear(SKColors.Transparent); + } + + [GlobalCleanup] + public void Cleanup() + { + foreach ((SKPath path, SKPaint fill, SKPaint stroke) in this.skElements) + { + path.Dispose(); + fill?.Dispose(); + stroke?.Dispose(); + } + + this.skSurface.Dispose(); + + foreach ((GraphicsPath path, SDSolidBrush fill, SDPen stroke) in this.sdElements) + { + path.Dispose(); + fill?.Dispose(); + stroke?.Dispose(); + } + + this.sdGraphics.Dispose(); + this.sdBitmap.Dispose(); + + this.image.Dispose(); + + WebGPUTestNativeSurfaceAllocator.Release( + this.webGpuNativeTextureHandle, + this.webGpuNativeTextureViewHandle); + this.webGpuNativeTextureHandle = 0; + this.webGpuNativeTextureViewHandle = 0; + this.webGpuBackend.Dispose(); + } + + [Benchmark(Baseline = true)] + public void SkiaSharp() + { + SKCanvas canvas = this.skSurface.Canvas; + foreach ((SKPath path, SKPaint fillPaint, SKPaint strokePaint) in this.skElements) + { + if (fillPaint is not null) + { + canvas.DrawPath(path, fillPaint); + } + + if (strokePaint is not null) + { + canvas.DrawPath(path, strokePaint); + } + } + } + + [Benchmark] + public void SystemDrawing() + { + foreach ((GraphicsPath path, SDSolidBrush fill, SDPen stroke) in this.sdElements) + { + if (fill is not null) + { + this.sdGraphics.FillPath(fill, path); + } + + if (stroke is not null) + { + this.sdGraphics.DrawPath(stroke, path); + } + } + } + + [Benchmark] + public void ImageSharp() + => this.image.Mutate(c => c.ProcessWithCanvas(canvas => + { + foreach ((IPath path, Processing.SolidBrush fill, SolidPen stroke) in this.isElements) + { + if (fill is not null) + { + canvas.Fill(fill, path); + } + + if (stroke is not null) + { + canvas.Draw(stroke, path); + } + } + })); + + [Benchmark] + public void ImageSharpWebGPU() + { + using DrawingCanvas canvas = new(this.webGpuConfiguration, this.webGpuNativeFrame, new DrawingOptions()); + foreach ((IPath path, Processing.SolidBrush fill, SolidPen stroke) in this.isElements) + { + if (fill is not null) + { + canvas.Fill(fill, path); + } + + if (stroke is not null) + { + canvas.Draw(stroke, path); + } + } + + canvas.Flush(); + } + + public static void VerifyOutput() + { + FillTiger bench = new(); + bench.Setup(); + + bench.SkiaSharp(); + bench.SystemDrawing(); + bench.ImageSharp(); + bench.ImageSharpWebGPU(); + + //SvgBenchmarkHelper.VerifyOutput( + // "tiger", + // Width, + // Height, + // bench.skSurface, + // bench.sdBitmap, + // bench.image, + // bench.webGpuNativeTextureHandle); + + bench.Cleanup(); + } +} diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs index 63fc8bcc6..b0b250c4b 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/FillWithPattern.cs @@ -17,34 +17,27 @@ public class FillWithPattern [Benchmark(Baseline = true, Description = "System.Drawing Fill with Pattern")] public void DrawPatternPolygonSystemDrawing() { - using (Bitmap destination = new(800, 800)) - using (Graphics graphics = Graphics.FromImage(destination)) - { - graphics.SmoothingMode = SmoothingMode.AntiAlias; - - using (HatchBrush brush = new(HatchStyle.BackwardDiagonal, System.Drawing.Color.HotPink)) - { - graphics.FillRectangle(brush, new SDRectangle(0, 0, 800, 800)); // can't find a way to flood fill with a brush - } + using Bitmap destination = new(800, 800); + using Graphics graphics = Graphics.FromImage(destination); + graphics.SmoothingMode = SmoothingMode.AntiAlias; - using (MemoryStream stream = new()) - { - destination.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp); - } + using (HatchBrush brush = new(HatchStyle.BackwardDiagonal, System.Drawing.Color.HotPink)) + { + graphics.FillRectangle(brush, new SDRectangle(0, 0, 800, 800)); // can't find a way to flood fill with a brush } + + using MemoryStream stream = new(); + destination.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp); } [Benchmark(Description = "ImageSharp Fill with Pattern")] public void DrawPatternPolygon3Core() { - using (Image image = new(800, 800)) - { - image.Mutate(x => x.Fill(CoreBrushes.BackwardDiagonal(Color.HotPink))); + using Image image = new(800, 800); + image.Mutate(x => x.ProcessWithCanvas( + canvas => canvas.Fill(CoreBrushes.BackwardDiagonal(Color.HotPink)))); - using (MemoryStream stream = new()) - { - image.SaveAsBmp(stream); - } - } + using MemoryStream stream = new(); + image.SaveAsBmp(stream); } } diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs deleted file mode 100644 index 45f09159f..000000000 --- a/tests/ImageSharp.Drawing.Benchmarks/Drawing/Rounding.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; -using BenchmarkDotNet.Attributes; - -namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; -public class Rounding -{ - private PointF[] vertices; - private float[] destination; - private float[] destinationSse41; - private float[] destinationAvx; - - [GlobalSetup] - public void Setup() - { - this.vertices = new PointF[1000]; - this.destination = new float[this.vertices.Length]; - this.destinationSse41 = new float[this.vertices.Length]; - this.destinationAvx = new float[this.vertices.Length]; - Random r = new(42); - for (int i = 0; i < this.vertices.Length; i++) - { - this.vertices[i] = new PointF((float)r.NextDouble(), (float)r.NextDouble()); - } - } - - [Benchmark] - public void RoundYAvx() => RoundYAvx(this.vertices, this.destinationAvx, 16); - - [Benchmark] - public void RoundYSse41() => RoundYSse41(this.vertices, this.destinationSse41, 16); - - [Benchmark(Baseline = true)] - public void RoundY() => RoundY(this.vertices, this.destination, 16); - - private static void RoundYAvx(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); - - // 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.Multiply(pointsY, ssRatio)); - Unsafe.Add(ref destinationBase, i) = Avx.Multiply(rounded, inverseSsRatio); - } - } - } - - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; - } - } - - private static void RoundYSse41(ReadOnlySpan vertices, Span destination, float subsamplingRatio) - { - int ri = 0; - 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); - - // 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 points1Y = Sse.Shuffle(points1, points1, 0b11_01_11_01); - Vector128 points2Y = Sse.Shuffle(points2, points2, 0b11_01_11_01); - Vector128 pointsY = Vector128.Create(points1Y.GetLower(), points2Y.GetLower()); - - // 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.Multiply(pointsY, ssRatio)); - Unsafe.Add(ref destinationBase, i) = Sse.Multiply(rounded, inverseSsRatio); - } - } - } - - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; - } - } - - private static void RoundY(ReadOnlySpan vertices, Span destination, float subsamplingRatio) - { - int ri = 0; - for (; ri < vertices.Length; ri++) - { - destination[ri] = MathF.Round(vertices[ri].Y * subsamplingRatio, MidpointRounding.AwayFromZero) / subsamplingRatio; - } - } -} diff --git a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj index 0a2f32ce1..ed7507c0b 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj +++ b/tests/ImageSharp.Drawing.Benchmarks/ImageSharp.Drawing.Benchmarks.csproj @@ -9,11 +9,16 @@ - + + - CA1822 - CA1416 + + + CA1822;CA1416;CA1001;CS0029;CA1861;CA2201 @@ -40,6 +45,7 @@ + @@ -55,6 +61,9 @@ TestEnvironment.cs + + SvgBenchmarkHelper.cs + diff --git a/tests/ImageSharp.Drawing.Benchmarks/Program.cs b/tests/ImageSharp.Drawing.Benchmarks/Program.cs index 9822ba4ea..24f4995d7 100644 --- a/tests/ImageSharp.Drawing.Benchmarks/Program.cs +++ b/tests/ImageSharp.Drawing.Benchmarks/Program.cs @@ -8,7 +8,6 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; using BenchmarkDotNet.Running; -using BenchmarkDotNet.Toolchains.InProcess.Emit; namespace SixLabors.ImageSharp.Drawing.Benchmarks; @@ -22,14 +21,15 @@ public InProcessConfig() this.AddExporter(DefaultExporters.Html, DefaultExporters.Csv); - // Use a long, stable job for rasterization benchmarks where scheduler noise and - // thread-pool startup can otherwise dominate short in-process runs. + // Use high warmup to ensure tiered JIT has fully promoted all hot paths. + // Server GC reduces pause times for allocation-heavy rasterization benchmarks. this.AddJob( Job.Default .WithLaunchCount(3) - .WithWarmupCount(15) + .WithWarmupCount(40) .WithIterationCount(40) - .WithToolchain(InProcessEmitToolchain.Instance)); + .WithGcServer(true) + .WithGcForce(false)); } } @@ -37,6 +37,47 @@ public class Program { public static void Main(string[] args) { + if (args.Length > 0 && args[0] == "--verify") + { + string target = args.Length > 1 ? args[1] : "tiger"; + switch (target.ToLowerInvariant()) + { + case "tiger": + Drawing.FillTiger.VerifyOutput(); + break; + case "paris": + Drawing.FillParis.VerifyOutput(); + break; + default: + Console.WriteLine($"Unknown verify target: {target}. Use 'tiger' or 'paris'."); + break; + } + + return; + } + + if (args.Length > 0 && args[0] == "--profile") + { + string target = args.Length > 1 ? args[1] : string.Empty; + int iterations = args.Length > 2 && int.TryParse(args[2], out int parsedIterations) + ? parsedIterations + : 20; + switch (target.ToLowerInvariant()) + { + case "paris-cpu": + Drawing.FillParis.ProfileCpu(iterations); + break; + case "paris-webgpu": + Drawing.FillParis.ProfileWebGpu(iterations); + break; + default: + Console.WriteLine($"Unknown profile target: {target}. Use 'paris-cpu' or 'paris-webgpu'."); + break; + } + + return; + } + new BenchmarkSwitcher(typeof(Program).GetTypeInfo().Assembly).Run(args, new InProcessConfig()); } } diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ClearSolidBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ClearSolidBrushTests.cs deleted file mode 100644 index cc2fc17f9..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/ClearSolidBrushTests.cs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class ClearSolidBrushTests -{ - [Theory] - [WithBlankImage(1, 1, PixelTypes.Rgba32)] - [WithBlankImage(7, 4, PixelTypes.Rgba32)] - [WithBlankImage(16, 7, PixelTypes.Rgba32)] - [WithBlankImage(33, 32, PixelTypes.Rgba32)] - [WithBlankImage(400, 500, PixelTypes.Rgba32)] - public void DoesNotDependOnSize(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Clear(color)); - - image.DebugSave(provider, appendPixelTypeToFileName: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] - public void DoesNotDependOnSinglePixelType(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Clear(color)); - - image.DebugSave(provider, appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] - [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void WhenColorIsOpaque_OverridePreviousColor( - TestImageProvider provider, - string newColorName) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = TestUtils.GetColorByName(newColorName); - image.Mutate(c => c.Clear(color)); - - image.DebugSave( - provider, - newColorName, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] - [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void ClearAlwaysOverridesPreviousColor( - TestImageProvider provider, - string newColorName) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = TestUtils.GetColorByName(newColorName); - color = color.WithAlpha(0.5f); - - image.Mutate(c => c.Clear(color)); - - image.DebugSave( - provider, - newColorName, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion(TestImageProvider provider, int x0, int y0, int w, int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTest(c => c.Clear(color, region), testDetails, ImageComparer.Exact); - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion_WorksOnWrappedMemoryImage( - TestImageProvider provider, - int x0, - int y0, - int w, - int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTestOnWrappedMemoryImage( - c => c.Clear(color, region), - testDetails, - ImageComparer.Exact, - useReferenceOutputFrom: nameof(this.FillRegion)); - } - - public static readonly TheoryData BlendData = - new() - { - { false, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - }; -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs deleted file mode 100644 index c8892b878..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class ClipTests -{ - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -100, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20, 0.5)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 40, 60, 0.2)] - public void Clip(TestImageProvider provider, float dx, float dy, float sizeMult) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"offset_x{dx}_y{dy}"; - provider.RunValidatingProcessorTest( - x => - { - Size size = x.GetCurrentSize(); - int outerRadii = (int)(Math.Min(size.Width, size.Height) * sizeMult); - Star star = new(new PointF(size.Width / 2, size.Height / 2), 5, outerRadii / 2, outerRadii); - - Matrix3x2 builder = Matrix3x2.CreateTranslation(new Vector2(dx, dy)); - x.Clip(star.Transform(builder), x => x.DetectEdges()); - }, - testOutputDetails: testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)] - public void Clip_ConstrainsOperationToClipBounds(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.RunValidatingProcessorTest( - x => - { - Size size = x.GetCurrentSize(); - RectangleF rect = new(0, 0, size.Width / 2, size.Height / 2); - RectangularPolygon clipRect = new(rect); - x.Clip(clipRect, ctx => ctx.Flip(FlipMode.Vertical)); - }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - [Fact] - public void Issue250_Vertical_Horizontal_Count_Should_Match() - { - PathCollection clip = new(new RectangularPolygon(new PointF(24, 16), new PointF(777, 385))); - - Path vert = new(new LinearLineSegment(new PointF(26, 384), new PointF(26, 163))); - Path horiz = new(new LinearLineSegment(new PointF(26, 163), new PointF(176, 163))); - - IPath reverse = vert.Clip(clip); - IEnumerable> result1 = vert.Clip(reverse).Flatten().Select(x => x.Points); - - reverse = horiz.Clip(clip); - IEnumerable> result2 = horiz.Clip(reverse).Flatten().Select(x => x.Points); - - bool same = result1.Count() == result2.Count(); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ComputeLength.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ComputeLength.cs similarity index 91% rename from tests/ImageSharp.Drawing.Tests/Drawing/Paths/ComputeLength.cs rename to tests/ImageSharp.Drawing.Tests/Drawing/ComputeLength.cs index d7d7bb4a1..cba7efcec 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ComputeLength.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/ComputeLength.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; +namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; public class ComputeLength { diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs deleted file mode 100644 index 550590187..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawBezierTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawBezierTests -{ - public static readonly TheoryData DrawPathData - = new() - { - { "White", 255, 1.5f }, - { "Red", 255, 3 }, - { "HotPink", 255, 5 }, - { "HotPink", 150, 5 }, - { "White", 255, 15 }, - }; - - [Theory] - [WithSolidFilledImages(nameof(DrawPathData), 300, 450, "Blue", PixelTypes.Rgba32)] - public void DrawBeziers(TestImageProvider provider, string colorName, byte alpha, float thickness) - where TPixel : unmanaged, IPixel - { - PointF[] points = - [ - new Vector2(10, 400), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 400) - ]; - - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); - - FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; - - provider.RunValidatingProcessorTest( - x => x.DrawBeziers(color, 5f, points), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs deleted file mode 100644 index 34026ab54..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawComplexPolygonTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -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, false, false, true)] - public void DrawComplexPolygon(TestImageProvider provider, bool overlap, bool transparent, bool dashed) - where TPixel : unmanaged, IPixel - { - Polygon simplePath = new(new LinearLineSegment( - new Vector2(10, 10), - new Vector2(200, 150), - new Vector2(50, 300))); - - Polygon hole1 = new(new LinearLineSegment( - new Vector2(37, 85), - overlap ? new Vector2(130, 40) : new Vector2(93, 85), - new Vector2(65, 137))); - - IPath clipped = simplePath.Clip(hole1); - - Color color = Color.White; - if (transparent) - { - color = color.WithAlpha(150 / 255F); - } - - string testDetails = string.Empty; - if (overlap) - { - testDetails += "_Overlap"; - } - - if (transparent) - { - testDetails += "_Transparent"; - } - - if (dashed) - { - testDetails += "_Dashed"; - } - - Pen pen = dashed ? Pens.Dash(color, 5f) : Pens.Solid(color, 5f); - - // clipped = new RectangularPolygon(RectangleF.FromLTRB(60, 260, 200, 280)); - - provider.RunValidatingProcessorTest( - x => x.Draw(pen, clipped), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs deleted file mode 100644 index b2ba8752b..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawLinesTests.cs +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawLinesTests -{ - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 2.5, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, 10, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, 10, true)] - public void DrawLines_Simple(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1f, true)] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5f, true)] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1f, false)] - [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5f, false)] - public void DrawLinesInvalidPoints(TestImageProvider provider, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - SolidPen pen = new(Color.Black, thickness); - PointF[] path = [new Vector2(15f, 15f), new Vector2(15f, 15f)]; - - GraphicsOptions options = new() - { - Antialias = antialias - }; - - string aa = antialias ? string.Empty : "_NoAntialias"; - FormattableString outputDetails = $"T({thickness}){aa}"; - - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).DrawLine(pen, path), - outputDetails, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] - public void DrawLines_Dash(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.Dash(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "LightGreen", 1f, 5, false)] - public void DrawLines_Dot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.Dot(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, false)] - public void DrawLines_DashDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.DashDot(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Black", 1f, 5, false)] - public void DrawLines_DashDotDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - Pen pen = Pens.DashDotDot(color, thickness); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, true)] - public void DrawLines_EndCapRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) - { - StrokeOptions = new StrokeOptions { LineCap = LineCap.Round }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, true)] - public void DrawLines_EndCapButt(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) - { - StrokeOptions = new StrokeOptions { LineCap = LineCap.Butt }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 5, true)] - public void DrawLines_EndCapSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - PatternPen pen = new(new PenOptions(color, thickness, [3f, 3f]) - { - StrokeOptions = new StrokeOptions { LineCap = LineCap.Square }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 10, true)] - public void DrawLines_JointStyleRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) - { - StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 10, true)] - public void DrawLines_JointStyleSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) - { - StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Bevel }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1f, 10, true)] - public void DrawLines_JointStyleMiter(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) -where TPixel : unmanaged, IPixel - { - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - SolidPen pen = new(new PenOptions(color, thickness) - { - StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter }, - }); - - DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); - } - - private static void DrawLinesImpl( - TestImageProvider provider, - string colorName, - float alpha, - float thickness, - bool antialias, - Pen pen) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = [new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300)]; - - GraphicsOptions options = new() - { Antialias = antialias }; - - string aa = antialias ? string.Empty : "_NoAntialias"; - FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; - - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).DrawLine(pen, simplePath), - outputDetails, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs deleted file mode 100644 index b01b83c11..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPathTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawPathTests -{ - public static readonly TheoryData DrawPathData = - new() - { - { "White", 255, 1.5f }, - { "Red", 255, 3 }, - { "HotPink", 255, 5 }, - { "HotPink", 150, 5 }, - { "White", 255, 15 }, - }; - - [Theory] - [WithSolidFilledImages(nameof(DrawPathData), 300, 600, "Blue", PixelTypes.Rgba32)] - public void DrawPath(TestImageProvider provider, string colorName, byte alpha, float thickness) - where TPixel : unmanaged, IPixel - { - LinearLineSegment linearSegment = new( - new Vector2(10, 10), - new Vector2(200, 150), - new Vector2(50, 300)); - CubicBezierLineSegment bezierSegment = new( - new Vector2(50, 300), - new Vector2(500, 500), - new Vector2(60, 10), - new Vector2(10, 400)); - - ArcLineSegment ellipticArcSegment1 = new(new Vector2(10, 400), new Vector2(150, 450), new SizeF((float)Math.Sqrt(5525), 40), GeometryUtilities.RadianToDegree((float)Math.Atan2(25, 70)), true, true); - ArcLineSegment ellipticArcSegment2 = new(new PointF(150, 450), new PointF(149F, 450), new SizeF(140, 70), 0, true, true); - - Path path = new(linearSegment, bezierSegment, ellipticArcSegment1, ellipticArcSegment2); - - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); - - FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; - - provider.RunValidatingProcessorTest( - x => x.Draw(color, thickness, path), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(256, 256, "Black", PixelTypes.Rgba32)] - public void PathExtendingOffEdgeOfImageShouldNotBeCropped(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Color color = Color.White; - SolidPen pen = Pens.Solid(color, 5f); - - provider.RunValidatingProcessorTest( - x => - { - for (int i = 0; i < 300; i += 20) - { - PointF[] points = [new Vector2(100, 2), new Vector2(-10, i)]; - x.DrawLine(pen, points); - } - }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(40, 40, "White", PixelTypes.Rgba32)] - public void DrawPathClippedOnTop(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] points = - [ - new(10f, -10f), - new(20f, 20f), - new(30f, -30f) - ]; - - IPath path = new PathBuilder().AddLines(points).Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Draw(Color.Black, 1, path)), - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 360)] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 359)] - public void DrawCircleUsingAddArc(TestImageProvider provider, float sweep) - where TPixel : unmanaged, IPixel - { - IPath path = new PathBuilder().AddArc(new Point(150, 150), 50, 50, 0, 40, sweep).Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Draw(Color.Black, 1, path)), - testOutputDetails: $"{sweep}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, true)] - [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, false)] - public void DrawCircleUsingArcTo(TestImageProvider provider, bool sweep) - where TPixel : unmanaged, IPixel - { - Point origin = new(150, 150); - IPath path = new PathBuilder().MoveTo(origin).ArcTo(50, 50, 0, true, sweep, origin).Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Draw(Color.Black, 1, path)), - testOutputDetails: $"{sweep}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawPolygonTests.cs deleted file mode 100644 index 425bdba81..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawPolygonTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawPolygonTests -{ - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 2.5, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, 10, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, 5, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, 10, true)] - public void DrawPolygon(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) - ]; - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - - GraphicsOptions options = new() { Antialias = antialias }; - - string aa = antialias ? string.Empty : "_NoAntialias"; - FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; - - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).DrawPolygon(color, thickness, simplePath), - outputDetails, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] - public void DrawPolygon_Transformed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) - ]; - - provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200))) - .DrawPolygon(Color.White, 2.5f, simplePath)); - } - - [Theory] - [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] - public void DrawRectangularPolygon_Transformed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - RectangularPolygon polygon = new(25, 25, 50, 50); - - provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) - .Draw(Color.White, 2.5f, polygon)); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs deleted file mode 100644 index 00b538d66..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingProfilingBenchmarks.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using GeoJSON.Net.Feature; -using Newtonsoft.Json; -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -public class DrawingProfilingBenchmarks : IDisposable -{ - private readonly Image image; - private readonly Polygon[] polygons; - - public DrawingProfilingBenchmarks() - { - string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); - - FeatureCollection featureCollection = JsonConvert.DeserializeObject(jsonContent); - - PointF[][] points = GetPoints(featureCollection); - this.polygons = points.Select(pts => new Polygon(new LinearLineSegment(pts))).ToArray(); - - this.image = new Image(1000, 1000); - - static PointF[][] GetPoints(FeatureCollection features) - { - Feature state = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); - return PolygonFactory.GetGeoJsonPoints(state, transform).ToArray(); - } - } - - [Theory(Skip = "For local profiling only")] - [InlineData(IntersectionRule.EvenOdd)] - [InlineData(IntersectionRule.NonZero)] - public void FillPolygon(IntersectionRule intersectionRule) - { - const int times = 100; - - for (int i = 0; i < times; i++) - { - this.image.Mutate(c => - { - c.SetShapeOptions(new ShapeOptions() - { - IntersectionRule = intersectionRule - }); - foreach (Polygon polygon in this.polygons) - { - c.Fill(Color.White, polygon); - } - }); - } - } - - [Theory(Skip = "For local profiling only")] - [InlineData(1)] - [InlineData(10)] - public void DrawText(int textIterations) - { - const int times = 20; - const string textPhrase = "asdfghjkl123456789{}[]+$%?"; - string textToRender = string.Join("/n", Enumerable.Repeat(textPhrase, textIterations)); - - Font font = SystemFonts.CreateFont("Arial", 12); - SolidBrush brush = Brushes.Solid(Color.HotPink); - RichTextOptions textOptions = new(font) - { - WrappingLength = 780, - Origin = new PointF(10, 10) - }; - - for (int i = 0; i < times; i++) - { - this.image.Mutate(x => x - .SetGraphicsOptions(o => o.Antialias = true) - .DrawText( - textOptions, - textToRender, - brush)); - } - } - - public void Dispose() => this.image.Dispose(); -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs deleted file mode 100644 index 6d230ee2c..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/DrawingRobustnessTests.cs +++ /dev/null @@ -1,364 +0,0 @@ -// 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; -using SkiaSharp; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class DrawingRobustnessTests -{ - [Theory(Skip = "For local testing")] - [WithSolidFilledImages(32, 32, "Black", PixelTypes.Rgba32)] - public void CompareToSkiaResults_SmallCircle(TestImageProvider provider) - { - EllipsePolygon circle = new(16, 16, 10); - - CompareToSkiaResultsImpl(provider, circle); - } - - [Theory(Skip = "For local testing")] - [WithSolidFilledImages(64, 64, "Black", PixelTypes.Rgba32)] - public void CompareToSkiaResults_StarCircle(TestImageProvider provider) - { - EllipsePolygon circle = new(32, 32, 30); - Star star = new(32, 32, 7, 10, 27); - IPath shape = circle.Clip(star); - - CompareToSkiaResultsImpl(provider, shape); - } - - private static void CompareToSkiaResultsImpl(TestImageProvider provider, IPath shape) - { - using Image image = provider.GetImage(); - image.Mutate(c => c.Fill(Color.White, shape)); - image.DebugSave(provider, "ImageSharp", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - - using SKBitmap bitmap = new(new SKImageInfo(image.Width, image.Height)); - - using SKPath skPath = new(); - - foreach (ISimplePath loop in shape.Flatten()) - { - ReadOnlySpan points = MemoryMarshal.Cast(loop.Points.Span); - skPath.AddPoly(points.ToArray()); - } - - using SKPaint paint = new() - { - Style = SKPaintStyle.Fill, - Color = SKColors.White, - IsAntialias = true, - }; - - using SKCanvas canvas = new(bitmap); - canvas.Clear(new SKColor(0, 0, 0)); - canvas.DrawPath(skPath, paint); - - using Image skResultImage = - Image.LoadPixelData(bitmap.GetPixelSpan(), image.Width, image.Height); - skResultImage.DebugSave( - provider, - "SkiaSharp", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - ImageSimilarityReport result = ImageComparer.Exact.CompareImagesOrFrames(image, skResultImage); - throw new ImagesSimilarityException(result.DifferencePercentageString); - } - - [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) - { - string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile)); - - PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix3x2.CreateScale(sx, sy)); - - using Image image = provider.GetImage(); - DrawingOptions options = new() - { - GraphicsOptions = new GraphicsOptions() { Antialias = aa > 0 }, - }; - foreach (PointF[] loop in points) - { - image.Mutate(c => c.DrawLine(options, Color.White, 1.0f, loop)); - } - - string details = $"_{System.IO.Path.GetFileName(geoJsonFile)}_{sx}x{sy}_aa{aa}"; - - image.DebugSave( - provider, - details, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(7200, 3300, "Black", PixelTypes.Rgba32)] - public void LargeGeoJson_States_Fill(TestImageProvider provider) - { - 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 static Image FillGeoJsonPolygons(TestImageProvider provider, string geoJsonFile, bool aa, Vector2 scale, Vector2 pixelOffset) - { - string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile)); - - PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix3x2.CreateScale(scale) * Matrix3x2.CreateTranslation(pixelOffset)); - - Image image = provider.GetImage(); - DrawingOptions options = new() - { - GraphicsOptions = new GraphicsOptions() { Antialias = aa }, - }; - Random rnd = new(42); - byte[] rgb = new byte[3]; - foreach (PointF[] loop in points) - { - rnd.NextBytes(rgb); - - Color color = Color.FromPixel(new Rgb24(rgb[0], rgb[1], rgb[2])); - image.Mutate(c => c.FillPolygon(options, color, loop)); - } - - return image; - } - - [Theory] - [WithSolidFilledImages(400, 400, "Black", PixelTypes.Rgba32, 0)] - [WithSolidFilledImages(6000, 6000, "Black", PixelTypes.Rgba32, 5500)] - public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, int pixelOffset) - { - 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) - * Matrix3x2.CreateTranslation(pixelOffset, pixelOffset); - IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); - - using Image image = provider.GetImage(); - foreach (PointF[] loop in points) - { - image.Mutate(c => c.DrawLine(Color.White, 1.0f, loop)); - } - - // Strict comparer, because the image is sparse: - ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); - - string details = $"PixelOffset({pixelOffset})"; - image.DebugSave(provider, details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(400 * 3, 400 * 3, "Black", PixelTypes.Rgba32, 3)] - [WithSolidFilledImages(400 * 5, 400 * 5, "Black", PixelTypes.Rgba32, 5)] - [WithSolidFilledImages(400 * 10, 400 * 10, "Black", PixelTypes.Rgba32, 10)] - public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provider, int scale) - { - string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); - - FeatureCollection features = JsonConvert.DeserializeObject(jsonContent); - - Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); - - Matrix3x2 transform = Matrix3x2.CreateTranslation(-87, -54) - * Matrix3x2.CreateScale(60, 60); - IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); - - using Image image = provider.GetImage(); - var pen = new SolidPen(new SolidBrush(Color.White), 1.0f); - foreach (PointF[] loop in points) - { - IPath outline = pen.GeneratePath(new Path(loop).Transform(Matrix3x2.CreateTranslation(0.5F, 0.5F))); - outline = outline.Transform(Matrix3x2.CreateScale(scale, scale)); - image.Mutate(c => c.Fill(pen.StrokeFill, outline)); - } - - // Strict comparer, because the image is sparse: - ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); - - string details = $"Scale({scale})"; - image.DebugSave(provider, details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - } - - [Theory(Skip = "For local experiments only")] - [InlineData(0)] - [InlineData(5000)] - [InlineData(9000)] - public void Missisippi_Skia(int offset) - { - 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) - * Matrix3x2.CreateTranslation(offset, offset); - IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); - - SKPath path = new(); - - foreach (PointF[] pts in points.Where(p => p.Length > 2)) - { - path.MoveTo(pts[0].X, pts[0].Y); - - for (int i = 0; i < pts.Length; i++) - { - path.LineTo(pts[i].X, pts[i].Y); - } - - path.LineTo(pts[0].X, pts[0].Y); - } - - SKImageInfo imageInfo = new(10000, 10000); - - using SKPaint paint = new() - { - Style = SKPaintStyle.Stroke, - Color = SKColors.White, - StrokeWidth = 1f, - IsAntialias = true, - }; - - using SKSurface surface = SKSurface.Create(imageInfo); - SKCanvas canvas = surface.Canvas; - canvas.Clear(new SKColor(0, 0, 0)); - canvas.DrawPath(path, paint); - - string outDir = TestEnvironment.CreateOutputDirectory("Skia"); - string fn = System.IO.Path.Combine(outDir, $"Missisippi_Skia_{offset}.png"); - - using SKImage image = surface.Snapshot(); - using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); - - 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/FillComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs deleted file mode 100644 index 81d77075f..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillComplexPolygonTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillComplexPolygonTests -{ - [Theory] - [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, false)] - [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, true, false)] - [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, true)] - public void ComplexPolygon_SolidFill(TestImageProvider provider, bool overlap, bool transparent) - where TPixel : unmanaged, IPixel - { - Polygon simplePath = new(new LinearLineSegment( - new Vector2(10, 10), - new Vector2(200, 150), - new Vector2(50, 300))); - - Polygon hole1 = new(new LinearLineSegment( - new Vector2(37, 85), - overlap ? new Vector2(130, 40) : new Vector2(93, 85), - new Vector2(65, 137))); - - IPath clipped = simplePath.Clip(hole1); - - Color color = Color.HotPink; - if (transparent) - { - color = color.WithAlpha(150 / 255F); - } - - string testDetails = string.Empty; - if (overlap) - { - testDetails += "_Overlap"; - } - - if (transparent) - { - testDetails += "_Transparent"; - } - - provider.RunValidatingProcessorTest( - x => x.Fill(color, clipped), - testDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillEllipticGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillEllipticGradientBrushTests.cs deleted file mode 100644 index 0bedd4d3f..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillEllipticGradientBrushTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillEllipticGradientBrushTests -{ - private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void WithEqualColorsReturnsUnicolorImage( - TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Color red = Color.Red; - - using (Image image = provider.GetImage()) - { - EllipticGradientBrush unicolorLinearGradientBrush = - new( - new Point(0, 0), - new Point(10, 0), - 1.0f, - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - - // no need for reference image in this test: - image.ComparePixelBufferTo(red); - } - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.2)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.6)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 2.0)] - public void AxisParallelEllipsesWithDifferentRatio( - TestImageProvider provider, - float ratio) - where TPixel : unmanaged, IPixel - { - Color yellow = Color.Yellow; - Color red = Color.Red; - Color black = Color.Black; - - provider.VerifyOperation( - TolerantComparer, - image => - { - EllipticGradientBrush unicolorLinearGradientBrush = new( - new Point(image.Width / 2, image.Height / 2), - new Point(image.Width / 2, image.Width * 2 / 3), - ratio, - GradientRepetitionMode.None, - new ColorStop(0, yellow), - new ColorStop(1, red), - new ColorStop(1, black)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - $"{ratio:F2}", - false, - false); - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 0)] - - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 45)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 45)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 45)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 45)] - - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 90)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 90)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 90)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 90)] - - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 30)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 30)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 30)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 30)] - public void RotatedEllipsesWithDifferentRatio( - TestImageProvider provider, - float ratio, - float rotationInDegree) - where TPixel : unmanaged, IPixel - { - FormattableString variant = $"{ratio:F2}_AT_{rotationInDegree:00}deg"; - - provider.VerifyOperation( - TolerantComparer, - image => - { - Color yellow = Color.Yellow; - Color red = Color.Red; - Color black = Color.Black; - - Point center = new(image.Width / 2, image.Height / 2); - - double rotation = Math.PI * rotationInDegree / 180.0; - double cos = Math.Cos(rotation); - double sin = Math.Sin(rotation); - - int offsetY = image.Height / 6; - int axisX = center.X + (int)-(offsetY * sin); - int axisY = center.Y + (int)(offsetY * cos); - - EllipticGradientBrush unicolorLinearGradientBrush = new( - center, - new Point(axisX, axisY), - ratio, - GradientRepetitionMode.None, - new ColorStop(0, yellow), - new ColorStop(1, red), - new ColorStop(1, black)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - variant, - false, - false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs deleted file mode 100644 index 7afd37a42..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Drawing; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillImageBrushTests -{ - [Fact] - public void DoesNotDisposeImage() - { - using (Image src = new(5, 5)) - { - ImageBrush brush = new(src); - using (Image dest = new(10, 10)) - { - dest.Mutate(c => c.Fill(brush, new Rectangle(0, 0, 10, 10))); - dest.Mutate(c => c.Fill(brush, new Rectangle(0, 0, 10, 10))); - } - } - } - - [Theory] - [WithTestPatternImage(200, 200, PixelTypes.Rgba32 | PixelTypes.Bgra32)] - public void UseBrushOfDifferentPixelType(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; - using Image background = provider.GetImage(); - using Image overlay = provider.PixelType == PixelTypes.Rgba32 - ? Image.Load(data) - : Image.Load(data); - - ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush)); - - background.DebugSave(provider, appendSourceFileOrDescription: false); - background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - - [Theory] - [WithTestPatternImage(200, 200, PixelTypes.Rgba32)] - public void CanDrawLandscapeImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; - using Image background = provider.GetImage(); - using Image overlay = Image.Load(data); - - overlay.Mutate(c => c.Crop(new Rectangle(0, 0, 125, 90))); - - ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush)); - - background.DebugSave(provider, appendSourceFileOrDescription: false); - background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - - [Theory] - [WithTestPatternImage(200, 200, PixelTypes.Rgba32)] - public void CanDrawPortraitImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; - using Image background = provider.GetImage(); - using Image overlay = Image.Load(data); - - overlay.Mutate(c => c.Crop(new Rectangle(0, 0, 90, 125))); - - ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush)); - - background.DebugSave(provider, appendSourceFileOrDescription: false); - background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - - [Theory] - [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] - public void CanOffsetImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; - using Image background = provider.GetImage(); - using Image overlay = Image.Load(data); - - ImageBrush brush = new(overlay); - background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200))); - background.Mutate(c => c.Fill(brush, new RectangularPolygon(-100, 200, 500, 200))); - - background.DebugSave(provider, appendSourceFileOrDescription: false); - background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - - [Theory] - [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] - public void CanOffsetViaBrushImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; - using Image background = provider.GetImage(); - using Image overlay = Image.Load(data); - - ImageBrush brush = new(overlay); - ImageBrush brushOffset = new(overlay, new Point(100, 0)); - background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200))); - background.Mutate(c => c.Fill(brushOffset, new RectangularPolygon(0, 200, 400, 200))); - - background.DebugSave(provider, appendSourceFileOrDescription: false); - background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] - public void CanDrawOffsetImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; - using Image background = provider.GetImage(); - - using Image templateImage = Image.Load(data); - using Image finalTexture = BuildMultiRowTexture(templateImage); - - finalTexture.Mutate(c => c.Resize(100, 200)); - - ImageBrush brush = new(finalTexture); - background.Mutate(c => c.Fill(brush)); - - background.DebugSave(provider, appendSourceFileOrDescription: false); - background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - - Image BuildMultiRowTexture(Image sourceTexture) - { - int halfWidth = sourceTexture.Width / 2; - - Image final = sourceTexture.Clone(x => x.Resize(new ResizeOptions - { - Size = new Size(templateImage.Width, templateImage.Height * 2), - Position = AnchorPositionMode.TopLeft, - Mode = ResizeMode.Pad, - }) - .DrawImage(templateImage, new Point(halfWidth, sourceTexture.Height), new Rectangle(0, 0, halfWidth, sourceTexture.Height), 1) - .DrawImage(templateImage, new Point(0, templateImage.Height), new Rectangle(halfWidth, 0, halfWidth, sourceTexture.Height), 1)); - return final; - } - } - - [Theory] - [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] - public void CanDrawNegativeOffsetImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; - using Image background = provider.GetImage(); - using Image overlay = Image.Load(data); - - overlay.Mutate(c => c.Resize(100, 100)); - - ImageBrush halfBrush = new(overlay, new RectangleF(50, 0, 50, 100)); - ImageBrush fullBrush = new(overlay); - background.Mutate(c => DrawFull(c, new Size(100, 100), fullBrush, halfBrush, background.Width, background.Height)); - - background.DebugSave(provider, appendSourceFileOrDescription: false); - background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - - private static void DrawFull(IImageProcessingContext ctx, Size size, ImageBrush brush, ImageBrush halfBrush, int width, int height) - { - int j = 0; - while (j < height) - { - bool half = false; - int limitWidth = width; - int i = 0; - if ((j / size.Height) % 2 != 0) - { - half = true; - } - - while (i < limitWidth) - { - if (half) - { - ctx.Fill(halfBrush, new RectangleF(i, j, size.Width / 2f, size.Height)); - i += (int)(size.Width / 2f); - half = false; - } - else - { - ctx.Fill(brush, new RectangleF(new PointF(i, j), size)); - i += size.Width; - } - } - - j += size.Height; - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs deleted file mode 100644 index 9b823400e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillLinearGradientBrushTests.cs +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Globalization; -using System.Text; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillLinearGradientBrushTests -{ - public static ImageComparer TolerantComparer { get; } = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void WithEqualColorsReturnsUnicolorImage(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color red = Color.Red; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(10, 0), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - - // no need for reference image in this test: - image.ComparePixelBufferTo(red); - } - } - - [Theory] - [WithBlankImage(20, 10, PixelTypes.Rgba32)] - [WithBlankImage(20, 10, PixelTypes.Argb32)] - [WithBlankImage(20, 10, PixelTypes.Rgb24)] - public void DoesNotDependOnSinglePixelType(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(image.Width, 0), - GradientRepetitionMode.None, - new ColorStop(0, Color.Blue), - new ColorStop(1, Color.Yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - appendSourceFileOrDescription: false); - - [Theory] - [WithBlankImage(500, 10, PixelTypes.Rgba32)] - public void HorizontalReturnsUnicolorColumns(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(image.Width, 0), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - false, - false); - - [Theory] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.DontFill)] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.None)] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Repeat)] - [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Reflect)] - public void HorizontalGradientWithRepMode( - TestImageProvider provider, - GradientRepetitionMode repetitionMode) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(image.Width / 10, 0), - repetitionMode, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - $"{repetitionMode}", - false, - false); - - [Theory] - [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.5f })] - [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.2f, 0.4f, 0.6f, 0.8f })] - [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.1f, 0.3f, 0.6f })] - public void WithDoubledStopsProduceDashedPatterns( - TestImageProvider provider, - float[] pattern) - where TPixel : unmanaged, IPixel - { - string variant = string.Join("_", pattern.Select(i => i.ToString(CultureInfo.InvariantCulture))); - - // ensure the input data is valid - Assert.True(pattern.Length > 0); - - Color black = Color.Black; - Color white = Color.White; - - // create the input pattern: 0, followed by each of the arguments twice, followed by 1.0 - toggling black and white. - ColorStop[] colorStops = - Enumerable.Repeat(new ColorStop(0, black), 1) - .Concat( - pattern.SelectMany( - (f, index) => - new[] - { - new ColorStop(f, index % 2 == 0 ? black : white), - new ColorStop(f, index % 2 == 0 ? white : black) - })) - .Concat(Enumerable.Repeat(new ColorStop(1, pattern.Length % 2 == 0 ? black : white), 1)) - .ToArray(); - - using (Image image = provider.GetImage()) - { - LinearGradientBrush unicolorLinearGradientBrush = - new( - new Point(0, 0), - new Point(image.Width, 0), - GradientRepetitionMode.None, - colorStops); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - image.DebugSave( - provider, - variant, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - // the result must be a black and white pattern, no other color should occur: - Assert.All( - Enumerable.Range(0, image.Width).Select(i => image[i, 0]), - color => Assert.True( - color.Equals(black.ToPixel()) || color.Equals(white.ToPixel()))); - - image.CompareToReferenceOutput( - TolerantComparer, - provider, - variant, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - } - - [Theory] - [WithBlankImage(10, 500, PixelTypes.Rgba32)] - public void VerticalBrushReturnsUnicolorRows( - TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - provider.VerifyOperation( - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(0, 0), - new Point(0, image.Height), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - - VerifyAllRowsAreUnicolor(image); - }, - false, - false); - - static void VerifyAllRowsAreUnicolor(Image image) - { - for (int y = 0; y < image.Height; y++) - { - Span row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y); - TPixel firstColorOfRow = row[0]; - foreach (TPixel p in row) - { - Assert.Equal(firstColorOfRow, p); - } - } - } - } - - public enum ImageCorner - { - TopLeft = 0, - TopRight = 1, - BottomLeft = 2, - BottomRight = 3 - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.TopLeft)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.TopRight)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.BottomLeft)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, ImageCorner.BottomRight)] - public void DiagonalReturnsCorrectImages( - TestImageProvider provider, - ImageCorner startCorner) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Assert.True(image.Height == image.Width, "For the math check block at the end the image must be squared, but it is not."); - - int startX = (int)startCorner % 2 == 0 ? 0 : image.Width - 1; - int startY = startCorner > ImageCorner.TopRight ? 0 : image.Height - 1; - int endX = image.Height - startX - 1; - int endY = image.Width - startY - 1; - - Color red = Color.Red; - Color yellow = Color.Yellow; - - LinearGradientBrush unicolorLinearGradientBrush = - new( - new Point(startX, startY), - new Point(endX, endY), - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - image.DebugSave( - provider, - startCorner, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - int verticalSign = startY == 0 ? 1 : -1; - int horizontalSign = startX == 0 ? 1 : -1; - - for (int i = 0; i < image.Height; i++) - { - // it's diagonal, so for any (a, a) on the gradient line, for all (a-x, b+x) - +/- depending on the diagonal direction - must be the same color) - TPixel colorOnDiagonal = image[i, i]; - - // TODO: This is incorrect. from -0 to < 0 ?? - int orthoCount = 0; - for (int offset = -orthoCount; offset < orthoCount; offset++) - { - Assert.Equal(colorOnDiagonal, image[i + (horizontalSign * offset), i + (verticalSign * offset)]); - } - } - - image.CompareToReferenceOutput( - TolerantComparer, - provider, - startCorner, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - } - - [Theory] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .2f, .5f, .9f }, new[] { 0, 0, 1, 1 })] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 499, 499, 0, new[] { 0f, 0.2f, 0.5f, 0.9f }, new[] { 0, 1, 2, 3 })] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 499, 499, 0, 0, new[] { 0f, 0.7f, 0.8f, 0.9f }, new[] { 0, 1, 2, 0 })] - [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .5f, 1f }, new[] { 0, 1, 3 })] - public void ArbitraryGradients( - TestImageProvider provider, - int startX, - int startY, - int endX, - int endY, - float[] stopPositions, - int[] stopColorCodes) - where TPixel : unmanaged, IPixel - { - Color[] colors = - [ - Color.Navy, Color.LightGreen, Color.Yellow, - Color.Red - ]; - - StringBuilder coloringVariant = new(); - ColorStop[] colorStops = new ColorStop[stopPositions.Length]; - - for (int i = 0; i < stopPositions.Length; i++) - { - Color color = colors[stopColorCodes[i % colors.Length]]; - float position = stopPositions[i]; - colorStops[i] = new ColorStop(position, color); - coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); - } - - FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; - - provider.VerifyOperation( - image => - { - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(startX, startY), - new Point(endX, endY), - GradientRepetitionMode.None, - colorStops); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - variant, - false, - false); - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0, 199, 199, new[] { 0f, .25f, .5f, .75f, 1f }, new[] { 0, 1, 2, 3, 4 })] - public void MultiplePointGradients( - TestImageProvider provider, - int startX, - int startY, - int endX, - int endY, - float[] stopPositions, - int[] stopColorCodes) - where TPixel : unmanaged, IPixel - { - Color[] colors = - [ - Color.Black, Color.Blue, Color.Red, - Color.White, Color.Lime - ]; - - StringBuilder coloringVariant = new(); - ColorStop[] colorStops = new ColorStop[stopPositions.Length]; - - for (int i = 0; i < stopPositions.Length; i++) - { - Color color = colors[stopColorCodes[i % colors.Length]]; - float position = stopPositions[i]; - colorStops[i] = new ColorStop(position, color); - coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); - } - - FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; - - provider.VerifyOperation( - image => - { - LinearGradientBrush unicolorLinearGradientBrush = new( - new Point(startX, startY), - new Point(endX, endY), - GradientRepetitionMode.None, - colorStops); - - image.Mutate(x => x.Fill(unicolorLinearGradientBrush)); - }, - variant, - false, - false); - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32)] - public void GradientsWithTransparencyOnExistingBackground(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - provider.VerifyOperation( - image => - { - image.Mutate(i => i.Fill(Color.Red)); - image.Mutate(ApplyGloss); - }); - - void ApplyGloss(IImageProcessingContext ctx) - { - Size size = ctx.GetCurrentSize(); - IPathCollection glossPath = BuildGloss(size.Width, size.Height); - GraphicsOptions graphicsOptions = new() - { - Antialias = true, - ColorBlendingMode = PixelColorBlendingMode.Normal, - AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop - }; - LinearGradientBrush linearGradientBrush = new(new Point(0, 0), new Point(0, size.Height / 2), GradientRepetitionMode.Repeat, new ColorStop(0, Color.White.WithAlpha(0.5f)), new ColorStop(1, Color.White.WithAlpha(0.25f))); - ctx.SetGraphicsOptions(graphicsOptions).Fill(linearGradientBrush, glossPath); - } - - IPathCollection BuildGloss(int imageWidth, int imageHeight) - { - PathBuilder pathBuilder = new(); - pathBuilder.AddLine(new PointF(0, 0), new PointF(imageWidth, 0)); - pathBuilder.AddLine(new PointF(imageWidth, 0), new PointF(imageWidth, imageHeight * 0.4f)); - pathBuilder.AddQuadraticBezier(new PointF(imageWidth, imageHeight * 0.4f), new PointF(imageWidth / 2, imageHeight * 0.6f), new PointF(0, imageHeight * 0.4f)); - pathBuilder.CloseFigure(); - return new PathCollection(pathBuilder.Build()); - } - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgb24)] - public void BrushApplicatorIsThreadSafeIssue1044(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - img => - { - PathGradientBrush brush = new( - [new PointF(0, 0), new PointF(200, 0), new PointF(200, 200), new PointF(0, 200), new PointF(0, 0)], - [Color.Red, Color.Yellow, Color.Green, Color.DarkCyan, Color.Red]); - - img.Mutate(m => m.Fill(brush)); - }, - false, - false); - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32)] - public void RotatedGradient(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - image => - { - Color red = Color.Red; - Color yellow = Color.Yellow; - - // Start -> End along TL->BR, rotated to horizontal via p2 - LinearGradientBrush brush = new( - new Point(0, 0), - new Point(200, 200), - new Point(0, 100), // p2 picks horizontal axis - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, yellow)); - image.Mutate(x => x.Fill(brush)); - }, - false, - false); -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs deleted file mode 100644 index bdbd43e06..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillOutsideBoundsTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillOutsideBoundsTests -{ - [Theory] - [InlineData(-100)] // Crash - [InlineData(-99)] // Fine - [InlineData(99)] // Fine - [InlineData(100)] // Crash - public void DrawRectactangleOutsideBoundsDrawingArea(int xpos) - { - int width = 100; - int height = 100; - - using (Image image = new(width, height, Color.Red.ToPixel())) - { - Rectangle rectangle = new(xpos, 0, width, height); - - image.Mutate(x => x.Fill(Color.Black, rectangle)); - } - } - - public static TheoryData CircleCoordinates { get; } = new() - { - { -110, -60 }, { 0, -60 }, { 110, -60 }, - { -110, -50 }, { 0, -50 }, { 110, -50 }, - { -110, -49 }, { 0, -49 }, { 110, -49 }, - { -110, -20 }, { 0, -20 }, { 110, -20 }, - { -110, -50 }, { 0, -60 }, { 110, -60 }, - { -110, 0 }, { -99, 0 }, { 0, 0 }, { 99, 0 }, { 110, 0 }, - }; - - [Theory] - [WithSolidFilledImages(nameof(CircleCoordinates), 100, 100, nameof(Color.Red), PixelTypes.Rgba32)] - public void DrawCircleOutsideBoundsDrawingArea(TestImageProvider provider, int xpos, int ypos) - { - int width = 100; - int height = 100; - - using Image image = provider.GetImage(); - EllipsePolygon circle = new(xpos, ypos, width, height); - - provider.RunValidatingProcessorTest( - x => x.Fill(Color.Black, circle), - $"({xpos}_{ypos})", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs deleted file mode 100644 index 15750e4ce..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathGradientBrushTests.cs +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillPathGradientBrushTests -{ - private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void FillRectangleWithDifferentColors(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; - - PathGradientBrush brush = new(points, colors); - - image.Mutate(x => x.Fill(brush)); - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - }); - - [Theory] - [WithBlankImage(20, 20, PixelTypes.Rgba32)] - public void FillTriangleWithDifferentColors(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; - Color[] colors = [Color.Red, Color.Green, Color.Blue]; - - PathGradientBrush brush = new(points, colors); - - image.Mutate(x => x.Fill(brush)); - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - }); - - [Theory] - [WithBlankImage(20, 20, PixelTypes.HalfSingle)] - public void FillTriangleWithGreyscale(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - ImageComparer.TolerantPercentage(0.02f), - image => - { - PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; - - Color c1 = Color.FromPixel(new HalfSingle(-1)); - Color c2 = Color.FromPixel(new HalfSingle(0)); - Color c3 = Color.FromPixel(new HalfSingle(1)); - - Color[] colors = [c1, c2, c3]; - - PathGradientBrush brush = new(points, colors); - - image.Mutate(x => x.Fill(brush)); - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - }); - - [Theory] - [WithBlankImage(20, 20, PixelTypes.Rgba32)] - public void FillTriangleWithDifferentColorsCenter(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; - Color[] colors = [Color.Red, Color.Green, Color.Blue]; - - PathGradientBrush brush = new(points, colors, Color.White); - - image.Mutate(x => x.Fill(brush)); - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - }); - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void FillRectangleWithSingleColor(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - Color[] colors = [Color.Red]; - - PathGradientBrush brush = new(points, colors); - - image.Mutate(x => x.Fill(brush)); - - image.ComparePixelBufferTo(Color.Red); - } - } - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void ShouldRotateTheColorsWhenThereAreMorePoints(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - Color[] colors = [Color.Red, Color.Yellow]; - - PathGradientBrush brush = new(points, colors); - - image.Mutate(x => x.Fill(brush)); - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - }); - - [Theory] - [WithBlankImage(10, 10, PixelTypes.Rgba32)] - public void FillWithCustomCenterColor(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; - - PathGradientBrush brush = new(points, colors, Color.White); - - image.Mutate(x => x.Fill(brush)); - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - }); - - [Fact] - public void ShouldThrowArgumentNullExceptionWhenLinesAreNull() - { - Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; - - PathGradientBrush Create() => new(null, colors, Color.White); - - Assert.Throws(Create); - } - - [Fact] - public void ShouldThrowArgumentOutOfRangeExceptionWhenLessThan3PointsAreGiven() - { - PointF[] points = [new(0, 0), new(10, 0)]; - Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; - - PathGradientBrush Create() => new(points, colors, Color.White); - - Assert.Throws(Create); - } - - [Fact] - public void ShouldThrowArgumentNullExceptionWhenColorsAreNull() - { - PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - - PathGradientBrush Create() => new(points, null, Color.White); - - Assert.Throws(Create); - } - - [Fact] - public void ShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven() - { - PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; - - Color[] colors = []; - - PathGradientBrush Create() => new(points, colors, Color.White); - - Assert.Throws(Create); - } - - [Theory] - [WithBlankImage(100, 100, PixelTypes.Rgba32)] - public void FillComplex(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - new TolerantImageComparer(0.2f), - image => - { - Star star = new(50, 50, 5, 20, 45); - PointF[] points = star.Points.ToArray(); - Color[] colors = - [ - Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.Purple, - Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.Purple - ]; - - PathGradientBrush brush = new(points, colors, Color.White); - - image.Mutate(x => x.Fill(brush)); - }, - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs deleted file mode 100644 index 1a6bcb5e9..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPathTests.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillPathTests -{ - // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths - [Theory] - [WithSolidFilledImages(325, 325, "White", PixelTypes.Rgba32)] - public void FillPathSVGArcs(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PathBuilder pb = new(); - - pb.MoveTo(new Vector2(80, 80)) - .ArcTo(45, 45, 0, false, false, new Vector2(125, 125)) - .LineTo(new Vector2(125, 80)) - .CloseFigure(); - - IPath path = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(230, 80)) - .ArcTo(45, 45, 0, true, false, new Vector2(275, 125)) - .LineTo(new Vector2(275, 80)) - .CloseFigure(); - - IPath path2 = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(80, 230)) - .ArcTo(45, 45, 0, false, true, new Vector2(125, 275)) - .LineTo(new Vector2(125, 230)) - .CloseFigure(); - - IPath path3 = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(230, 230)) - .ArcTo(45, 45, 0, true, true, new Vector2(275, 275)) - .LineTo(new Vector2(275, 230)) - .CloseFigure(); - - IPath path4 = pb.Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Fill(Color.Green, path).Fill(Color.Red, path2).Fill(Color.Purple, path3).Fill(Color.Blue, path4)), - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); - } - - // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc - [Theory] - [WithSolidFilledImages(150, 200, "White", PixelTypes.Rgba32)] - public void FillPathCanvasArcs(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - ImageComparer.TolerantPercentage(5e-3f), - image => - { - for (int i = 0; i <= 3; i++) - { - for (int j = 0; j <= 2; j++) - { - PathBuilder pb = new(); - - float x = 25 + (j * 50); // x coordinate - float y = 25 + (i * 50); // y coordinate - float radius = 20; // Arc radius - float startAngle = 0; // Starting point on circle - float endAngle = 180F + (180F * j / 2F); // End point on circle - bool counterclockwise = i % 2 == 1; // Draw counterclockwise - - // To move counterclockwise we offset our sweepAngle parameter - // Canvas likely does something similar. - if (counterclockwise) - { - // 360 becomes zero and we don't accept that as a parameter (won't render). - if (endAngle < 360F) - { - endAngle = (360F - endAngle) % 360F; - } - - endAngle *= -1; - } - - pb.AddArc(x, y, radius, radius, 0, startAngle, endAngle); - - if (i > 1) - { - image.Mutate(x => x.Fill(Color.Black, pb.Build())); - } - else - { - image.Mutate(x => x.Draw(new SolidPen(Color.Black, 1), pb.Build())); - } - } - } - }, - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); - - [Theory] - [WithSolidFilledImages(400, 250, "White", PixelTypes.Rgba32)] - public void FillPathArcToAlternates(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - // Test alternate syntax. Both should overlap creating an orange arc. - PathBuilder pb = new(); - - pb.MoveTo(new Vector2(50, 50)); - pb.ArcTo(20, 50, -72, false, true, new Vector2(200, 200)); - IPath path = pb.Build(); - - pb = new PathBuilder(); - pb.MoveTo(new Vector2(50, 50)); - pb.AddSegment(new ArcLineSegment(new Vector2(50, 50), new Vector2(200, 200), new SizeF(20, 50), -72F, true, true)); - IPath path2 = pb.Build(); - - provider.VerifyOperation( - image => image.Mutate(x => x.Fill(Color.Yellow, path).Fill(Color.Red.WithAlpha(.5F), path2)), - appendSourceFileOrDescription: false, - appendPixelTypeToFileName: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs deleted file mode 100644 index 3fbba995f..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPatternBrushTests.cs +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -public class FillPatternBrushTests -{ - private void Test(string name, Color background, Brush brush, Color[,] expectedPattern) - { - string path = TestEnvironment.CreateOutputDirectory("Drawing", "FillPatternBrushTests"); - using (Image image = new(20, 20)) - { - image.Mutate(x => x.Fill(background).Fill(brush)); - - image.Save($"{path}/{name}.png"); - - Buffer2D sourcePixels = image.GetRootFramePixelBuffer(); - - // lets pick random spots to start checking - Random r = new(); - DenseMatrix expectedPatternFast = new(expectedPattern); - int xStride = expectedPatternFast.Columns; - int yStride = expectedPatternFast.Rows; - int offsetX = r.Next(image.Width / xStride) * xStride; - int offsetY = r.Next(image.Height / yStride) * yStride; - for (int x = 0; x < xStride; x++) - { - for (int y = 0; y < yStride; y++) - { - int actualX = x + offsetX; - int actualY = y + offsetY; - Rgba32 expected = expectedPatternFast[y, x].ToPixel(); // inverted pattern - Rgba32 actual = sourcePixels[actualX, actualY]; - if (expected != actual) - { - Assert.True(false, $"Expected {expected} but found {actual} at ({actualX},{actualY})"); - } - } - } - - image.Mutate(x => x.Resize(80, 80, KnownResamplers.NearestNeighbor)); - image.Save($"{path}/{name}x4.png"); - } - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent10() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "Percent10", - Color.Blue, - Brushes.Percent10(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent10Transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue } - }; - - Test( - "Percent10_Transparent", - Color.Blue, - Brushes.Percent10(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent20() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen } - }; - - Test( - "Percent20", - Color.Blue, - Brushes.Percent20(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithPercent20_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue } - }; - - Test( - "Percent20_Transparent", - Color.Blue, - Brushes.Percent20(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithHorizontal() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "Horizontal", - Color.Blue, - Brushes.Horizontal(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithHorizontal_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue } - }; - - Test( - "Horizontal_Transparent", - Color.Blue, - Brushes.Horizontal(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithMin() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } - }; - - Test( - "Min", - Color.Blue, - Brushes.Min(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithMin_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, - { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, - }; - - Test( - "Min_Transparent", - Color.Blue, - Brushes.Min(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithVertical() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "Vertical", - Color.Blue, - Brushes.Vertical(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithVertical_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue } - }; - - Test( - "Vertical_Transparent", - Color.Blue, - Brushes.Vertical(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithForwardDiagonal() - { - Color[,] expectedPattern = new Color[,] - { - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } - }; - - Test( - "ForwardDiagonal", - Color.Blue, - Brushes.ForwardDiagonal(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithForwardDiagonal_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.Blue, Color.Blue, Color.Blue, Color.HotPink }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue } - }; - - Test( - "ForwardDiagonal_Transparent", - Color.Blue, - Brushes.ForwardDiagonal(Color.HotPink), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithBackwardDiagonal() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, - { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink } - }; - - Test( - "BackwardDiagonal", - Color.Blue, - Brushes.BackwardDiagonal(Color.HotPink, Color.LimeGreen), - expectedPattern); - } - - [Fact] - public void ImageShouldBeFloodFilledWithBackwardDiagonal_transparent() - { - Color[,] expectedPattern = new Color[,] - { - { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, - { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, - { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, - { Color.Blue, Color.Blue, Color.Blue, Color.HotPink } - }; - - Test( - "BackwardDiagonal_Transparent", - Color.Blue, - Brushes.BackwardDiagonal(Color.HotPink), - expectedPattern); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs deleted file mode 100644 index 3fc6f89fb..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillPolygonTests.cs +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillPolygonTests -{ - [Theory] - [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 0)] - [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 8)] - [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 16)] - public void FillPolygon_Solid_Basic(TestImageProvider provider, int antialias) - where TPixel : unmanaged, IPixel - { - 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 }; - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options) - .FillPolygon(Color.White, polygon1) - .FillPolygon(Color.White, polygon2), - testOutputDetails: $"aa{antialias}", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, true)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, false)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, true)] - public void FillPolygon_Solid(TestImageProvider provider, string colorName, float alpha, bool antialias) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) - ]; - Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); - - GraphicsOptions options = new() { Antialias = antialias }; - - string aa = antialias ? string.Empty : "_NoAntialias"; - FormattableString outputDetails = $"{colorName}_A{alpha}{aa}"; - - provider.RunValidatingProcessorTest( - c => c.SetGraphicsOptions(options).FillPolygon(color, simplePath), - outputDetails, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] - public void FillPolygon_Solid_Transformed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) - ]; - - provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200))) - .FillPolygon(Color.White, simplePath)); - } - - [Theory] - [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] - public void Fill_RectangularPolygon_Solid_Transformed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - RectangularPolygon polygon = new(25, 25, 50, 50); - - provider.RunValidatingProcessorTest( - c => c.SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) - .Fill(Color.White, polygon)); - } - - [Theory] - [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] - public void Fill_RectangularPolygon_Solid_TransformedUsingConfiguration(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - RectangularPolygon polygon = new(25, 25, 50, 50); - provider.Configuration.SetDrawingTransform(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))); - provider.RunValidatingProcessorTest(c => c.Fill(Color.White, polygon)); - } - - public static TheoryData FillPolygon_Complex_Data { get; } = - new() - { - { false, IntersectionRule.EvenOdd }, - { false, IntersectionRule.NonZero }, - { true, IntersectionRule.EvenOdd }, - { true, IntersectionRule.NonZero }, - }; - - [Theory] - [WithBasicTestPatternImages(nameof(FillPolygon_Complex_Data), 100, 100, PixelTypes.Rgba32)] - public void FillPolygon_Complex(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule) - where TPixel : unmanaged, IPixel - { - PointF[] contour = PolygonFactory.CreatePointArray((20, 20), (80, 20), (80, 80), (20, 80)); - PointF[] hole = PolygonFactory.CreatePointArray((40, 40), (40, 60), (60, 60), (60, 40)); - - if (reverse) - { - Array.Reverse(contour); - Array.Reverse(hole); - } - - ComplexPolygon polygon = new( - new Path(new LinearLineSegment(contour)), - new Path(new LinearLineSegment(hole))); - - provider.RunValidatingProcessorTest( - c => - { - c.SetShapeOptions(new ShapeOptions() - { - IntersectionRule = intersectionRule - }); - c.Fill(Color.White, polygon); - }, - testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", - comparer: ImageComparer.TolerantPercentage(0.01f), - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, false)] - [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, true)] - public void FillPolygon_Concave(TestImageProvider provider, bool reverse) - where TPixel : unmanaged, IPixel - { - PointF[] points = - [ - new Vector2(8, 8), - new Vector2(64, 8), - new Vector2(64, 64), - new Vector2(120, 64), - new Vector2(120, 120), - new Vector2(8, 120) - ]; - if (reverse) - { - Array.Reverse(points); - } - - Color color = Color.LightGreen; - - provider.RunValidatingProcessorTest( - c => c.FillPolygon(color, points), - testOutputDetails: $"Reverse({reverse})", - comparer: ImageComparer.TolerantPercentage(0.01f), - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(64, 64, "Black", PixelTypes.Rgba32)] - public void FillPolygon_StarCircle(TestImageProvider provider) - { - EllipsePolygon circle = new(32, 32, 30); - Star star = new(32, 32, 7, 10, 27); - IPath shape = circle.Clip(star); - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White, shape), - comparer: ImageComparer.TolerantPercentage(0.01f), - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [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. - ShapeOptions options = new() { BooleanOperation = operation }; - IPath shape = star.Clip(options, circle); - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.DeepPink, circle).Fill(Color.LightGray, star).Fill(Color.ForestGreen, shape), - testOutputDetails: operation.ToString(), - comparer: ImageComparer.TolerantPercentage(0.01F), - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] - public void FillPolygon_Pattern(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) - ]; - Color color = Color.Yellow; - - PatternBrush brush = Brushes.Horizontal(color); - - provider.RunValidatingProcessorTest( - c => c.FillPolygon(brush, simplePath), - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Png.Ducky)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Bmp.Car)] - public void FillPolygon_ImageBrush(TestImageProvider provider, string brushImageName) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200) - ]; - - using (Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes)) - { - ImageBrush brush = new(brushImage); - - provider.RunValidatingProcessorTest( - c => c.FillPolygon(brush, simplePath), - System.IO.Path.GetFileNameWithoutExtension(brushImageName), - appendSourceFileOrDescription: false); - } - } - - [Theory] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Png.Ducky)] - [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Bmp.Car)] - public void FillPolygon_ImageBrush_Rect(TestImageProvider provider, string brushImageName) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200) - ]; - - using (Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes)) - { - float top = brushImage.Height / 4; - float left = brushImage.Width / 4; - float height = top * 2; - float width = left * 2; - - ImageBrush brush = new(brushImage, new RectangleF(left, top, width, height)); - - provider.RunValidatingProcessorTest( - c => c.FillPolygon(brush, simplePath), - System.IO.Path.GetFileNameWithoutExtension(brushImageName) + "_rect", - appendSourceFileOrDescription: false); - } - } - - [Theory] - [WithBasicTestPatternImages(250, 250, PixelTypes.Rgba32)] - public void Fill_RectangularPolygon(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - RectangularPolygon polygon = new(10, 10, 190, 140); - Color color = Color.White; - - provider.RunValidatingProcessorTest( - c => c.Fill(color, polygon), - appendSourceFileOrDescription: false); - } - - [Theory] - [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 50, 0f)] - [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 60, 20f)] - [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 60, -180f)] - [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 5, 70, 0f)] - [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 7, 80, -180f)] - public void Fill_RegularPolygon(TestImageProvider provider, int vertices, float radius, float angleDeg) - where TPixel : unmanaged, IPixel - { - float angle = GeometryUtilities.DegreeToRadian(angleDeg); - RegularPolygon polygon = new(100, 100, vertices, radius, angle); - Color color = Color.Yellow; - - FormattableString testOutput = $"V({vertices})_R({radius})_Ang({angleDeg})"; - provider.RunValidatingProcessorTest( - c => c.Fill(color, polygon), - testOutput, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - public static readonly TheoryData Fill_EllipsePolygon_Data = - new() - { - { false, IntersectionRule.EvenOdd }, - { false, IntersectionRule.NonZero }, - { true, IntersectionRule.EvenOdd }, - { true, IntersectionRule.NonZero }, - }; - - [Theory] - [WithBasicTestPatternImages(nameof(Fill_EllipsePolygon_Data), 200, 200, PixelTypes.Rgba32)] - public void Fill_EllipsePolygon(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule) - where TPixel : unmanaged, IPixel - { - IPath polygon = new EllipsePolygon(100, 100, 80, 120); - if (reverse) - { - polygon = polygon.Reverse(); - } - - Color color = Color.Azure; - - provider.RunValidatingProcessorTest( - c => - { - c.SetShapeOptions(new ShapeOptions() - { - IntersectionRule = intersectionRule - }); - c.Fill(color, polygon); - }, - testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Theory] - [WithSolidFilledImages(60, 60, "Blue", PixelTypes.Rgba32)] - public void Fill_IntersectionRules_OddEven(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image img = provider.GetImage()) - { - Polygon poly = new(new LinearLineSegment( - new PointF(10, 30), - new PointF(10, 20), - new PointF(50, 20), - new PointF(50, 50), - new PointF(20, 50), - new PointF(20, 10), - new PointF(30, 10), - new PointF(30, 40), - new PointF(40, 40), - new PointF(40, 30), - new PointF(10, 30))); - - img.Mutate(c => c.Fill( - new DrawingOptions - { - ShapeOptions = { IntersectionRule = IntersectionRule.EvenOdd }, - }, - Color.HotPink, - poly)); - - provider.Utility.SaveTestOutputFile(img); - - Assert.Equal(Color.Blue.ToPixel(), img[25, 25]); - } - } - - [Theory] - [WithSolidFilledImages(60, 60, "Blue", PixelTypes.Rgba32)] - public void Fill_IntersectionRules_Nonzero(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Configuration.Default.MaxDegreeOfParallelism = 1; - using (Image img = provider.GetImage()) - { - Polygon poly = new(new LinearLineSegment( - new PointF(10, 30), - new PointF(10, 20), - new PointF(50, 20), - new PointF(50, 50), - new PointF(20, 50), - new PointF(20, 10), - new PointF(30, 10), - new PointF(30, 40), - new PointF(40, 40), - new PointF(40, 30), - new PointF(10, 30))); - img.Mutate(c => c.Fill( - new DrawingOptions - { - ShapeOptions = { IntersectionRule = IntersectionRule.NonZero }, - }, - Color.HotPink, - poly)); - - provider.Utility.SaveTestOutputFile(img); - Assert.Equal(Color.HotPink.ToPixel(), img[25, 25]); - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillRadialGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillRadialGradientBrushTests.cs deleted file mode 100644 index 3fb5cbf94..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillRadialGradientBrushTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillRadialGradientBrushTests -{ - public static ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32)] - public void WithEqualColorsReturnsUnicolorImage( - TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color red = Color.Red; - - RadialGradientBrush unicolorRadialGradientBrush = - new( - new Point(0, 0), - 100, - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(unicolorRadialGradientBrush)); - - image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); - - // no need for reference image in this test: - image.ComparePixelBufferTo(red); - } - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 100)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 0)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 100)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, -40, 100)] - public void WithDifferentCentersReturnsImage( - TestImageProvider provider, - int centerX, - int centerY) - where TPixel : unmanaged, IPixel - { - provider.VerifyOperation( - TolerantComparer, - image => - { - RadialGradientBrush brush = new( - new Point(centerX, centerY), - image.Width / 2f, - GradientRepetitionMode.None, - new ColorStop(0, Color.Red), - new ColorStop(1, Color.Yellow)); - - image.Mutate(x => x.Fill(brush)); - }, - $"center({centerX},{centerY})", - false, - false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillSolidBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillSolidBrushTests.cs deleted file mode 100644 index 8ecbdef49..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillSolidBrushTests.cs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class FillSolidBrushTests -{ - [Theory] - [WithBlankImage(1, 1, PixelTypes.Rgba32)] - [WithBlankImage(7, 4, PixelTypes.Rgba32)] - [WithBlankImage(16, 7, PixelTypes.Rgba32)] - [WithBlankImage(33, 32, PixelTypes.Rgba32)] - [WithBlankImage(400, 500, PixelTypes.Rgba32)] - public void DoesNotDependOnSize(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Fill(color)); - - image.DebugSave(provider, appendPixelTypeToFileName: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] - public void DoesNotDependOnSinglePixelType(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = Color.HotPink; - image.Mutate(c => c.Fill(color)); - - image.DebugSave(provider, appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] - [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] - public void WhenColorIsOpaque_OverridePreviousColor( - TestImageProvider provider, - string newColorName) - where TPixel : unmanaged, IPixel - { - using (Image image = provider.GetImage()) - { - Color color = TestUtils.GetColorByName(newColorName); - image.Mutate(c => c.Fill(color)); - - image.DebugSave( - provider, - newColorName, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - image.ComparePixelBufferTo(color); - } - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion(TestImageProvider provider, int x0, int y0, int w, int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTest(c => c.Fill(color, region), testDetails, ImageComparer.Exact); - } - - [Theory] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] - [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] - public void FillRegion_WorksOnWrappedMemoryImage( - TestImageProvider provider, - int x0, - int y0, - int w, - int h) - where TPixel : unmanaged, IPixel - { - FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; - RectangleF region = new(x0, y0, w, h); - Color color = TestUtils.GetColorByName("Blue"); - - provider.RunValidatingProcessorTestOnWrappedMemoryImage( - c => c.Fill(color, region), - testDetails, - ImageComparer.Exact, - useReferenceOutputFrom: nameof(this.FillRegion)); - } - - public static readonly TheoryData BlendData = - new() - { - { false, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, - { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, - { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, - { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, - { true, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, - }; - - [Theory] - [WithSolidFilledImages(nameof(BlendData), 16, 16, "Red", PixelTypes.Rgba32)] - public void BlendFillColorOverBackground( - TestImageProvider provider, - bool triggerFillRegion, - string newColorName, - float alpha, - PixelColorBlendingMode blenderMode, - float blendPercentage) - where TPixel : unmanaged, IPixel - { - Color fillColor = TestUtils.GetColorByName(newColorName).WithAlpha(alpha); - - using (Image image = provider.GetImage()) - { - TPixel bgColor = image[0, 0]; - - DrawingOptions options = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = false, - ColorBlendingMode = blenderMode, - BlendPercentage = blendPercentage - } - }; - - if (triggerFillRegion) - { - RectangularPolygon path = new(0, 0, 16, 16); - image.Mutate(c => c.SetGraphicsOptions(options.GraphicsOptions).Fill(new SolidBrush(fillColor), path)); - } - else - { - image.Mutate(c => c.Fill(options, new SolidBrush(fillColor))); - } - - var testOutputDetails = new - { - triggerFillRegion, - newColorName, - alpha, - blenderMode, - blendPercentage - }; - - image.DebugSave( - provider, - testOutputDetails, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - PixelBlender blender = PixelOperations.Instance.GetPixelBlender( - blenderMode, - PixelAlphaCompositionMode.SrcOver); - TPixel expectedPixel = blender.Blend(bgColor, fillColor.ToPixel(), blendPercentage); - - image.ComparePixelBufferTo(expectedPixel); - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillSweepGradientBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillSweepGradientBrushTests.cs deleted file mode 100644 index cc4518e6d..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillSweepGradientBrushTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing/GradientBrushes")] -public class FillSweepGradientBrushTests -{ - private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.01f); - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 0f, 360f)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 90f, 450f)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 180f, 540f)] - [WithBlankImage(200, 200, PixelTypes.Rgba32, 270f, 630f)] - public void SweepGradientBrush_RendersFullSweep_Every90Degrees(TestImageProvider provider, float start, float end) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - TolerantComparer, - image => - { - Color red = Color.Red; - Color green = Color.Green; - Color blue = Color.Blue; - Color yellow = Color.Yellow; - - SweepGradientBrush brush = new( - new Point(100, 100), - start, - end, - GradientRepetitionMode.None, - new ColorStop(0, red), - new ColorStop(0.25F, yellow), - new ColorStop(0.5F, green), - new ColorStop(0.75F, blue), - new ColorStop(1, red)); - - image.Mutate(x => x.Fill(brush)); - }, - $"start({start},end{end})", - false, - false); -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs deleted file mode 100644 index 178dfa48e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Clear.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class Clear : BaseImageOperationsExtensionTest -{ - private readonly DrawingOptions nonDefaultOptions = new() - { - GraphicsOptions = - { - AlphaCompositionMode = PixelFormats.PixelAlphaCompositionMode.Clear, - BlendPercentage = 0.5f, - ColorBlendingMode = PixelFormats.PixelColorBlendingMode.Darken - } - }; - - private readonly Brush brush = new SolidBrush(Color.HotPink); - - [Fact] - public void Brush() - { - this.operations.Clear(this.nonDefaultOptions, this.brush); - - FillProcessor processor = this.Verify(); - - DrawingOptions expectedOptions = this.nonDefaultOptions; - Assert.Equal(expectedOptions.ShapeOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Clear(this.brush); - - FillProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Clear(this.nonDefaultOptions, Color.Red); - - FillProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.NotEqual(expectedOptions, processor.Options.ShapeOptions); - - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorSetDefaultOptions() - { - this.operations.Clear(Color.Red); - - FillProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearPath.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearPath.cs deleted file mode 100644 index 155602407..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearPath.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class ClearPath : BaseImageOperationsExtensionTest -{ - private readonly DrawingOptions nonDefaultOptions = new() - { - GraphicsOptions = - { - AlphaCompositionMode = PixelFormats.PixelAlphaCompositionMode.Clear, - BlendPercentage = 0.5f, - ColorBlendingMode = PixelFormats.PixelColorBlendingMode.Darken - } - }; - - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path = new Star(1, 10, 5, 23, 56); - - [Fact] - public void Brush() - { - this.operations.Clear(this.nonDefaultOptions, this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.NotEqual(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Clear(this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Clear(this.nonDefaultOptions, Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.NotEqual(expectedOptions, processor.Options.ShapeOptions); - - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Clear(Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - ShapeOptions expectedOptions = this.shapeOptions; - Assert.Equal(expectedOptions, processor.Options.ShapeOptions); - Assert.Equal(1, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(PixelFormats.PixelAlphaCompositionMode.Src, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(PixelFormats.PixelColorBlendingMode.Normal, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearRectangle.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearRectangle.cs deleted file mode 100644 index c7d249851..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/ClearRectangle.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class ClearRectangle : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private RectangleF rectangle = new(10, 10, 20, 20); - - private RectangularPolygon RectanglePolygon => new(this.rectangle); - - [Fact] - public void Brush() - { - this.operations.Clear(new DrawingOptions(), this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Clear(this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Clear(new DrawingOptions(), Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Clear(Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs deleted file mode 100644 index 91d566714..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawBezier.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawBezier : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly PointF[] points = - [ - new(10, 10), - new(20, 20), - new(20, 50), - new(50, 10) - ]; - - private void VerifyPoints(PointF[] expectedPoints, IPath path) - { - Path innerPath = Assert.IsType(path); - ILineSegment segment = Assert.Single(innerPath.LineSegments); - CubicBezierLineSegment bezierSegment = Assert.IsType(segment); - Assert.Equal(expectedPoints, bezierSegment.ControlPoints.ToArray()); - - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.False(simplePath.IsClosed); - } - - [Fact] - public void Pen() - { - this.operations.DrawBeziers(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.DrawBeziers(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.DrawBeziers(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.DrawBeziers(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.DrawBeziers(new DrawingOptions(), Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.DrawBeziers(Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.DrawBeziers(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.DrawBeziers(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - 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 deleted file mode 100644 index 5ab5ae86c..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawLine.cs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawLine : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly PointF[] points = - [ - new(10, 10), - new(20, 20), - new(20, 50), - new(50, 10) - ]; - - private void VerifyPoints(PointF[] expectedPoints, IPath path) - { - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.False(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Pen() - { - this.operations.DrawLine(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.DrawLine(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.DrawLine(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.DrawLine(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.DrawLine(new DrawingOptions(), Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.DrawLine(Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.DrawLine(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.DrawLine(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - 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 deleted file mode 100644 index 8c283ed26..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPath.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawPath : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly IPath path = new EllipsePolygon(10, 10, 100); - - [Fact] - public void Pen() - { - this.operations.Draw(new DrawingOptions(), this.pen, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.Draw(this.pen, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen.StrokeFill, processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - Assert.Equal(this.pen.StrokeFill, processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.Draw(new DrawingOptions(), Color.Red, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Draw(Color.Red, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.path); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - 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 deleted file mode 100644 index cb104bbbd..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPathCollection.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawPathCollection : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 1); - private readonly IPath path1 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPath path2 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPathCollection pathCollection; - - public DrawPathCollection() - => this.pathCollection = new PathCollection(this.path1, this.path2); - - [Fact] - public void Pen() - { - this.operations.Draw(new DrawingOptions(), this.pen, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen, p.Pen); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.Draw(this.pen, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen, p.Pen); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen.StrokeFill, p.Pen.StrokeFill); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.pen.StrokeFill, p.Pen.StrokeFill); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.Draw(new DrawingOptions(), Color.Pink, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Pen.StrokeFill); - Assert.Equal(Color.Pink, brush.Color); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Draw(Color.Pink, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Pen.StrokeFill); - Assert.Equal(Color.Pink, brush.Color); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(10, pPen.StrokeWidth); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - SolidPen pPen = Assert.IsType(p.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, pPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, pPen.StrokeOptions.LineCap); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Path), - p => Assert.Equal(this.path2, p.Path)); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs deleted file mode 100644 index fbc3cbee3..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawPolygon.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawPolygon : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private readonly PointF[] points = - [ - new PointF(10, 10), - new PointF(10, 20), - new PointF(20, 20), - new PointF(25, 25), - new PointF(25, 10) - ]; - - private static void VerifyPoints(PointF[] expectedPoints, IPath path) - { - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.True(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Pen() - { - this.operations.DrawPolygon(new DrawingOptions(), this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void PenDefaultOptions() - { - this.operations.DrawPolygon(this.pen, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.DrawPolygon(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.DrawPolygon(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.DrawPolygon(new DrawingOptions(), Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.DrawPolygon(Color.Red, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.DrawPolygon(new DrawingOptions(), this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeOptions.LineJoin, processorPen.StrokeOptions.LineJoin); - Assert.Equal(this.pen.StrokeOptions.LineCap, processorPen.StrokeOptions.LineCap); - } - - [Fact] - public void JointAndEndCapStyleDefaultOptions() - { - this.operations.DrawPolygon(this.pen.StrokeFill, 10, this.points); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - VerifyPoints(this.points, processor.Path); - SolidPen processorPen = Assert.IsType(processor.Pen); - 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 deleted file mode 100644 index 5e5ed3304..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/DrawRectangle.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class DrawRectangle : BaseImageOperationsExtensionTest -{ - private readonly SolidPen pen = Pens.Solid(Color.HotPink, 2); - private RectangleF rectangle = new(10, 10, 20, 20); - - private RectangularPolygon RectanglePolygon => new(this.rectangle); - - [Fact] - public void CorrectlySetsPenAndPath() - { - this.operations.Draw(new DrawingOptions(), this.pen, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void CorrectlySetsPenAndPathDefaultOptions() - { - this.operations.Draw(this.pen, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.Equal(this.pen, processor.Pen); - } - - [Fact] - public void BrushAndThickness() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - Assert.Equal(this.pen.StrokeFill, processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void BrushAndThicknessDefaultOptions() - { - this.operations.Draw(this.pen.StrokeFill, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(this.pen.StrokeFill, processorPen.StrokeFill); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThickness() - { - this.operations.Draw(new DrawingOptions(), Color.Red, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Draw(Color.Red, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidBrush brush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, brush.Color); - SolidPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(10, processorPen.StrokeWidth); - } - - [Fact] - public void JointAndEndCapStyle() - { - this.operations.Draw(new DrawingOptions(), this.pen.StrokeFill, 10, this.rectangle); - - DrawPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Path)); - Assert.NotEqual(this.pen, processor.Pen); - SolidPen processorPen = Assert.IsType(processor.Pen); - 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/Fill.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Fill.cs deleted file mode 100644 index f4cdcd2bf..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/Fill.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class Fill : BaseImageOperationsExtensionTest -{ - private readonly DrawingOptions nonDefaultOptions = new(); - private readonly Brush brush = new SolidBrush(Color.HotPink); - - [Fact] - public void Brush() - { - this.operations.Fill(this.nonDefaultOptions, this.brush); - - FillProcessor processor = this.Verify(); - - DrawingOptions expectedOptions = this.nonDefaultOptions; - Assert.Equal(expectedOptions, processor.Options); - Assert.Equal(expectedOptions.GraphicsOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.GraphicsOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.GraphicsOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush); - - FillProcessor processor = this.Verify(); - - GraphicsOptions expectedOptions = this.graphicsOptions; - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(expectedOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(this.nonDefaultOptions, Color.Red); - - FillProcessor processor = this.Verify(); - - DrawingOptions expectedOptions = this.nonDefaultOptions; - Assert.Equal(expectedOptions, processor.Options); - - Assert.Equal(expectedOptions.GraphicsOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.GraphicsOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.GraphicsOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorSetDefaultOptions() - { - this.operations.Fill(Color.Red); - - FillProcessor processor = this.Verify(); - - GraphicsOptions expectedOptions = this.graphicsOptions; - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(expectedOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); - Assert.Equal(expectedOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); - Assert.Equal(expectedOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); - - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPath.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPath.cs deleted file mode 100644 index bf895e21e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPath.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPath : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path = new Star(1, 10, 5, 23, 56); - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Fill(Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.Equal(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathBuilder.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathBuilder.cs deleted file mode 100644 index 070f2577e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathBuilder.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPathBuilder : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path = null; - private readonly Action builder = pb => - { - pb.StartFigure(); - pb.AddLine(10, 10, 20, 20); - pb.AddLine(60, 450, 120, 340); - pb.AddLine(120, 340, 10, 10); - pb.CloseAllFigures(); - }; - - public FillPathBuilder() - { - PathBuilder pb = new(); - this.builder(pb); - this.path = pb.Build(); - } - - private void VerifyPoints(IPath expectedPath, IPath path) - { - ISimplePath simplePathExpected = Assert.Single(expectedPath.Flatten()); - PointF[] expectedPoints = simplePathExpected.Points.ToArray(); - - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.True(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Red, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Fill(Color.Red, this.builder); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathCollection.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathCollection.cs deleted file mode 100644 index aca2d2e05..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPathCollection.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPathCollection : BaseImageOperationsExtensionTest -{ - private readonly Color color = Color.HotPink; - private readonly SolidBrush brush = Brushes.Solid(Color.HotPink); - private readonly IPath path1 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPath path2 = new Path(new LinearLineSegment( - [ - new Vector2(10, 10), - new Vector2(20, 10), - new Vector2(20, 10), - new Vector2(30, 10) - ])); - - private readonly IPathCollection pathCollection; - - public FillPathCollection() - => this.pathCollection = new PathCollection(this.path1, this.path2); - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.brush, p.Brush); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } - - [Fact] - public void BrushWithDefault() - { - this.operations.Fill(this.brush, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - Assert.Equal(this.brush, p.Brush); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Pink, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.NotEqual(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Brush); - Assert.Equal(Color.Pink, brush.Color); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } - - [Fact] - public void ColorWithDefault() - { - this.operations.Fill(Color.Pink, this.pathCollection); - IEnumerable processors = this.VerifyAll(); - - Assert.All(processors, p => - { - Assert.Equal(this.shapeOptions, p.Options.ShapeOptions); - SolidBrush brush = Assert.IsType(p.Brush); - Assert.Equal(Color.Pink, brush.Color); - }); - - Assert.Collection( - processors, - p => Assert.Equal(this.path1, p.Region), - p => Assert.Equal(this.path2, p.Region)); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPolygon.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPolygon.cs deleted file mode 100644 index bac4ffb04..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillPolygon.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillPolygon : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private readonly PointF[] path = - [ - new PointF(10, 10), - new PointF(10, 20), - new PointF(20, 20), - new PointF(25, 25), - new PointF(25, 10) - ]; - - private void VerifyPoints(PointF[] expectedPoints, IPath path) - { - ISimplePath simplePath = Assert.Single(path.Flatten()); - Assert.True(simplePath.IsClosed); - Assert.Equal(expectedPoints, simplePath.Points.ToArray()); - } - - [Fact] - public void Brush() - { - this.operations.FillPolygon(new DrawingOptions(), this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.FillPolygon(this.brush, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.FillPolygon(new DrawingOptions(), Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.FillPolygon(Color.Red, this.path); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - this.VerifyPoints(this.path, processor.Region); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillRectangle.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillRectangle.cs deleted file mode 100644 index a13537d46..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Paths/FillRectangle.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing; -using SixLabors.ImageSharp.Drawing.Tests.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Paths; - -public class FillRectangle : BaseImageOperationsExtensionTest -{ - private readonly Brush brush = Brushes.Solid(Color.HotPink); - private RectangleF rectangle = new(10, 10, 20, 20); - - private RectangularPolygon RectanglePolygon => new(this.rectangle); - - [Fact] - public void Brush() - { - this.operations.Fill(new DrawingOptions(), this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void BrushDefaultOptions() - { - this.operations.Fill(this.brush, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.Equal(this.brush, processor.Brush); - } - - [Fact] - public void ColorSet() - { - this.operations.Fill(new DrawingOptions(), Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.NotEqual(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } - - [Fact] - public void ColorAndThicknessDefaultOptions() - { - this.operations.Fill(Color.Red, this.rectangle); - - FillPathProcessor processor = this.Verify(); - - Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); - Assert.True(RectangularPolygonValueComparer.Equals(this.RectanglePolygon, processor.Region)); - Assert.NotEqual(this.brush, processor.Brush); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs new file mode 100644 index 000000000..08a859061 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Drawing/ProcessWithCanvas.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; + +public class ProcessWithCanvas : BaseImageOperationsExtensionTest +{ + private readonly DrawingOptions nonDefaultOptions = new(); + + [Fact] + public void CanvasActionDefaultOptions() + { + this.operations.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.Red))); + + ProcessWithCanvasProcessor processor = this.Verify(); + + GraphicsOptions expectedOptions = this.graphicsOptions; + Assert.Equal(this.shapeOptions, processor.Options.ShapeOptions); + Assert.Equal(expectedOptions.BlendPercentage, processor.Options.GraphicsOptions.BlendPercentage); + Assert.Equal(expectedOptions.AlphaCompositionMode, processor.Options.GraphicsOptions.AlphaCompositionMode); + Assert.Equal(expectedOptions.ColorBlendingMode, processor.Options.GraphicsOptions.ColorBlendingMode); + } + + [Fact] + public void CanvasActionWithOptions() + { + this.operations.ProcessWithCanvas( + this.nonDefaultOptions, + canvas => canvas.Clear(Brushes.Solid(Color.Red))); + + ProcessWithCanvasProcessor processor = this.Verify(); + Assert.Equal(this.nonDefaultOptions, processor.Options); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/RecolorImageTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/RecolorImageTests.cs deleted file mode 100644 index 289971c86..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/RecolorImageTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class RecolorImageTests -{ - [Theory] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, "Yellow", "Pink", 0.2f)] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] - [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] - [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.6f)] - public void Recolor(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) - where TPixel : unmanaged, IPixel - { - Color sourceColor = TestUtils.GetColorByName(sourceColorName); - Color targetColor = TestUtils.GetColorByName(targetColorName); - RecolorBrush brush = new(sourceColor, targetColor, threshold); - - FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; - provider.RunValidatingProcessorTest(x => x.Fill(brush), testInfo); - } - - [Theory] - [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] - [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] - public void Recolor_InBox(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) - where TPixel : unmanaged, IPixel - { - Color sourceColor = TestUtils.GetColorByName(sourceColorName); - Color targetColor = TestUtils.GetColorByName(targetColorName); - RecolorBrush brush = new(sourceColor, targetColor, threshold); - - FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; - provider.RunValidatingProcessorTest( - x => - { - Size size = x.GetCurrentSize(); - Rectangle rectangle = new(0, (size.Height / 2) - (size.Height / 4), size.Width, size.Height / 2); - x.Fill(brush, rectangle); - }, - testInfo); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs deleted file mode 100644 index 11e80c5c2..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/SolidBezierTests.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class SolidBezierTests -{ - [Theory] - [WithBlankImage(500, 500, PixelTypes.Rgba32)] - public void FilledBezier(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 400), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 400) - ]; - - Color blue = Color.Blue; - Color hotPink = Color.HotPink; - - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.BackgroundColor(blue)); - image.Mutate(x => x.Fill(hotPink, new Polygon(new CubicBezierLineSegment(simplePath)))); - image.DebugSave(provider); - image.CompareToReferenceOutput(provider); - } - } - - [Theory] - [WithBlankImage(500, 500, PixelTypes.Rgba32)] - public void OverlayByFilledPolygonOpacity(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - PointF[] simplePath = - [ - new Vector2(10, 400), - new Vector2(30, 10), - new Vector2(240, 30), - new Vector2(300, 400) - ]; - - Color color = Color.HotPink.WithAlpha(150 / 255F); - - using (Image image = provider.GetImage() as Image) - { - image.Mutate(x => x.BackgroundColor(Color.Blue)); - image.Mutate(x => x.Fill(color, new Polygon(new CubicBezierLineSegment(simplePath)))); - image.DebugSave(provider); - image.CompareToReferenceOutput(provider); - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/SolidFillBlendedShapesTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/SolidFillBlendedShapesTests.cs deleted file mode 100644 index f6464394e..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/SolidFillBlendedShapesTests.cs +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing; - -[GroupOutput("Drawing")] -public class SolidFillBlendedShapesTests -{ - public static IEnumerable Modes { get; } = GetAllModeCombinations(); - - private static IEnumerable GetAllModeCombinations() - { - foreach (object composition in Enum.GetValues(typeof(PixelAlphaCompositionMode))) - { - foreach (object blending in Enum.GetValues(typeof(PixelColorBlendingMode))) - { - yield return [blending, composition]; - } - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendHotPinkRect( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image img = provider.GetImage()) - { - int scaleX = img.Width / 100; - int scaleY = img.Height / 100; - img.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)) - - .Fill( - new DrawingOptions - { - GraphicsOptions = - { - Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition - } - }, - Color.HotPink, - new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY))); - - VerifyImage(provider, blending, composition, img); - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image img = provider.GetImage()) - { - int scaleX = img.Width / 100; - int scaleY = img.Height / 100; - img.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY))); - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - Color.HotPink, - new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY))); - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - Color.Transparent, - new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))); - - VerifyImage(provider, blending, composition, img); - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image img = provider.GetImage()) - { - int scaleX = img.Width / 100; - int scaleY = img.Height / 100; - img.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY))); - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - Color.HotPink, - new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY))); - - Color transparentRed = Color.Red.WithAlpha(0.5f); - - img.Mutate( - x => x.Fill( - new DrawingOptions - { - GraphicsOptions = new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition } - }, - transparentRed, - new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))); - - VerifyImage(provider, blending, composition, img); - } - } - - [Theory] - [WithBlankImage(nameof(Modes), 250, 250, PixelTypes.Rgba32)] -#pragma warning disable IDE1006 // Naming Styles -#pragma warning disable SA1300 // Element should begin with upper-case letter - public void _1DarkBlueRect_2BlendBlackEllipse( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition) - where TPixel : unmanaged, IPixel -#pragma warning restore SA1300 // Element should begin with upper-case letter -#pragma warning restore IDE1006 // Naming Styles - { - using (Image dstImg = provider.GetImage(), srcImg = provider.GetImage()) - { - int scaleX = dstImg.Width / 100; - int scaleY = dstImg.Height / 100; - - dstImg.Mutate( - x => x.Fill( - Color.DarkBlue, - new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY))); - - srcImg.Mutate( - x => x.Fill( - Color.Black, - new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY))); - - dstImg.Mutate( - x => x.DrawImage(srcImg, new GraphicsOptions { Antialias = true, ColorBlendingMode = blending, AlphaCompositionMode = composition })); - - VerifyImage(provider, blending, composition, dstImg); - } - } - - private static void VerifyImage( - TestImageProvider provider, - PixelColorBlendingMode blending, - PixelAlphaCompositionMode composition, - Image img) - where TPixel : unmanaged, IPixel - { - img.DebugSave( - provider, - new { composition, blending }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - - ImageComparer comparer = ImageComparer.TolerantPercentage(0.01f, 3); - img.CompareFirstFrameToReferenceOutput( - comparer, - provider, - new { composition, blending }, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawText.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawText.cs deleted file mode 100644 index 4355855ee..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawText.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.Fonts; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; -using SixLabors.ImageSharp.Drawing.Tests.Processing; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Text; - -public class DrawText : BaseImageOperationsExtensionTest -{ - private readonly FontCollection fontCollection; - private readonly RichTextOptions textOptions; - private readonly DrawingOptions otherDrawingOptions = new() - { - GraphicsOptions = new GraphicsOptions() - }; - - private readonly Font font; - - public DrawText() - { - this.fontCollection = new FontCollection(); - this.font = this.fontCollection.Add(TestFontUtilities.GetPath("SixLaborsSampleAB.woff")).CreateFont(12); - this.textOptions = new RichTextOptions(this.font) { WrappingLength = 99 }; - } - - [Fact] - public void FillsForEachACharacterWhenBrushSetAndNotPen() - { - this.operations.DrawText( - this.otherDrawingOptions, - "123", - this.font, - Brushes.Solid(Color.Red), - null, - Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenBrushSetAndNotPenDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Brushes.Solid(Color.Red)); - - DrawTextProcessor processor = this.Verify(0); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenBrushSet() - { - this.operations.DrawText(this.otherDrawingOptions, "123", this.font, Brushes.Solid(Color.Red), Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenBrushSetDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Brushes.Solid(Color.Red)); - - DrawTextProcessor processor = this.Verify(0); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenColorSet() - { - this.operations.DrawText(this.otherDrawingOptions, "123", this.font, Color.Red, Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void FillsForEachACharacterWhenColorSetDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Color.Red); - - DrawTextProcessor processor = this.Verify(0); - - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetAndNotBrush() - { - this.operations.DrawText( - this.otherDrawingOptions, - "123", - this.font, - null, - Pens.Dash(Color.Red, 1), - Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetAndNotBrushDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Pens.Dash(Color.Red, 1)); - - DrawTextProcessor processor = this.Verify(0); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSet() - { - this.operations.DrawText(this.otherDrawingOptions, "123", this.font, Pens.Dash(Color.Red, 1), Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetDefaultOptions() - { - this.operations.DrawText(this.textOptions, "123", Pens.Dash(Color.Red, 1)); - - DrawTextProcessor processor = this.Verify(0); - - Assert.Equal("123", processor.Text); - Assert.Equal(this.font, processor.TextOptions.Font); - SolidBrush penBrush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, penBrush.Color); - PatternPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(1, processorPen.StrokeWidth); - Assert.Equal(PointF.Empty, processor.Location); - Assert.Equal(this.textOptions, processor.TextOptions); - Assert.Equal(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } - - [Fact] - public void DrawForEachACharacterWhenPenSetAndFillFroEachWhenBrushSet() - { - this.operations.DrawText( - this.otherDrawingOptions, - "123", - this.font, - Brushes.Solid(Color.Red), - Pens.Dash(Color.Red, 1), - Vector2.Zero); - - DrawTextProcessor processor = this.Verify(0); - - Assert.Equal("123", processor.Text); - Assert.Equal(this.font, processor.TextOptions.Font); - SolidBrush brush = Assert.IsType(processor.Brush); - Assert.Equal(Color.Red, brush.Color); - Assert.Equal(PointF.Empty, processor.Location); - SolidBrush penBrush = Assert.IsType(processor.Pen.StrokeFill); - Assert.Equal(Color.Red, penBrush.Color); - PatternPen processorPen = Assert.IsType(processor.Pen); - Assert.Equal(1, processorPen.StrokeWidth); - Assert.NotEqual(this.textOptions, processor.TextOptions); - Assert.NotEqual(this.graphicsOptions, processor.DrawingOptions.GraphicsOptions); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs deleted file mode 100644 index e7379712a..000000000 --- a/tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs +++ /dev/null @@ -1,979 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Text; -using SixLabors.Fonts; -using SixLabors.Fonts.Unicode; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.Drawing.Text; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using Xunit.Abstractions; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Text; - -[GroupOutput("Drawing/Text")] -[ValidateDisposedMemoryAllocations] -public class DrawTextOnImageTests -{ - private const string AB = "AB\nAB"; - - private const string TestText = "Sphinx of black quartz, judge my vow\n0123456789"; - - private static readonly ImageComparer TextDrawingComparer = ImageComparer.TolerantPercentage(1e-2f); - - private static readonly ImageComparer OutlinedTextDrawingComparer = ImageComparer.TolerantPercentage(0.0069F); - - public DrawTextOnImageTests(ITestOutputHelper output) - => this.Output = output; - - private ITestOutputHelper Output { get; } - - [Theory] - [WithSolidFilledImages(1276, 336, "White", PixelTypes.Rgba32, ColorFontSupport.ColrV0)] - [WithSolidFilledImages(1276, 336, "White", PixelTypes.Rgba32, ColorFontSupport.None)] - public void EmojiFontRendering(TestImageProvider provider, ColorFontSupport colorFontSupport) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 70); - FontFamily emojiFontFamily = CreateFont(TestFonts.TwemojiMozilla, 36).Family; - - Color color = Color.Black; - string text = "A short piece of text 😀 with an emoji"; - - provider.VerifyOperation( - TextDrawingComparer, - img => - { - RichTextOptions textOptions = new(font) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = TextAlignment.Center, - FallbackFontFamilies = [emojiFontFamily], - ColorFontSupport = colorFontSupport, - Origin = new PointF(img.Width / 2, img.Height / 2) - }; - - img.Mutate(i => i.DrawText(textOptions, text, color)); - }, - $"ColorFontsEnabled-{colorFontSupport == ColorFontSupport.ColrV0}"); - } - - [Theory] - [WithSolidFilledImages(400, 200, "White", PixelTypes.Rgba32)] - public void FallbackFontRendering(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - // https://github.com/SixLabors/Fonts/issues/171 - FontCollection collection = new(); - Font whitney = CreateFont(TestFonts.WhitneyBook, 25); - FontFamily malgun = CreateFont(TestFonts.Malgun, 25).Family; - - Color color = Color.Black; - const string text = "亞DARKSOUL亞"; - - provider.VerifyOperation( - TextDrawingComparer, - img => - { - RichTextOptions textOptions = new(whitney) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = TextAlignment.Center, - FallbackFontFamilies = [malgun], - KerningMode = KerningMode.Standard, - Origin = new PointF(img.Width / 2, img.Height / 2) - }; - - img.Mutate(i => i.DrawText(textOptions, text, color)); - }); - } - - [Theory] - [WithSolidFilledImages(276, 336, "White", PixelTypes.Rgba32)] - public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 36); - Color color = Color.Black; - const string text = "A short piece of text"; - - using Image img = provider.GetImage(); - - // Measure the text size - FontRectangle size = TextMeasurer.MeasureSize(text, new RichTextOptions(font)); - - // Find out how much we need to scale the text to fill the space (up or down) - float scalingFactor = Math.Min(img.Width / size.Width, img.Height / size.Height); - - // Create a new font - Font scaledFont = new(font, scalingFactor * font.Size); - RichTextOptions textOptions = new(scaledFont) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = TextAlignment.Center, - Origin = new PointF(img.Width / 2, img.Height / 2) - }; - - img.Mutate(i => i.DrawText(textOptions, text, color)); - } - - [Theory] - [WithSolidFilledImages(1500, 500, "White", PixelTypes.Rgba32)] - public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688_2(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 39); - string text = new('a', 10000); - Color color = Color.Black; - PointF point = new(100, 100); - - using Image img = provider.GetImage(); - img.Mutate(ctx => ctx.DrawText(text, font, color, point)); - } - - [Theory] - [WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32)] - public void OpenSansJWithNoneZeroShouldntExtendPastGlyphe(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 50); - Color color = Color.Black; - - using Image img = provider.GetImage(); - img.Mutate(ctx => ctx.DrawText(TestText, font, Color.Black, new PointF(-50, 2))); - - Assert.Equal(Color.White.ToPixel(), img[173, 2]); - } - - [Theory] - [WithSolidFilledImages(20, 50, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, "i")] - [WithSolidFilledImages(200, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.SixLaborsSampleAB, AB)] - [WithSolidFilledImages(900, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, TestText)] - [WithSolidFilledImages(400, 45, "White", PixelTypes.Rgba32, 20, 0, 0, TestFonts.OpenSans, TestText)] - [WithSolidFilledImages(1100, 200, "White", PixelTypes.Rgba32, 50, 150, 50, TestFonts.OpenSans, TestText)] - public void FontShapesAreRenderedCorrectly( - TestImageProvider provider, - int fontSize, - int x, - int y, - string fontName, - string text) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, fontSize); - - provider.RunValidatingProcessorTest( - c => c.DrawText(text, font, Color.Black, new PointF(x, y)), - $"{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(50, 50, "White", PixelTypes.Rgba32, 50, 25, 25, TestFonts.OpenSans, "i", 45, 25, 25)] - [WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32, 50, 100, 100, TestFonts.SixLaborsSampleAB, AB, 45, 100, 100)] - [WithSolidFilledImages(1100, 1100, "White", PixelTypes.Rgba32, 50, 550, 550, TestFonts.OpenSans, TestText, 45, 550, 550)] - [WithSolidFilledImages(400, 400, "White", PixelTypes.Rgba32, 20, 200, 200, TestFonts.OpenSans, TestText, 45, 200, 200)] - public void FontShapesAreRenderedCorrectly_WithRotationApplied( - TestImageProvider provider, - int fontSize, - int x, - int y, - string fontName, - string text, - float angle, - float rotationOriginX, - float rotationOriginY) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, fontSize); - float radians = GeometryUtilities.DegreeToRadian(angle); - - RichTextOptions textOptions = new(font) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = TextAlignment.Center, - Origin = new PointF(x, y) - }; - - provider.RunValidatingProcessorTest( - x => x - .SetDrawingTransform(Matrix3x2.CreateRotation(radians, new Vector2(rotationOriginX, rotationOriginY))) - .DrawText(textOptions, text, Color.Black), - $"F({fontName})-S({fontSize})-A({angle})-{ToTestOutputDisplayText(text)}-({x},{y})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(50, 50, "White", PixelTypes.Rgba32, 50, 25, 25, TestFonts.OpenSans, "i", -12, 0, 25, 25)] - [WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32, 50, 100, 100, TestFonts.SixLaborsSampleAB, AB, 10, 0, 100, 100)] - [WithSolidFilledImages(1100, 1100, "White", PixelTypes.Rgba32, 50, 550, 550, TestFonts.OpenSans, TestText, 0, 10, 550, 550)] - [WithSolidFilledImages(400, 400, "White", PixelTypes.Rgba32, 20, 200, 200, TestFonts.OpenSans, TestText, 0, -10, 200, 200)] - public void FontShapesAreRenderedCorrectly_WithSkewApplied( - TestImageProvider provider, - int fontSize, - int x, - int y, - string fontName, - string text, - float angleX, - float angleY, - float rotationOriginX, - float rotationOriginY) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, fontSize); - float radianX = GeometryUtilities.DegreeToRadian(angleX); - float radianY = GeometryUtilities.DegreeToRadian(angleY); - - RichTextOptions textOptions = new(font) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - TextAlignment = TextAlignment.Center, - Origin = new PointF(x, y) - }; - - provider.RunValidatingProcessorTest( - x => x - .SetDrawingTransform(Matrix3x2.CreateSkew(radianX, radianY, new Vector2(rotationOriginX, rotationOriginY))) - .DrawText(textOptions, text, Color.Black), - $"F({fontName})-S({fontSize})-A({angleX},{angleY})-{ToTestOutputDisplayText(text)}-({x},{y})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - /// - /// Based on: - /// https://github.com/SixLabors/ImageSharp/issues/572 - /// - [Theory] - [WithSolidFilledImages(2480, 3508, "White", PixelTypes.Rgba32)] - public void FontShapesAreRenderedCorrectly_LargeText( - TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 36); - - StringBuilder sb = new(); - string str = Repeat(" ", 78) + "THISISTESTWORDSTHISISTESTWORDSTHISISTESTWORDSTHISISTESTWORDSTHISISTESTWORDS"; - sb.Append(str); - - string newLines = Repeat("\r\n", 61); - sb.Append(newLines); - - for (int i = 0; i < 10; i++) - { - sb.AppendLine(str); - } - - // Strict comparer, because the image is sparse: - ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); - - provider.VerifyOperation( - comparer, - img => img.Mutate(c => c.DrawText(sb.ToString(), font, Color.Black, new PointF(10, 1))), - false, - false); - } - - [Theory] - [WithSolidFilledImages(400, 550, "White", PixelTypes.Rgba32, 1, 5, true)] - [WithSolidFilledImages(400, 550, "White", PixelTypes.Rgba32, 1.5, 3, true)] - [WithSolidFilledImages(400, 550, "White", PixelTypes.Rgba32, 2, 2, true)] - [WithSolidFilledImages(400, 100, "White", PixelTypes.Rgba32, 1, 5, false)] - [WithSolidFilledImages(400, 100, "White", PixelTypes.Rgba32, 1.5, 3, false)] - [WithSolidFilledImages(400, 100, "White", PixelTypes.Rgba32, 2, 2, false)] - public void FontShapesAreRenderedCorrectly_WithLineSpacing( - TestImageProvider provider, - float lineSpacing, - int lineCount, - bool wrap) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 16); - - StringBuilder sb = new(); - string str = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna."; - - for (int i = 0; i < lineCount; i++) - { - sb.AppendLine(str); - } - - RichTextOptions textOptions = new(font) - { - KerningMode = KerningMode.Standard, - VerticalAlignment = VerticalAlignment.Top, - HorizontalAlignment = HorizontalAlignment.Left, - LineSpacing = lineSpacing, - Origin = new PointF(10, 1) - }; - - if (wrap) - { - textOptions.WrappingLength = 300; - } - - Color color = Color.Black; - - // NET472 is 0.0045 different. - ImageComparer comparer = ImageComparer.TolerantPercentage(0.0046F); - - provider.VerifyOperation( - comparer, - img => img.Mutate(c => c.DrawText(textOptions, sb.ToString(), color)), - $"linespacing_{lineSpacing}_linecount_{lineCount}_wrap_{wrap}", - false, - false); - } - - [Theory] - [WithSolidFilledImages(200, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.SixLaborsSampleAB, AB)] - [WithSolidFilledImages(900, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, TestText)] - [WithSolidFilledImages(1100, 200, "White", PixelTypes.Rgba32, 50, 150, 50, TestFonts.OpenSans, TestText)] - public void FontShapesAreRenderedCorrectlyWithAPen( - TestImageProvider provider, - int fontSize, - int x, - int y, - string fontName, - string text) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, fontSize); - Color color = Color.Black; - - provider.VerifyOperation( - OutlinedTextDrawingComparer, - img => img.Mutate(c => c.DrawText(text, new Font(font, fontSize), Pens.Solid(color, 1), new PointF(x, y))), - $"pen_{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(200, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.SixLaborsSampleAB, AB)] - [WithSolidFilledImages(900, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, TestText)] - [WithSolidFilledImages(1100, 200, "White", PixelTypes.Rgba32, 50, 150, 50, TestFonts.OpenSans, TestText)] - public void FontShapesAreRenderedCorrectlyWithAPenPatterned( - TestImageProvider provider, - int fontSize, - int x, - int y, - string fontName, - string text) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, fontSize); - Color color = Color.Black; - - provider.VerifyOperation( - OutlinedTextDrawingComparer, - img => img.Mutate(c => c.DrawText(text, new Font(font, fontSize), Pens.DashDot(color, 3), new PointF(x, y))), - $"pen_{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(1000, 1500, "White", PixelTypes.Rgba32, TestFonts.OpenSans)] - public void TextPositioningIsRobust(TestImageProvider provider, string fontName) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, 30); - - string text = Repeat( - "Beware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch!\n", - 20); - - RichTextOptions textOptions = new(font) - { - WrappingLength = 1000, - Origin = new PointF(10, 50) - }; - - string details = fontName.Replace(" ", string.Empty); - - // Based on the reported 0.1755% difference with AccuracyMultiple = 8 - // We should avoid quality regressions leading to higher difference! - ImageComparer comparer = ImageComparer.TolerantPercentage(0.2f); - - provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.Black), - details, - comparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: false); - } - - [Fact] - public void CanDrawTextWithEmptyPath() - { - // The following font/text combination generates an empty path. - Font font = CreateFont(TestFonts.WendyOne, 72); - const string text = "Hello\0World"; - RichTextOptions textOptions = new(font); - FontRectangle textSize = TextMeasurer.MeasureSize(text, textOptions); - - Assert.NotEqual(FontRectangle.Empty, textSize); - - using Image image = new(Configuration.Default, (int)textSize.Width + 20, (int)textSize.Height + 20); - image.Mutate(x => x.DrawText( - text, - font, - Color.Black, - Vector2.Zero)); - } - - [Theory] - [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 32, 75F)] - [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 40, 90F)] - public void CanRotateFilledFont_Issue175( - TestImageProvider provider, - string fontName, - int fontSize, - float angle) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, fontSize); - const string text = "QuickTYZ"; - AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle); - - RichTextOptions textOptions = new(font); - FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); - Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height))); - - provider.RunValidatingProcessorTest( - x => x.SetDrawingTransform(transform).DrawText(textOptions, text, Color.Black), - $"F({fontName})-S({fontSize})-A({angle})-{ToTestOutputDisplayText(text)})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 32, 75F, 1)] - [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 40, 90F, 2)] - public void CanRotateOutlineFont_Issue175( - TestImageProvider provider, - string fontName, - int fontSize, - float angle, - int strokeWidth) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(fontName, fontSize); - const string text = "QuickTYZ"; - AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle); - - RichTextOptions textOptions = new(font); - FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions); - Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height))); - - provider.RunValidatingProcessorTest( - x => x.SetDrawingTransform(transform) - .DrawText(textOptions, text, Pens.Solid(Color.Black, strokeWidth)), - $"F({fontName})-S({fontSize})-A({angle})-STR({strokeWidth})-{ToTestOutputDisplayText(text)})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(500, 200, nameof(Color.Black), PixelTypes.Rgba32, 32)] - [WithSolidFilledImages(500, 300, nameof(Color.Black), PixelTypes.Rgba32, 40)] - public void DrawRichText( - TestImageProvider provider, - int fontSize) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, fontSize); - Font font2 = CreateFont(TestFonts.OpenSans, fontSize * 1.5f); - const string text = "The quick brown fox jumps over the lazy dog"; - - RichTextOptions textOptions = new(font) - { - Origin = new Vector2(15), - WrappingLength = 400, - TextRuns = - [ - new RichTextRun - { - Start = 0, - End = 3, - OverlinePen = Pens.Solid(Color.Yellow, 1), - StrikeoutPen = Pens.Solid(Color.HotPink, 5), - }, - - new RichTextRun - { - Start = 4, - End = 10, - TextDecorations = TextDecorations.Strikeout, - StrikeoutPen = Pens.Solid(Color.Red), - OverlinePen = Pens.Solid(Color.Green, 9), - Brush = Brushes.Solid(Color.Red), - }, - - new RichTextRun - { - Start = 10, - End = 13, - Font = font2, - TextDecorations = TextDecorations.Strikeout, - StrikeoutPen = Pens.Solid(Color.White, 6), - OverlinePen = Pens.Solid(Color.Orange, 2), - }, - - new RichTextRun - { - Start = 19, - End = 23, - TextDecorations = TextDecorations.Underline, - UnderlinePen = Pens.Dot(Color.Fuchsia, 5), - Brush = Brushes.Solid(Color.Blue), - }, - - new RichTextRun - { - Start = 23, - End = 25, - TextDecorations = TextDecorations.Underline, - UnderlinePen = Pens.Solid(Color.White), - } - ] - }; - provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), - $"RichText-F({fontSize})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(500, 200, nameof(Color.Black), PixelTypes.Rgba32, 32)] - [WithSolidFilledImages(500, 300, nameof(Color.Black), PixelTypes.Rgba32, 40)] - public void DrawRichTextArabic( - TestImageProvider provider, - int fontSize) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.MeQuranVolyNewmet, fontSize); - string text = "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ"; - - RichTextOptions textOptions = new(font) - { - Origin = new Vector2(15), - WrappingLength = 400, - TextRuns = - [ - new RichTextRun { Start = 0, End = CodePoint.GetCodePointCount(text.AsSpan()), TextDecorations = TextDecorations.Underline } - ] - }; - provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), - $"RichText-Arabic-F({fontSize})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(500, 200, nameof(Color.Black), PixelTypes.Rgba32, 32)] - [WithSolidFilledImages(500, 300, nameof(Color.Black), PixelTypes.Rgba32, 40)] - public void DrawRichTextRainbow( - TestImageProvider provider, - int fontSize) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, fontSize); - const string text = "The quick brown fox jumps over the lazy dog"; - - SolidPen[] colors = - [ - new SolidPen(Color.Red), - new SolidPen(Color.Orange), - new SolidPen(Color.Yellow), - new SolidPen(Color.Green), - new SolidPen(Color.Blue), - new SolidPen(Color.Indigo), - new SolidPen(Color.Violet) - ]; - - List runs = []; - for (int i = 0; i < text.Length; i++) - { - SolidPen pen = colors[i % colors.Length]; - runs.Add(new RichTextRun - { - Start = i, - End = i + 1, - UnderlinePen = pen - }); - } - - RichTextOptions textOptions = new(font) - { - Origin = new Vector2(15), - WrappingLength = 400, - TextRuns = runs, - }; - - provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), - $"RichText-Rainbow-F({fontSize})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, "M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50", "spiral")] - [WithSolidFilledImages(350, 350, nameof(Color.Black), PixelTypes.Rgba32, "M275 175 A100 100 0 1 1 275 174", "circle")] - [WithSolidFilledImages(120, 120, nameof(Color.Black), PixelTypes.Rgba32, "M50,10 L 90 90 L 10 90 L50 10", "triangle")] - public void CanDrawRichTextAlongPathHorizontal(TestImageProvider provider, string svgPath, string exampleImageKey) - where TPixel : unmanaged, IPixel - { - bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); - Assert.True(parsed); - - Font font = CreateFont(TestFonts.OpenSans, 13); - - const string text = "Quick brown fox jumps over the lazy dog."; - RichTextRun run = new() - { - Start = 0, - End = text.GetGraphemeCount(), - StrikeoutPen = new SolidPen(Color.Red) - }; - - RichTextOptions textOptions = new(font) - { - WrappingLength = path.ComputeLength(), - VerticalAlignment = VerticalAlignment.Bottom, - HorizontalAlignment = HorizontalAlignment.Left, - Path = path, - TextRuns = [run] - }; - - provider.RunValidatingProcessorTest( - x => x.DrawText(textOptions, text, Color.White), - $"RichText-Path-({exampleImageKey})", - TextDrawingComparer, - appendPixelTypeToFileName: false, - appendSourceFileOrDescription: true); - } - - [Theory] - [WithBlankImage(100, 100, PixelTypes.Rgba32, "M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50", "spiral")] - [WithBlankImage(350, 350, PixelTypes.Rgba32, "M275 175 A100 100 0 1 1 275 174", "circle")] - [WithBlankImage(120, 120, PixelTypes.Rgba32, "M50,10 L 90 90 L 10 90 L50 10", "triangle")] - public void CanDrawTextAlongPathHorizontal(TestImageProvider provider, string svgPath, string exampleImageKey) - where TPixel : unmanaged, IPixel - { - bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); - Assert.True(parsed); - - const string text = "Quick brown fox jumps over the lazy dog."; - - Font font = CreateFont(TestFonts.OpenSans, 13); - RichTextOptions textOptions = new(font) - { - WrappingLength = path.ComputeLength(), - VerticalAlignment = VerticalAlignment.Bottom, - HorizontalAlignment = HorizontalAlignment.Left, - TextRuns = [new RichTextRun { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Strikeout }], - }; - - IPathCollection glyphs = TextBuilder.GeneratePaths(text, path, textOptions); - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Draw(Color.Red, 1, path).Fill(Color.Black, glyphs), - new { type = exampleImageKey }, - comparer: ImageComparer.TolerantPercentage(0.0025f)); - } - - [Theory] - [WithBlankImage(350, 350, PixelTypes.Rgba32, "M225 175 A50 50 0 1 1 225 174", "circle")] - [WithBlankImage(250, 250, PixelTypes.Rgba32, "M100,60 L 140 140 L 60 140 L100 60", "triangle")] - public void CanDrawTextAlongPathVertical(TestImageProvider provider, string svgPath, string exampleImageKey) - where TPixel : unmanaged, IPixel - { - bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); - Assert.True(parsed); - - Font font = CreateFont(TestFonts.OpenSans, 13); - RichTextOptions textOptions = new(font) - { - WrappingLength = path.ComputeLength() / 4, - VerticalAlignment = VerticalAlignment.Bottom, - HorizontalAlignment = HorizontalAlignment.Left, - LayoutMode = LayoutMode.VerticalLeftRight - }; - - const string text = "Quick brown fox jumps over the lazy dog."; - IPathCollection glyphs = TextBuilder.GeneratePaths(text, path, textOptions); - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Draw(Color.Red, 1, path).Fill(Color.Black, glyphs), - new { type = exampleImageKey }, - comparer: ImageComparer.TolerantPercentage(0.002f)); - } - - [Theory] - [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] - public void PathAndTextDrawingMatch(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - // https://github.com/SixLabors/ImageSharp.Drawing/issues/234 - Font font = CreateFont(TestFonts.NettoOffc, 300); - const string text = "all"; - - provider.VerifyOperation( - TextDrawingComparer, - img => - { - foreach (HorizontalAlignment ha in (HorizontalAlignment[])Enum.GetValues(typeof(HorizontalAlignment))) - { - foreach (VerticalAlignment va in (VerticalAlignment[])Enum.GetValues(typeof(VerticalAlignment))) - { - TextOptions to = new(font) - { - HorizontalAlignment = ha, - VerticalAlignment = va, - }; - - FontRectangle bounds = TextMeasurer.MeasureBounds(text, to); - float x = (img.Size.Width - bounds.Width) / 2; - PointF[] pathLine = - [ - new PointF(x, 500), - new PointF(x + bounds.Width, 500) - ]; - - IPath path = new PathBuilder().AddLine(pathLine[0], pathLine[1]).Build(); - - RichTextOptions rto = new(font) - { - Origin = pathLine[0], - HorizontalAlignment = ha, - VerticalAlignment = va, - }; - - IPathCollection tb = TextBuilder.GeneratePaths(text, path, to); - - img.Mutate( - i => i.DrawLine(new SolidPen(Color.Red, 30), pathLine) - .DrawText(rto, text, Color.Black) - .Fill(Brushes.ForwardDiagonal(Color.HotPink), tb)); - } - } - }); - } - - [Theory] - [WithBlankImage(500, 400, PixelTypes.Rgba32)] - public void CanFillTextVertical(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 36); - Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); - - const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; - RichTextOptions textOptions = new(font) - { - Origin = new Vector2(0, 0), - FallbackFontFamilies = [fallback.Family], - WrappingLength = 300, - LayoutMode = LayoutMode.VerticalLeftRight, - TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } - ] - }; - - IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, textOptions); - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Fill(Color.Black, glyphs), - comparer: ImageComparer.TolerantPercentage(0.002f)); - } - - [Theory] - [WithBlankImage(500, 400, PixelTypes.Rgba32)] - public void CanFillTextVerticalMixed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 36); - Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); - - const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; - RichTextOptions textOptions = new(font) - { - FallbackFontFamilies = [fallback.Family], - WrappingLength = 400, - LayoutMode = LayoutMode.VerticalMixedLeftRight, - TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } - ] - }; - - IPathCollection glyphs = TextBuilder.GeneratePaths(text, textOptions); - - DrawingOptions options = new() { ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } }; - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Fill(options, Color.Black, glyphs), - comparer: ImageComparer.TolerantPercentage(0.002f)); - } - - [Theory] - [WithBlankImage(500, 400, PixelTypes.Rgba32)] - public void CanDrawTextVertical(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 36); - Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); - - const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; - RichTextOptions textOptions = new(font) - { - FallbackFontFamilies = [fallback.Family], - WrappingLength = 400, - LayoutMode = LayoutMode.VerticalLeftRight, - LineSpacing = 1.4F, - TextRuns = [ - new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } - ] - }; - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), - comparer: ImageComparer.TolerantPercentage(0.002f)); - } - - [Theory] - [WithBlankImage(48, 935, PixelTypes.Rgba32)] - public void CanDrawTextVertical2(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - if (SystemFonts.TryGet("Yu Gothic", out FontFamily fontFamily)) - { - Font font = fontFamily.CreateFont(30F); - const string text = "あいうえお、「こんにちはー」。もしもし。ABCDEFG 日本語"; - RichTextOptions textOptions = new(font) - { - LayoutMode = LayoutMode.VerticalLeftRight, - LineSpacing = 1.4F, - TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline }] - }; - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), - comparer: ImageComparer.TolerantPercentage(0.002f)); - } - } - - [Theory] - [WithBlankImage(500, 400, PixelTypes.Rgba32)] - public void CanDrawTextVerticalMixed(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - Font font = CreateFont(TestFonts.OpenSans, 36); - Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); - - const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; - RichTextOptions textOptions = new(font) - { - FallbackFontFamilies = [fallback.Family], - WrappingLength = 400, - LayoutMode = LayoutMode.VerticalMixedLeftRight, - LineSpacing = 1.4F, - TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline }] - }; - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), - comparer: ImageComparer.TolerantPercentage(0.002f)); - } - - [Theory] - [WithBlankImage(48, 839, PixelTypes.Rgba32)] - public void CanDrawTextVerticalMixed2(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - if (SystemFonts.TryGet("Yu Gothic", out FontFamily fontFamily)) - { - Font font = fontFamily.CreateFont(30F); - const string text = "あいうえお、「こんにちはー」。もしもし。ABCDEFG 日本語"; - RichTextOptions textOptions = new(font) - { - LayoutMode = LayoutMode.VerticalMixedLeftRight, - LineSpacing = 1.4F, - TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } - ] - }; - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).DrawText(textOptions, text, Brushes.Solid(Color.Black)), - comparer: ImageComparer.TolerantPercentage(0.002f)); - } - } - - [Theory] - [WithBlankImage(200, 200, PixelTypes.Rgba32)] - public void CanRenderTextOutOfBoundsIssue301(TestImageProvider provider) - where TPixel : unmanaged, IPixel - => provider.VerifyOperation( - ImageComparer.TolerantPercentage(0.01f), - img => - { - Font font = CreateFont(TestFonts.OpenSans, 70); - - const string txt = "V"; - FontRectangle size = TextMeasurer.MeasureBounds(txt, new TextOptions(font)); - - img.Mutate(x => x.Resize((int)size.Width, (int)size.Height)); - - RichTextOptions options = new(font) - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - Origin = new Vector2(size.Width / 2, size.Height / 2) - }; - - LinearGradientBrush brush = new( - new PointF(0, 0), - new PointF(20, 20), - GradientRepetitionMode.Repeat, - new ColorStop(0, Color.Red), - new ColorStop(0.5f, Color.Green), - new ColorStop(0.5f, Color.Yellow), - new ColorStop(1f, Color.Blue)); - - img.Mutate(m => m.DrawText(options, txt, brush)); - }, - false, - false); - - private static string Repeat(string str, int times) => string.Concat(Enumerable.Repeat(str, times)); - - private static string ToTestOutputDisplayText(string text) - { - string fnDisplayText = text.Replace("\n", string.Empty); - return fnDisplayText[..Math.Min(fnDisplayText.Length, 4)]; - } - - private static Font CreateFont(string fontName, float size) - => TestFontUtilities.GetFont(fontName, size); -} diff --git a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs index 6700b36d9..685fe348c 100644 --- a/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/GraphicsOptionsTests.cs @@ -13,7 +13,7 @@ public class GraphicsOptionsTests private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone(); [Fact] - public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); + public void CloneGraphicsOptionsIsNotNull() => Assert.NotNull(this.cloneGraphicsOptions); [Fact] public void DefaultGraphicsOptionsAntialias() @@ -25,25 +25,33 @@ public void DefaultGraphicsOptionsAntialias() [Fact] public void DefaultGraphicsOptionsBlendPercentage() { - const float Expected = 1F; - Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); - Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); + const float expected = 1F; + Assert.Equal(expected, this.newGraphicsOptions.BlendPercentage); + Assert.Equal(expected, this.cloneGraphicsOptions.BlendPercentage); } [Fact] public void DefaultGraphicsOptionsColorBlendingMode() { - const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; - Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); + const PixelColorBlendingMode expected = PixelColorBlendingMode.Normal; + Assert.Equal(expected, this.newGraphicsOptions.ColorBlendingMode); + Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode); + } + + [Fact] + public void DefaultGraphicsOptionsAntialiasThreshold() + { + const float expected = 0.5F; + Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold); + Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold); } [Fact] public void DefaultGraphicsOptionsAlphaCompositionMode() { - const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; - Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); + const PixelAlphaCompositionMode expected = PixelAlphaCompositionMode.SrcOver; + Assert.Equal(expected, this.newGraphicsOptions.AlphaCompositionMode); + Assert.Equal(expected, this.cloneGraphicsOptions.AlphaCompositionMode); } [Fact] @@ -53,6 +61,7 @@ public void NonDefaultClone() { AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, Antialias = false, + AntialiasThreshold = .25F, BlendPercentage = .25F, ColorBlendingMode = PixelColorBlendingMode.HardLight, }; @@ -70,19 +79,10 @@ public void CloneIsDeep() actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.Antialias = false; + actual.AntialiasThreshold = .25F; actual.BlendPercentage = .25F; actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; Assert.NotEqual(expected, actual, GraphicsOptionsComparer); } - - [Fact] - public void IsOpaqueColor() - { - Assert.True(new GraphicsOptions().IsOpaqueColorWithoutBlending(Color.Red)); - Assert.False(new GraphicsOptions { BlendPercentage = .5F }.IsOpaqueColorWithoutBlending(Color.Red)); - Assert.False(new GraphicsOptions().IsOpaqueColorWithoutBlending(Color.Transparent)); - Assert.False(new GraphicsOptions { ColorBlendingMode = PixelColorBlendingMode.Lighten, BlendPercentage = 1F }.IsOpaqueColorWithoutBlending(Color.Red)); - Assert.False(new GraphicsOptions { ColorBlendingMode = PixelColorBlendingMode.Normal, AlphaCompositionMode = PixelAlphaCompositionMode.DestOver, BlendPercentage = 1f }.IsOpaqueColorWithoutBlending(Color.Red)); - } } diff --git a/tests/ImageSharp.Drawing.Tests/Helpers/BrushWorkspaceTests.cs b/tests/ImageSharp.Drawing.Tests/Helpers/BrushWorkspaceTests.cs new file mode 100644 index 000000000..15a71591b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Helpers/BrushWorkspaceTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Helpers; + +public class BrushWorkspaceTests +{ + private readonly TestMemoryAllocator memoryAllocator = new(); + + [Fact] + public void ReturnsRowSizedSlicesFromSharedBuffers() + { + using BrushWorkspace workspace = new(this.memoryAllocator, 100); + + Span amounts = workspace.GetAmounts(8); + Span overlays = workspace.GetOverlays(8); + + Assert.Equal(8, amounts.Length); + Assert.Equal(8, overlays.Length); + + amounts[0] = 10; + overlays[0] = new Rgb24(10, 20, 30); + + Assert.Equal(10, workspace.GetAmounts(8)[0]); + Assert.Equal((byte)10, workspace.GetOverlays(8)[0].R); + Assert.Equal((byte)20, workspace.GetOverlays(8)[0].G); + Assert.Equal((byte)30, workspace.GetOverlays(8)[0].B); + } + + [Fact] + public void Dispose_ReturnsSharedBuffers() + { + BrushWorkspace workspace = new(this.memoryAllocator, 100); + + workspace.GetAmounts(16)[0] = 42; + workspace.GetOverlays(16)[0] = new Rgb24(1, 2, 3); + + workspace.Dispose(); + + Assert.Equal(2, this.memoryAllocator.ReturnLog.Count); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Helpers/PolygonUtilitiesTests.cs b/tests/ImageSharp.Drawing.Tests/Helpers/PolygonUtilitiesTests.cs new file mode 100644 index 000000000..7c2aa4f20 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Helpers/PolygonUtilitiesTests.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Helpers; + +namespace SixLabors.ImageSharp.Drawing.Tests.Helpers; + +public class PolygonUtilitiesTests +{ + private static PointF[] CreateTestPoints() + => PolygonFactory.CreatePointArray( + (10, 0), + (20, 0), + (20, 30), + (10, 30), + (10, 20), + (0, 20), + (0, 10), + (10, 10), + (10, 0)); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnsureOrientation_Positive(bool isPositive) + { + PointF[] expected = CreateTestPoints(); + PointF[] polygon = expected.CloneArray(); + + if (!isPositive) + { + polygon.AsSpan().Reverse(); + } + + PolygonUtilities.EnsureOrientation(polygon, 1); + + Assert.Equal(expected, polygon); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnsureOrientation_Negative(bool isNegative) + { + PointF[] expected = CreateTestPoints(); + expected.AsSpan().Reverse(); + + PointF[] polygon = expected.CloneArray(); + + if (!isNegative) + { + polygon.AsSpan().Reverse(); + } + + PolygonUtilities.EnsureOrientation(polygon, -1); + + Assert.Equal(expected, polygon); + } + + public static TheoryData<(float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y)?> LineSegmentToLineSegment_Data { get; } = + new() + { + { (0, 0), (2, 3), (1, 3), (1, 0), (1, 1.5f) }, + { (3, 1), (3, 3), (3, 2), (4, 2), (3, 2) }, + { (1, -3), (3, -1), (3, -4), (2, -2), (2, -2) }, + { (0, 0), (2, 1), (2, 1.0001f), (5, 2), (2, 1) }, // Robust to inaccuracies + { (0, 0), (2, 3), (1, 3), (1, 2), null }, + { (-3, 3), (-1, 3), (-3, 2), (-1, 2), null }, + { (-4, 3), (-4, 1), (-5, 3), (-5, 1), null }, + { (0, 0), (4, 1), (4, 1), (8, 2), null }, // Collinear intersections are ignored + { (0, 0), (4, 1), (4, 1.0001f), (8, 2), null }, // Collinear intersections are ignored + }; + + [Theory] + [MemberData(nameof(LineSegmentToLineSegment_Data))] + public void LineSegmentToLineSegmentNoCollinear( + (float X, float Y) a0, + (float X, float Y) a1, + (float X, float Y) b0, + (float X, float Y) b1, + (float X, float Y)? expected) + { + Vector2 ip = default; + + bool result = PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(P(a0), P(a1), P(b0), P(b1), ref ip); + Assert.Equal(result, expected.HasValue); + if (expected.HasValue) + { + Assert.Equal(P(expected.Value), ip, new ApproximateFloatComparer(1e-3f)); + } + + static Vector2 P((float X, float Y) p) => new(p.X, p.Y); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index a7b7f0564..860284701 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -24,15 +24,18 @@ - - + + + + + + - - + @@ -51,8 +54,17 @@ + + + + + + + + + + - diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs new file mode 100644 index 000000000..9584a1561 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_134.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_134 +{ + [Theory] + [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgba32, true)] + [WithSolidFilledImages(128, 64, nameof(Color.White), PixelTypes.Rgba32, false)] + public void LowFontSizeRenderOK(TestImageProvider provider, bool antialias) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.IsWindows) + { + return; + } + + provider.RunValidatingProcessorTest( + c => + { + c.SetGraphicsOptions( + new GraphicsOptions + { + Antialias = antialias, + AntialiasThreshold = .33F + }); + + c.ProcessWithCanvas(canvas => + { + Brush brush = Brushes.Solid(Color.Black); + Font font = SystemFonts.Get("Tahoma").CreateFont(8); + RichTextOptions options = new(font) + { + WrappingLength = c.GetCurrentSize().Width / 2, + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + Origin = new PointF(c.GetCurrentSize().Width / 2, c.GetCurrentSize().Height / 2) + }; + + canvas.DrawText(options, "Lorem ipsum dolor sit amet", brush, null); + }); + }, + testOutputDetails: $"{antialias}", + appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_19.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_19.cs similarity index 96% rename from tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_19.cs rename to tests/ImageSharp.Drawing.Tests/Issues/Issue_19.cs index d920fa9d0..21fbd5784 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_19.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_19.cs @@ -3,7 +3,7 @@ using System.Numerics; -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; /// /// see https://github.com/issues/19 diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_224.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_224.cs similarity index 97% rename from tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_224.cs rename to tests/ImageSharp.Drawing.Tests/Issues/Issue_224.cs index aa9391361..c682f1935 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Issues/Issue_224.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_224.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; /// /// see https://github.com/SixLabors/ImageSharp.Drawing/issues/224 diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs index 1d208a0da..a29b32676 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_241.cs @@ -27,6 +27,6 @@ public void DoesNotThrowArgumentOutOfRangeException() const string content = "TEST"; using Image image = new Image(512, 256, Color.Black.ToPixel()); - image.Mutate(x => x.DrawText(opt, content, Brushes.Horizontal(Color.Orange))); + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(opt, content, Brushes.Horizontal(Color.Orange), pen: null))); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs new file mode 100644 index 000000000..1375e254b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_244.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_244 +{ + [Fact] + public void DoesNotHang() + { + PathBuilder pathBuilder = new(); + Matrix4x4 transform = new(Matrix3x2.CreateRotation(-0.04433158f, new Vector2(948, 640))); + pathBuilder.SetTransform(transform); + pathBuilder.AddQuadraticBezier(new PointF(-2147483648, 677), new PointF(-2147483648, 675), new PointF(-2147483648, 675)); + IPath path = pathBuilder.Build(); + + IPath outline = path.GenerateOutline(2); + + Assert.NotEqual(Rectangle.Empty, outline.Bounds); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs index 43ad525be..3f47fc6d9 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_270.cs @@ -31,9 +31,9 @@ public void DoesNotThrowArgumentOutOfRangeException() using Image targetImage = new(targetImageWidth, targetImageHeight, Color.Wheat.ToPixel()); using Image imageBrushImage = new(sourceImageWidth, sourceImageHeight, Color.Black.ToPixel()); - ImageBrush imageBrush = new(imageBrushImage); + ImageBrush imageBrush = new(imageBrushImage); - targetImage.Mutate(x => x.DrawText(CreateTextOptions(font, targetImageWidth), text, imageBrush, pen)); + targetImage.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(CreateTextOptions(font, targetImageWidth), text, imageBrush, pen))); } private static RichTextOptions CreateTextOptions(Font font, int wrappingLength) diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs index d18810746..f05526c45 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs @@ -9,8 +9,6 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Issues; public class Issue_28_108 { - private Rgba32 red = Color.Red.ToPixel(); - [Theory] [InlineData(1F)] [InlineData(1.5F)] @@ -19,13 +17,13 @@ public class Issue_28_108 public void DrawingLineAtTopShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(0, 0), - new PointF(100, 0))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(0, 0), + new PointF(100, 0)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 0)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); @@ -39,13 +37,13 @@ public void DrawingLineAtTopShouldDisplay(float stroke) public void DrawingLineAtBottomShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(0, 99), - new PointF(100, 99))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(0, 99), + new PointF(100, 99)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 99)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); @@ -59,13 +57,13 @@ public void DrawingLineAtBottomShouldDisplay(float stroke) public void DrawingLineAtLeftShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(0, 0), - new PointF(0, 99))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(0, 0), + new PointF(0, 99)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 0, y: i)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); @@ -79,15 +77,24 @@ public void DrawingLineAtLeftShouldDisplay(float stroke) public void DrawingLineAtRightShouldDisplay(float stroke) { using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); - image.Mutate(x => x - .SetGraphicsOptions(g => g.Antialias = false) - .DrawLine( - Color.Red, - stroke, - new PointF(99, 0), - new PointF(99, 99))); + DrawingOptions options = CreateAliasedDrawingOptions(); + image.Mutate(x => x.ProcessWithCanvas( + options, + canvas => canvas.DrawLine( + Pens.Solid(Color.Red, stroke), + new PointF(99, 0), + new PointF(99, 99)))); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 99, y: i)); Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } + + private static DrawingOptions CreateAliasedDrawingOptions() => + new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false + } + }; } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs index c77648fe9..e605ca4f2 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_323.cs @@ -18,15 +18,15 @@ public void DrawPolygonMustDrawoutlineOnly(TestImageProvider pro where TPixel : unmanaged, IPixel { Color color = Color.RebeccaPurple; + PointF[] points = + [ + new(5, 5), + new(5, 150), + new(190, 150), + ]; + provider.RunValidatingProcessorTest( - x => x.DrawPolygon( - color, - scale, - [ - new(5, 5), - new(5, 150), - new(190, 150), - ]), + x => x.ProcessWithCanvas(canvas => canvas.Draw(Pens.Solid(color, scale), new Polygon(points))), new { scale }); } @@ -40,15 +40,16 @@ public void DrawPolygonMustDrawoutlineOnly_Pattern(TestImageProvider { Color color = Color.RebeccaPurple; + PointF[] points = + [ + new(5, 5), + new(5, 150), + new(190, 150), + ]; + PatternPen pen = Pens.DashDot(color, scale); provider.RunValidatingProcessorTest( - x => x.DrawPolygon( - pen, - [ - new(5, 5), - new(5, 150), - new(190, 150), - ]), - new { scale }); + x => x.ProcessWithCanvas(canvas => canvas.Draw(pen, new Polygon(points))), + new { scale }); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs index 26e151ddd..3fb1e24c5 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs @@ -19,46 +19,46 @@ public void OffsetTextOutlines(TestImageProvider provider) Font bibfont = fontFamily.CreateFont(600, FontStyle.Bold); Font namefont = fontFamily.CreateFont(140, FontStyle.Bold); - provider.RunValidatingProcessorTest(p => - { - p.DrawText( - new RichTextOptions(bibfont) - { - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Center, - TextAlignment = TextAlignment.Center, - TextDirection = TextDirection.LeftToRight, - Origin = new Point(1156, 1024), - }, - "9999", - Brushes.Solid(Color.White), - Pens.Solid(Color.Black, 20)); + provider.RunValidatingProcessorTest(p => p.ProcessWithCanvas(canvas => + { + canvas.DrawText( + new RichTextOptions(bibfont) + { + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextAlignment = TextAlignment.Center, + TextDirection = TextDirection.LeftToRight, + Origin = new Point(1156, 1024), + }, + "9999", + Brushes.Solid(Color.White), + Pens.Solid(Color.Black, 20)); - p.DrawText( - new RichTextOptions(namefont) - { - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Center, - TextAlignment = TextAlignment.Center, - TextDirection = TextDirection.LeftToRight, - Origin = new Point(1156, 713), - }, - "JOHAN", - Brushes.Solid(Color.White), - Pens.Solid(Color.Black, 5)); + canvas.DrawText( + new RichTextOptions(namefont) + { + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextAlignment = TextAlignment.Center, + TextDirection = TextDirection.LeftToRight, + Origin = new Point(1156, 713), + }, + "JOHAN", + Brushes.Solid(Color.White), + Pens.Solid(Color.Black, 5)); - p.DrawText( - new RichTextOptions(namefont) - { - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Center, - TextAlignment = TextAlignment.Center, - TextDirection = TextDirection.LeftToRight, - Origin = new Point(1156, 1381), - }, - "TIGERTECH", - Brushes.Solid(Color.White), - Pens.Solid(Color.Black, 5)); - }); + canvas.DrawText( + new RichTextOptions(namefont) + { + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Center, + TextAlignment = TextAlignment.Center, + TextDirection = TextDirection.LeftToRight, + Origin = new Point(1156, 1381), + }, + "TIGERTECH", + Brushes.Solid(Color.White), + Pens.Solid(Color.Black, 5)); + })); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_344.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_344.cs new file mode 100644 index 000000000..697cf3db0 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_344.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_344 +{ + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void CanDrawWhereSegmentsOverlap(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + Pen pen = Pens.Solid(Color.Aqua.WithAlpha(.3F), 1); + canvas.DrawLine(pen, new PointF(10, 10), new PointF(90, 10), new PointF(20, 10)); + })); + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void CanDrawWhereSegmentsOverlap_PathBuilder(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + PathBuilder pathBuilder = new(); + pathBuilder.MoveTo(10, 10); + pathBuilder.LineTo(90, 10); + pathBuilder.LineTo(20, 10); + + Pen pen = Pens.Solid(Color.Aqua.WithAlpha(.3F), 1); + canvas.Draw(pen, pathBuilder); + })); +} diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs new file mode 100644 index 000000000..46bf3624f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_367.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +public class Issue_367 +{ + [Theory] + [WithSolidFilledImages(512, 72, nameof(Color.White), PixelTypes.Rgba32)] + public void BrushAndTextAlign(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (!TestEnvironment.IsWindows) + { + return; + } + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + Pen pen = Pens.Solid(Color.Green, 1); + Brush brush = Brushes.Solid(Color.Red); + + Font font = SystemFonts.Get("Arial").CreateFont(64); + RichTextOptions options = new(font); + + canvas.DrawText(options, "Hello, world!", brush, pen); + }), + appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs index 31e156033..0748409c8 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_37.cs @@ -23,20 +23,20 @@ public void CanRenderLargeFont() Fonts.Font font = Fonts.SystemFonts.CreateFont("Arial", 40, Fonts.FontStyle.Regular); GraphicsOptions graphicsOptions = new() { Antialias = false }; + DrawingOptions drawingOptions = new() { GraphicsOptions = graphicsOptions }; + RichTextOptions textOptions = new(font) { Origin = new PointF(50, 50) }; image.Mutate( - x => x.BackgroundColor(Color.White) - .DrawLine( - new DrawingOptions { GraphicsOptions = graphicsOptions }, - Color.Black, - 1, - new PointF(0, 50), - new PointF(150, 50)) - .DrawText( - new DrawingOptions { GraphicsOptions = graphicsOptions }, - text, - font, - Color.Black, - new PointF(50, 50))); + x => x.ProcessWithCanvas( + drawingOptions, + canvas => + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawLine( + Pens.Solid(Color.Black, 1), + new PointF(0, 50), + new PointF(150, 50)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + })); } } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs index 41e6d410c..fe2f43322 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_46.cs @@ -32,7 +32,8 @@ public void CanRenderCustomFont() float textX = ((imageSize - rect.Width) * 0.5F) + rect.Left; float textY = ((imageSize - rect.Height) * 0.5F) + (rect.Top * 0.25F); - image.Mutate(x => x.DrawText(iconText, font, Color.Black, new PointF(textX, textY))); + RichTextOptions textOptions = new(font) { Origin = new PointF(textX, textY) }; + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, iconText, Brushes.Solid(Color.Black), pen: null))); image.Save(TestFontUtilities.GetPath("e96.png")); } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs index 378b0f53a..25087a427 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_462.cs @@ -42,7 +42,7 @@ public void CanDrawEmojiFont(TestImageProvider provider, ColorFo }; provider.RunValidatingProcessorTest( - c => c.DrawText(options, text, Brushes.Solid(Color.Black)), + c => c.ProcessWithCanvas(canvas => canvas.DrawText(options, text, Brushes.Solid(Color.Black), pen: null)), testOutputDetails: $"{support}-draw", comparer: ImageComparer.TolerantPercentage(0.002f)); @@ -50,7 +50,7 @@ public void CanDrawEmojiFont(TestImageProvider provider, ColorFo c => { Pen pen = Pens.Solid(Color.Black, 2); - c.Fill(pen.StrokeFill, pen, TextBuilder.GenerateGlyphs(text, options)); + c.ProcessWithCanvas(canvas => canvas.DrawGlyphs(pen.StrokeFill, pen, TextBuilder.GenerateGlyphs(text, options))); }, testOutputDetails: $"{support}-fill", comparer: ImageComparer.TolerantPercentage(0.002f)); diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs index 32e594f8b..7393b8616 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_54.cs @@ -37,7 +37,7 @@ public void CanDrawWithoutMemoryException() string text = "sample text"; // Draw the text - image.Mutate(x => x.DrawText(textOptions, text, brush, pen)); + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, brush, pen))); } } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs index 1e25f7617..7aabda00c 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issues_55_59.cs @@ -24,7 +24,8 @@ public void SimplifyOutOfRangeExceptionDrawLines() ]; using Image image = new(100, 100); - image.Mutate(imageContext => imageContext.DrawLine(Color.FromPixel(new Rgba32(255, 0, 0)), 1, line)); + image.Mutate(imageContext => imageContext.ProcessWithCanvas( + canvas => canvas.DrawLine(Pens.Solid(Color.FromPixel(new Rgba32(255, 0, 0)), 1), line))); } [Fact] @@ -37,6 +38,7 @@ public void SimplifyOutOfRangeExceptionDraw() new LinearLineSegment(new PointF(592.916f, 1155.754f), new PointF(592.0153f, 1156.238f))); using Image image = new(2000, 2000); - image.Mutate(imageContext => imageContext.Draw(Color.FromPixel(new Rgba32(255, 0, 0)), 1, path)); + image.Mutate(imageContext => imageContext.ProcessWithCanvas( + canvas => canvas.Draw(Pens.Solid(Color.FromPixel(new Rgba32(255, 0, 0)), 1), path))); } } diff --git a/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs b/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs new file mode 100644 index 000000000..4e741462c --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.PolygonGeometry; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Drawing.Tests.PolygonGeometry; + +public class PolygonClippingTests +{ + private readonly RectangularPolygon bigSquare = new(10, 10, 40, 40); + private readonly RectangularPolygon hole = new(20, 20, 10, 10); + private readonly RectangularPolygon topLeft = new(0, 0, 20, 20); + private readonly RectangularPolygon topRight = new(30, 0, 20, 20); + private readonly RectangularPolygon topMiddle = new(20, 0, 10, 20); + + private readonly Polygon bigTriangle = new(new LinearLineSegment( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300))); + + private readonly Polygon littleTriangle = new(new LinearLineSegment( + new Vector2(37, 85), + new Vector2(130, 40), + new Vector2(65, 137))); + + private static ComplexPolygon Clip(IPath shape, params IPath[] hole) + => ClippedShapeGenerator.GenerateClippedShapes(BooleanOperation.Difference, shape, hole); + + [Fact] + public void OverlappingTriangleCutRightSide() + { + Polygon triangle = new(new LinearLineSegment( + new Vector2(0, 50), + new Vector2(70, 0), + new Vector2(50, 100))); + + Polygon cutout = new(new LinearLineSegment( + new Vector2(20, 0), + new Vector2(70, 0), + new Vector2(70, 100), + new Vector2(20, 100))); + + ComplexPolygon shapes = Clip(triangle, cutout); + Assert.Single(shapes.Paths); + Assert.DoesNotContain(triangle, shapes.Paths); + } + + [Fact] + public void OverlappingTriangles() + { + ComplexPolygon shapes = Clip(this.bigTriangle, this.littleTriangle); + Assert.Single(shapes.Paths); + PointF[] path = shapes.Paths.Single().Flatten().First().Points.ToArray(); + + Assert.Equal(7, path.Length); + foreach (Vector2 p in this.bigTriangle.Flatten().First().Points.ToArray()) + { + Assert.Contains(p, path, new ApproximateFloatComparer(RectangularPolygonValueComparer.DefaultTolerance)); + } + } + + [Fact] + public void NonOverlapping() + { + IEnumerable shapes = Clip(this.topLeft, this.topRight).Paths + .OfType().Select(x => (RectangularPolygon)x); + + Assert.Single(shapes); + Assert.Contains( + shapes, x => RectangularPolygonValueComparer.Equals(this.topLeft, x)); + + Assert.DoesNotContain(this.topRight, shapes); + } + + [Fact] + public void OverLappingReturns1NewShape() + { + ComplexPolygon shapes = Clip(this.bigSquare, this.topLeft); + + 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 = Clip(this.bigSquare, this.hole).Paths + .OfType().Select(x => (RectangularPolygon)x); + + Assert.Equal(2, shapes.Count()); + + Assert.Contains(shapes, x => RectangularPolygonValueComparer.Equals(this.bigSquare, x)); + Assert.Contains(shapes, x => RectangularPolygonValueComparer.Equals(this.hole, x)); + } + + [Fact] + public void TouchingButNotOverlapping() + { + 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] + public void ClippingRectanglesCreateCorrectNumberOfPoints() + { + IEnumerable paths = new RectangularPolygon(10, 10, 40, 40) + .Clip(new RectangularPolygon(20, 0, 20, 20)) + .Flatten(); + + Assert.Single(paths); + PointF[] points = paths.First().Points.ToArray(); + + Assert.Equal(8, points.Length); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs new file mode 100644 index 000000000..77ffe960b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -0,0 +1,2179 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +[GroupOutput("Drawing")] +public partial class WebGPUDrawingBackendTests +{ + public static TheoryData GraphicsOptionsModePairs { get; } = + new() + { + { PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver }, + { PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop }, + { PixelColorBlendingMode.Add, PixelAlphaCompositionMode.Src }, + { PixelColorBlendingMode.Subtract, PixelAlphaCompositionMode.DestOut }, + { PixelColorBlendingMode.Screen, PixelAlphaCompositionMode.DestOver }, + { PixelColorBlendingMode.Darken, PixelAlphaCompositionMode.DestAtop }, + { PixelColorBlendingMode.Lighten, PixelAlphaCompositionMode.DestIn }, + { PixelColorBlendingMode.Overlay, PixelAlphaCompositionMode.SrcIn }, + { PixelColorBlendingMode.HardLight, PixelAlphaCompositionMode.Xor }, + { PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Clear } + }; + + [WebGPUTheory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); + Brush brush = Brushes.Solid(Color.Black); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, polygon); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "FillPath", defaultImage, nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.25F } + }; + + EllipsePolygon ellipse = new(256, 256, 200, 150); + Brush brush = Brushes.Solid(Color.Black); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_AliasedThreshold", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "FillPath_AliasedThreshold", defaultImage, nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void FillPath_WithImageBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon polygon = new(36.5F, 26.25F, 312.5F, 188.5F); + Brush clearBrush = Brushes.Solid(Color.White); + + using Image foreground = provider.GetImage(); + Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); + void DrawAction(DrawingCanvas canvas) + { + canvas.Clear(clearBrush); + canvas.Fill(brush, polygon); + } + + using Image defaultImage = new(384, 256); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + (Action>)DrawAction); + + DebugSaveBackendPair(provider, "FillPath_ImageBrush", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "FillPath_ImageBrush", defaultImage, nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true }, + ShapeOptions = new ShapeOptions + { + IntersectionRule = IntersectionRule.NonZero + } + }; + + PathBuilder pathBuilder = new(); + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(16, 16), + new PointF(240, 16), + new PointF(240, 240), + new PointF(16, 240) + ]); + pathBuilder.CloseFigure(); + + // Inner contour keeps the same winding direction as outer contour. + // Non-zero fill should therefore keep this region filled. + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(80, 80), + new PointF(176, 80), + new PointF(176, 176), + new PointF(80, 176) + ]); + pathBuilder.CloseFigure(); + + IPath path = pathBuilder.Build(); + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_NonZeroNestedContours", defaultImage, nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + + // Non-zero winding semantics must still match on an interior point. + Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); + AssertBackendPairReferenceOutputs(provider, "FillPath_NonZeroNestedContours", defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithBasicTestPatternImages(nameof(GraphicsOptionsModePairs), 384, 256, PixelTypes.Rgba32)] + public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput( + TestImageProvider provider, + PixelColorBlendingMode colorMode, + PixelAlphaCompositionMode alphaMode) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(26.5F, 18.25F, 324.5F, 208.75F); + Brush brush = Brushes.Solid(Color.OrangeRed.WithAlpha(0.78F)); + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + BlendPercentage = 0.73F, + ColorBlendingMode = colorMode, + AlphaCompositionMode = alphaMode + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, polygon); + + using Image baseImage = provider.GetImage(); + using Image defaultImage = baseImage.Clone(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + baseImage); + + DebugSaveBackendPair( + provider, + $"FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", + defaultImage, + nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.1F); + AssertBackendPairReferenceOutputs( + provider, + $"FillPath_GraphicsOptions_SolidBrush_{colorMode}_{alphaMode}", + defaultImage, + nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithBasicTestPatternImages(nameof(GraphicsOptionsModePairs), 384, 256, PixelTypes.Rgba32)] + public void FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput( + TestImageProvider provider, + PixelColorBlendingMode colorMode, + PixelAlphaCompositionMode alphaMode) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(26.5F, 18.25F, 324.5F, 208.75F); + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + BlendPercentage = 0.73F, + ColorBlendingMode = colorMode, + AlphaCompositionMode = alphaMode + } + }; + + using Image foreground = provider.GetImage(); + Brush brush = new ImageBrush(foreground, new RectangleF(32, 24, 192, 144), new Point(13, -9)); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, polygon); + + using Image baseImage = provider.GetImage(); + using Image defaultImage = baseImage.Clone(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + baseImage); + + DebugSaveBackendPair( + provider, + $"FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", + defaultImage, + nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.1F); + AssertBackendPairReferenceOutputs( + provider, + $"FillPath_GraphicsOptions_ImageBrush_{colorMode}_{alphaMode}", + defaultImage, + nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(1200, 280, "White", PixelTypes.Rgba32)] + public void DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 54); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(18, 28) + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = "Sphinx of black quartz, judge my vow\n0123456789"; + Brush brush = Brushes.Solid(Color.Black); + Pen pen = Pens.Solid(Color.OrangeRed, 2F); + void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, brush, pen); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "DrawText", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + Rectangle textRegion = Rectangle.Intersect( + new Rectangle(0, 0, defaultImage.Width, defaultImage.Height), + new Rectangle(8, 12, defaultImage.Width - 16, Math.Min(220, defaultImage.Height - 12))); + AssertBackendPairSimilarityInRegion(defaultImage, nativeSurfaceImage, textRegion, 0.009F); + AssertBackendPairReferenceOutputs(provider, "DrawText", defaultImage, nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon polygon = new(48.25F, 63.5F, 401.25F, 302.75F); + Brush brush = Brushes.Solid(Color.Black); + Brush clearBrush = Brushes.Solid(Color.White); + void DrawAction(DrawingCanvas canvas) + { + canvas.Clear(clearBrush); + canvas.Fill(brush, polygon); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_NativeSurfaceParity", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); + AssertBackendPairReferenceOutputs(provider, "FillPath_NativeSurfaceParity", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + Rectangle region = new(72, 64, 320, 240); + RectangularPolygon localPolygon = new(16.25F, 24.5F, 250.5F, 160.75F); + Brush brush = Brushes.Solid(Color.Black); + Brush clearBrush = Brushes.Solid(Color.White); + void DrawAction(DrawingCanvas canvas) + { + canvas.Clear(clearBrush); + + using DrawingCanvas regionCanvas = canvas.CreateRegion(region); + regionCanvas.Fill(brush, localPolygon); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_NativeSurfaceSubregionParity", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); + AssertBackendPairReferenceOutputs(provider, "FillPath_NativeSurfaceSubregionParity", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_WithWebGPUBackend_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + IPath blurPath = CreateBlurEllipsePath(); + IPath pixelatePath = CreatePixelateTrianglePath(); + void DrawAction(DrawingCanvas canvas) + { + DrawProcessScenario(canvas); + canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + (Action>)DrawAction); + + DebugSaveBackendPair(provider, "Process", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0516F); + AssertBackendPairReferenceOutputs(provider, "Process", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithBasicTestPatternImages(420, 220, PixelTypes.Rgba32)] + public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = new('A', 200); + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, brush, null); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "RepeatedGlyphs", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 2F); + AssertBackendPairReferenceOutputs(provider, "RepeatedGlyphs", defaultImage, nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithBlankImage(1200, 280, PixelTypes.Rgba32)] + public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + DrawingOptions clearOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + } + }; + + const int glyphCount = 200; + string text = new('A', glyphCount); + Brush drawBrush = Brushes.Solid(Color.HotPink); + Brush clearBrush = Brushes.Solid(Color.White); + using Image defaultImage = provider.GetImage(); + using (DrawingCanvas defaultClearCanvas = new(Configuration.Default, GetFrameRegion(defaultImage), clearOptions)) + { + defaultClearCanvas.Fill(clearBrush); + defaultClearCanvas.Flush(); + } + + using (DrawingCanvas defaultDrawCanvas = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) + { + defaultDrawCanvas.DrawText(textOptions, text, drawBrush, null); + defaultDrawCanvas.Flush(); + } + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + defaultImage.Width, + defaultImage.Height, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration nativeSurfaceConfiguration = Configuration.Default.Clone(); + nativeSurfaceConfiguration.SetDrawingBackend(nativeSurfaceBackend); + Rectangle targetBounds = defaultImage.Bounds; + + using (DrawingCanvas nativeSurfaceClearCanvas = + new(nativeSurfaceConfiguration, new NativeCanvasFrame(targetBounds, nativeSurface), clearOptions)) + { + nativeSurfaceClearCanvas.Fill(clearBrush); + nativeSurfaceClearCanvas.Flush(); + } + + using (DrawingCanvas nativeSurfaceDrawCanvas = + new(nativeSurfaceConfiguration, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) + { + nativeSurfaceDrawCanvas.DrawText(textOptions, text, drawBrush, null); + nativeSurfaceDrawCanvas.Flush(); + } + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + textureHandle, + defaultImage.Width, + defaultImage.Height, + out Image nativeSurfaceImage, + out string readError), + readError); + + using (nativeSurfaceImage) + { + DebugSaveBackendPair(provider, "RepeatedGlyphs_AfterClear", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 2F); + AssertBackendPairReferenceOutputs(provider, "RepeatedGlyphs_AfterClear", defaultImage, nativeSurfaceImage); + } + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + + private static void RenderWithDefaultBackend(Image image, DrawingOptions options, Action> drawAction) + where TPixel : unmanaged, IPixel + { + using DrawingCanvas canvas = new(Configuration.Default, GetFrameRegion(image), options); + drawAction(canvas); + canvas.Flush(); + } + + private static EllipsePolygon CreateBlurEllipsePath() + => new(new PointF(55, 40), new SizeF(110, 80)); + + private static void DrawProcessScenario(DrawingCanvas canvas) + where TPixel : unmanaged, IPixel + { + canvas.Clear(Brushes.Solid(Color.White)); + + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 220, 140)); + canvas.DrawEllipse(Pens.Solid(Color.CornflowerBlue, 6), new PointF(120, 80), new SizeF(110, 70)); + canvas.DrawArc( + Pens.Solid(Color.ForestGreen, 4), + new PointF(120, 80), + new SizeF(90, 46), + rotation: 15, + startAngle: -25, + sweepAngle: 220); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(18, 140), + new PointF(76, 28), + new PointF(166, 126), + new PointF(222, 20)); + canvas.DrawBezier( + Pens.Solid(Color.MediumVioletRed, 4), + new PointF(20, 80), + new PointF(70, 18), + new PointF(168, 144), + new PointF(220, 78)); + } + + private static IPath CreatePixelateTrianglePath() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(110, 80, 220, 80); + pathBuilder.AddLine(220, 80, 165, 160); + pathBuilder.AddLine(165, 160, 110, 80); + pathBuilder.CloseAllFigures(); + return pathBuilder.Build(); + } + + private static Image RenderWithNativeSurfaceWebGpuBackend( + int width, + int height, + WebGPUDrawingBackend backend, + DrawingOptions options, + Action> drawAction, + Image initialImage = null) + where TPixel : unmanaged, IPixel + { + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + width, + height, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + Rectangle targetBounds = new(0, 0, width, height); + + using DrawingCanvas canvas = + new(configuration, new NativeCanvasFrame(targetBounds, nativeSurface), options); + if (initialImage is not null) + { + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryWriteTexture( + textureHandle, + width, + height, + initialImage, + out string uploadError), + uploadError); + } + + drawAction(canvas); + canvas.Flush(); + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + textureHandle, + width, + height, + out Image image, + out string readError), + readError); + + return image; + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + + private static void DebugSaveBackendPair( + TestImageProvider provider, + string testName, + Image defaultImage, + Image nativeSurfaceImage, + float tolerantPercentage = 0.0003F) + where TPixel : unmanaged, IPixel + { + defaultImage.DebugSave( + provider, + $"{testName}_Default", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + nativeSurfaceImage.DebugSave( + provider, + $"{testName}_WebGPU_NativeSurface", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + private static void AssertBackendPairSimilarity( + Image defaultImage, + Image nativeSurfaceImage, + float defaultTolerancePercent) + where TPixel : unmanaged, IPixel + { + ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(defaultTolerancePercent); + tolerantComparer.VerifySimilarity(defaultImage, nativeSurfaceImage); + } + + private static void AssertBackendPairReferenceOutputs( + TestImageProvider provider, + string testName, + Image defaultImage, + Image nativeSurfaceImage, + float tolerantPercentage = 0.0003F) + where TPixel : unmanaged, IPixel + { + ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(tolerantPercentage); + defaultImage.CompareToReferenceOutput( + tolerantComparer, + provider, + $"{testName}_Default", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + nativeSurfaceImage.CompareToReferenceOutput( + tolerantComparer, + provider, + $"{testName}_WebGPU_NativeSurface", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + private static void AssertBackendPairSimilarityInRegion( + Image defaultImage, + Image nativeSurfaceImage, + Rectangle region, + float defaultTolerancePercent) + where TPixel : unmanaged, IPixel + { + using Image defaultRegion = defaultImage.Clone(ctx => ctx.Crop(region)); + using Image nativeRegion = nativeSurfaceImage.Clone(ctx => ctx.Crop(region)); + AssertBackendPairSimilarity(defaultRegion, nativeRegion, defaultTolerancePercent); + } + + private static void AssertGpuPathWhenRequired(WebGPUDrawingBackend backend) + { + bool requireGpuPath = string.Equals( + Environment.GetEnvironmentVariable("IMAGESHARP_REQUIRE_WEBGPU"), + "1", + StringComparison.Ordinal); + + if (!requireGpuPath) + { + return; + } + + Assert.True( + backend.TestingLastFlushUsedGPU, + backend.TestingLastGPUInitializationFailure ?? "The last flush did not use the staged path."); + } + + [WebGPUTheory] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32)] + public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + PathBuilder pb = new(); + pb.AddLine(new PointF(30, 50), new PointF(370, 250)); + pb.AddLine(new PointF(370, 250), new PointF(200, 20)); + pb.CloseFigure(); + IPath path = pb.Build(); + Pen pen = Pens.Solid(Color.DarkBlue, 4F); + void DrawAction(DrawingCanvas canvas) => canvas.Draw(pen, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "DrawPath_Stroke", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); + AssertBackendPairReferenceOutputs(provider, "DrawPath_Stroke", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + public static TheoryData LineJoinValues { get; } = new() + { + LineJoin.Miter, + LineJoin.MiterRevert, + LineJoin.MiterRound, + LineJoin.Bevel, + LineJoin.Round + }; + + [WebGPUTheory] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.Miter)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.MiterRevert)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.MiterRound)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.Bevel)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineJoin.Round)] + public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput(TestImageProvider provider, LineJoin lineJoin) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + // Sharp angles to exercise join behavior. + PathBuilder pb = new(); + pb.AddLine(new PointF(30, 250), new PointF(100, 30)); + pb.AddLine(new PointF(100, 30), new PointF(170, 250)); + pb.AddLine(new PointF(170, 250), new PointF(240, 30)); + pb.AddLine(new PointF(240, 30), new PointF(370, 150)); + IPath path = pb.Build(); + + Pen pen = new SolidPen(new PenOptions(Color.DarkBlue, 12F) + { + StrokeOptions = new StrokeOptions { LineJoin = lineJoin } + }); + + void DrawAction(DrawingCanvas canvas) => canvas.Draw(pen, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, $"DrawPath_Stroke_LineJoin_{lineJoin}", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); + AssertBackendPairReferenceOutputs( + provider, + $"DrawPath_Stroke_LineJoin_{lineJoin}", + defaultImage, + nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Butt)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Square)] + [WithSolidFilledImages(400, 300, "White", PixelTypes.Rgba32, LineCap.Round)] + public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvider provider, LineCap lineCap) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + // Open path to exercise cap behavior at endpoints. + PathBuilder pb = new(); + pb.AddLine(new PointF(50, 150), new PointF(200, 50)); + pb.AddLine(new PointF(200, 50), new PointF(350, 150)); + IPath path = pb.Build(); + + Pen pen = new SolidPen(new PenOptions(Color.DarkBlue, 16F) + { + StrokeOptions = new StrokeOptions { LineCap = lineCap } + }); + + void DrawAction(DrawingCanvas canvas) => canvas.Draw(pen, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, $"DrawPath_Stroke_LineCap_{lineCap}", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); + AssertBackendPairReferenceOutputs( + provider, + $"DrawPath_Stroke_LineCap_{lineCap}", + defaultImage, + nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(512, 512, "White", PixelTypes.Rgba32)] + public void FillPath_MultipleSeparatePaths_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) + { + for (int i = 0; i < 20; i++) + { + float x = 20 + (i * 24); + float y = 20 + (i * 22); + canvas.Fill(brush, new RectangularPolygon(x, y, 80, 60)); + } + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_MultipleSeparate", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "FillPath_MultipleSeparate", defaultImage, nativeSurfaceImage); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true }, + ShapeOptions = new ShapeOptions + { + IntersectionRule = IntersectionRule.EvenOdd + } + }; + + PathBuilder pathBuilder = new(); + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(16, 16), + new PointF(240, 16), + new PointF(240, 240), + new PointF(16, 240) + ]); + pathBuilder.CloseFigure(); + + // Inner contour with same winding — EvenOdd should create a hole. + pathBuilder.StartFigure(); + pathBuilder.AddLines( + [ + new PointF(80, 80), + new PointF(176, 80), + new PointF(176, 176), + new PointF(80, 176) + ]); + pathBuilder.CloseFigure(); + + IPath path = pathBuilder.Build(); + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, path); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_EvenOdd", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + + // EvenOdd with same winding inner contour should create a hole at center. + Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); + AssertBackendPairReferenceOutputs(provider, "FillPath_EvenOdd", defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithSolidFilledImages(800, 600, "White", PixelTypes.Rgba32)] + public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + // Large polygon spanning most of the image to exercise many tiles. + Brush brush = Brushes.Solid(Color.Black); + EllipsePolygon ellipse = new(new PointF(400, 300), new SizeF(700, 500)); + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_LargeTileCount", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "FillPath_LargeTileCount", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(300, 200, "White", PixelTypes.Rgba32)] + public void MultipleFlushes_OnSameBackend_ProduceCorrectResults(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Brush redBrush = Brushes.Solid(Color.Red); + Brush blueBrush = Brushes.Solid(Color.Blue); + RectangularPolygon rect1 = new(20, 20, 120, 80); + RectangularPolygon rect2 = new(160, 100, 120, 80); + + // Default backend: two separate flushes + using Image defaultImage = provider.GetImage(); + using (DrawingCanvas canvas1 = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) + { + canvas1.Fill(redBrush, rect1); + canvas1.Flush(); + } + + using (DrawingCanvas canvas2 = new(Configuration.Default, GetFrameRegion(defaultImage), drawingOptions)) + { + canvas2.Fill(blueBrush, rect2); + canvas2.Flush(); + } + + // Native surface: two separate flushes reusing same backend + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + defaultImage.Width, + defaultImage.Height, + out NativeSurface nativeSurface, + out nint textureHandle, + out nint textureViewHandle, + out string createError), + createError); + + try + { + Configuration nativeConfig = Configuration.Default.Clone(); + nativeConfig.SetDrawingBackend(nativeSurfaceBackend); + Rectangle targetBounds = defaultImage.Bounds; + + // Upload initial white content + using Image initialImage = provider.GetImage(); + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryWriteTexture( + textureHandle, + defaultImage.Width, + defaultImage.Height, + initialImage, + out string uploadError), + uploadError); + + using (DrawingCanvas canvas1 = + new(nativeConfig, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) + { + canvas1.Fill(redBrush, rect1); + canvas1.Flush(); + } + + using (DrawingCanvas canvas2 = + new(nativeConfig, new NativeCanvasFrame(targetBounds, nativeSurface), drawingOptions)) + { + canvas2.Fill(blueBrush, rect2); + canvas2.Flush(); + } + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryReadTexture( + textureHandle, + defaultImage.Width, + defaultImage.Height, + out Image nativeSurfaceImage, + out string readError), + readError); + + using (nativeSurfaceImage) + { + DebugSaveBackendPair(provider, "MultipleFlushes", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "MultipleFlushes", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + } + finally + { + WebGPUTestNativeSurfaceAllocator.Release(textureHandle, textureViewHandle); + } + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + EllipsePolygon ellipse = new(128, 128, 100); + Brush brush = new LinearGradientBrush( + new PointF(28, 28), + new PointF(228, 228), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(0.5F, Color.Green), + new ColorStop(1, Color.Blue)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + // MacOS on CI has some outliers with this test, so using a slightly higher tolerance here to avoid noise. + DebugSaveBackendPair(provider, "FillPath_LinearGradient", defaultImage, nativeSurfaceImage, tolerantPercentage: 0.0007F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.03F); + AssertBackendPairReferenceOutputs( + provider, + "FillPath_LinearGradient", + defaultImage, + nativeSurfaceImage, + tolerantPercentage: 0.0007F); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new LinearGradientBrush( + new PointF(64, 64), + new PointF(128, 128), + GradientRepetitionMode.Repeat, + new ColorStop(0, Color.Yellow), + new ColorStop(1, Color.Purple)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_LinearGradient_Repeat", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.02F); + AssertBackendPairReferenceOutputs(provider, "FillPath_LinearGradient_Repeat", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new RadialGradientBrush( + new PointF(128, 128), + 100F, + GradientRepetitionMode.None, + new ColorStop(0, Color.White), + new ColorStop(1, Color.DarkRed)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_RadialGradient_Single", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.02F); + AssertBackendPairReferenceOutputs(provider, "FillPath_RadialGradient_Single", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new RadialGradientBrush( + new PointF(100, 100), + 20F, + new PointF(128, 128), + 110F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Yellow), + new ColorStop(1, Color.Navy)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_RadialGradient_TwoCircle", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0171F); + AssertBackendPairReferenceOutputs(provider, "FillPath_RadialGradient_TwoCircle", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new EllipticGradientBrush( + new PointF(128, 128), + new PointF(228, 128), + 0.6F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Cyan), + new ColorStop(1, Color.Magenta)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_EllipticGradient", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.035F); + AssertBackendPairReferenceOutputs(provider, "FillPath_EllipticGradient", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + EllipsePolygon ellipse = new(128, 128, 100); + Brush brush = new SweepGradientBrush( + new PointF(128, 128), + 0F, + 360F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(0.33F, Color.Green), + new ColorStop(0.67F, Color.Blue), + new ColorStop(1, Color.Red)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_SweepGradient", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0304F); + AssertBackendPairReferenceOutputs(provider, "FillPath_SweepGradient", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new SweepGradientBrush( + new PointF(128, 128), + 45F, + 270F, + GradientRepetitionMode.Reflect, + new ColorStop(0, Color.Orange), + new ColorStop(1, Color.Teal)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + // MacOS on CI has some outliers with this test, so using a slightly higher tolerance here to avoid noise. + DebugSaveBackendPair( + provider, + "FillPath_SweepGradient_PartialArc", + defaultImage, + nativeSurfaceImage, + tolerantPercentage: 0.0280F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0280F); + AssertBackendPairReferenceOutputs( + provider, + "FillPath_SweepGradient_PartialArc", + defaultImage, + nativeSurfaceImage, + tolerantPercentage: 0.0280F); + + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void FillPath_WithPathGradientBrush_FallsBackCleanly(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Rectangle region = new(72, 40, 240, 176); + RectangularPolygon localPolygon = new(12, 10, 216, 156); + EllipsePolygon persistedShape = new(new PointF(176, 128), new SizeF(320, 176)); + Brush persistedBrush = Brushes.Solid(Color.DarkSlateBlue); + Brush brush = new PathGradientBrush( + [ + new PointF(108, 6), + new PointF(206, 54), + new PointF(192, 142), + new PointF(78, 170), + new PointF(10, 82) + ], + [ + Color.Red, + Color.Gold, + Color.LimeGreen, + Color.DeepSkyBlue, + Color.BlueViolet + ], + Color.White); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(persistedBrush, persistedShape); + canvas.Flush(); + + using DrawingCanvas regionCanvas = canvas.CreateRegion(region); + regionCanvas.Fill(brush, localPolygon); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_PathGradient_Fallback", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); + + Assert.False(nativeSurfaceBackend.TestingLastFlushUsedGPU); + Assert.Equal( + "The staged WebGPU scene pipeline does not support brush type 'PathGradientBrush'.", + nativeSurfaceBackend.TestingLastGPUInitializationFailure); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithPatternBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = Brushes.Horizontal(Color.Black, Color.White); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_PatternBrush_Horizontal", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.045F); + AssertBackendPairReferenceOutputs(provider, "FillPath_PatternBrush_Horizontal", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + EllipsePolygon ellipse = new(128, 128, 100); + Brush brush = Brushes.ForwardDiagonal(Color.DarkGreen, Color.LightGray); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, ellipse); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_PatternBrush_Diagonal", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, "FillPath_PatternBrush_Diagonal", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "Red", PixelTypes.Rgba32)] + public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new RecolorBrush(Color.Red, Color.Blue, 0.5F); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_RecolorBrush", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, "FillPath_RecolorBrush", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(16, 16, 224, 224); + Brush brush = new LinearGradientBrush( + new PointF(64, 128), + new PointF(192, 128), + new PointF(128, 64), + GradientRepetitionMode.None, + new ColorStop(0, Color.Coral), + new ColorStop(1, Color.SteelBlue)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_LinearGradient_ThreePoint", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0065F); + AssertBackendPairReferenceOutputs(provider, "FillPath_LinearGradient_ThreePoint", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] + public void FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectangularPolygon rect = new(8, 8, 240, 240); + Brush brush = new EllipticGradientBrush( + new PointF(128, 128), + new PointF(180, 160), + 0.4F, + GradientRepetitionMode.Reflect, + new ColorStop(0, Color.Gold), + new ColorStop(0.5F, Color.DarkViolet), + new ColorStop(1, Color.White)); + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, rect); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "FillPath_EllipticGradient_Reflect", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0398F); + AssertBackendPairReferenceOutputs(provider, "FillPath_EllipticGradient_Reflect", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(500, 400, "Black", PixelTypes.Rgba32)] + public void CanApplyPerspectiveTransform_StarWarsCrawl(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 32); + + const string text = @"A long time ago in a galaxy +far, far away.... + +It is a period of civil war. +Rebel spaceships, striking +from a hidden base, have won +their first victory against +the evil Galactic Empire."; + + RichTextOptions textOptions = new(font) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Bottom, + TextAlignment = TextAlignment.Center, + Origin = new PointF(250, 360) + }; + + const float originX = 250; + const float originY = 380; + Matrix4x4 toOrigin = Matrix4x4.CreateTranslation(-originX, -originY, 0); + Matrix4x4 taperMatrix = Matrix4x4.Identity; + taperMatrix.M24 = -0.003F; + Matrix4x4 fromOrigin = Matrix4x4.CreateTranslation(originX, originY, 0); + Matrix4x4 perspective = toOrigin * taperMatrix * fromOrigin; + + DrawingOptions perspectiveOptions = new() { Transform = perspective }; + + // Star Destroyer geometry. + PointF[] sternFace = + [ + new(0, 0), new(300, 0), new(300, 80), new(0, 80), + ]; + + RectangularPolygon sternHighlightRect = new(4, 4, 292, 72); + + EllipsePolygon thrusterLeft = new(50, 40, 42, 42); + EllipsePolygon thrusterCenter = new(150, 40, 48, 48); + EllipsePolygon thrusterRight = new(250, 40, 42, 42); + + ProjectiveTransformBuilder transformBuilder = new(); + + Rectangle sternBounds = new(0, 0, 300, 80); + Matrix4x4 sternTransform = transformBuilder + .AppendQuadDistortion( + topLeft: new PointF(70, 80), + topRight: new PointF(380, 90), + bottomRight: new PointF(400, 135), + bottomLeft: new PointF(50, 140)) + .BuildMatrix(sternBounds); + + PointF[] bottomHull = + [ + new(0, 0), new(300, 0), new(150, 80), + ]; + + EllipsePolygon hullDome = new(117, 80, 96, 96); + + Rectangle hullBounds = new(0, 0, 300, 80); + Matrix4x4 hullTransform = transformBuilder.Clear() + .AppendQuadDistortion( + topLeft: new PointF(50, 140), + topRight: new PointF(400, 135), + bottomRight: new PointF(310, 170), + bottomLeft: new PointF(-40, 170)) + .BuildMatrix(hullBounds); + + PointF[] towerStem = + [ + new(14, 8), new(26, 8), new(26, 20), new(14, 20), + ]; + + PointF[] towerTop = + [ + new(0, 0), new(40, 0), new(40, 10), new(0, 10), + ]; + + Rectangle towerBounds = new(0, 0, 40, 20); + Matrix4x4 towerTransform = transformBuilder.Clear() + .AppendQuadDistortion( + topLeft: new PointF(175, 66), + topRight: new PointF(240, 68), + bottomRight: new PointF(238, 85), + bottomLeft: new PointF(177, 84)) + .BuildMatrix(towerBounds); + + Color sternColorLeft = Color.FromPixel(new Rgba32(70, 75, 85, 255)); + Color sternColorRight = Color.FromPixel(new Rgba32(35, 38, 45, 255)); + Color hullColorLeft = Color.FromPixel(new Rgba32(85, 90, 100, 255)); + Color hullColorRight = Color.FromPixel(new Rgba32(45, 50, 58, 255)); + Color highlightColorLeft = Color.FromPixel(new Rgba32(135, 140, 150, 255)); + Color highlightColorRight = Color.FromPixel(new Rgba32(65, 70, 80, 255)); + Color thrusterInnerGlow = Color.White; + Color thrusterOuterGlow = Color.Blue; + + LinearGradientBrush sternBrush = new( + new PointF(0, 40), + new PointF(300, 40), + GradientRepetitionMode.None, + new ColorStop(0, sternColorLeft), + new ColorStop(1, sternColorRight)); + + LinearGradientBrush hullBrush = new( + new PointF(0, 40), + new PointF(300, 40), + GradientRepetitionMode.None, + new ColorStop(0, hullColorLeft), + new ColorStop(1, hullColorRight)); + + LinearGradientBrush highlightBrush = new( + new PointF(0, 40), + new PointF(300, 40), + GradientRepetitionMode.None, + new ColorStop(0, highlightColorLeft), + new ColorStop(1, highlightColorRight)); + + LinearGradientBrush towerBrush = new( + new PointF(0, 10), + new PointF(40, 10), + GradientRepetitionMode.None, + new ColorStop(0, sternColorLeft), + new ColorStop(1, sternColorRight)); + + LinearGradientBrush towerTopBrush = new( + new PointF(0, 5), + new PointF(40, 5), + GradientRepetitionMode.None, + new ColorStop(0, highlightColorLeft), + new ColorStop(1, highlightColorRight)); + + LinearGradientBrush domeBrush = new( + new PointF(21, 80), + new PointF(213, 80), + GradientRepetitionMode.None, + new ColorStop(0, highlightColorLeft), + new ColorStop(1, highlightColorRight)); + + EllipticGradientBrush thrusterBrushLeft = new( + new PointF(50, 40), + new PointF(50 + 42, 40), + 1f, + GradientRepetitionMode.None, + new ColorStop(0, thrusterInnerGlow), + new ColorStop(.75F, thrusterOuterGlow)); + + EllipticGradientBrush thrusterBrushCenter = new( + new PointF(150, 40), + new PointF(150 + 48, 40), + 1f, + GradientRepetitionMode.None, + new ColorStop(0, thrusterInnerGlow), + new ColorStop(.75F, thrusterOuterGlow)); + + EllipticGradientBrush thrusterBrushRight = new( + new PointF(250, 40), + new PointF(250 + 42, 40), + 1f, + GradientRepetitionMode.None, + new ColorStop(0, thrusterInnerGlow), + new ColorStop(.75F, thrusterOuterGlow)); + + DrawingOptions sternOptions = new() { Transform = sternTransform }; + DrawingOptions hullOptions = new() { Transform = hullTransform }; + DrawingOptions towerOptions = new() { Transform = towerTransform }; + + void DrawAction(DrawingCanvas canvas) + { + // Bottom hull (draw first — behind stern). + canvas.Save(hullOptions); + canvas.Fill(highlightBrush, new Polygon(bottomHull)); + canvas.Restore(); + + // Stern face with thrusters, highlight, and dome. + canvas.Save(sternOptions); + canvas.Fill(domeBrush, hullDome); + canvas.Draw(Pens.Solid(highlightColorRight, 2), hullDome); + canvas.Fill(sternBrush, new Polygon(sternFace)); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), sternHighlightRect); + canvas.Fill(thrusterBrushLeft, thrusterLeft); + canvas.Fill(thrusterBrushCenter, thrusterCenter); + canvas.Fill(thrusterBrushRight, thrusterRight); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), thrusterLeft); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), thrusterCenter); + canvas.Draw(Pens.Solid(highlightColorLeft, 2), thrusterRight); + canvas.Restore(); + + // Bridge tower. + canvas.Save(towerOptions); + canvas.Fill(towerTopBrush, new Polygon(towerTop)); + canvas.Fill(towerBrush, new Polygon(towerStem)); + canvas.Restore(); + + // Text crawl with perspective. + canvas.Save(perspectiveOptions); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Yellow), pen: null); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "StarWarsCrawl", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0071F); + AssertBackendPairReferenceOutputs(provider, "StarWarsCrawl", defaultImage, nativeSurfaceImage); + AssertGpuPathWhenRequired(nativeSurfaceBackend); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_FullOpacity_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Red); + RectangularPolygon polygon = new(10, 10, 80, 80); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.SaveLayer(); + canvas.Fill(brush, polygon); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_FullOpacity", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "SaveLayer_FullOpacity", defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_HalfOpacity_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Red); + RectangularPolygon polygon = new(10, 10, 80, 80); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + canvas.Fill(brush, polygon); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_HalfOpacity", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "SaveLayer_HalfOpacity", defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_NestedLayers_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Outer layer: red fill. + canvas.SaveLayer(); + canvas.Fill(Brushes.Solid(Color.Red), new RectangularPolygon(0, 0, 128, 128)); + + // Inner layer: blue fill over center. + canvas.SaveLayer(); + canvas.Fill(Brushes.Solid(Color.Blue), new RectangularPolygon(32, 32, 64, 64)); + canvas.Restore(); // Composites blue onto red. + + canvas.Restore(); // Composites red+blue onto white. + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_NestedLayers", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "SaveLayer_NestedLayers", defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_WithBlendMode_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.Red), new RectangularPolygon(20, 20, 88, 88)); + + canvas.SaveLayer(new GraphicsOptions + { + ColorBlendingMode = PixelColorBlendingMode.Multiply, + AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver, + BlendPercentage = 1f + }); + + canvas.Fill(Brushes.Solid(Color.Blue), new RectangularPolygon(40, 40, 88, 88)); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_WithBlendMode", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "SaveLayer_WithBlendMode", defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_WithBounds_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Layer restricted to a sub-region; draw within the layer's local bounds. + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(16, 16, 96, 96)); + canvas.Fill(Brushes.Solid(Color.Green), new RectangularPolygon(0, 0, 96, 96)); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_WithBounds", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "SaveLayer_WithBounds", defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] + public void SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + int before = canvas.SaveCount; + canvas.Save(); // plain save + canvas.SaveLayer(); // layer + canvas.Save(); // plain save + + canvas.Fill(Brushes.Solid(Color.Green), new RectangularPolygon(0, 0, 128, 128)); + + canvas.RestoreTo(before); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, "SaveLayer_MixedSaveAndSaveLayer", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairReferenceOutputs(provider, "SaveLayer_MixedSaveAndSaveLayer", defaultImage, nativeSurfaceImage); + } + + private static Buffer2DRegion GetFrameRegion(Image image) + where TPixel : unmanaged, IPixel + => new(image.Frames.RootFrame.PixelBuffer, image.Bounds); +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs new file mode 100644 index 000000000..49f17e736 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class WebGPUTextureFormatMapperTests +{ + [Fact] + public void Mapper_UsesExactSilkEnumValues_ForAllSupportedFormats() + { + (WebGPUTextureFormatId Drawing, TextureFormat Silk)[] mappings = + [ + (WebGPUTextureFormatId.Rgba8Unorm, TextureFormat.Rgba8Unorm), + (WebGPUTextureFormatId.Rgba8Snorm, TextureFormat.Rgba8Snorm), + (WebGPUTextureFormatId.Bgra8Unorm, TextureFormat.Bgra8Unorm), + (WebGPUTextureFormatId.Rgba16Float, TextureFormat.Rgba16float) + ]; + + Assert.Equal(Enum.GetValues().Length, mappings.Length); + + foreach ((WebGPUTextureFormatId drawing, TextureFormat silk) in mappings) + { + Assert.Equal((int)silk, (int)drawing); + Assert.Equal(silk, WebGPUTextureFormatMapper.ToSilk(drawing)); + Assert.Equal(drawing, WebGPUTextureFormatMapper.FromSilk(silk)); + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs new file mode 100644 index 000000000..0e5521513 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -0,0 +1,246 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class DrawingCanvasBatcherTests +{ + [Fact] + public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(40, 40); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + IPath path = new RectangularPolygon(4, 6, 18, 12); + DrawingOptions options = new(); + using DrawingCanvas canvas = new(configuration, region, options); + Brush brushA = Brushes.Solid(Color.Red); + Brush brushB = Brushes.Solid(Color.Blue); + + canvas.Fill(brushA, path); + canvas.Fill(brushB, path); + canvas.Flush(); + + Assert.True(backend.HasDefinition); + Assert.NotNull(backend.LastDefinition.Definition.PreparedPath); + Assert.Equal(2, backend.LastDefinition.Commands.Count); + Assert.Same(brushA, backend.LastDefinition.Commands[0].Brush); + Assert.Same(brushB, backend.LastDefinition.Commands[1].Brush); + } + + [Fact] + public void Flush_SamePathDifferentBrushes_ReusesPreparedPath() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(40, 40); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + IPath path = new RectangularPolygon(4, 6, 18, 12); + DrawingOptions options = new(); + using DrawingCanvas canvas = new(configuration, region, options); + + canvas.Fill(Brushes.Solid(Color.Red), path); + canvas.Fill(Brushes.Solid(Color.Blue), path); + canvas.Flush(); + + Assert.Equal(2, backend.PreparedCommands.Count); + Assert.NotNull(backend.PreparedCommands[0].PreparedPath); + Assert.Same(backend.PreparedCommands[0].PreparedPath, backend.PreparedCommands[1].PreparedPath); + } + + [Fact] + public void Flush_SamePathReusedMultipleTimes_GroupsCommandsByCoverageDefinition() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(100, 100); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + IPath path = new RectangularPolygon(10, 10, 40, 40); + DrawingOptions options = new(); + using DrawingCanvas canvas = new(configuration, region, options); + + for (int i = 0; i < 10; i++) + { + canvas.Fill(Brushes.Solid(Color.FromPixel(new Rgba32((byte)i, 0, 0, 255))), path); + } + + canvas.Flush(); + + Assert.Single(backend.Definitions); + Assert.Equal(10, backend.Definitions[0].Commands.Count); + } + + [Fact] + public void Flush_RepeatedGlyphs_ReusesCoverageDefinitions() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(420, 220); + Buffer2DRegion region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = new('A', 200); + Brush brush = Brushes.Solid(Color.Black); + + using DrawingCanvas canvas = new(configuration, region, drawingOptions); + canvas.DrawText(textOptions, text, brush, pen: null); + canvas.Flush(); + + int totalCommands = backend.Definitions.Sum(b => b.Commands.Count); + Assert.True(totalCommands > 0); + Assert.True( + backend.Definitions.Count < 200, + $"Expected coverage reuse but got {backend.Definitions.Count} coverage definitions for 200 glyphs."); + } + + private sealed class CapturingBackend : IDrawingBackend + { + public List Definitions { get; } = []; + + public IReadOnlyList PreparedCommands { get; private set; } = Array.Empty(); + + public bool HasDefinition { get; private set; } + + public CapturedCoverageDefinition LastDefinition { get; private set; } = new( + new CompositionCoverageDefinition( + 0, + EmptyPath.ClosedPath, + new RasterizerOptions( + Rectangle.Empty, + IntersectionRule.NonZero, + RasterizationMode.Aliased, + RasterizerSamplingOrigin.PixelBoundary, + 0.5f)), + []); + + public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene) + where TPixel : unmanaged, IPixel + { + this.PreparedCommands = compositionScene.Commands.ToArray(); + + Dictionary definitionIndices = []; + for (int i = 0; i < compositionScene.Commands.Count; i++) + { + CompositionCommand command = compositionScene.Commands[i]; + if (!command.IsVisible) + { + continue; + } + + IPath preparedPath = command.PreparedPath + ?? throw new InvalidOperationException("Composition commands must be prepared before backend flush."); + RasterizerOptions rasterizerOptions = command.RasterizerOptions; + + CoverageDefinitionKey key = new(command); + if (!definitionIndices.TryGetValue(key, out int definitionIndex)) + { + definitionIndex = this.Definitions.Count; + definitionIndices.Add(key, definitionIndex); + this.Definitions.Add( + new CapturedCoverageDefinition( + new CompositionCoverageDefinition( + command.DefinitionKey, + preparedPath, + in rasterizerOptions, + command.DestinationOffset), + [command])); + } + else + { + this.Definitions[definitionIndex].Commands.Add(command); + } + } + + if (this.Definitions.Count == 0) + { + return; + } + + this.LastDefinition = this.Definitions[^1]; + this.HasDefinition = true; + } + + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel + => false; + + public sealed class CapturedCoverageDefinition(CompositionCoverageDefinition definition, List commands) + { + public CompositionCoverageDefinition Definition { get; } = definition; + + public List Commands { get; } = commands; + } + + private readonly struct CoverageDefinitionKey : IEquatable + { + private readonly int definitionKey; + private readonly Rectangle interest; + private readonly IntersectionRule intersectionRule; + private readonly RasterizationMode rasterizationMode; + private readonly RasterizerSamplingOrigin samplingOrigin; + private readonly int antialiasThresholdBits; + + public CoverageDefinitionKey(CompositionCommand command) + { + this.definitionKey = command.DefinitionKey; + this.interest = command.RasterizerOptions.Interest; + this.intersectionRule = command.RasterizerOptions.IntersectionRule; + this.rasterizationMode = command.RasterizerOptions.RasterizationMode; + this.samplingOrigin = command.RasterizerOptions.SamplingOrigin; + this.antialiasThresholdBits = BitConverter.SingleToInt32Bits(command.RasterizerOptions.AntialiasThreshold); + } + + public bool Equals(CoverageDefinitionKey other) + => this.definitionKey == other.definitionKey && + this.interest.Equals(other.interest) && + this.intersectionRule == other.intersectionRule && + this.rasterizationMode == other.rasterizationMode && + this.samplingOrigin == other.samplingOrigin && + this.antialiasThresholdBits == other.antialiasThresholdBits; + + public override bool Equals(object? obj) + => obj is CoverageDefinitionKey other && this.Equals(other); + + public override int GetHashCode() + => HashCode.Combine( + this.definitionKey, + this.interest, + (int)this.intersectionRule, + (int)this.rasterizationMode, + (int)this.samplingOrigin, + this.antialiasThresholdBits); + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs new file mode 100644 index 000000000..6b8cb7095 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.BrushAndPenStyles.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(320, 200, PixelTypes.Rgba32)] + public void Fill_WithGradientAndPatternBrushes_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Brush linearBrush = new LinearGradientBrush( + new PointF(18, 22), + new PointF(192, 140), + GradientRepetitionMode.None, + new ColorStop(0F, Color.LightYellow), + new ColorStop(0.5F, Color.DeepSkyBlue.WithAlpha(0.85F)), + new ColorStop(1F, Color.MediumBlue.WithAlpha(0.9F))); + + Brush radialBrush = new RadialGradientBrush( + new PointF(238, 88), + 66F, + GradientRepetitionMode.Reflect, + new ColorStop(0F, Color.Orange.WithAlpha(0.95F)), + new ColorStop(1F, Color.MediumVioletRed.WithAlpha(0.25F))); + + Brush hatchBrush = Brushes.ForwardDiagonal(Color.DarkSlateGray.WithAlpha(0.7F), Color.Transparent); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(linearBrush, new Rectangle(14, 14, 176, 126)); + canvas.Fill(radialBrush, new EllipsePolygon(new PointF(236, 90), new SizeF(132, 98))); + canvas.Fill(hatchBrush, CreateClosedPathBuilder()); + canvas.Draw(Pens.DashDot(Color.Black, 3), new Rectangle(10, 10, 300, 180)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 200, PixelTypes.Rgba32)] + public void Draw_WithPatternAndGradientPens_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Brush gradientBrush = new LinearGradientBrush( + new PointF(0, 0), + new PointF(320, 0), + GradientRepetitionMode.Repeat, + new ColorStop(0F, Color.CornflowerBlue), + new ColorStop(0.5F, Color.Gold), + new ColorStop(1F, Color.MediumSeaGreen)); + + Brush patternBrush = Brushes.Vertical(Color.DarkRed.WithAlpha(0.75F), Color.Transparent); + Brush percentBrush = Brushes.Percent20(Color.DarkOrange.WithAlpha(0.85F), Color.Transparent); + + Pen dashPen = Pens.Dash(gradientBrush, 6F); + Pen dotPen = Pens.Dot(patternBrush, 5F); + Pen dashDotPen = Pens.DashDot(percentBrush, 4F); + Pen dashDotDotPen = Pens.DashDotDot(Color.Black.WithAlpha(0.75F), 3F); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Draw(dashPen, new Rectangle(16, 14, 288, 170)); + canvas.DrawEllipse(dotPen, new PointF(162, 100), new SizeF(206, 116)); + canvas.DrawArc(dashDotPen, new PointF(160, 100), new SizeF(148, 84), rotation: 0, startAngle: 20, sweepAngle: 300); + canvas.DrawLine(dashDotDotPen, new PointF(26, 174), new PointF(108, 22), new PointF(212, 164), new PointF(292, 26)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs new file mode 100644 index 000000000..8ce866531 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(256, 160, PixelTypes.Rgba32)] + public void Clear_RegionAndPath_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Fill(Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F))); + canvas.Fill(Brushes.Solid(Color.Crimson.WithAlpha(0.8F)), new Rectangle(22, 16, 188, 118)); + canvas.DrawEllipse(Pens.Solid(Color.Gold, 5), new PointF(128, 80), new SizeF(140, 90)); + + canvas.Clear(Brushes.Solid(Color.LightYellow.WithAlpha(0.45F)), new Rectangle(56, 36, 108, 64)); + IPath clearPath = new EllipsePolygon(new PointF(178, 80), new SizeF(74, 56)); + canvas.Clear(Brushes.Solid(Color.Transparent), clearPath); + + canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(10, 10, 236, 140)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 200, PixelTypes.Rgba32)] + public void Clear_WithClipPath_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.MidnightBlue.WithAlpha(0.95F)), new Rectangle(0, 0, 320, 200)); + canvas.Fill(Brushes.Solid(Color.Crimson.WithAlpha(0.78F)), new Rectangle(26, 18, 268, 164)); + canvas.DrawEllipse(Pens.Solid(Color.Gold, 5F), new PointF(160, 100), new SizeF(196, 116)); + + IPath clipPath = new EllipsePolygon(new PointF(160, 100), new SizeF(214, 126)); + _ = canvas.Save(new DrawingOptions(), clipPath); + + canvas.Clear(Brushes.Solid(Color.LightYellow.WithAlpha(0.85F))); + canvas.Clear(Brushes.Solid(Color.MediumPurple.WithAlpha(0.72F)), new Rectangle(40, 24, 108, 72)); + canvas.Clear(Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F)), new Rectangle(172, 96, 110, 70)); + canvas.Clear(Brushes.Solid(Color.Transparent), new EllipsePolygon(new PointF(164, 98), new SizeF(74, 48))); + + canvas.Restore(); + + canvas.Draw(Pens.DashDot(Color.Black, 3F), clipPath); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 304, 184)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs new file mode 100644 index 000000000..52c902fc3 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] + public void DrawImage_WithRotationTransform_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image foreground = provider.GetImage(); + using Image target = new(384, 256); + + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation(MathF.PI / 4F, new Vector2(192F, 128F))) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawImage( + foreground, + foreground.Bounds, + new RectangleF(72, 48, 240, 160), + KnownResamplers.NearestNeighbor); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(320, 220, PixelTypes.Rgba32)] + public void DrawImage_WithSourceClippingAndScaling_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image foreground = provider.GetImage(); + using Image target = new(320, 220); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawImage( + foreground, + new Rectangle(-48, 18, 196, 148), + new RectangleF(18, 20, 170, 120), + KnownResamplers.Bicubic); + canvas.DrawImage( + foreground, + new Rectangle(220, 100, 160, 140), + new RectangleF(170, 72, 130, 110), + KnownResamplers.NearestNeighbor); + canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(8, 8, 304, 204)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(360, 240, PixelTypes.Rgba32)] + public void DrawImage_WithClipPathAndTransform_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image foreground = provider.GetImage(); + using Image target = new(360, 240); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + DrawingOptions transformedOptions = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.32F, new Vector2(180, 120))) + }; + + IPath clipPath = new EllipsePolygon(new PointF(180, 120), new SizeF(208, 126)); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.LightGray.WithAlpha(0.45F)), new Rectangle(18, 16, 324, 208)); + + _ = canvas.Save(transformedOptions, clipPath); + canvas.DrawImage( + foreground, + new Rectangle(10, 8, 234, 180), + new RectangleF(64, 36, 232, 164), + KnownResamplers.Bicubic); + canvas.DrawImage( + foreground, + new Rectangle(102, 32, 196, 166), + new RectangleF(92, 58, 210, 148), + KnownResamplers.NearestNeighbor); + canvas.Restore(); + + canvas.Draw(Pens.DashDot(Color.DarkSlateGray, 3F), clipPath); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 344, 224)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Factory.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Factory.cs new file mode 100644 index 000000000..bf557661f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Factory.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void FromFrame_TargetsProvidedFrame() + { + using Image image = new(48, 36); + + using (DrawingCanvas canvas = DrawingCanvas.FromFrame( + image.Frames.RootFrame, + new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.SeaGreen)); + canvas.Flush(); + } + + Assert.Equal(Color.SeaGreen.ToPixel(), image[12, 10]); + } + + [Fact] + public void FromImage_TargetsRequestedFrame() + { + using Image image = new(40, 30); + image.Frames.AddFrame(image.Frames.RootFrame); + + using (DrawingCanvas rootCanvas = DrawingCanvas.FromRootFrame(image, new DrawingOptions())) + { + rootCanvas.Clear(Brushes.Solid(Color.White)); + rootCanvas.Flush(); + } + + using (DrawingCanvas secondCanvas = DrawingCanvas.FromImage(image, 1, new DrawingOptions())) + { + secondCanvas.Clear(Brushes.Solid(Color.MediumPurple)); + secondCanvas.Flush(); + } + + Assert.Equal(Color.White.ToPixel(), image.Frames.RootFrame[8, 8]); + Assert.Equal(Color.MediumPurple.ToPixel(), image.Frames[1][8, 8]); + } + + [Fact] + public void FromImage_InvalidFrameIndex_Throws() + { + using Image image = new(20, 20); + image.Frames.AddFrame(image.Frames.RootFrame); + + ArgumentOutOfRangeException low = Assert.Throws( + () => DrawingCanvas.FromImage(image, -1, new DrawingOptions())); + ArgumentOutOfRangeException high = Assert.Throws( + () => DrawingCanvas.FromImage(image, image.Frames.Count, new DrawingOptions())); + + Assert.Equal("frameIndex", low.ParamName); + Assert.Equal("frameIndex", high.ParamName); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Guards.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Guards.cs new file mode 100644 index 000000000..2fbbdb861 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Guards.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void RestoreTo_InvalidCount_Throws() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas( + provider, + target, + new DrawingOptions()); + + ArgumentOutOfRangeException low = Assert.Throws(() => canvas.RestoreTo(0)); + ArgumentOutOfRangeException high = Assert.Throws(() => canvas.RestoreTo(2)); + + Assert.Equal("saveCount", low.ParamName); + Assert.Equal("saveCount", high.ParamName); + } + + [Fact] + public void Dispose_ThenOperations_ThrowObjectDisposedException() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(96, 96); + using Image source = new(24, 24); + using DrawingCanvas canvas = CreateCanvas( + provider, + target, + new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 16); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(10, 28) + }; + + canvas.Dispose(); + + Assert.Throws(() => canvas.Fill(Brushes.Solid(Color.Black))); + Assert.Throws(() => canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 60, 60))); + Assert.Throws(() => canvas.DrawText(textOptions, "Disposed", Brushes.Solid(Color.DarkBlue), pen: null)); + Assert.Throws(() => canvas.DrawImage(source, source.Bounds, new RectangleF(12, 12, 48, 48))); + Assert.Throws(canvas.Flush); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs new file mode 100644 index 000000000..f369fe93e --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderDraw.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(192, 128, PixelTypes.Rgba32)] + public void Draw_PathBuilder_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation(-0.15F, new Vector2(96F, 64F))) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + PathBuilder pathBuilder = CreateOpenPathBuilder(); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.CornflowerBlue, 6F), pathBuilder); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs new file mode 100644 index 000000000..de5d52f87 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathBuilderFill.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(192, 128, PixelTypes.Rgba32)] + public void Fill_PathBuilder_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.2F, new Vector2(96F, 64F))) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + PathBuilder pathBuilder = CreateClosedPathBuilder(); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), pathBuilder); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs new file mode 100644 index 000000000..95afcbf46 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(360, 220, PixelTypes.Rgba32)] + public void Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + IPath leftPath = CreatePentagramPath(new PointF(96, 110), 78F); + IPath rightPath = CreatePentagramPath(new PointF(264, 110), 78F); + + DrawingOptions evenOddOptions = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + DrawingOptions nonZeroOptions = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.AliceBlue.WithAlpha(0.7F)), new Rectangle(12, 12, 336, 196)); + + _ = canvas.Save(evenOddOptions); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), leftPath); + canvas.Restore(); + + _ = canvas.Save(nonZeroOptions); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), rightPath); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.Black, 3F), leftPath); + canvas.Draw(Pens.Solid(Color.Black, 3F), rightPath); + canvas.DrawLine(Pens.Dash(Color.Gray, 2F), new PointF(180, 20), new PointF(180, 200)); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 344, 204)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + private static IPath CreatePentagramPath(PointF center, float radius) + { + PointF[] points = new PointF[5]; + for (int i = 0; i < points.Length; i++) + { + float angle = (-MathF.PI / 2F) + (i * (MathF.PI * 2F / points.Length)); + points[i] = new PointF( + center.X + (radius * MathF.Cos(angle)), + center.Y + (radius * MathF.Sin(angle))); + } + + int[] order = [0, 2, 4, 1, 3, 0]; + PathBuilder builder = new(); + for (int i = 0; i < order.Length - 1; i++) + { + PointF a = points[order[i]]; + PointF b = points[order[i + 1]]; + builder.AddLine(a.X, a.Y, b.X, b.Y); + } + + builder.CloseAllFigures(); + return builder.Build(); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs new file mode 100644 index 000000000..16e3e5f04 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Primitives.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(50, 50, PixelTypes.Rgba32)] + public void FillRectangle_AliasedRendersFullCorners(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + const int x = 10; + const int y = 10; + const int w = 30; + const int h = 20; + + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false } + }; + + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + + canvas.Clear(Brushes.Solid(Color.Black)); + canvas.Fill(Brushes.Solid(Color.White), new Rectangle(x, y, w, h)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + // Verify all four corner pixels are fully white. + Rgba32 topLeft = target[x, y].ToRgba32(); + Rgba32 topRight = target[x + w - 1, y].ToRgba32(); + Rgba32 bottomLeft = target[x, y + h - 1].ToRgba32(); + Rgba32 bottomRight = target[x + w - 1, y + h - 1].ToRgba32(); + + Assert.Equal(255, topLeft.R); + Assert.Equal(255, topRight.R); + Assert.Equal(255, bottomLeft.R); + Assert.Equal(255, bottomRight.R); + + // Verify pixels just outside each corner are still black. + Assert.Equal(0, target[x - 1, y].ToRgba32().R); + Assert.Equal(0, target[x, y - 1].ToRgba32().R); + Assert.Equal(0, target[x + w, y].ToRgba32().R); + Assert.Equal(0, target[x + w - 1, y - 1].ToRgba32().R); + Assert.Equal(0, target[x - 1, y + h - 1].ToRgba32().R); + Assert.Equal(0, target[x, y + h].ToRgba32().R); + Assert.Equal(0, target[x + w, y + h - 1].ToRgba32().R); + Assert.Equal(0, target[x + w - 1, y + h].ToRgba32().R); + + // Verify interior pixel count matches expected area. + int whiteCount = 0; + target.ProcessPixelRows(accessor => + { + for (int row = 0; row < accessor.Height; row++) + { + Span span = accessor.GetRowSpan(row); + for (int col = 0; col < span.Length; col++) + { + if (span[col].ToRgba32().R == 255) + { + whiteCount++; + } + } + } + }); + + Assert.Equal(w * h, whiteCount); + } + + [Theory] + [WithBlankImage(50, 50, PixelTypes.Rgba32)] + public void DrawRectangle_AliasedRendersFullCorners(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // A 2px pen centered on the rectangle edge places 1px inside and 1px outside. + // For a rect at (10,10) size 30x20, the outer stroke boundary is (9,9)-(40,30) + // and the inner boundary is (11,11)-(38,28). + // Miter join ensures corners are fully filled (bevel would cut them). + const int x = 10; + const int y = 10; + const int w = 30; + const int h = 20; + const float penWidth = 2; + + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.01F } + }; + + SolidPen pen = new(new PenOptions(Color.White, penWidth, null) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter } + }); + + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + + canvas.Clear(Brushes.Solid(Color.Black)); + canvas.Draw(pen, new Rectangle(x, y, w, h)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + // Outer corners of the stroke (1px outside the rect edge). + Assert.Equal(255, target[x - 1, y - 1].ToRgba32().R); + Assert.Equal(255, target[x + w, y - 1].ToRgba32().R); + Assert.Equal(255, target[x - 1, y + h].ToRgba32().R); + Assert.Equal(255, target[x + w, y + h].ToRgba32().R); + + // Inner corners of the stroke (1px inside the rect edge). + Assert.Equal(255, target[x, y].ToRgba32().R); + Assert.Equal(255, target[x + w - 1, y].ToRgba32().R); + Assert.Equal(255, target[x, y + h - 1].ToRgba32().R); + Assert.Equal(255, target[x + w - 1, y + h - 1].ToRgba32().R); + + // Well outside the stroke boundary should be black. + Assert.Equal(0, target[x - 3, y - 3].ToRgba32().R); + Assert.Equal(0, target[x + w + 2, y - 3].ToRgba32().R); + Assert.Equal(0, target[x - 3, y + h + 2].ToRgba32().R); + Assert.Equal(0, target[x + w + 2, y + h + 2].ToRgba32().R); + + // Interior of the rectangle (well inside the stroke) should be black. + Assert.Equal(0, target[x + (w / 2), y + (h / 2)].ToRgba32().R); + } + + [Theory] + [WithBlankImage(240, 160, PixelTypes.Rgba32)] + public void DrawPrimitiveHelpers_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 220, 140)); + canvas.DrawEllipse(Pens.Solid(Color.CornflowerBlue, 6), new PointF(120, 80), new SizeF(110, 70)); + canvas.DrawArc( + Pens.Solid(Color.ForestGreen, 4), + new PointF(120, 80), + new SizeF(90, 46), + rotation: 15, + startAngle: -25, + sweepAngle: 220); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(18, 140), + new PointF(76, 28), + new PointF(166, 126), + new PointF(222, 20)); + canvas.DrawBezier( + Pens.Solid(Color.MediumVioletRed, 4), + new PointF(20, 80), + new PointF(70, 18), + new PointF(168, 144), + new PointF(220, 78)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs new file mode 100644 index 000000000..ac2ff8924 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -0,0 +1,232 @@ +// 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.Tests.TestUtilities; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_PathBuilder_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + PathBuilder blurBuilder = new(); + blurBuilder.AddArc(new PointF(55, 40), 55, 40, 0, 0, 360); + blurBuilder.CloseAllFigures(); + + PathBuilder pixelateBuilder = new(); + pixelateBuilder.AddLine(110, 80, 220, 80); + pixelateBuilder.AddLine(220, 80, 165, 160); + pixelateBuilder.AddLine(165, 160, 110, 80); + pixelateBuilder.CloseAllFigures(); + + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + DrawProcessScenario(canvas); + canvas.Process(blurBuilder, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelateBuilder, ctx => ctx.Pixelate(10)); + canvas.Flush(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_Path_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + IPath blurPath = CreateBlurEllipsePath(); + IPath pixelatePath = CreatePixelateTrianglePath(); + + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + DrawProcessScenario(canvas); + canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); + canvas.Flush(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(220, 160, PixelTypes.Rgba32)] + public void Process_NoCpuFrame_WithReadbackCapability_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + IPath blurPath = CreateBlurEllipsePath(); + IPath pixelatePath = CreatePixelateTrianglePath(); + + Buffer2DRegion targetRegion = new(target.Frames.RootFrame.PixelBuffer, target.Bounds); + MemoryCanvasFrame proxyFrame = new(targetRegion); + MirroringCpuReadbackTestBackend mirroringBackend = new(proxyFrame, target); + + NativeSurface nativeSurface = new(TPixel.GetPixelTypeInfo()); + Configuration configuration = provider.Configuration.Clone(); + configuration.SetDrawingBackend(mirroringBackend); + + using (DrawingCanvas canvas = new( + configuration, + new NativeCanvasFrame(target.Bounds, nativeSurface), + new DrawingOptions())) + { + DrawProcessScenario(canvas); + canvas.Process(blurPath, ctx => ctx.GaussianBlur(6F)); + canvas.Process(pixelatePath, ctx => ctx.Pixelate(10)); + canvas.Flush(); + } + + Assert.True(mirroringBackend.ReadbackCallCount > 0); + Assert.Same(configuration, mirroringBackend.LastReadbackConfiguration); + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Fact] + public void Process_UsesCanvasConfigurationForOperationContext() + { + Configuration configuration = Configuration.Default.Clone(); + using Image target = new(configuration, 48, 36); + Buffer2DRegion targetRegion = new(target.Frames.RootFrame.PixelBuffer, target.Bounds); + using DrawingCanvas canvas = new(configuration, targetRegion, new DrawingOptions()); + + bool callbackInvoked = false; + bool sameConfiguration = false; + + canvas.Fill(Brushes.Solid(Color.CornflowerBlue)); + canvas.Process(new Rectangle(8, 6, 28, 20), ctx => + { + callbackInvoked = true; + sameConfiguration = ReferenceEquals(configuration, ctx.Configuration); + ctx.GaussianBlur(2F); + }); + canvas.Flush(); + + Assert.True(callbackInvoked); + Assert.True(sameConfiguration); + } + + private static void DrawProcessScenario(IDrawingCanvas canvas) + { + canvas.Clear(Brushes.Solid(Color.White)); + + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 220, 140)); + canvas.DrawEllipse(Pens.Solid(Color.CornflowerBlue, 6), new PointF(120, 80), new SizeF(110, 70)); + canvas.DrawArc( + Pens.Solid(Color.ForestGreen, 4), + new PointF(120, 80), + new SizeF(90, 46), + rotation: 15, + startAngle: -25, + sweepAngle: 220); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(18, 140), + new PointF(76, 28), + new PointF(166, 126), + new PointF(222, 20)); + canvas.DrawBezier( + Pens.Solid(Color.MediumVioletRed, 4), + new PointF(20, 80), + new PointF(70, 18), + new PointF(168, 144), + new PointF(220, 78)); + } + + private static EllipsePolygon CreateBlurEllipsePath() + => new(new PointF(55, 40), new SizeF(110, 80)); + + private static IPath CreatePixelateTrianglePath() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(110, 80, 220, 80); + pathBuilder.AddLine(220, 80, 165, 160); + pathBuilder.AddLine(165, 160, 110, 80); + pathBuilder.CloseAllFigures(); + return pathBuilder.Build(); + } + + /// + /// Test backend that mirrors composition output into a CPU frame and optionally serves readback + /// from a backing image so Process-path tests can exercise both readback and shadow-fallback flows. + /// + private sealed class MirroringCpuReadbackTestBackend : IDrawingBackend + where TPixel : unmanaged, IPixel + { + private readonly ICanvasFrame proxyFrame; + private readonly Image? readbackSource; + + public MirroringCpuReadbackTestBackend(ICanvasFrame proxyFrame, Image? readbackSource = null) + { + this.proxyFrame = proxyFrame; + this.readbackSource = readbackSource; + } + + public int ReadbackCallCount { get; private set; } + + public Configuration? LastReadbackConfiguration { get; private set; } + + public void FlushCompositions( + Configuration configuration, + ICanvasFrame target, + CompositionScene compositionScene) + where TTargetPixel : unmanaged, IPixel + { + if (this.proxyFrame is not ICanvasFrame typedProxyFrame) + { + throw new NotSupportedException("Mirroring test backend pixel format mismatch."); + } + + DefaultDrawingBackend.Instance.FlushCompositions(configuration, typedProxyFrame, compositionScene); + } + + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TTargetPixel : unmanaged, IPixel + { + this.LastReadbackConfiguration = configuration; + + if (this.readbackSource is null) + { + return false; + } + + this.ReadbackCallCount++; + + Rectangle clipped = Rectangle.Intersect(this.readbackSource.Bounds, sourceRectangle); + if (clipped.Width <= 0 || clipped.Height <= 0) + { + return false; + } + + using Image cropped = this.readbackSource.Clone(ctx => ctx.Crop(clipped)); + using Image converted = cropped.CloneAs(); + Buffer2D source = converted.Frames.RootFrame.PixelBuffer; + int copyWidth = Math.Min(source.Width, destination.Width); + int copyHeight = Math.Min(source.Height, destination.Height); + for (int y = 0; y < copyHeight; y++) + { + source.DangerousGetRowSpan(y).Slice(0, copyWidth).CopyTo(destination.DangerousGetRowSpan(y)); + } + + return true; + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs new file mode 100644 index 000000000..19312eb92 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(256, 160, PixelTypes.Rgba32)] + public void CreateRegion_LocalCoordinates_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + using (DrawingCanvas regionCanvas = canvas.CreateRegion(new Rectangle(40, 24, 140, 96))) + { + regionCanvas.Fill(Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F)), new Rectangle(10, 8, 80, 46)); + regionCanvas.Draw(Pens.Solid(Color.DarkBlue, 5), new Rectangle(0, 0, 140, 96)); + regionCanvas.DrawLine( + Pens.Solid(Color.OrangeRed, 4), + new PointF(0, 95), + new PointF(139, 0)); + } + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(192, 128, PixelTypes.Rgba32)] + public void SaveRestore_ClipPath_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + IPath clipPath = new EllipsePolygon(new PointF(96, 64), new SizeF(120, 76)); + _ = canvas.Save(new DrawingOptions(), clipPath); + + canvas.Fill(Brushes.Solid(Color.MediumVioletRed.WithAlpha(0.85F)), new Rectangle(0, 0, 192, 128)); + canvas.Draw(Pens.Solid(Color.Black, 3), new Rectangle(24, 16, 144, 96)); + + canvas.Restore(); + + canvas.Fill(Brushes.Solid(Color.SteelBlue.WithAlpha(0.75F)), new Rectangle(0, 96, 192, 32)); + canvas.Draw(Pens.Solid(Color.DarkGreen, 4), new Rectangle(8, 8, 176, 112)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + ImageComparer tolerantComparer = ImageComparer.TolerantPercentage(0.0003F); + target.CompareToReferenceOutput(tolerantComparer, provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(224, 160, PixelTypes.Rgba32)] + public void RestoreTo_MultipleStates_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + + DrawingOptions firstOptions = new() + { + Transform = Matrix4x4.CreateTranslation(20F, 12F, 0) + }; + + int firstSaveCount = canvas.Save(firstOptions, new RectangularPolygon(20, 20, 144, 104)); + canvas.Fill(Brushes.Solid(Color.SkyBlue.WithAlpha(0.8F)), new Rectangle(0, 0, 120, 84)); + + DrawingOptions secondOptions = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.24F, new Vector2(112, 80))) + }; + + _ = canvas.Save(secondOptions, new EllipsePolygon(new PointF(112, 80), new SizeF(130, 90))); + canvas.Draw(Pens.Solid(Color.MediumPurple, 6), new Rectangle(34, 26, 152, 108)); + + canvas.RestoreTo(firstSaveCount); + canvas.DrawLine( + Pens.Solid(Color.OrangeRed, 5), + new PointF(0, 100), + new PointF(76, 18), + new PointF(168, 92)); + + canvas.RestoreTo(1); + canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.7F)), new Rectangle(156, 106, 48, 34)); + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 4), new Rectangle(8, 8, 208, 144)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 220, PixelTypes.Rgba32)] + public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F)), new Rectangle(12, 12, 296, 196)); + + DrawingOptions rootOptions = new() + { + Transform = Matrix4x4.CreateTranslation(6F, 4F, 0) + }; + + IPath rootClip = new EllipsePolygon(new PointF(160, 110), new SizeF(252, 164)); + _ = canvas.Save(rootOptions, rootClip); + + using (DrawingCanvas outerRegion = canvas.CreateRegion(new Rectangle(30, 24, 240, 156))) + { + outerRegion.Fill(Brushes.Solid(Color.LightBlue.WithAlpha(0.35F)), new Rectangle(0, 0, 240, 156)); + outerRegion.Draw(Pens.Solid(Color.DarkBlue, 3F), new Rectangle(0, 0, 240, 156)); + + DrawingOptions outerOptions = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.18F, new Vector2(120, 78))) + }; + + _ = outerRegion.Save(outerOptions, new RectangularPolygon(18, 14, 204, 128)); + + outerRegion.Fill(Brushes.Solid(Color.MediumPurple.WithAlpha(0.35F)), new Rectangle(16, 16, 208, 124)); + + using (DrawingCanvas innerRegion = outerRegion.CreateRegion(new Rectangle(52, 34, 132, 82))) + { + innerRegion.Clear(Brushes.Solid(Color.LightGoldenrodYellow.WithAlpha(0.8F))); + + DrawingOptions innerOptions = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateSkew(0.18F, 0F)) + }; + + _ = innerRegion.Save(innerOptions, new EllipsePolygon(new PointF(66, 41), new SizeF(102, 58))); + + innerRegion.Fill(Brushes.Solid(Color.SeaGreen.WithAlpha(0.55F)), new Rectangle(0, 0, 132, 82)); + innerRegion.DrawLine( + Pens.Solid(Color.DarkRed, 4F), + new PointF(0, 80), + new PointF(66, 0), + new PointF(132, 74)); + + innerRegion.Restore(); + + innerRegion.Draw(Pens.DashDot(Color.Black.WithAlpha(0.75F), 2F), new Rectangle(4, 4, 124, 74)); + } + + outerRegion.Restore(); + + outerRegion.Fill(Brushes.Solid(Color.OrangeRed.WithAlpha(0.6F)), new Rectangle(8, 112, 90, 30)); + outerRegion.DrawLine(Pens.Solid(Color.Black, 3F), new PointF(8, 8), new PointF(232, 148)); + } + + canvas.RestoreTo(1); + + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 3F), new Rectangle(8, 8, 304, 204)); + canvas.DrawLine(Pens.Dash(Color.Gray, 2F), new PointF(20, 200), new PointF(300, 20)); + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs new file mode 100644 index 000000000..5ddbfc48a --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void SaveCount_InitialValue_IsOne() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void Save_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + + int count1 = canvas.Save(); + Assert.Equal(2, count1); + Assert.Equal(2, canvas.SaveCount); + + int count2 = canvas.Save(); + Assert.Equal(3, count2); + Assert.Equal(3, canvas.SaveCount); + } + + [Fact] + public void SaveWithOptions_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int count = canvas.Save(new DrawingOptions(), new RectangularPolygon(0, 0, 32, 32)); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void Restore_DecrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + _ = canvas.Save(); + _ = canvas.Save(); + Assert.Equal(3, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(2, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void Restore_AtRootState_DoesNotDecrementBelowOne() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void RestoreTo_SetsSaveCountToSpecifiedLevel() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + _ = canvas.Save(); + int mid = canvas.Save(); + _ = canvas.Save(); + _ = canvas.Save(); + Assert.Equal(5, canvas.SaveCount); + + canvas.RestoreTo(mid); + Assert.Equal(mid, canvas.SaveCount); + } + + [Fact] + public void RestoreTo_One_RestoresToRoot() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + _ = canvas.Save(); + _ = canvas.Save(); + _ = canvas.Save(); + + canvas.RestoreTo(1); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void Save_ReturnValue_MatchesSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + for (int i = 0; i < 5; i++) + { + int returned = canvas.Save(); + Assert.Equal(canvas.SaveCount, returned); + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs new file mode 100644 index 000000000..4efc5a21b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs @@ -0,0 +1,228 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Fact] + public void SaveLayer_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Assert.Equal(1, canvas.SaveCount); + + int count = canvas.SaveLayer(); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_WithOptions_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int count = canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_WithBounds_IncrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int count = canvas.SaveLayer(new GraphicsOptions(), new Rectangle(10, 10, 32, 32)); + Assert.Equal(2, count); + Assert.Equal(2, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_Restore_DecrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + canvas.SaveLayer(); + Assert.Equal(2, canvas.SaveCount); + + canvas.Restore(); + Assert.Equal(1, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_RestoreTo_DecrementsSaveCount() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + int before = canvas.SaveCount; + canvas.SaveLayer(); + canvas.Save(); + Assert.Equal(3, canvas.SaveCount); + + canvas.RestoreTo(before); + Assert.Equal(before, canvas.SaveCount); + } + + [Fact] + public void SaveLayer_DrawAndRestore_CompositesLayerOntoTarget() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + // Fill background white. + canvas.Fill(new SolidBrush(Color.White)); + + // SaveLayer with full opacity, draw red rectangle, then restore. + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Red), new RectangularPolygon(10, 10, 20, 20)); + canvas.Restore(); + } + + // The red rectangle should be composited onto the white background. + Rgba32 center = target[20, 20]; + Assert.Equal(new Rgba32(255, 0, 0, 255), center); + + // Outside the filled region should remain white. + Rgba32 corner = target[0, 0]; + Assert.Equal(new Rgba32(255, 255, 255, 255), corner); + } + + [Fact] + public void SaveLayer_WithHalfOpacity_CompositesWithBlend() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + // Fill background white. + canvas.Fill(new SolidBrush(Color.White)); + + // SaveLayer with 50% opacity, draw red rectangle, then restore. + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + canvas.Fill(new SolidBrush(Color.Red), new RectangularPolygon(10, 10, 20, 20)); + canvas.Restore(); + } + + // The red should be blended at ~50% onto white, giving approximately (255, 128, 128). + Rgba32 center = target[20, 20]; + Assert.InRange(center.R, 120, 255); + Assert.InRange(center.G, 100, 140); + Assert.InRange(center.B, 100, 140); + } + + [Fact] + public void SaveLayer_Dispose_CompositesActiveLayer() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + + // Create canvas, push a layer, draw, and dispose without explicit Restore. + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(new SolidBrush(Color.White)); + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Blue), new RectangularPolygon(0, 0, 32, 32)); + + // Dispose should composite the layer. + } + + // After dispose, the blue fill should be visible. + Rgba32 pixel = target[16, 16]; + Assert.Equal(new Rgba32(0, 0, 255, 255), pixel); + } + + [Fact] + public void SaveLayer_Dispose_UsesStoredLayerOptions() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image restoredTarget = new(64, 64); + using Image disposedTarget = new(64, 64); + + using (DrawingCanvas canvas = CreateCanvas(provider, restoredTarget, new DrawingOptions())) + { + canvas.Fill(new SolidBrush(Color.White)); + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + canvas.Fill(new SolidBrush(Color.Blue), new RectangularPolygon(0, 0, 32, 32)); + canvas.Restore(); + } + + using (DrawingCanvas canvas = CreateCanvas(provider, disposedTarget, new DrawingOptions())) + { + canvas.Fill(new SolidBrush(Color.White)); + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5f }); + canvas.Fill(new SolidBrush(Color.Blue), new RectangularPolygon(0, 0, 32, 32)); + } + + ImageComparer.Exact.VerifySimilarity(restoredTarget, disposedTarget); + } + + [Fact] + public void SaveLayer_NestedLayers_CompositeCorrectly() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(new SolidBrush(Color.White)); + + // Outer layer. + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Red), new RectangularPolygon(0, 0, 64, 64)); + + // Inner layer. + canvas.SaveLayer(); + canvas.Fill(new SolidBrush(Color.Blue), new RectangularPolygon(16, 16, 32, 32)); + canvas.Restore(); // Closes blue onto red. + + canvas.Restore(); // Closes red+blue onto white. + } + + // Center should be blue (inner layer overwrites outer). + Rgba32 center = target[32, 32]; + Assert.Equal(new Rgba32(0, 0, 255, 255), center); + + // Corner should be red (outer layer only). + Rgba32 corner = target[5, 5]; + Assert.Equal(new Rgba32(255, 0, 0, 255), corner); + } + + [Fact] + public void SaveLayer_MixedSaveAndSaveLayer_WorksCorrectly() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(new SolidBrush(Color.White)); + + canvas.Save(); // SaveCount = 2 (plain save) + canvas.SaveLayer(); // SaveCount = 3 (layer) + canvas.Save(); // SaveCount = 4 (plain save) + Assert.Equal(4, canvas.SaveCount); + + canvas.Fill(new SolidBrush(Color.Green), new RectangularPolygon(0, 0, 64, 64)); + + // RestoreTo(1) should pop all states including the layer. + canvas.RestoreTo(1); + Assert.Equal(1, canvas.SaveCount); + } + + Rgba32 pixel = target[32, 32]; + Assert.Equal(new Rgba32(0, 128, 0, 255), pixel); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs new file mode 100644 index 000000000..a6d0411ca --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(360, 220, PixelTypes.Rgba32)] + public void Draw_NormalizeOutputFalse_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + IPath leftPath = CreateBowTiePath(new RectangleF(28, 34, 128, 152)); + IPath rightPath = CreateBowTiePath(new RectangleF(204, 34, 128, 152)); + + SolidPen pen = new(Color.CornflowerBlue.WithAlpha(0.88F), 24F); + pen.StrokeOptions.LineJoin = LineJoin.Round; + pen.StrokeOptions.LineCap = LineCap.Round; + + DrawingOptions evenOddOptions = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F)), new Rectangle(12, 12, 336, 196)); + + _ = canvas.Save(evenOddOptions); + canvas.Draw(pen, leftPath); + canvas.Draw(pen, rightPath); + canvas.Restore(); + + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 2F), leftPath); + canvas.Draw(Pens.Solid(Color.DarkSlateGray, 2F), rightPath); + canvas.DrawLine(Pens.DashDot(Color.Gray, 2F), new PointF(180, 20), new PointF(180, 200)); + canvas.Draw(Pens.Solid(Color.Black, 2F), new Rectangle(8, 8, 344, 204)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0001F), provider, appendSourceFileOrDescription: false); + } + + private static IPath CreateBowTiePath(RectangleF bounds) + { + float left = bounds.Left; + float right = bounds.Right; + float top = bounds.Top; + float bottom = bounds.Bottom; + + PathBuilder builder = new(); + builder.AddLine(left, top, right, bottom); + builder.AddLine(right, bottom, left, bottom); + builder.AddLine(left, bottom, right, top); + builder.AddLine(right, top, left, top); + builder.CloseAllFigures(); + return builder.Build(); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs new file mode 100644 index 000000000..ed523dff9 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs @@ -0,0 +1,253 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithSolidFilledImages(492, 360, nameof(Color.White), PixelTypes.Rgba32, ColorFontSupport.ColrV1)] + [WithSolidFilledImages(492, 360, nameof(Color.White), PixelTypes.Rgba32, ColorFontSupport.Svg)] + public void DrawGlyphs_EmojiFont_MatchesReference(TestImageProvider provider, ColorFontSupport support) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.NotoColorEmojiRegular, 100); + Font fallback = TestFontUtilities.GetFont(TestFonts.OpenSans, 100); + const string text = "a😨 b😅\r\nc🥲 d🤩"; + + RichTextOptions textOptions = new(font) + { + ColorFontSupport = support, + LineSpacing = 1.8F, + FallbackFontFamilies = [fallback.Family], + TextRuns = + [ + new RichTextRun + { + Start = 0, + End = text.GetGraphemeCount(), + TextDecorations = TextDecorations.Strikeout | TextDecorations.Underline | TextDecorations.Overline + } + ] + }; + + IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, textOptions); + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawGlyphs(Brushes.Solid(Color.Black), Pens.Solid(Color.Black, 2F), glyphs); + canvas.Flush(); + + target.DebugSave(provider, $"{support}-draw-glyphs", appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, $"{support}-draw-glyphs", appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(760, 320, PixelTypes.Rgba32)] + public void DrawText_Multiline_WithLineMetricsGuides_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = Matrix4x4.CreateTranslation(24F, 22F, 0) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 32); + + string text = "Quick wafting zephyrs vex bold Jim.\n" + + "How quickly daft jumping zebras vex.\n" + + "Sphinx of black quartz, judge my vow."; + + RichTextOptions textOptions = new(font) + { + Origin = PointF.Empty, + LineSpacing = 1.45F + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.LightSteelBlue.WithAlpha(0.25F)), new Rectangle(0, 0, 712, 276)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + + LineMetrics[] lineMetrics = canvas.GetTextLineMetrics(textOptions, text); + float lineOriginY = textOptions.Origin.Y; + for (int i = 0; i < lineMetrics.Length; i++) + { + LineMetrics metrics = lineMetrics[i]; + float startX = metrics.Start; + float endX = metrics.Start + metrics.Extent; + float topY = lineOriginY; + float ascenderY = lineOriginY + metrics.Ascender; + float baselineY = lineOriginY + metrics.Baseline; + float descenderY = lineOriginY + metrics.Descender; + float lineHeightY = lineOriginY + metrics.LineHeight; + + canvas.DrawLine(Pens.Solid(Color.DimGray.WithAlpha(0.8F), 1), new PointF(startX, topY), new PointF(endX, topY)); + canvas.DrawLine(Pens.Solid(Color.RoyalBlue.WithAlpha(0.9F), 1), new PointF(startX, ascenderY), new PointF(endX, ascenderY)); + canvas.DrawLine(Pens.Solid(Color.Crimson.WithAlpha(0.9F), 1), new PointF(startX, baselineY), new PointF(endX, baselineY)); + canvas.DrawLine(Pens.Solid(Color.DarkOrange.WithAlpha(0.9F), 1), new PointF(startX, descenderY), new PointF(endX, descenderY)); + canvas.DrawLine(Pens.Solid(Color.SeaGreen.WithAlpha(0.9F), 1), new PointF(startX, lineHeightY), new PointF(endX, lineHeightY)); + canvas.DrawLine(Pens.Solid(Color.DimGray.WithAlpha(0.8F), 1), new PointF(startX, topY), new PointF(startX, lineHeightY)); + canvas.DrawLine(Pens.Solid(Color.DimGray.WithAlpha(0.8F), 1), new PointF(endX, topY), new PointF(endX, lineHeightY)); + + lineOriginY += metrics.LineHeight; + } + + canvas.Draw(Pens.Solid(Color.Black, 2), new Rectangle(0, 0, 712, 276)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(420, 220, PixelTypes.Rgba32)] + public void DrawText_FillAndStroke_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation(-0.08F, new Vector2(210, 110))) + }; + + using DrawingCanvas canvas = CreateCanvas(provider, target, options); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 36); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(24, 36), + WrappingLength = 372 + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText( + textOptions, + "Canvas text\nwith fill + stroke", + Brushes.Solid(Color.MidnightBlue.WithAlpha(0.82F)), + Pens.Solid(Color.Gold, 2F)); + canvas.Draw(Pens.Solid(Color.DimGray, 3), new Rectangle(10, 10, 400, 200)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(320, 180, PixelTypes.Rgba32)] + public void DrawText_PenOnly_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 52); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(18, 42) + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.LightSkyBlue.WithAlpha(0.45F)), new Rectangle(12, 14, 296, 152)); + canvas.DrawText(textOptions, "OUTLINE", brush: null, pen: Pens.Solid(Color.SeaGreen, 3.5F)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(360, 220, PixelTypes.Rgba32)] + public void DrawText_AlongPathWithOrigin_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + IPath textPath = new EllipsePolygon(new PointF(172, 112), new SizeF(246, 112)); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 21); + RichTextOptions textOptions = new(font) + { + Path = textPath, + Origin = new PointF(16, -10), + WrappingLength = textPath.ComputeLength(), + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Bottom + }; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.SlateGray, 2), textPath); + canvas.DrawText( + textOptions, + "Sphinx of black quartz, judge my vow.", + Brushes.Solid(Color.DarkRed.WithAlpha(0.9F)), + pen: null); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(840, 420, PixelTypes.Rgba32)] + public void DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 28); + Rectangle layoutBounds = new(120, 50, 600, 320); + + RichTextOptions textOptions = new(font) + { + Origin = new PointF( + layoutBounds.Left + (layoutBounds.Width / 2F), + layoutBounds.Top + (layoutBounds.Height / 2F)), + WrappingLength = layoutBounds.Width - 64F, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + LineSpacing = 2.1F + }; + + string text = + "Pack my box with five dozen liquor jugs while zephyrs drift across the bay.\n" + + "Sphinx of black quartz, judge my vow."; + + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.LightGoldenrodYellow.WithAlpha(0.45F)), layoutBounds); + canvas.Draw(Pens.Solid(Color.SlateGray, 2F), layoutBounds); + canvas.DrawLine( + Pens.Dash(Color.Gray.WithAlpha(0.8F), 1.5F), + new PointF(textOptions.Origin.X, layoutBounds.Top), + new PointF(textOptions.Origin.X, layoutBounds.Bottom)); + canvas.DrawLine( + Pens.Dash(Color.Gray.WithAlpha(0.8F), 1.5F), + new PointF(layoutBounds.Left, textOptions.Origin.Y), + new PointF(layoutBounds.Right, textOptions.Origin.Y)); + + canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.DarkBlue.WithAlpha(0.86F)), + Pens.Solid(Color.DarkRed.WithAlpha(0.55F), 1.1F)); + + canvas.Draw(Pens.Solid(Color.Black, 3F), new Rectangle(10, 10, 820, 400)); + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs new file mode 100644 index 000000000..15f69e270 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.TextMeasuring.cs @@ -0,0 +1,285 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class DrawingCanvasTests +{ + [Theory] + [WithBlankImage(600, 400, PixelTypes.Rgba32)] + public void TextMeasuring_RenderedMetrics_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 36); + const string text = "Sphinx of black quartz,\njudge my vow."; + + RichTextOptions textOptions = new(font) + { + Origin = new PointF(60, 60), + LineSpacing = 1.8F + }; + + canvas.Clear(Brushes.Solid(Color.White)); + + PointF origin = textOptions.Origin; + + // Line metrics: colored bands with ascender/baseline/descender guides. + int lineCount = canvas.CountTextLines(textOptions, text); + LineMetrics[] lineMetrics = canvas.GetTextLineMetrics(textOptions, text); + Assert.Equal(lineCount, lineMetrics.Length); + + float lineOriginY = origin.Y; + Color[] bandColors = + [ + Color.LightCoral.WithAlpha(0.4F), + Color.Khaki.WithAlpha(0.6F), + Color.LightGreen.WithAlpha(0.4F), + ]; + + for (int i = 0; i < lineMetrics.Length; i++) + { + LineMetrics metrics = lineMetrics[i]; + float startX = origin.X + metrics.Start; + float endX = startX + metrics.Extent; + + canvas.Fill( + Brushes.Solid(bandColors[i % bandColors.Length]), + new RectangularPolygon(startX, lineOriginY, endX - startX, metrics.LineHeight)); + + canvas.DrawLine( + Pens.Solid(Color.Teal.WithAlpha(0.9F), 1.5F), + new PointF(startX, lineOriginY + metrics.Ascender), + new PointF(endX, lineOriginY + metrics.Ascender)); + + canvas.DrawLine( + Pens.Solid(Color.Crimson.WithAlpha(0.9F), 1.5F), + new PointF(startX, lineOriginY + metrics.Baseline), + new PointF(endX, lineOriginY + metrics.Baseline)); + + canvas.DrawLine( + Pens.Solid(Color.DarkOrange.WithAlpha(0.9F), 1.5F), + new PointF(startX, lineOriginY + metrics.Descender), + new PointF(endX, lineOriginY + metrics.Descender)); + + lineOriginY += metrics.LineHeight; + } + + // Character renderable bounds: outlined rectangles positioned at each glyph. + if (canvas.TryMeasureCharacterRenderableBounds(textOptions, text, out ReadOnlySpan charRenderableBounds)) + { + Color[] renderableColors = + [ + Color.Black, + Color.Black + ]; + + for (int i = 0; i < charRenderableBounds.Length; i++) + { + FontRectangle rb = charRenderableBounds[i].Bounds; + canvas.Draw( + Pens.Solid(renderableColors[i % renderableColors.Length], 1), + new RectangularPolygon(rb.X, rb.Y, rb.Width, rb.Height)); + } + } + + // Character bounds: alternating filled rectangles behind the glyphs. + if (canvas.TryMeasureCharacterBounds(textOptions, text, out ReadOnlySpan charBounds)) + { + Color[] charColors = + [ + Color.Gold.WithAlpha(0.5F), + Color.MediumPurple.WithAlpha(0.5F), + ]; + + for (int i = 0; i < charBounds.Length; i++) + { + FontRectangle b = charBounds[i].Bounds; + canvas.Fill( + Brushes.Solid(charColors[i % charColors.Length]), + new RectangularPolygon(b.X, b.Y, b.Width, b.Height)); + } + } + + // Render the text. + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + + // Advance rectangle (green outline). + RectangleF advance = canvas.MeasureTextAdvance(textOptions, text); + canvas.Draw( + Pens.Solid(Color.SeaGreen, 2), + new RectangularPolygon(origin.X + advance.X, origin.Y + advance.Y, advance.Width, advance.Height)); + + // Bounds rectangle (dodger blue outline). + RectangleF bounds = canvas.MeasureTextBounds(textOptions, text); + canvas.Draw( + Pens.Solid(Color.DodgerBlue, 2), + new RectangularPolygon(bounds.X, bounds.Y, bounds.Width, bounds.Height)); + + // Renderable bounds rectangle (black outline). + RectangleF renderableBounds = canvas.MeasureTextRenderableBounds(textOptions, text); + canvas.Draw( + Pens.Solid(Color.Black, 2), + new RectangularPolygon(renderableBounds.X, renderableBounds.Y, renderableBounds.Width, renderableBounds.Height)); + + // Origin crosshair. + canvas.DrawLine(Pens.Solid(Color.Gray, 1), new PointF(origin.X - 12, origin.Y), new PointF(origin.X + 12, origin.Y)); + canvas.DrawLine(Pens.Solid(Color.Gray, 1), new PointF(origin.X, origin.Y - 12), new PointF(origin.X, origin.Y + 12)); + + // Key. + Font keyFont = TestFontUtilities.GetFont(TestFonts.OpenSans, 13); + float keyX = 16; + float keyY = 280; + const float swatchW = 24; + const float swatchH = 12; + const float rowHeight = 20; + const float labelOffset = swatchW + 6; + + (string Label, Color Color1, Color? Color2, bool IsFill)[] keyEntries = + [ + ("Advance", Color.SeaGreen, null, false), + ("Bounds", Color.DodgerBlue, null, false), + ("Renderable Bounds", Color.Black, null, false), + ("Ascender", Color.Teal.WithAlpha(0.9F), null, true), + ("Baseline", Color.Crimson.WithAlpha(0.9F), null, true), + ("Descender", Color.DarkOrange.WithAlpha(0.9F), null, true), + ("Char Bounds", Color.Gold.WithAlpha(0.5F), Color.MediumPurple.WithAlpha(0.5F), true), + ("Char Renderable Bounds", Color.Black, null, false), + ("Line Band", Color.LightCoral.WithAlpha(0.4F), Color.Khaki.WithAlpha(0.6F), true), + ("Origin", Color.Gray, null, false), + ]; + + for (int i = 0; i < keyEntries.Length; i++) + { + float col = i < 5 ? 0 : 300; + float row = i < 5 ? i : i - 5; + float x = keyX + col; + float y = keyY + (row * rowHeight); + float halfW = swatchW / 2F; + + if (keyEntries[i].IsFill) + { + if (keyEntries[i].Color2 is Color c2) + { + canvas.Fill( + Brushes.Solid(keyEntries[i].Color1), + new RectangularPolygon(x, y, halfW, swatchH)); + canvas.Fill( + Brushes.Solid(c2), + new RectangularPolygon(x + halfW, y, halfW, swatchH)); + } + else + { + canvas.Fill( + Brushes.Solid(keyEntries[i].Color1), + new RectangularPolygon(x, y, swatchW, swatchH)); + } + } + else + { + canvas.Draw( + Pens.Solid(keyEntries[i].Color1, 2), + new RectangularPolygon(x, y, swatchW, swatchH)); + } + + RichTextOptions keyTextOptions = new(keyFont) { Origin = new PointF(x + labelOffset, y - 1) }; + canvas.DrawText(keyTextOptions, keyEntries[i].Label, Brushes.Solid(Color.Black), pen: null); + } + + canvas.Flush(); + + target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Fact] + public void MeasureTextSize_ReturnsNonEmptyRectangle() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + RectangleF size = canvas.MeasureTextSize(textOptions, "Hello"); + + Assert.True(size.Width > 0, "Width should be positive."); + Assert.True(size.Height > 0, "Height should be positive."); + } + + [Fact] + public void MeasureTextSize_EmptyText_ReturnsEmpty() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + RectangleF size = canvas.MeasureTextSize(textOptions, ReadOnlySpan.Empty); + + Assert.Equal(RectangleF.Empty, size); + } + + [Fact] + public void MeasureTextSize_LongerText_IsWider() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + RectangleF shortSize = canvas.MeasureTextSize(textOptions, "Hi"); + RectangleF longSize = canvas.MeasureTextSize(textOptions, "Hello World"); + + Assert.True(longSize.Width > shortSize.Width, "Longer text should produce a wider measurement."); + } + + [Fact] + public void TryMeasureCharacterAdvances_ReturnsAdvancesForEachCharacter() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + const string text = "ABC"; + bool result = canvas.TryMeasureCharacterAdvances(textOptions, text, out ReadOnlySpan advances); + + Assert.True(result); + Assert.Equal(text.Length, advances.Length); + + for (int i = 0; i < advances.Length; i++) + { + Assert.True(advances[i].Bounds.Width > 0, $"Advance width for character {i} should be positive."); + } + } + + [Fact] + public void TryMeasureCharacterAdvances_EmptyText_ReturnsFalse() + { + TestImageProvider provider = TestImageProvider.Blank(1, 1); + using Image target = new(64, 64); + using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); + + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + RichTextOptions textOptions = new(font) { Origin = new PointF(0, 0) }; + + bool result = canvas.TryMeasureCharacterAdvances(textOptions, ReadOnlySpan.Empty, out ReadOnlySpan advances); + + Assert.False(result); + Assert.True(advances.IsEmpty); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.cs new file mode 100644 index 000000000..262c9f759 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.cs @@ -0,0 +1,51 @@ +// 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.Memory; +using SixLabors.ImageSharp.PixelFormats; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +[GroupOutput("Drawing")] +public partial class DrawingCanvasTests +{ + private readonly ITestOutputHelper output; + + public DrawingCanvasTests(ITestOutputHelper output) + { + this.output = output; + } + + private static DrawingCanvas CreateCanvas( + TestImageProvider provider, + Image image, + DrawingOptions options) + where TPixel : unmanaged, IPixel + => new( + provider.Configuration, + image.Frames.RootFrame.PixelBuffer.GetRegion(), + options); + + private static PathBuilder CreateClosedPathBuilder() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(22, 24, 124, 30); + pathBuilder.AddLine(124, 30, 168, 98); + pathBuilder.AddLine(168, 98, 40, 108); + pathBuilder.AddLine(40, 108, 22, 24); + pathBuilder.CloseAllFigures(); + return pathBuilder; + } + + private static PathBuilder CreateOpenPathBuilder() + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(20, 98, 54, 22); + pathBuilder.AddLine(54, 22, 114, 76); + pathBuilder.AddLine(114, 76, 170, 26); + return pathBuilder; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/FillParisTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/FillParisTests.cs new file mode 100644 index 000000000..769d5d39c --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/FillParisTests.cs @@ -0,0 +1,110 @@ +// 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.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class FillParisTests +{ + private const float Scale = 1f; + private const int Width = 1096; + private const int Height = 1060; + + private static readonly string SvgFilePath = + TestFile.GetInputFileFullPath(TestImages.Svg.Paris30k); + + private static readonly List elements = SvgBenchmarkHelper.ParseSvg(SvgFilePath); + private static readonly List<(IPath Path, SolidBrush Fill, SolidPen Stroke)> isElements = + SvgBenchmarkHelper.BuildImageSharpElements(elements, Scale); + + [Fact] + public void FillParis_ImageSharp_CPU() + { + using Image image = new(Width, Height); + image.Mutate(c => c.ProcessWithCanvas(canvas => + { + foreach ((IPath path, SolidBrush fill, SolidPen stroke) in isElements) + { + if (fill is not null) + { + canvas.Fill(fill, path); + } + + if (stroke is not null) + { + canvas.Draw(stroke, path); + } + } + })); + } + + [WebGPUFact] + public void FillParis_ImageSharp_WebGPU() + { + using FillParisWebGpuContext webGpu = new(); + using DrawingCanvas canvas = new(webGpu.Configuration, webGpu.NativeFrame, new DrawingOptions()); + + foreach ((IPath path, SolidBrush fill, SolidPen stroke) in isElements) + { + if (fill is not null) + { + canvas.Fill(fill, path); + } + + if (stroke is not null) + { + canvas.Draw(stroke, path); + } + } + + canvas.Flush(); + + Assert.True( + webGpu.Backend.DiagnosticLastFlushUsedGPU, + webGpu.Backend.DiagnosticLastSceneFailure ?? "The last flush did not use the staged path."); + } + + private sealed class FillParisWebGpuContext : IDisposable + { + private readonly nint textureHandle; + private readonly nint textureViewHandle; + + public FillParisWebGpuContext() + { + this.Backend = new WebGPUDrawingBackend(); + this.Configuration = Configuration.Default.Clone(); + this.Configuration.SetDrawingBackend(this.Backend); + + Assert.True( + WebGPUTestNativeSurfaceAllocator.TryCreate( + Width, + Height, + out NativeSurface nativeSurface, + out this.textureHandle, + out this.textureViewHandle, + out string createError), + createError); + + this.NativeFrame = new NativeCanvasFrame( + new Rectangle(0, 0, Width, Height), + nativeSurface); + } + + public WebGPUDrawingBackend Backend { get; } + + public Configuration Configuration { get; } + + public NativeCanvasFrame NativeFrame { get; } + + public void Dispose() + { + WebGPUTestNativeSurfaceAllocator.Release(this.textureHandle, this.textureViewHandle); + this.Backend.Dispose(); + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs deleted file mode 100644 index 0d49fa0e4..000000000 --- a/tests/ImageSharp.Drawing.Tests/Processing/FillPathProcessorTests.cs +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Reflection; -using Moq; -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; - -namespace SixLabors.ImageSharp.Drawing.Tests.Processing; - -public class FillPathProcessorTests -{ - [Fact] - public void FillOffCanvas() - { - Rectangle bounds = new(-100, -10, 10, 10); - - // Specifically not using RectangularPolygon here to ensure the FillPathProcessor is used. - LinearLineSegment[] points = - [ - new(new PointF(bounds.Left, bounds.Top), new PointF(bounds.Right, bounds.Top)), - new(new PointF(bounds.Right, bounds.Top), new PointF(bounds.Right, bounds.Bottom)), - new(new PointF(bounds.Right, bounds.Bottom), new PointF(bounds.Left, bounds.Bottom)), - new(new PointF(bounds.Left, bounds.Bottom), new PointF(bounds.Left, bounds.Top)) - ]; - Path path = new(points); - Mock brush = new(); - GraphicsOptions options = new() { Antialias = true }; - FillPathProcessor processor = new(new DrawingOptions() { GraphicsOptions = options }, brush.Object, path); - Image img = new(10, 10); - processor.Execute(img.Configuration, img, bounds); - } - - [Fact] - public void DrawOffCanvas() - { - using (Image img = new(10, 10)) - { - img.Mutate(x => x.DrawLine( - new SolidPen(Color.Black, 10), - new Vector2(-10, 5), - new Vector2(20, 5))); - } - } - - [Fact] - public void OtherShape() - { - Rectangle imageSize = new(0, 0, 500, 500); - EllipsePolygon path = new(1, 1, 23); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = true } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - - Assert.IsType>(pixelProcessor); - } - - [Fact] - public void RectangleFloatAndAntialias() - { - Rectangle imageSize = new(0, 0, 500, 500); - RectangleF floatRect = new(10.5f, 10.5f, 400.6f, 400.9f); - Rectangle expectedRect = new(10, 10, 400, 400); - RectangularPolygon path = new(floatRect); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = true } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - - Assert.IsType>(pixelProcessor); - } - - [Fact] - public void IntRectangle() - { - Rectangle imageSize = new(0, 0, 500, 500); - Rectangle expectedRect = new(10, 10, 400, 400); - RectangularPolygon path = new(expectedRect); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = true } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - - FillProcessor fill = Assert.IsType>(pixelProcessor); - Assert.Equal(expectedRect, fill.GetProtectedValue("SourceRectangle")); - } - - [Fact] - public void FloatRectAntialiasingOff() - { - Rectangle imageSize = new(0, 0, 500, 500); - RectangleF floatRect = new(10.5f, 10.5f, 400.6f, 400.9f); - Rectangle expectedRect = new(10, 10, 400, 400); - RectangularPolygon path = new(floatRect); - FillPathProcessor processor = new( - new DrawingOptions() - { - GraphicsOptions = { Antialias = false } - }, - Brushes.Solid(Color.Red), - path); - - IImageProcessor pixelProcessor = processor.CreatePixelSpecificProcessor(null, null, imageSize); - FillProcessor fill = Assert.IsType>(pixelProcessor); - - Assert.Equal(expectedRect, fill.GetProtectedValue("SourceRectangle")); - } - - [Fact] - public void DoesNotThrowForIssue928() - { - RectangleF rectText = new(0, 0, 2000, 2000); - using (Image img = new((int)rectText.Width, (int)rectText.Height)) - { - img.Mutate(x => x.Fill(Color.Transparent)); - - img.Mutate( - ctx => ctx.DrawLine( - Color.Red, - 0.984252f, - new PointF(104.762581f, 1074.99365f), - new PointF(104.758667f, 1075.01721f), - new PointF(104.757675f, 1075.04114f), - new PointF(104.759628f, 1075.065f), - new PointF(104.764488f, 1075.08838f), - new PointF(104.772186f, 1075.111f), - new PointF(104.782608f, 1075.13245f), - new PointF(104.782608f, 1075.13245f))); - } - } - - [Fact] - public void DoesNotThrowFillingTriangle() - { - using (Image image = new(28, 28)) - { - Polygon path = new( - new LinearLineSegment(new PointF(17.11f, 13.99659f), new PointF(14.01433f, 27.06201f)), - new LinearLineSegment(new PointF(14.01433f, 27.06201f), new PointF(13.79267f, 14.00023f)), - new LinearLineSegment(new PointF(13.79267f, 14.00023f), new PointF(17.11f, 13.99659f))); - - 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 -{ - internal static T GetProtectedValue(this object obj, string name) - => (T)obj.GetType() - .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/ImageOperationTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ImageOperationTests.cs index fb141083f..a3d68f075 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ImageOperationTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ImageOperationTests.cs @@ -108,7 +108,11 @@ public void ApplyProcessors_ListOfProcessors_AppliesAllProcessorsToOperation() Assert.Contains(this.processorDefinition, operations.Applied.Select(x => x.NonGenericProcessor)); } - public void Dispose() => this.image.Dispose(); + public void Dispose() + { + this.image.Dispose(); + GC.SuppressFinalize(this); + } [Fact] public void GenericMutate_WhenDisposed_Throws() diff --git a/tests/ImageSharp.Drawing.Tests/Processing/PenTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/PenTests.cs new file mode 100644 index 000000000..2e52b5fad --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/PenTests.cs @@ -0,0 +1,324 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class PenTests +{ + // Constructor / property tests + [Fact] + public void SolidPen_ColorConstructor_SetsProperties() + { + SolidPen pen = new(Color.Red); + + Assert.Equal(1, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_ColorWidthConstructor_SetsProperties() + { + SolidPen pen = new(Color.Blue, 5); + + Assert.Equal(5, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_BrushConstructor_SetsProperties() + { + Brush brush = Brushes.Solid(Color.Green); + SolidPen pen = new(brush); + + Assert.Equal(1, pen.StrokeWidth); + Assert.Same(brush, pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_BrushWidthConstructor_SetsProperties() + { + Brush brush = Brushes.Solid(Color.Green); + SolidPen pen = new(brush, 7.5F); + + Assert.Equal(7.5F, pen.StrokeWidth); + Assert.Same(brush, pen.StrokeFill); + Assert.True(pen.StrokePattern.IsEmpty); + } + + [Fact] + public void SolidPen_PenOptionsConstructor_SetsProperties() + { + PenOptions options = new(Color.Coral, 4) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round } + }; + + SolidPen pen = new(options); + + Assert.Equal(4, pen.StrokeWidth); + Assert.Equal(LineJoin.Round, pen.StrokeOptions.LineJoin); + } + + [Fact] + public void PatternPen_ColorPatternConstructor_SetsProperties() + { + float[] pattern = [3f, 1f]; + PatternPen pen = new(Color.Black, pattern); + + Assert.Equal(1, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void PatternPen_ColorWidthPatternConstructor_SetsProperties() + { + float[] pattern = [2f, 1f, 1f, 1f]; + PatternPen pen = new(Color.Navy, 3, pattern); + + Assert.Equal(3, pen.StrokeWidth); + Assert.IsType(pen.StrokeFill); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void PatternPen_BrushWidthPatternConstructor_SetsProperties() + { + Brush brush = Brushes.Solid(Color.Teal); + float[] pattern = [1f, 1f]; + PatternPen pen = new(brush, 2.5F, pattern); + + Assert.Equal(2.5F, pen.StrokeWidth); + Assert.Same(brush, pen.StrokeFill); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void PatternPen_PenOptionsConstructor_SetsProperties() + { + float[] pattern = [3f, 1f, 1f, 1f]; + PenOptions options = new(Color.Red, 6, pattern) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Round } + }; + + PatternPen pen = new(options); + + Assert.Equal(6, pen.StrokeWidth); + Assert.Equal(LineCap.Round, pen.StrokeOptions.LineCap); + Assert.True(pen.StrokePattern.Span.SequenceEqual(pattern)); + } + + [Fact] + public void SolidPen_DefaultStrokeOptions_UsesDefaults() + { + SolidPen pen = new(Color.Black, 2); + + Assert.Equal(LineJoin.Bevel, pen.StrokeOptions.LineJoin); + Assert.Equal(LineCap.Butt, pen.StrokeOptions.LineCap); + Assert.Equal(InnerJoin.Miter, pen.StrokeOptions.InnerJoin); + Assert.Equal(4D, pen.StrokeOptions.MiterLimit); + } + + [Fact] + public void Pens_Dash_HasExpectedPattern() + { + PatternPen pen = Pens.Dash(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual([3f, 1f])); + } + + [Fact] + public void Pens_Dot_HasExpectedPattern() + { + PatternPen pen = Pens.Dot(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual([1f, 1f])); + } + + [Fact] + public void Pens_DashDot_HasExpectedPattern() + { + PatternPen pen = Pens.DashDot(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual([3f, 1f, 1f, 1f])); + } + + [Fact] + public void Pens_DashDotDot_HasExpectedPattern() + { + PatternPen pen = Pens.DashDotDot(Color.Black, 1); + + Assert.True(pen.StrokePattern.Span.SequenceEqual([3f, 1f, 1f, 1f, 1f, 1f])); + } + + // Equality tests + [Fact] + public void SolidPen_Equal_WhenSameColorAndWidth() + { + SolidPen a = Pens.Solid(Color.Red, 3); + SolidPen b = Pens.Solid(Color.Red, 3); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void SolidPen_NotEqual_WhenDifferentColor() + { + SolidPen a = Pens.Solid(Color.Red, 3); + SolidPen b = Pens.Solid(Color.Blue, 3); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void SolidPen_NotEqual_WhenDifferentWidth() + { + SolidPen a = Pens.Solid(Color.Red, 3); + SolidPen b = Pens.Solid(Color.Red, 5); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_Equal_WhenSamePattern() + { + PatternPen a = Pens.Dash(Color.Black, 2); + PatternPen b = Pens.Dash(Color.Black, 2); + + Assert.True(a.Equals(b)); + Assert.True(b.Equals(a)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void PatternPen_NotEqual_WhenDifferentPattern() + { + PatternPen a = Pens.Dash(Color.Black, 2); + PatternPen b = Pens.Dot(Color.Black, 2); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_NotEqual_WhenDifferentColor() + { + PatternPen a = Pens.Dash(Color.Red, 2); + PatternPen b = Pens.Dash(Color.Green, 2); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_NotEqual_WhenDifferentWidth() + { + PatternPen a = Pens.Dash(Color.Black, 2); + PatternPen b = Pens.Dash(Color.Black, 4); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void DashDot_Equal_WhenSameParameters() + { + PatternPen a = Pens.DashDot(Color.Navy, 3); + PatternPen b = Pens.DashDot(Color.Navy, 3); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void DashDotDot_Equal_WhenSameParameters() + { + PatternPen a = Pens.DashDotDot(Color.Teal, 1.5F); + PatternPen b = Pens.DashDotDot(Color.Teal, 1.5F); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void DashDot_NotEqual_ToDashDotDot() + { + PatternPen a = Pens.DashDot(Color.Black, 2); + PatternPen b = Pens.DashDotDot(Color.Black, 2); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void SolidPen_NotEqual_ToPatternPen() + { + SolidPen solid = Pens.Solid(Color.Black, 2); + PatternPen pattern = Pens.Dash(Color.Black, 2); + + Assert.False(solid.Equals(pattern)); + Assert.False(pattern.Equals(solid)); + } + + [Fact] + public void PatternPen_CustomPattern_Equal_WhenSameValues() + { + float[] pattern = [2f, 1f, 0.5f, 1f]; + PatternPen a = new(Color.Red, 3, pattern); + PatternPen b = new(Color.Red, 3, pattern); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void PatternPen_CustomPattern_NotEqual_WhenDifferentValues() + { + PatternPen a = new(Color.Red, 3, [2f, 1f, 0.5f, 1f]); + PatternPen b = new(Color.Red, 3, [2f, 1f, 1f, 1f]); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void PatternPen_CustomPattern_NotEqual_WhenDifferentLength() + { + PatternPen a = new(Color.Red, 3, [2f, 1f]); + PatternPen b = new(Color.Red, 3, [2f, 1f, 1f]); + + Assert.False(a.Equals(b)); + } + + [Fact] + public void Pen_Equals_Null_ReturnsFalse() + { + PatternPen pen = Pens.Dash(Color.Black, 2); + + Assert.False(pen.Equals(null)); + Assert.False(pen.Equals((object)null)); + } + + [Fact] + public void Pen_Equals_Object_WhenSame() + { + PatternPen a = Pens.Dash(Color.Black, 2); + PatternPen b = Pens.Dash(Color.Black, 2); + + Assert.True(a.Equals((object)b)); + } + + [Fact] + public void PatternPen_WithBrush_Equal_WhenSameBrushAndPattern() + { + Brush brush = Brushes.Solid(Color.Coral); + PatternPen a = Pens.Dash(brush, 4); + PatternPen b = Pens.Dash(brush, 4); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs new file mode 100644 index 000000000..2d562e01b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithCanvasExtensionsTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class ProcessWithCanvasExtensionsTests +{ + [Fact] + public void ProcessWithCanvas_Mutate_AppliesToAllFrames() + { + using Image image = new(24, 16); + image.Frames.AddFrame(image.Frames.RootFrame); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.OrangeRed)))); + + Assert.Equal(Color.OrangeRed.ToPixel(), image.Frames.RootFrame[8, 6]); + Assert.Equal(Color.OrangeRed.ToPixel(), image.Frames[1][8, 6]); + } + + [Fact] + public void ProcessWithCanvas_Clone_AppliesToAllFrames_WithoutMutatingSource() + { + using Image source = new(24, 16); + source.Frames.AddFrame(source.Frames.RootFrame); + source.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.White)))); + + using Image clone = source.Clone( + ctx => ctx.ProcessWithCanvas(canvas => canvas.Clear(Brushes.Solid(Color.MediumPurple)))); + + Assert.Equal(Color.White.ToPixel(), source.Frames.RootFrame[8, 6]); + Assert.Equal(Color.White.ToPixel(), source.Frames[1][8, 6]); + Assert.Equal(Color.MediumPurple.ToPixel(), clone.Frames.RootFrame[8, 6]); + Assert.Equal(Color.MediumPurple.ToPixel(), clone.Frames[1][8, 6]); + } + + [Fact] + public void ProcessWithCanvas_Mutate_DrawImage_AppliesToAllFrames() + { + using Image image = new(24, 16); + image.Frames.AddFrame(image.Frames.RootFrame); + + using Image source = new(8, 8, Color.HotPink.ToPixel()); + + Rectangle sourceRect = new(2, 1, 4, 5); + RectangleF destinationRect = new(6, 4, 10, 6); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawImage(source, sourceRect, destinationRect); + })); + + Rgba32 expectedFill = Color.HotPink.ToPixel(); + Rgba32 expectedBackground = Color.White.ToPixel(); + + Assert.Equal(expectedFill, image.Frames.RootFrame[10, 6]); + Assert.Equal(expectedFill, image.Frames[1][10, 6]); + Assert.Equal(expectedBackground, image.Frames.RootFrame[1, 1]); + Assert.Equal(expectedBackground, image.Frames[1][1, 1]); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.cs new file mode 100644 index 000000000..360d02740 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AntialiasThreshold.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.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_AliasedWithDefaultThreshold(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + DrawingOptions options = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); + + int whitePixels = CountPixelsAbove(image, 250); + int partialPixels = CountPixelsBetween(image, 1, 250); + + // Aliased mode should produce no partial-coverage pixels. + Assert.Equal(0, partialPixels); + Assert.True(whitePixels > 0, "Expected some white pixels from the filled circle."); + } + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_AliasedLowThreshold_ProducesMorePixelsThanHighThreshold(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + + DrawingOptions lowOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.1F } + }; + + DrawingOptions highOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false, AntialiasThreshold = 0.9F } + }; + + using Image lowImage = provider.GetImage(); + lowImage.Mutate(ctx => ctx.ProcessWithCanvas(lowOptions, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); + int lowCount = CountPixelsAbove(lowImage, 250); + + using Image highImage = provider.GetImage(); + highImage.Mutate(ctx => ctx.ProcessWithCanvas(highOptions, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); + int highCount = CountPixelsAbove(highImage, 250); + + // A lower threshold includes more edge pixels, so the fill area should be larger. + Assert.True(lowCount > highCount, $"Low threshold ({lowCount} pixels) should produce more pixels than high threshold ({highCount} pixels)."); + } + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32)] + public void Fill_AntialiasedIgnoresThreshold(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + EllipsePolygon circle = new(50, 50, 40); + + DrawingOptions options1 = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true, AntialiasThreshold = 0.1F } + }; + + DrawingOptions options2 = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true, AntialiasThreshold = 0.9F } + }; + + using Image image1 = provider.GetImage(); + image1.Mutate(ctx => ctx.ProcessWithCanvas(options1, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); + + using Image image2 = provider.GetImage(); + image2.Mutate(ctx => ctx.ProcessWithCanvas(options2, canvas => canvas.Fill(Brushes.Solid(Color.White), circle))); + + // In antialiased mode the threshold is irrelevant; images should be identical. + ImageComparer.Exact.VerifySimilarity(image1, image2); + } + + private static int CountPixelsAbove(Image image, byte threshold) + where TPixel : unmanaged, IPixel + { + int count = 0; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + Rgba32 rgba = row[x].ToRgba32(); + if (rgba.R > threshold) + { + count++; + } + } + } + }); + + return count; + } + + private static int CountPixelsBetween(Image image, byte low, byte high) + where TPixel : unmanaged, IPixel + { + int count = 0; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + Rgba32 rgba = row[x].ToRgba32(); + if (rgba.R >= low && rgba.R < high) + { + count++; + } + } + } + }); + + return count; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs new file mode 100644 index 000000000..be13d9094 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Blending.cs @@ -0,0 +1,190 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static IEnumerable BlendingsModes { get; } = GetAllModeCombinations(); + + private static IEnumerable GetAllModeCombinations() + { + foreach (object composition in Enum.GetValues(typeof(PixelAlphaCompositionMode))) + { + foreach (object blending in Enum.GetValues(typeof(PixelColorBlendingMode))) + { + yield return [blending, composition]; + } + } + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendHotPinkRect( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + int scaleX = image.Width / 100; + int scaleY = image.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)); + })); + + VerifyImage(provider, blending, composition, image); + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + int scaleX = image.Width / 100; + int scaleY = image.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(20 * scaleX, 0 * scaleY, 30 * scaleX, 100 * scaleY)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(Brushes.Solid(Color.Transparent), new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)); + })); + + VerifyImage(provider, blending, composition, image); + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + int scaleX = image.Width / 100; + int scaleY = image.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + Color transparentRed = Color.Red.WithAlpha(0.5F); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + // Keep legacy shape coordinates identical to the original test. + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40, 100 * scaleX, 20 * scaleY)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(Brushes.Solid(Color.HotPink), new Rectangle(20 * scaleX, 0, 30 * scaleX, 100 * scaleY)); + })); + + image.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.Fill(Brushes.Solid(transparentRed), new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)); + })); + + VerifyImage(provider, blending, composition, image); + } + + [Theory] + [WithBlankImage(nameof(BlendingsModes), 250, 250, PixelTypes.Rgba32)] + public void BlendingsDarkBlueRectBlendBlackEllipse( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + where TPixel : unmanaged, IPixel + { + using Image destinationImage = provider.GetImage(); + using Image sourceImage = provider.GetImage(); + + int scaleX = destinationImage.Width / 100; + int scaleY = destinationImage.Height / 100; + + DrawingOptions blendOptions = CreateBlendOptions(blending, composition); + + destinationImage.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.DarkBlue), new Rectangle(0 * scaleX, 40 * scaleY, 100 * scaleX, 20 * scaleY)); + })); + + sourceImage.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.Black), new EllipsePolygon(40 * scaleX, 50 * scaleY, 50 * scaleX, 50 * scaleY)); + })); + + destinationImage.Mutate(ctx => ctx.ProcessWithCanvas(blendOptions, canvas => + { + canvas.DrawImage( + sourceImage, + sourceImage.Bounds, + new RectangleF(0, 0, destinationImage.Width, destinationImage.Height)); + })); + + VerifyImage(provider, blending, composition, destinationImage); + } + + private static DrawingOptions CreateBlendOptions( + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) => + new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + ColorBlendingMode = blending, + AlphaCompositionMode = composition + } + }; + + private static void VerifyImage( + TestImageProvider provider, + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition, + Image image) + where TPixel : unmanaged, IPixel + { + image.DebugSave( + provider, + new { composition, blending }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + ImageComparer comparer = ImageComparer.TolerantPercentage(0.01F, 3); + image.CompareFirstFrameToReferenceOutput( + comparer, + provider, + new { composition, blending }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs new file mode 100644 index 000000000..c837a76c9 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clear.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithBlankImage(1, 1, PixelTypes.Rgba32)] + [WithBlankImage(7, 4, PixelTypes.Rgba32)] + [WithBlankImage(16, 7, PixelTypes.Rgba32)] + [WithBlankImage(33, 32, PixelTypes.Rgba32)] + [WithBlankImage(400, 500, PixelTypes.Rgba32)] + public void Clear_DoesNotDependOnSize(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave(provider, appendPixelTypeToFileName: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] + public void Clear_DoesNotDependOnSinglePixelType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave(provider, appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] + [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] + public void Clear_WhenColorIsOpaque_OverridePreviousColor( + TestImageProvider provider, + string newColorName) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = TestUtils.GetColorByName(newColorName); + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave( + provider, + newColorName, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] + [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] + public void Clear_AlwaysOverridesPreviousColor( + TestImageProvider provider, + string newColorName) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = TestUtils.GetColorByName(newColorName).WithAlpha(0.5F); + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(color)))); + + image.DebugSave( + provider, + newColorName, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void Clear_Region(TestImageProvider provider, int x0, int y0, int w, int h) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color clearColor = Color.Blue; + Color backgroundColor = Color.Red; + Rectangle region = new(x0, y0, w, h); + DrawingOptions options = new(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(clearColor), region))); + + image.DebugSave(provider, $"(x{x0},y{y0},w{w},h{h})", appendPixelTypeToFileName: false); + AssertRegionFill(image, region, clearColor, backgroundColor); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void Clear_Region_WorksOnWrappedMemoryImage( + TestImageProvider provider, + int x0, + int y0, + int w, + int h) + where TPixel : unmanaged, IPixel + { + using Image source = provider.GetImage(); + Assert.True(source.DangerousTryGetSinglePixelMemory(out Memory sourcePixels)); + TestMemoryManager memoryManager = TestMemoryManager.CreateAsCopyOf(sourcePixels.Span); + using Image wrapped = Image.WrapMemory(memoryManager.Memory, source.Width, source.Height); + + Color clearColor = Color.Blue; + Color backgroundColor = Color.Red; + Rectangle region = new(x0, y0, w, h); + DrawingOptions options = new(); + + wrapped.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Clear(Brushes.Solid(clearColor), region))); + + wrapped.DebugSave(provider, $"(x{x0},y{y0},w{w},h{h})", appendPixelTypeToFileName: false); + AssertRegionFill(wrapped, region, clearColor, backgroundColor); + } + + private static void AssertRegionFill( + Image image, + Rectangle region, + Color inside, + Color outside) + where TPixel : unmanaged, IPixel + { + TPixel insidePixel = inside.ToPixel(); + TPixel outsidePixel = outside.ToPixel(); + Buffer2D buffer = image.Frames.RootFrame.PixelBuffer; + + for (int y = 0; y < image.Height; y++) + { + Span row = buffer.DangerousGetRowSpan(y); + for (int x = 0; x < image.Width; x++) + { + TPixel expected = region.Contains(x, y) ? insidePixel : outsidePixel; + Assert.Equal(expected, row[x]); + } + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs new file mode 100644 index 000000000..2f680d51e --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Linq; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -100, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 40, 60, 0.2)] + public void ClipOffset(TestImageProvider provider, float dx, float dy, float sizeMult) + where TPixel : unmanaged, IPixel + { + FormattableString testDetails = $"offset_x{dx}_y{dy}"; + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => + { + Rectangle bounds = canvas.Bounds; + int outerRadii = (int)(Math.Min(bounds.Width, bounds.Height) * sizeMult); + Star star = new(new PointF(bounds.Width / 2F, bounds.Height / 2F), 5, outerRadii / 2F, outerRadii); + Matrix4x4 builder = Matrix4x4.CreateTranslation(dx, dy, 0); + canvas.Process(star.Transform(builder), ctx => ctx.DetectEdges()); + }), + testOutputDetails: testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32)] + public void ClipConstrainsOperationToClipBounds(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => + { + Rectangle bounds = canvas.Bounds; + RectangleF rect = new(0, 0, bounds.Width / 2F, bounds.Height / 2F); + RectangularPolygon clipRect = new(rect); + canvas.Process(clipRect, ctx => ctx.Flip(FlipMode.Vertical)); + }), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + [Fact] + public void ClipIssue250VerticalHorizontalCountShouldMatch() + { + PathCollection clip = new(new RectangularPolygon(new PointF(24, 16), new PointF(777, 385))); + + Path vertical = new(new LinearLineSegment(new PointF(26, 384), new PointF(26, 163))); + Path horizontal = new(new LinearLineSegment(new PointF(26, 163), new PointF(176, 163))); + + IPath reverse = vertical.Clip(clip); + int verticalCount = vertical.Clip(reverse).Flatten().Select(x => x.Points).Count(); + + reverse = horizontal.Clip(clip); + int horizontalCount = horizontal.Clip(reverse).Flatten().Select(x => x.Points).Count(); + + Assert.Equal(verticalCount, horizontalCount); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs new file mode 100644 index 000000000..6cb0e4424 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillOutsideBounds.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static TheoryData FillOutsideBoundsCircleCoordinates { get; } = new() + { + { -110, -60 }, { 0, -60 }, { 110, -60 }, + { -110, -50 }, { 0, -50 }, { 110, -50 }, + { -110, -49 }, { 0, -49 }, { 110, -49 }, + { -110, -20 }, { 0, -20 }, { 110, -20 }, + { -110, -50 }, { 0, -60 }, { 110, -60 }, + { -110, 0 }, { -99, 0 }, { 0, 0 }, { 99, 0 }, { 110, 0 }, + }; + + [Theory] + [InlineData(-100)] + [InlineData(-99)] + [InlineData(99)] + [InlineData(100)] + public void FillOutsideBoundsDrawRectactangleOutsideBoundsDrawingArea(int xpos) + { + int width = 100; + int height = 100; + + using Image image = new(width, height, Color.Red.ToPixel()); + + Rectangle rectangle = new(xpos, 0, width, height); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.Black), rectangle))); + } + + [Theory] + [WithSolidFilledImages(nameof(FillOutsideBoundsCircleCoordinates), 100, 100, nameof(Color.Red), PixelTypes.Rgba32)] + public void FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea(TestImageProvider provider, int xpos, int ypos) + { + int width = 100; + int height = 100; + + EllipsePolygon circle = new(xpos, ypos, width, height); + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.Black), circle)), + $"({xpos}_{ypos})", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs new file mode 100644 index 000000000..d9e92df9c --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillPath.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + // https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths + [Theory] + [WithSolidFilledImages(325, 325, "White", PixelTypes.Rgba32)] + public void FillPathSVGArcs(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PathBuilder pb = new(); + + pb.MoveTo(new Vector2(80, 80)) + .ArcTo(45, 45, 0, false, false, new Vector2(125, 125)) + .LineTo(new Vector2(125, 80)) + .CloseFigure(); + + IPath path = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(230, 80)) + .ArcTo(45, 45, 0, true, false, new Vector2(275, 125)) + .LineTo(new Vector2(275, 80)) + .CloseFigure(); + + IPath path2 = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(80, 230)) + .ArcTo(45, 45, 0, false, true, new Vector2(125, 275)) + .LineTo(new Vector2(125, 230)) + .CloseFigure(); + + IPath path3 = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(230, 230)) + .ArcTo(45, 45, 0, true, true, new Vector2(275, 275)) + .LineTo(new Vector2(275, 230)) + .CloseFigure(); + + IPath path4 = pb.Build(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.Green), path); + canvas.Fill(Brushes.Solid(Color.Red), path2); + canvas.Fill(Brushes.Solid(Color.Purple), path3); + canvas.Fill(Brushes.Solid(Color.Blue), path4); + })); + + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + } + + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/arc + [Theory] + [WithSolidFilledImages(150, 200, "White", PixelTypes.Rgba32)] + public void FillPathCanvasArcs(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + for (int i = 0; i <= 3; i++) + { + for (int j = 0; j <= 2; j++) + { + PathBuilder pb = new(); + + float x = 25 + (j * 50); // x coordinate + float y = 25 + (i * 50); // y coordinate + float radius = 20; // Arc radius + float startAngle = 0; // Starting point on circle + float endAngle = 180F + (180F * j / 2F); // End point on circle + bool counterclockwise = i % 2 == 1; // Draw counterclockwise + + // To move counterclockwise we offset our sweepAngle parameter + // Canvas likely does something similar. + if (counterclockwise) + { + // 360 becomes zero and we don't accept that as a parameter (won't render). + if (endAngle < 360F) + { + endAngle = (360F - endAngle) % 360F; + } + + endAngle *= -1; + } + + pb.AddArc(x, y, radius, radius, 0, startAngle, endAngle); + + if (i > 1) + { + canvas.Fill(Brushes.Solid(Color.Black), pb.Build()); + } + else + { + canvas.Draw(Pens.Solid(Color.Black, 1F), pb.Build()); + } + } + } + })); + + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(5e-3f), + provider, + appendSourceFileOrDescription: false, + appendPixelTypeToFileName: false); + } + + [Theory] + [WithSolidFilledImages(400, 250, "White", PixelTypes.Rgba32)] + public void FillPathArcToAlternates(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Test alternate syntax. Both should overlap creating an orange arc. + PathBuilder pb = new(); + + pb.MoveTo(new Vector2(50, 50)); + pb.ArcTo(20, 50, -72, false, true, new Vector2(200, 200)); + IPath path = pb.Build(); + + pb = new PathBuilder(); + pb.MoveTo(new Vector2(50, 50)); + pb.AddSegment(new ArcLineSegment(new Vector2(50, 50), new Vector2(200, 200), new SizeF(20, 50), -72F, true, true)); + IPath path2 = pb.Build(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.Yellow), path); + canvas.Fill(Brushes.Solid(Color.Red.WithAlpha(.5F)), path2); + })); + + image.DebugSave(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs new file mode 100644 index 000000000..1d695bffc --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.FillSolidBrush.cs @@ -0,0 +1,192 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static readonly TheoryData FillSolidBrush_BlendData = + new() + { + { false, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, + { false, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, + { false, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, + { false, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, + { false, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, + { false, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, + { false, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, + { false, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, + { false, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, + { false, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, + { false, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, + { false, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, + { true, "Blue", 0.5f, PixelColorBlendingMode.Normal, 1.0f }, + { true, "Blue", 1.0f, PixelColorBlendingMode.Normal, 0.5f }, + { true, "Green", 0.5f, PixelColorBlendingMode.Normal, 0.3f }, + { true, "HotPink", 0.8f, PixelColorBlendingMode.Normal, 0.8f }, + { true, "Blue", 0.5f, PixelColorBlendingMode.Multiply, 1.0f }, + { true, "Blue", 1.0f, PixelColorBlendingMode.Multiply, 0.5f }, + { true, "Green", 0.5f, PixelColorBlendingMode.Multiply, 0.3f }, + { true, "HotPink", 0.8f, PixelColorBlendingMode.Multiply, 0.8f }, + { true, "Blue", 0.5f, PixelColorBlendingMode.Add, 1.0f }, + { true, "Blue", 1.0f, PixelColorBlendingMode.Add, 0.5f }, + { true, "Green", 0.5f, PixelColorBlendingMode.Add, 0.3f }, + { true, "HotPink", 0.8f, PixelColorBlendingMode.Add, 0.8f }, + }; + + [Theory] + [WithBlankImage(1, 1, PixelTypes.Rgba32)] + [WithBlankImage(7, 4, PixelTypes.Rgba32)] + [WithBlankImage(16, 7, PixelTypes.Rgba32)] + [WithBlankImage(33, 32, PixelTypes.Rgba32)] + [WithBlankImage(400, 500, PixelTypes.Rgba32)] + public void FillSolidBrush_DoesNotDependOnSize(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color)))); + + image.DebugSave(provider, appendPixelTypeToFileName: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithBlankImage(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] + public void FillSolidBrush_DoesNotDependOnSinglePixelType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = Color.HotPink; + DrawingOptions options = new(); + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color)))); + + image.DebugSave(provider, appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] + [WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] + public void FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor( + TestImageProvider provider, + string newColorName) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color color = TestUtils.GetColorByName(newColorName); + DrawingOptions options = new(); + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color)))); + + image.DebugSave( + provider, + newColorName, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.ComparePixelBufferTo(color); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void FillSolidBrush_Region(TestImageProvider provider, int x0, int y0, int w, int h) + where TPixel : unmanaged, IPixel + { + FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; + Rectangle region = new(x0, y0, w, h); + Color color = Color.Blue; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), region)), + testDetails, + ImageComparer.Exact); + } + + [Theory] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 5, 7, 3, 8)] + [WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, 8, 5, 6, 4)] + public void FillSolidBrush_Region_WorksOnWrappedMemoryImage( + TestImageProvider provider, + int x0, + int y0, + int w, + int h) + where TPixel : unmanaged, IPixel + { + FormattableString testDetails = $"(x{x0},y{y0},w{w},h{h})"; + Rectangle region = new(x0, y0, w, h); + Color color = Color.Blue; + + provider.RunValidatingProcessorTestOnWrappedMemoryImage( + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), region)), + testDetails, + ImageComparer.Exact, + useReferenceOutputFrom: nameof(this.FillSolidBrush_Region)); + } + + [Theory] + [WithSolidFilledImages(nameof(FillSolidBrush_BlendData), 16, 16, "Red", PixelTypes.Rgba32)] + public void FillSolidBrush_BlendFillColorOverBackground( + TestImageProvider provider, + bool triggerFillRegion, + string newColorName, + float alpha, + PixelColorBlendingMode blenderMode, + float blendPercentage) + where TPixel : unmanaged, IPixel + { + Color fillColor = TestUtils.GetColorByName(newColorName).WithAlpha(alpha); + + using Image image = provider.GetImage(); + TPixel bgColor = image[0, 0]; + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + ColorBlendingMode = blenderMode, + BlendPercentage = blendPercentage + } + }; + + if (triggerFillRegion) + { + RectangularPolygon path = new(0, 0, 16, 16); + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(fillColor), path))); + } + else + { + image.Mutate(c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(fillColor)))); + } + + var testOutputDetails = new + { + triggerFillRegion, + newColorName, + alpha, + blenderMode, + blendPercentage + }; + + image.DebugSave( + provider, + testOutputDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + PixelBlender blender = PixelOperations.Instance.GetPixelBlender( + blenderMode, + PixelAlphaCompositionMode.SrcOver); + TPixel expectedPixel = blender.Blend(bgColor, fillColor.ToPixel(), blendPercentage); + image.ComparePixelBufferTo(expectedPixel); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs new file mode 100644 index 000000000..c69466242 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs @@ -0,0 +1,661 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Text; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + private static readonly ImageComparer EllipticGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + private static readonly ImageComparer LinearGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + private static readonly ImageComparer RadialGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + private static readonly ImageComparer SweepGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01F); + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0F, 360F)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 90F, 450F)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 180F, 540F)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 270F, 630F)] + public void FillSweepGradientBrush_RendersFullSweep_Every90Degrees( + TestImageProvider provider, + float start, + float end) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + SweepGradientTolerantComparer, + image => + { + SweepGradientBrush brush = new( + new Point(100, 100), + start, + end, + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(0.25F, Color.Yellow), + new ColorStop(0.5F, Color.Green), + new ColorStop(0.75F, Color.Blue), + new ColorStop(1, Color.Red)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + $"start({start},end{end})", + false, + false); + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32)] + public void FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color red = Color.Red; + + RadialGradientBrush brush = + new( + new Point(0, 0), + 100, + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // No reference image needed: the whole output should be a single color. + image.ComparePixelBufferTo(red); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 100)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 100, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 100)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, -40, 100)] + public void FillRadialGradientBrushWithDifferentCentersReturnsImage( + TestImageProvider provider, + int centerX, + int centerY) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + RadialGradientTolerantComparer, + image => + { + RadialGradientBrush brush = new( + new Point(centerX, centerY), + image.Width / 2F, + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + $"center({centerX},{centerY})", + false, + false); + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color red = Color.Red; + + using Image image = provider.GetImage(); + + EllipticGradientBrush unicolorEllipticGradientBrush = + new( + new Point(0, 0), + new Point(10, 0), + 1.0F, + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + DrawingOptions options = new(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(unicolorEllipticGradientBrush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // No reference image needed: the whole output should be a single color. + image.ComparePixelBufferTo(red); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.2)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.6)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 2.0)] + public void FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio(TestImageProvider provider, float ratio) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Color yellow = Color.Yellow; + Color red = Color.Red; + Color black = Color.Black; + + EllipticGradientBrush brush = new( + new Point(image.Width / 2, image.Height / 2), + new Point(image.Width / 2, image.Width * 2 / 3), + ratio, + GradientRepetitionMode.None, + new ColorStop(0, yellow), + new ColorStop(1, red), + new ColorStop(1, black)); + + FormattableString outputDetails = $"{ratio:F2}"; + DrawingOptions options = new(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(brush))); + image.DebugSave(provider, outputDetails, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + EllipticGradientTolerantComparer, + provider, + outputDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 0)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 45)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 90)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 30)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 30)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.8, 30)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 1.0, 30)] + public void FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio( + TestImageProvider provider, + float ratio, + float rotationInDegree) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Color yellow = Color.Yellow; + Color red = Color.Red; + Color black = Color.Black; + + Point center = new(image.Width / 2, image.Height / 2); + + double rotation = Math.PI * rotationInDegree / 180.0; + double cos = Math.Cos(rotation); + double sin = Math.Sin(rotation); + + int offsetY = image.Height / 6; + int axisX = center.X + (int)-(offsetY * sin); + int axisY = center.Y + (int)(offsetY * cos); + + EllipticGradientBrush brush = new( + center, + new Point(axisX, axisY), + ratio, + GradientRepetitionMode.None, + new ColorStop(0, yellow), + new ColorStop(1, red), + new ColorStop(1, black)); + + FormattableString outputDetails = $"{ratio:F2}_AT_{rotationInDegree:00}deg"; + DrawingOptions options = new(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(brush))); + image.DebugSave(provider, outputDetails, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + EllipticGradientTolerantComparer, + provider, + outputDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + public enum FillLinearGradientBrushImageCorner + { + TopLeft = 0, + TopRight = 1, + BottomLeft = 2, + BottomRight = 3 + } + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + Color red = Color.Red; + + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(10, 0), + GradientRepetitionMode.None, + new ColorStop(0, red), + new ColorStop(1, red)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + // No reference image needed: the whole output should be a single color. + image.ComparePixelBufferTo(red); + } + + [Theory] + [WithBlankImage(20, 10, PixelTypes.Rgba32)] + [WithBlankImage(20, 10, PixelTypes.Argb32)] + [WithBlankImage(20, 10, PixelTypes.Rgb24)] + public void FillLinearGradientBrushDoesNotDependOnSinglePixelType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(image.Width, 0), + GradientRepetitionMode.None, + new ColorStop(0, Color.Blue), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + appendSourceFileOrDescription: false); + + [Theory] + [WithBlankImage(500, 10, PixelTypes.Rgba32)] + public void FillLinearGradientBrushHorizontalReturnsUnicolorColumns(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(image.Width, 0), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + false, + false); + + [Theory] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.DontFill)] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.None)] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Repeat)] + [WithBlankImage(500, 10, PixelTypes.Rgba32, GradientRepetitionMode.Reflect)] + public void FillLinearGradientBrushHorizontalGradientWithRepMode( + TestImageProvider provider, + GradientRepetitionMode repetitionMode) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(image.Width / 10, 0), + repetitionMode, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + $"{repetitionMode}", + false, + false); + + [Theory] + [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.5f })] + [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.2f, 0.4f, 0.6f, 0.8f })] + [WithBlankImage(200, 100, PixelTypes.Rgba32, new[] { 0.1f, 0.3f, 0.6f })] + public void FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns( + TestImageProvider provider, + float[] pattern) + where TPixel : unmanaged, IPixel + { + string variant = string.Join("_", pattern.Select(i => i.ToString(CultureInfo.InvariantCulture))); + + Assert.True(pattern.Length > 0); + + Color black = Color.Black; + Color white = Color.White; + + ColorStop[] colorStops = + Enumerable.Repeat(new ColorStop(0, black), 1) + .Concat( + pattern.SelectMany( + (f, index) => + new[] + { + new ColorStop(f, index % 2 == 0 ? black : white), + new ColorStop(f, index % 2 == 0 ? white : black) + })) + .Concat(Enumerable.Repeat(new ColorStop(1, pattern.Length % 2 == 0 ? black : white), 1)) + .ToArray(); + + using Image image = provider.GetImage(); + + LinearGradientBrush brush = + new( + new Point(0, 0), + new Point(image.Width, 0), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + image.DebugSave( + provider, + variant, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + Assert.All( + Enumerable.Range(0, image.Width).Select(i => image[i, 0]), + color => Assert.True( + color.Equals(black.ToPixel()) || color.Equals(white.ToPixel()))); + + image.CompareToReferenceOutput( + LinearGradientTolerantComparer, + provider, + variant, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(10, 500, PixelTypes.Rgba32)] + public void FillLinearGradientBrushVerticalBrushReturnsUnicolorRows( + TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(0, image.Height), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + VerifyAllRowsAreUnicolor(image); + }, + false, + false); + + static void VerifyAllRowsAreUnicolor(Image image) + { + for (int y = 0; y < image.Height; y++) + { + Span row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y); + TPixel firstColorOfRow = row[0]; + foreach (TPixel p in row) + { + Assert.Equal(firstColorOfRow, p); + } + } + } + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.TopLeft)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.TopRight)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.BottomLeft)] + [WithBlankImage(200, 200, PixelTypes.Rgba32, FillLinearGradientBrushImageCorner.BottomRight)] + public void FillLinearGradientBrushDiagonalReturnsCorrectImages( + TestImageProvider provider, + FillLinearGradientBrushImageCorner startCorner) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Assert.True( + image.Height == image.Width, + "For the math check block at the end the image must be squared, but it is not."); + + int startX = (int)startCorner % 2 == 0 ? 0 : image.Width - 1; + int startY = startCorner > FillLinearGradientBrushImageCorner.TopRight ? 0 : image.Height - 1; + int endX = image.Height - startX - 1; + int endY = image.Width - startY - 1; + + LinearGradientBrush brush = + new( + new Point(startX, startY), + new Point(endX, endY), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave( + provider, + startCorner, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + int verticalSign = startY == 0 ? 1 : -1; + int horizontalSign = startX == 0 ? 1 : -1; + + for (int i = 0; i < image.Height; i++) + { + TPixel colorOnDiagonal = image[i, i]; + int orthoCount = 0; + for (int offset = -orthoCount; offset < orthoCount; offset++) + { + Assert.Equal(colorOnDiagonal, image[i + (horizontalSign * offset), i + (verticalSign * offset)]); + } + } + + image.CompareToReferenceOutput( + LinearGradientTolerantComparer, + provider, + startCorner, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .2f, .5f, .9f }, new[] { 0, 0, 1, 1 })] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 499, 499, 0, new[] { 0f, 0.2f, 0.5f, 0.9f }, new[] { 0, 1, 2, 3 })] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 499, 499, 0, 0, new[] { 0f, 0.7f, 0.8f, 0.9f }, new[] { 0, 1, 2, 0 })] + [WithBlankImage(500, 500, PixelTypes.Rgba32, 0, 0, 499, 499, new[] { 0f, .5f, 1f }, new[] { 0, 1, 3 })] + public void FillLinearGradientBrushArbitraryGradients( + TestImageProvider provider, + int startX, + int startY, + int endX, + int endY, + float[] stopPositions, + int[] stopColorCodes) + where TPixel : unmanaged, IPixel + { + Color[] colors = + [ + Color.Navy, Color.LightGreen, Color.Yellow, + Color.Red + ]; + + StringBuilder coloringVariant = new(); + ColorStop[] colorStops = new ColorStop[stopPositions.Length]; + + for (int i = 0; i < stopPositions.Length; i++) + { + Color color = colors[stopColorCodes[i % colors.Length]]; + float position = stopPositions[i]; + colorStops[i] = new ColorStop(position, color); + coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); + } + + FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; + + provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(startX, startY), + new Point(endX, endY), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + variant, + false, + false); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32, 0, 0, 199, 199, new[] { 0f, .25f, .5f, .75f, 1f }, new[] { 0, 1, 2, 3, 4 })] + public void FillLinearGradientBrushMultiplePointGradients( + TestImageProvider provider, + int startX, + int startY, + int endX, + int endY, + float[] stopPositions, + int[] stopColorCodes) + where TPixel : unmanaged, IPixel + { + Color[] colors = + [ + Color.Black, Color.Blue, Color.Red, + Color.White, Color.Lime + ]; + + StringBuilder coloringVariant = new(); + ColorStop[] colorStops = new ColorStop[stopPositions.Length]; + + for (int i = 0; i < stopPositions.Length; i++) + { + Color color = colors[stopColorCodes[i % colors.Length]]; + float position = stopPositions[i]; + colorStops[i] = new ColorStop(position, color); + coloringVariant.AppendFormat(CultureInfo.InvariantCulture, "{0}@{1};", color.ToPixel().ToHex(), position); + } + + FormattableString variant = $"({startX},{startY})_TO_({endX},{endY})__[{coloringVariant}]"; + + provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(startX, startY), + new Point(endX, endY), + GradientRepetitionMode.None, + colorStops); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + variant, + false, + false); + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32)] + public void FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + provider.VerifyOperation( + image => + { + int width = image.Width; + int height = image.Height; + + image.Mutate(ctx => + { + ctx.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.Red))); + + DrawingOptions glossOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = true, + ColorBlendingMode = PixelColorBlendingMode.Normal, + AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop + } + }; + + IPathCollection glossPath = BuildGloss(width, height); + LinearGradientBrush linearGradientBrush = new( + new Point(0, 0), + new Point(0, height / 2), + GradientRepetitionMode.Repeat, + new ColorStop(0, Color.White.WithAlpha(0.5f)), + new ColorStop(1, Color.White.WithAlpha(0.25f))); + + ctx.ProcessWithCanvas(glossOptions, canvas => canvas.Fill(linearGradientBrush, glossPath)); + }); + }); + + static IPathCollection BuildGloss(int imageWidth, int imageHeight) + { + PathBuilder pathBuilder = new(); + pathBuilder.AddLine(new PointF(0, 0), new PointF(imageWidth, 0)); + pathBuilder.AddLine(new PointF(imageWidth, 0), new PointF(imageWidth, imageHeight * 0.4f)); + pathBuilder.AddQuadraticBezier( + new PointF(imageWidth, imageHeight * 0.4f), + new PointF(imageWidth / 2f, imageHeight * 0.6f), + new PointF(0, imageHeight * 0.4f)); + pathBuilder.CloseFigure(); + return new PathCollection(pathBuilder.Build()); + } + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgb24)] + public void FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + LinearGradientTolerantComparer, + image => + { + PathGradientBrush brush = new( + [new PointF(0, 0), new PointF(200, 0), new PointF(200, 200), new PointF(0, 200), new PointF(0, 0)], + [Color.Red, Color.Yellow, Color.Green, Color.DarkCyan, Color.Red]); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + false, + false); + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32)] + public void FillLinearGradientBrushRotatedGradient(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + image => + { + LinearGradientBrush brush = new( + new Point(0, 0), + new Point(200, 200), + new Point(0, 100), + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Yellow)); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + false, + false); +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs new file mode 100644 index 000000000..f8dc22ba5 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.ImageBrushes.cs @@ -0,0 +1,216 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Fact] + public void FillImageBrushDoesNotDisposeImage() + { + using (Image source = new(5, 5)) + { + ImageBrush brush = new(source); + using (Image destination = new(10, 10)) + { + destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush, new Rectangle(0, 0, 10, 10)))); + destination.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush, new Rectangle(0, 0, 10, 10)))); + } + } + } + + [Theory] + [WithTestPatternImage(200, 200, PixelTypes.Rgba32 | PixelTypes.Bgra32)] + public void FillImageBrushUseBrushOfDifferentPixelType(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + if (provider.PixelType == PixelTypes.Rgba32) + { + using Image overlay = Image.Load(data); + Brush brush = new ImageBrush(overlay); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + } + else + { + using Image overlay = Image.Load(data); + Brush brush = new ImageBrush(overlay); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + } + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithTestPatternImage(200, 200, PixelTypes.Rgba32)] + public void FillImageBrushCanDrawLandscapeImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + using Image overlay = Image.Load(data); + + overlay.Mutate(ctx => ctx.Crop(new Rectangle(0, 0, 125, 90))); + + ImageBrush brush = new(overlay); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithTestPatternImage(200, 200, PixelTypes.Rgba32)] + public void FillImageBrushCanDrawPortraitImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + using Image overlay = Image.Load(data); + + overlay.Mutate(ctx => ctx.Crop(new Rectangle(0, 0, 90, 125))); + + ImageBrush brush = new(overlay); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] + public void FillImageBrushCanOffsetImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + using Image overlay = Image.Load(data); + + ImageBrush brush = new(overlay); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(brush, new Rectangle(0, 0, 400, 200)); + canvas.Fill(brush, new Rectangle(-100, 200, 500, 200)); + })); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] + public void FillImageBrushCanOffsetViaBrushImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + using Image overlay = Image.Load(data); + + ImageBrush brush = new(overlay); + ImageBrush brushOffset = new(overlay, new Point(100, 0)); + + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(brush, new Rectangle(0, 0, 400, 200)); + canvas.Fill(brushOffset, new Rectangle(0, 200, 400, 200)); + })); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] + public void FillImageBrushCanDrawOffsetImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + + using Image templateImage = Image.Load(data); + using Image finalTexture = BuildMultiRowTexture(templateImage); + + finalTexture.Mutate(ctx => ctx.Resize(100, 200)); + + ImageBrush brush = new(finalTexture); + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + + Image BuildMultiRowTexture(Image sourceTexture) + { + int halfWidth = sourceTexture.Width / 2; + + Image final = sourceTexture.Clone(ctx => ctx.Resize(new ResizeOptions + { + Size = new Size(templateImage.Width, templateImage.Height * 2), + Position = AnchorPositionMode.TopLeft, + Mode = ResizeMode.Pad, + }) + .DrawImage(templateImage, new Point(halfWidth, sourceTexture.Height), new Rectangle(0, 0, halfWidth, sourceTexture.Height), 1) + .DrawImage(templateImage, new Point(0, templateImage.Height), new Rectangle(halfWidth, 0, halfWidth, sourceTexture.Height), 1)); + return final; + } + } + + [Theory] + [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] + public void FillImageBrushCanDrawNegativeOffsetImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + using Image overlay = Image.Load(data); + + overlay.Mutate(ctx => ctx.Resize(100, 100)); + + ImageBrush halfBrush = new(overlay, new RectangleF(50, 0, 50, 100)); + ImageBrush fullBrush = new(overlay); + + background.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + FillImageBrushDrawFull(canvas, new Size(100, 100), fullBrush, halfBrush, background.Width, background.Height))); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + private static void FillImageBrushDrawFull( + IDrawingCanvas canvas, + Size size, + ImageBrush brush, + ImageBrush halfBrush, + int width, + int height) + { + int y = 0; + while (y < height) + { + bool half = (y / size.Height) % 2 != 0; + int x = 0; + while (x < width) + { + if (half) + { + int halfWidth = size.Width / 2; + canvas.Fill(halfBrush, new Rectangle(x, y, halfWidth, size.Height)); + x += halfWidth; + half = false; + } + else + { + canvas.Fill(brush, new Rectangle(x, y, size.Width, size.Height)); + x += size.Width; + } + } + + y += size.Height; + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs new file mode 100644 index 000000000..f51dee4a3 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + private static readonly ImageComparer PathGradientTolerantComparer = ImageComparer.TolerantPercentage(0.01f); + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillPathGradientBrushFillRectangleWithDifferentColors(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + PathGradientTolerantComparer, + image => + { + PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; + Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; + + PathGradientBrush brush = new(points, colors); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + }); + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPathGradientBrushFillTriangleWithDifferentColors(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + PathGradientTolerantComparer, + image => + { + PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; + Color[] colors = [Color.Red, Color.Green, Color.Blue]; + + PathGradientBrush brush = new(points, colors); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + }); + + [Theory] + [WithBlankImage(20, 20, PixelTypes.HalfSingle)] + public void FillPathGradientBrushFillTriangleWithGreyscale(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + ImageComparer.TolerantPercentage(0.02f), + image => + { + PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; + + Color c1 = Color.FromPixel(new HalfSingle(-1)); + Color c2 = Color.FromPixel(new HalfSingle(0)); + Color c3 = Color.FromPixel(new HalfSingle(1)); + + Color[] colors = [c1, c2, c3]; + + PathGradientBrush brush = new(points, colors); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + }); + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPathGradientBrushFillTriangleWithDifferentColorsCenter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + PathGradientTolerantComparer, + image => + { + PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; + Color[] colors = [Color.Red, Color.Green, Color.Blue]; + + PathGradientBrush brush = new(points, colors, Color.White); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + }); + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillPathGradientBrushFillRectangleWithSingleColor(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; + Color[] colors = [Color.Red]; + + PathGradientBrush brush = new(points, colors); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.ComparePixelBufferTo(Color.Red); + } + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + PathGradientTolerantComparer, + image => + { + PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; + Color[] colors = [Color.Red, Color.Yellow]; + + PathGradientBrush brush = new(points, colors); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + }); + + [Theory] + [WithBlankImage(10, 10, PixelTypes.Rgba32)] + public void FillPathGradientBrushFillWithCustomCenterColor(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + PathGradientTolerantComparer, + image => + { + PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; + Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; + + PathGradientBrush brush = new(points, colors, Color.White); + + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + }); + + [Fact] + public void FillPathGradientBrushShouldThrowArgumentNullExceptionWhenLinesAreNull() + { + Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; + + PathGradientBrush Create() => new(null, colors, Color.White); + + Assert.Throws(Create); + } + + [Fact] + public void FillPathGradientBrushShouldThrowArgumentOutOfRangeExceptionWhenLessThan3PointsAreGiven() + { + PointF[] points = [new(0, 0), new(10, 0)]; + Color[] colors = [Color.Black, Color.Red, Color.Yellow, Color.Green]; + + PathGradientBrush Create() => new(points, colors, Color.White); + + Assert.Throws(Create); + } + + [Fact] + public void FillPathGradientBrushShouldThrowArgumentNullExceptionWhenColorsAreNull() + { + PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; + + PathGradientBrush Create() => new(points, null, Color.White); + + Assert.Throws(Create); + } + + [Fact] + public void FillPathGradientBrushShouldThrowArgumentOutOfRangeExceptionWhenEmptyColorArrayIsGiven() + { + PointF[] points = [new(0, 0), new(10, 0), new(10, 10), new(0, 10)]; + Color[] colors = []; + + PathGradientBrush Create() => new(points, colors, Color.White); + + Assert.Throws(Create); + } + + [Theory] + [WithBlankImage(100, 100, PixelTypes.Rgba32)] + public void FillPathGradientBrushFillComplex(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + new TolerantImageComparer(0.2f), + image => + { + Star star = new(50, 50, 5, 20, 45); + PointF[] points = star.Points.ToArray(); + Color[] colors = + [ + Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.Purple, + Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.Purple + ]; + + PathGradientBrush brush = new(points, colors, Color.White); + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.Fill(brush))); + }, + appendSourceFileOrDescription: false, + appendPixelTypeToFileName: false); +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PatternBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PatternBrushes.cs new file mode 100644 index 000000000..43f43f17b --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PatternBrushes.cs @@ -0,0 +1,285 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent10(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent10(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent10(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent20(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent20(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Percent20(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithHorizontal(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Horizontal(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Horizontal(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithMin(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.Min(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithMinTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.Blue }, + { Color.HotPink, Color.HotPink, Color.HotPink, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.Min(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithVertical(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.Vertical(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.Vertical(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen } + }; + + VerifyFloodFillPattern(provider, Brushes.ForwardDiagonal(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.Blue, Color.Blue, Color.Blue, Color.HotPink }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue } + }; + + VerifyFloodFillPattern(provider, Brushes.ForwardDiagonal(Color.HotPink), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.LimeGreen, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.HotPink, Color.LimeGreen, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.HotPink, Color.LimeGreen }, + { Color.LimeGreen, Color.LimeGreen, Color.LimeGreen, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.BackwardDiagonal(Color.HotPink, Color.LimeGreen), expectedPattern); + } + + [Theory] + [WithBlankImage(20, 20, PixelTypes.Rgba32)] + public void FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Color[,] expectedPattern = new Color[,] + { + { Color.HotPink, Color.Blue, Color.Blue, Color.Blue }, + { Color.Blue, Color.HotPink, Color.Blue, Color.Blue }, + { Color.Blue, Color.Blue, Color.HotPink, Color.Blue }, + { Color.Blue, Color.Blue, Color.Blue, Color.HotPink } + }; + + VerifyFloodFillPattern(provider, Brushes.BackwardDiagonal(Color.HotPink), expectedPattern); + } + + private static void VerifyFloodFillPattern( + TestImageProvider provider, + Brush brush, + Color[,] expectedPattern) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + ImageComparer.Exact, + image => + { + image.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.Blue)); + canvas.Fill(brush); + })); + + AssertPattern(image, expectedPattern); + }, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + private static void AssertPattern(Image image, Color[,] expectedPattern) + where TPixel : unmanaged, IPixel + { + int rows = expectedPattern.GetLength(0); + int columns = expectedPattern.GetLength(1); + + TPixel[,] expectedPixels = new TPixel[rows, columns]; + for (int y = 0; y < rows; y++) + { + for (int x = 0; x < columns; x++) + { + expectedPixels[y, x] = expectedPattern[y, x].ToPixel(); + } + } + + Buffer2D pixels = image.GetRootFramePixelBuffer(); + for (int y = 0; y < image.Height; y++) + { + Span row = pixels.DangerousGetRowSpan(y); + int patternY = y % rows; + for (int x = 0; x < image.Width; x++) + { + TPixel expected = expectedPixels[patternY, x % columns]; + Assert.Equal(expected, row[x]); + } + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs new file mode 100644 index 000000000..7e4ca80a5 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs @@ -0,0 +1,421 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static TheoryData FillPolygon_Complex_Data { get; } = + new() + { + { false, IntersectionRule.EvenOdd }, + { false, IntersectionRule.NonZero }, + { true, IntersectionRule.EvenOdd }, + { true, IntersectionRule.NonZero }, + }; + + public static readonly TheoryData FillPolygon_EllipsePolygon_Data = + new() + { + { false, IntersectionRule.EvenOdd }, + { false, IntersectionRule.NonZero }, + { true, IntersectionRule.EvenOdd }, + { true, IntersectionRule.NonZero }, + }; + + [Theory] + [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 0)] + [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 8)] + [WithSolidFilledImages(8, 12, nameof(Color.Black), PixelTypes.Rgba32, 16)] + public void FillPolygon_Solid_Basic(TestImageProvider provider, int antialias) + where TPixel : unmanaged, IPixel + { + PointF[] polygon1 = PolygonFactory.CreatePointArray((2, 2), (6, 2), (6, 4), (2, 4)); + PointF[] polygon2 = PolygonFactory.CreatePointArray((2, 8), (4, 6), (6, 8), (4, 10)); + Polygon shape1 = new(new LinearLineSegment(polygon1)); + Polygon shape2 = new(new LinearLineSegment(polygon2)); + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias > 0 } + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => + { + canvas.Fill(Brushes.Solid(Color.White), shape1); + canvas.Fill(Brushes.Solid(Color.White), shape2); + }), + testOutputDetails: $"aa{antialias}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6f, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1f, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1f, true)] + public void FillPolygon_Solid(TestImageProvider provider, string colorName, float alpha, bool antialias) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) + ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; + + string aa = antialias ? string.Empty : "_NoAntialias"; + FormattableString outputDetails = $"{colorName}_A{alpha}{aa}"; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color), polygon)), + outputDetails, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] + public void FillPolygon_Solid_Transformed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) + ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateSkew(GeometryUtilities.DegreeToRadian(-15), 0, new Vector2(200, 200))) + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon))); + } + + [Theory] + [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] + public void FillPolygon_RectangularPolygon_Solid_Transformed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(25, 25, 50, 50); + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon))); + } + + [Theory] + [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] + public void FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(25, 25, 50, 50); + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon))); + } + + [Theory] + [WithBasicTestPatternImages(nameof(FillPolygon_Complex_Data), 100, 100, PixelTypes.Rgba32)] + public void FillPolygon_Complex(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule) + where TPixel : unmanaged, IPixel + { + PointF[] contour = PolygonFactory.CreatePointArray((20, 20), (80, 20), (80, 80), (20, 80)); + PointF[] hole = PolygonFactory.CreatePointArray((40, 40), (40, 60), (60, 60), (60, 40)); + + if (reverse) + { + Array.Reverse(contour); + Array.Reverse(hole); + } + + ComplexPolygon polygon = new( + new Path(new LinearLineSegment(contour)), + new Path(new LinearLineSegment(hole))); + + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = intersectionRule } + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.White), polygon)), + testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", + comparer: ImageComparer.TolerantPercentage(0.01f), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, false)] + [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, true)] + public void FillPolygon_Concave(TestImageProvider provider, bool reverse) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new Vector2(8, 8), + new Vector2(64, 8), + new Vector2(64, 64), + new Vector2(120, 64), + new Vector2(120, 120), + new Vector2(8, 120) + ]; + if (reverse) + { + Array.Reverse(points); + } + + Polygon polygon = new(new LinearLineSegment(points)); + Color color = Color.LightGreen; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), polygon)), + testOutputDetails: $"Reverse({reverse})", + comparer: ImageComparer.TolerantPercentage(0.01f), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(64, 64, "Black", PixelTypes.Rgba32)] + public void FillPolygon_StarCircle(TestImageProvider provider) + { + EllipsePolygon circle = new(32, 32, 30); + Star star = new(32, 32, 7, 10, 27); + IPath shape = circle.Clip(star); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.White), shape)), + comparer: ImageComparer.TolerantPercentage(0.01f), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [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. + ShapeOptions shapeOptions = new() { BooleanOperation = operation }; + IPath shape = star.Clip(shapeOptions, circle); + DrawingOptions options = new() { ShapeOptions = shapeOptions }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => + { + canvas.Fill(Brushes.Solid(Color.DeepPink), circle); + canvas.Fill(Brushes.Solid(Color.LightGray), star); + canvas.Fill(Brushes.Solid(Color.ForestGreen), shape); + }), + testOutputDetails: operation.ToString(), + comparer: ImageComparer.TolerantPercentage(0.01F), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] + public void FillPolygon_Pattern(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300) + ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); + PatternBrush brush = Brushes.Horizontal(Color.Yellow); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(brush, polygon)), + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Png.Ducky)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Bmp.Car)] + public void FillPolygon_ImageBrush(TestImageProvider provider, string brushImageName) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200) + ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); + + using Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes); + ImageBrush brush = new(brushImage); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(brush, polygon)), + System.IO.Path.GetFileNameWithoutExtension(brushImageName), + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Png.Ducky)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, TestImages.Bmp.Car)] + public void FillPolygon_ImageBrush_Rect(TestImageProvider provider, string brushImageName) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), new Vector2(200, 50), new Vector2(50, 200) + ]; + Polygon polygon = new(new LinearLineSegment(simplePath)); + + using Image brushImage = Image.Load(TestFile.Create(brushImageName).Bytes); + + float top = brushImage.Height / 4F; + float left = brushImage.Width / 4F; + float height = top * 2; + float width = left * 2; + + ImageBrush brush = new(brushImage, new RectangleF(left, top, width, height)); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(brush, polygon)), + System.IO.Path.GetFileNameWithoutExtension(brushImageName) + "_rect", + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 250, PixelTypes.Rgba32)] + public void FillPolygon_RectangularPolygon(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(10, 10, 190, 140); + Color color = Color.White; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), polygon)), + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 50, 0f)] + [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 60, 20f)] + [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 3, 60, -180f)] + [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 5, 70, 0f)] + [WithBasicTestPatternImages(200, 200, PixelTypes.Rgba32, 7, 80, -180f)] + public void FillPolygon_RegularPolygon(TestImageProvider provider, int vertices, float radius, float angleDeg) + where TPixel : unmanaged, IPixel + { + float angle = GeometryUtilities.DegreeToRadian(angleDeg); + RegularPolygon polygon = new(100, 100, vertices, radius, angle); + Color color = Color.Yellow; + + FormattableString testOutput = $"V({vertices})_R({radius})_Ang({angleDeg})"; + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(color), polygon)), + testOutput, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(nameof(FillPolygon_EllipsePolygon_Data), 200, 200, PixelTypes.Rgba32)] + public void FillPolygon_EllipsePolygon(TestImageProvider provider, bool reverse, IntersectionRule intersectionRule) + where TPixel : unmanaged, IPixel + { + IPath polygon = new EllipsePolygon(100, 100, 80, 120); + if (reverse) + { + polygon = polygon.Reverse(); + } + + Color color = Color.Azure; + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = intersectionRule } + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color), polygon)), + testOutputDetails: $"Reverse({reverse})_IntersectionRule({intersectionRule})", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(60, 60, "Blue", PixelTypes.Rgba32)] + public void FillPolygon_IntersectionRules_OddEven(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Polygon poly = new(new LinearLineSegment( + new PointF(10, 30), + new PointF(10, 20), + new PointF(50, 20), + new PointF(50, 50), + new PointF(20, 50), + new PointF(20, 10), + new PointF(30, 10), + new PointF(30, 40), + new PointF(40, 40), + new PointF(40, 30), + new PointF(10, 30))); + + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly)), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(60, 60, "Blue", PixelTypes.Rgba32)] + public void FillPolygon_IntersectionRules_Nonzero(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Polygon poly = new(new LinearLineSegment( + new PointF(10, 30), + new PointF(10, 20), + new PointF(50, 20), + new PointF(50, 50), + new PointF(20, 50), + new PointF(20, 10), + new PointF(30, 10), + new PointF(30, 40), + new PointF(40, 40), + new PointF(40, 30), + new PointF(10, 30))); + + DrawingOptions options = new() + { + ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.HotPink), poly)), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs new file mode 100644 index 000000000..35c03cc03 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Primitives.cs @@ -0,0 +1,637 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + public static readonly TheoryData DrawBezierData = + new() + { + { "White", 255, 1.5F }, + { "Red", 255, 3F }, + { "HotPink", 255, 5F }, + { "HotPink", 150, 5F }, + { "White", 255, 15F }, + }; + + public static readonly TheoryData DrawPathData = + new() + { + { "White", 255, 1.5F }, + { "Red", 255, 3F }, + { "HotPink", 255, 5F }, + { "HotPink", 150, 5F }, + { "White", 255, 15F }, + }; + + [Theory] + [WithSolidFilledImages(nameof(DrawBezierData), 300, 450, "Blue", PixelTypes.Rgba32)] + public void DrawBeziers(TestImageProvider provider, string colorName, byte alpha, float thickness) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new Vector2(10, 400), + new Vector2(30, 10), + new Vector2(240, 30), + new Vector2(300, 400) + ]; + + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); + FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.DrawBezier(Pens.Solid(color, 5F), points))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(500, 500, PixelTypes.Rgba32)] + public void SolidBezierFilledBezier(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 400), + new Vector2(30, 10), + new Vector2(240, 30), + new Vector2(300, 400) + ]; + + Polygon polygon = new(new CubicBezierLineSegment(simplePath)); + SolidBrush brush = Brushes.Solid(Color.HotPink); + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Clear(Brushes.Solid(Color.Blue)); + canvas.Fill(brush, polygon); + })); + } + + [Theory] + [WithBlankImage(500, 500, PixelTypes.Rgba32)] + public void SolidBezierOverlayByFilledPolygonOpacity(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 400), + new Vector2(30, 10), + new Vector2(240, 30), + new Vector2(300, 400) + ]; + + Polygon polygon = new(new CubicBezierLineSegment(simplePath)); + SolidBrush brush = Brushes.Solid(Color.HotPink.WithAlpha(150 / 255F)); + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Clear(Brushes.Solid(Color.Blue)); + canvas.Fill(brush, polygon); + })); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 2.5F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6F, 10F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 5F, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1F, 10F, true)] + public void DrawLines_Simple(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1F, true)] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5F, true)] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 1F, false)] + [WithSolidFilledImages(30, 30, "White", PixelTypes.Rgba32, 5F, false)] + public void DrawLinesInvalidPoints(TestImageProvider provider, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + SolidPen pen = new(Color.Black, thickness); + PointF[] path = [new Vector2(15F, 15F), new Vector2(15F, 15F)]; + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; + + string aa = antialias ? string.Empty : "_NoAntialias"; + FormattableString outputDetails = $"T({thickness}){aa}"; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.DrawLine(pen, path))); + image.DebugSave(provider, outputDetails, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + outputDetails, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 5F, false)] + public void DrawLines_Dash(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.Dash(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "LightGreen", 1F, 5F, false)] + public void DrawLines_Dot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.Dot(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, false)] + public void DrawLines_DashDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.DashDot(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Black", 1F, 5F, false)] + public void DrawLines_DashDotDot(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + Pen pen = Pens.DashDotDot(color, thickness); + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, true)] + public void DrawLines_EndCapRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + PatternPen pen = new(new PenOptions(color, thickness, [3F, 3F]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Round }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, true)] + public void DrawLines_EndCapButt(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + PatternPen pen = new(new PenOptions(color, thickness, [3F, 3F]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Butt }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 5F, true)] + public void DrawLines_EndCapSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + PatternPen pen = new(new PenOptions(color, thickness, [3F, 3F]) + { + StrokeOptions = new StrokeOptions { LineCap = LineCap.Square }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 10F, true)] + public void DrawLines_JointStyleRound(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Round }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 10F, true)] + public void DrawLines_JointStyleSquare(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Bevel }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "Yellow", 1F, 10F, true)] + public void DrawLines_JointStyleMiter(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + SolidPen pen = new(new PenOptions(color, thickness) + { + StrokeOptions = new StrokeOptions { LineJoin = LineJoin.Miter }, + }); + + DrawLinesImpl(provider, colorName, alpha, thickness, antialias, pen); + } + + [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, false, false, true)] + public void DrawComplexPolygon(TestImageProvider provider, bool overlap, bool transparent, bool dashed) + where TPixel : unmanaged, IPixel + { + Polygon simplePath = new(new LinearLineSegment( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300))); + + Polygon hole1 = new(new LinearLineSegment( + new Vector2(37, 85), + overlap ? new Vector2(130, 40) : new Vector2(93, 85), + new Vector2(65, 137))); + + IPath clipped = simplePath.Clip(hole1); + + Color color = Color.White; + if (transparent) + { + color = color.WithAlpha(150 / 255F); + } + + string testDetails = string.Empty; + if (overlap) + { + testDetails += "_Overlap"; + } + + if (transparent) + { + testDetails += "_Transparent"; + } + + if (dashed) + { + testDetails += "_Dashed"; + } + + Pen pen = dashed ? Pens.Dash(color, 5F) : Pens.Solid(color, 5F); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(pen, clipped))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, false)] + [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, true, false)] + [WithSolidFilledImages(300, 400, "Blue", PixelTypes.Rgba32, false, true)] + public void FillComplexPolygon_SolidFill(TestImageProvider provider, bool overlap, bool transparent) + where TPixel : unmanaged, IPixel + { + Polygon simplePath = new(new LinearLineSegment( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300))); + + Polygon hole1 = new(new LinearLineSegment( + new Vector2(37, 85), + overlap ? new Vector2(130, 40) : new Vector2(93, 85), + new Vector2(65, 137))); + + IPath clipped = simplePath.Clip(hole1); + + Color color = Color.HotPink; + if (transparent) + { + color = color.WithAlpha(150 / 255F); + } + + string testDetails = string.Empty; + if (overlap) + { + testDetails += "_Overlap"; + } + + if (transparent) + { + testDetails += "_Transparent"; + } + + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(color), clipped))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 2.5F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 0.6F, 10F, true)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, "White", 1F, 5F, false)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Bgr24, "Yellow", 1F, 10F, true)] + public void DrawPolygon(TestImageProvider provider, string colorName, float alpha, float thickness, bool antialias) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300) + ]; + + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha); + IPath polygon = new Polygon(new LinearLineSegment(simplePath)); + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; + + string aa = antialias ? string.Empty : "_NoAntialias"; + FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(color, thickness), polygon))); + image.DebugSave(provider, outputDetails, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + outputDetails, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32)] + public void DrawPolygon_Transformed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = + [ + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300) + ]; + + IPath polygon = new Polygon(new LinearLineSegment(simplePath)); + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateSkew( + GeometryUtilities.DegreeToRadian(-15), + 0, + new Vector2(200, 200))) + }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.White, 2.5F), polygon))); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.001F), provider); + } + + [Theory] + [WithBasicTestPatternImages(100, 100, PixelTypes.Rgba32)] + public void DrawPolygonRectangular_Transformed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + RectangularPolygon polygon = new(25, 25, 50, 50); + DrawingOptions options = new() + { + Transform = new Matrix4x4(Matrix3x2.CreateRotation((float)Math.PI / 4, new PointF(50, 50))) + }; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.White, 2.5F), polygon))); + image.DebugSave(provider); + image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.001F), provider); + } + + [Theory] + [WithSolidFilledImages(nameof(DrawPathData), 300, 600, "Blue", PixelTypes.Rgba32)] + public void DrawPath(TestImageProvider provider, string colorName, byte alpha, float thickness) + where TPixel : unmanaged, IPixel + { + LinearLineSegment linearSegment = new( + new Vector2(10, 10), + new Vector2(200, 150), + new Vector2(50, 300)); + CubicBezierLineSegment bezierSegment = new( + new Vector2(50, 300), + new Vector2(500, 500), + new Vector2(60, 10), + new Vector2(10, 400)); + + ArcLineSegment ellipticArcSegment1 = new(new Vector2(10, 400), new Vector2(150, 450), new SizeF((float)Math.Sqrt(5525), 40), GeometryUtilities.RadianToDegree((float)Math.Atan2(25, 70)), true, true); + ArcLineSegment ellipticArcSegment2 = new(new PointF(150, 450), new PointF(149F, 450), new SizeF(140, 70), 0, true, true); + Path path = new(linearSegment, bezierSegment, ellipticArcSegment1, ellipticArcSegment2); + + Color color = TestUtils.GetColorByName(colorName).WithAlpha(alpha / 255F); + FormattableString testDetails = $"{colorName}_A{alpha}_T{thickness}"; + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(color, thickness), path))); + image.DebugSave( + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + testDetails, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(256, 256, "Black", PixelTypes.Rgba32)] + public void DrawPathExtendingOffEdgeOfImageShouldNotBeCropped(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + SolidPen pen = Pens.Solid(Color.White, 5F); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => + { + for (int i = 0; i < 300; i += 20) + { + PointF[] points = [new Vector2(100, 2), new Vector2(-10, i)]; + canvas.DrawLine(pen, points); + } + })); + image.DebugSave( + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(40, 40, "White", PixelTypes.Rgba32)] + public void DrawPathClippedOnTop(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new(10F, -10F), + new(20F, 20F), + new(30F, -30F) + ]; + + IPath path = new PathBuilder().AddLines(points).Build(); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.Black, 1F), path))); + image.DebugSave( + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 360F)] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 359F)] + public void DrawPathCircleUsingAddArc(TestImageProvider provider, float sweep) + where TPixel : unmanaged, IPixel + { + IPath path = new PathBuilder().AddArc(new Point(150, 150), 50, 50, 0, 40, sweep).Build(); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.Black, 1F), path))); + image.DebugSave( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, true)] + [WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, false)] + public void DrawPathCircleUsingArcTo(TestImageProvider provider, bool sweep) + where TPixel : unmanaged, IPixel + { + Point origin = new(150, 150); + IPath path = new PathBuilder().MoveTo(origin).ArcTo(50, 50, 0, true, sweep, origin).Build(); + DrawingOptions options = new(); + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.Draw(Pens.Solid(Color.Black, 1F), path))); + image.DebugSave( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + provider, + $"{sweep}", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + private static void DrawLinesImpl( + TestImageProvider provider, + string colorName, + float alpha, + float thickness, + bool antialias, + Pen pen) + where TPixel : unmanaged, IPixel + { + PointF[] simplePath = [new Vector2(10, 10), new Vector2(200, 150), new Vector2(50, 300)]; + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias } + }; + + string aa = antialias ? string.Empty : "_NoAntialias"; + FormattableString outputDetails = $"{colorName}_A({alpha})_T({thickness}){aa}"; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.ProcessWithCanvas(options, canvas => canvas.DrawLine(pen, simplePath))); + image.DebugSave(provider, outputDetails, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput( + ImageComparer.TolerantPercentage(0.001F), + provider, + outputDetails, + appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs new file mode 100644 index 000000000..c8adc0674 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Recolor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, "Yellow", "Pink", 0.2f)] + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] + [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] + [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.6f)] + public void RecolorImage(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) + where TPixel : unmanaged, IPixel + { + Color sourceColor = TestUtils.GetColorByName(sourceColorName); + Color targetColor = TestUtils.GetColorByName(targetColorName); + RecolorBrush brush = new(sourceColor, targetColor, threshold); + + FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; + provider.RunValidatingProcessorTest(x => x.ProcessWithCanvas(canvas => canvas.Fill(brush)), testInfo); + } + + [Theory] + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Bgra32, "Yellow", "Pink", 0.5f)] + [WithTestPatternImage(100, 100, PixelTypes.Rgba32, "Red", "Blue", 0.2f)] + public void RecolorImage_InBox(TestImageProvider provider, string sourceColorName, string targetColorName, float threshold) + where TPixel : unmanaged, IPixel + { + Color sourceColor = TestUtils.GetColorByName(sourceColorName); + Color targetColor = TestUtils.GetColorByName(targetColorName); + RecolorBrush brush = new(sourceColor, targetColor, threshold); + + FormattableString testInfo = $"{sourceColorName}-{targetColorName}-{threshold}"; + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => + { + Rectangle bounds = canvas.Bounds; + Rectangle region = new(0, (bounds.Height / 2) - (bounds.Height / 4), bounds.Width, bounds.Height / 2); + canvas.Fill(brush, region); + }), + testInfo); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs new file mode 100644 index 000000000..0bd59b8c4 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs @@ -0,0 +1,376 @@ +// 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 System.Linq; +using GeoJSON.Net.Feature; +using Newtonsoft.Json; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SkiaSharp; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory(Skip = "For local testing")] + [WithSolidFilledImages(32, 32, "Black", PixelTypes.Rgba32)] + public void CompareToSkiaResults_SmallCircle(TestImageProvider provider) + { + EllipsePolygon circle = new(16, 16, 10); + + CompareToSkiaResultsImpl(provider, circle); + } + + [Theory(Skip = "For local testing")] + [WithSolidFilledImages(64, 64, "Black", PixelTypes.Rgba32)] + public void CompareToSkiaResults_StarCircle(TestImageProvider provider) + { + EllipsePolygon circle = new(32, 32, 30); + Star star = new(32, 32, 7, 10, 27); + IPath shape = circle.Clip(star); + + CompareToSkiaResultsImpl(provider, shape); + } + + private static void CompareToSkiaResultsImpl(TestImageProvider provider, IPath shape) + { + using Image image = provider.GetImage(); + image.Mutate(c => c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.White), shape))); + image.DebugSave(provider, "ImageSharp", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + using SKBitmap bitmap = new(new SKImageInfo(image.Width, image.Height)); + + using SKPath skPath = new(); + + foreach (ISimplePath loop in shape.Flatten()) + { + ReadOnlySpan points = MemoryMarshal.Cast(loop.Points.Span); + skPath.AddPoly(points.ToArray()); + } + + using SKPaint paint = new() + { + Style = SKPaintStyle.Fill, + Color = SKColors.White, + IsAntialias = true, + }; + + using SKCanvas canvas = new(bitmap); + canvas.Clear(new SKColor(0, 0, 0)); + canvas.DrawPath(skPath, paint); + + using Image skResultImage = + Image.LoadPixelData(bitmap.GetPixelSpan(), image.Width, image.Height); + skResultImage.DebugSave( + provider, + "SkiaSharp", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + + ImageSimilarityReport result = ImageComparer.Exact.CompareImagesOrFrames(image, skResultImage); + throw new ImagesSimilarityException(result.DifferencePercentageString); + } + + [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) + { + string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile)); + + PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix4x4.CreateScale(sx, sy, 1)); + + using Image image = provider.GetImage(); + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = aa > 0 }, + }; + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => + { + Pen pen = Pens.Solid(Color.White, 1.0F); + foreach (PointF[] loop in points) + { + canvas.DrawLine(pen, loop); + } + })); + + string details = $"_{System.IO.Path.GetFileName(geoJsonFile)}_{sx}x{sy}_aa{aa}"; + + image.DebugSave( + provider, + details, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(7200, 3300, "Black", PixelTypes.Rgba32)] + public void LargeGeoJson_States_Fill(TestImageProvider provider) + { + 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 static Image FillGeoJsonPolygons(TestImageProvider provider, string geoJsonFile, bool aa, Vector2 scale, Vector2 pixelOffset) + { + string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(geoJsonFile)); + + PointF[][] points = PolygonFactory.GetGeoJsonPoints(jsonContent, Matrix4x4.CreateScale(scale.X, scale.Y, 1) * Matrix4x4.CreateTranslation(pixelOffset.X, pixelOffset.Y, 0)); + + Image image = provider.GetImage(); + DrawingOptions options = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = aa }, + }; + Random rnd = new(42); + byte[] rgb = new byte[3]; + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => + { + foreach (PointF[] loop in points) + { + rnd.NextBytes(rgb); + + Color color = Color.FromPixel(new Rgb24(rgb[0], rgb[1], rgb[2])); + canvas.Fill(Brushes.Solid(color), new Polygon(new LinearLineSegment(loop))); + } + })); + + return image; + } + + [Theory] + [WithSolidFilledImages(400, 400, "Black", PixelTypes.Rgba32, 0)] + [WithSolidFilledImages(6000, 6000, "Black", PixelTypes.Rgba32, 5500)] + public void LargeGeoJson_Mississippi_Lines(TestImageProvider provider, int pixelOffset) + { + 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"); + + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1) + * Matrix4x4.CreateTranslation(pixelOffset, pixelOffset, 0); + IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); + + using Image image = provider.GetImage(); + image.Mutate(c => c.ProcessWithCanvas(canvas => + { + Pen pen = Pens.Solid(Color.White, 1.0F); + foreach (PointF[] loop in points) + { + canvas.DrawLine(pen, loop); + } + })); + + // Strict comparer, because the image is sparse: + ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); + + string details = $"PixelOffset({pixelOffset})"; + image.DebugSave(provider, details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(400 * 3, 400 * 3, "Black", PixelTypes.Rgba32, 3)] + [WithSolidFilledImages(400 * 5, 400 * 5, "Black", PixelTypes.Rgba32, 5)] + [WithSolidFilledImages(400 * 10, 400 * 10, "Black", PixelTypes.Rgba32, 10)] + public void LargeGeoJson_Mississippi_LinesScaled(TestImageProvider provider, int scale) + { + string jsonContent = File.ReadAllText(TestFile.GetInputFileFullPath(TestImages.GeoJson.States)); + + FeatureCollection features = JsonConvert.DeserializeObject(jsonContent); + + Feature missisipiGeom = features.Features.Single(f => (string)f.Properties["NAME"] == "Mississippi"); + + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1); + IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); + + using Image image = provider.GetImage(); + SolidPen pen = new(new SolidBrush(Color.White), 1.0f); + + image.Mutate(c => c.ProcessWithCanvas(canvas => + { + foreach (PointF[] loop in points) + { + IPath outline = pen.GeneratePath(new Path(loop).Transform(Matrix4x4.CreateTranslation(0.5F, 0.5F, 0))); + outline = outline.Transform(Matrix4x4.CreateScale(scale, scale, 1)); + canvas.Fill(pen.StrokeFill, outline); + } + })); + + // Strict comparer, because the image is sparse: + ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); + + string details = $"Scale({scale})"; + image.DebugSave(provider, details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + image.CompareToReferenceOutput(comparer, provider, testOutputDetails: details, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + } + + [Theory(Skip = "For local experiments only")] + [InlineData(0)] + [InlineData(5000)] + [InlineData(9000)] + public void Missisippi_Skia(int offset) + { + 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"); + + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) + * Matrix4x4.CreateScale(60, 60, 1) + * Matrix4x4.CreateTranslation(offset, offset, 0); + IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); + + SKPath path = new(); + + foreach (PointF[] pts in points.Where(p => p.Length > 2)) + { + path.MoveTo(pts[0].X, pts[0].Y); + + for (int i = 0; i < pts.Length; i++) + { + path.LineTo(pts[i].X, pts[i].Y); + } + + path.LineTo(pts[0].X, pts[0].Y); + } + + SKImageInfo imageInfo = new(10000, 10000); + + using SKPaint paint = new() + { + Style = SKPaintStyle.Stroke, + Color = SKColors.White, + StrokeWidth = 1f, + IsAntialias = true, + }; + + using SKSurface surface = SKSurface.Create(imageInfo); + SKCanvas canvas = surface.Canvas; + canvas.Clear(new SKColor(0, 0, 0)); + canvas.DrawPath(path, paint); + + string outDir = TestEnvironment.CreateOutputDirectory("Skia"); + string fn = System.IO.Path.Combine(outDir, $"Missisippi_Skia_{offset}.png"); + + using SKImage image = surface.Snapshot(); + using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); + + 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"); + + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) * Matrix4x4.CreateScale(60, 60, 1); + IReadOnlyList points = PolygonFactory.GetGeoJsonPoints(missisipiGeom, transform); + + using Image image = provider.GetImage(); + + image.Mutate(c => c.ProcessWithCanvas(canvas => + { + Pen pen = Pens.Solid(Color.White, thickness); + foreach (PointF[] loop in points) + { + canvas.Draw(pen, new Polygon(new LinearLineSegment(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"); + + Matrix4x4 transform = Matrix4x4.CreateTranslation(-87, -54, 0) * Matrix4x4.CreateScale(60, 60, 1); + 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.ProcessWithCanvas(canvas => canvas.Draw(Pens.Solid(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); + Matrix4x4 transform = Matrix4x4.CreateTranslation(250, 250, 0); + DrawingOptions options = new() { Transform = transform }; + + using Image image = provider.GetImage(); + + image.Mutate(c => c.ProcessWithCanvas(options, canvas => + { + Pen pen = Pens.Solid(Color.White, thickness); + foreach (PointF[] loop in points) + { + canvas.Draw(pen, new Polygon(new LinearLineSegment(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/Processing/ProcessWithDrawingCanvasTests.SvgPath.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.SvgPath.cs new file mode 100644 index 000000000..87063f20d --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.SvgPath.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + [Theory] + [WithBlankImage(110, 70, PixelTypes.Rgba32, "M20,30 L40,5 L60,30 L80, 55 L100, 30", "zag")] + [WithBlankImage(110, 50, PixelTypes.Rgba32, "M20,30 Q40,5 60,30 T100,30", "wave")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, "M10,350 l 50,-25 a25,25 -30 0,1 50,-25 l 50,-25 a25,50 -30 0,1 50,-25 l 50,-25 a25,75 -30 0,1 50,-25 l 50,-25 a25,100 -30 0,1 50,-25 l 50,-25", "bumpy")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, "M300,200 h-150 a150,150 0 1,0 150,-150 z", "pie_small")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, "M275,175 v-150 a150,150 0 0,0 -150,150 z", "pie_big")] + [WithBlankImage(100, 100, PixelTypes.Rgba32, "M50,50 L50,20 L80,50 z M40,60 L40,90 L10,60 z", "arrows")] + [WithBlankImage(500, 400, PixelTypes.Rgba32, "M 10 315 L 110 215 A 30 50 0 0 1 162.55 162.45 L 172.55 152.45 A 30 50 -45 0 1 215.1 109.9 L 315 10", "chopped_oval")] + public void SvgPathRenderSvgPath(TestImageProvider provider, string svgPath, string exampleImageKey) + where TPixel : unmanaged, IPixel + { + bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); + Assert.True(parsed); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.Red, 5), path); + }), + new { type = exampleImageKey }, + comparer: ImageComparer.TolerantPercentage(0.0035F)); // NET 472 x86 requires higher percentage + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs new file mode 100644 index 000000000..19837768a --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs @@ -0,0 +1,1060 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Text; +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + private const string AB = "AB\nAB"; + + private const string TestText = "Sphinx of black quartz, judge my vow\n0123456789"; + + private static readonly ImageComparer TextDrawingComparer = ImageComparer.TolerantPercentage(1e-2f); + + private static readonly ImageComparer OutlinedTextDrawingComparer = ImageComparer.TolerantPercentage(0.0069F); + + [Theory] + [WithSolidFilledImages(1276, 336, "White", PixelTypes.Rgba32, ColorFontSupport.ColrV0)] + [WithSolidFilledImages(1276, 336, "White", PixelTypes.Rgba32, ColorFontSupport.None)] + public void EmojiFontRendering(TestImageProvider provider, ColorFontSupport colorFontSupport) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 70); + FontFamily emojiFontFamily = CreateFont(TestFonts.TwemojiMozilla, 36).Family; + + Color color = Color.Black; + string text = "A short piece of text 😀 with an emoji"; + + provider.VerifyOperation( + TextDrawingComparer, + img => + { + RichTextOptions textOptions = new(font) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + FallbackFontFamilies = [emojiFontFamily], + ColorFontSupport = colorFontSupport, + Origin = new PointF(img.Width / 2, img.Height / 2) + }; + + img.Mutate(i => i.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(color), pen: null))); + }, + $"ColorFontsEnabled-{colorFontSupport == ColorFontSupport.ColrV0}"); + } + + [Theory] + [WithSolidFilledImages(400, 200, "White", PixelTypes.Rgba32)] + public void FallbackFontRendering(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // https://github.com/SixLabors/Fonts/issues/171 + FontCollection collection = new(); + Font whitney = CreateFont(TestFonts.WhitneyBook, 25); + FontFamily malgun = CreateFont(TestFonts.Malgun, 25).Family; + + Color color = Color.Black; + const string text = "亞DARKSOUL亞"; + + provider.VerifyOperation( + TextDrawingComparer, + img => + { + RichTextOptions textOptions = new(whitney) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + FallbackFontFamilies = [malgun], + KerningMode = KerningMode.Standard, + Origin = new PointF(img.Width / 2, img.Height / 2) + }; + + img.Mutate(i => i.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(color), pen: null))); + }); + } + + [Theory] + [WithSolidFilledImages(276, 336, "White", PixelTypes.Rgba32)] + public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 36); + Color color = Color.Black; + const string text = "A short piece of text"; + + using Image img = provider.GetImage(); + + // Measure the text size + FontRectangle size = TextMeasurer.MeasureSize(text, new RichTextOptions(font)); + + // Find out how much we need to scale the text to fill the space (up or down) + float scalingFactor = Math.Min(img.Width / size.Width, img.Height / size.Height); + + // Create a new font + Font scaledFont = new(font, scalingFactor * font.Size); + RichTextOptions textOptions = new(scaledFont) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + Origin = new PointF(img.Width / 2, img.Height / 2) + }; + + img.Mutate(i => i.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(color), pen: null))); + } + + [Theory] + [WithSolidFilledImages(1500, 500, "White", PixelTypes.Rgba32)] + public void DoesntThrowExceptionWhenOverlappingRightEdge_Issue688_2(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 39); + string text = new('a', 10000); + Color color = Color.Black; + PointF point = new(100, 100); + + using Image img = provider.GetImage(); + img.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, point), + text, + Brushes.Solid(color), + pen: null))); + } + + [Theory] + [WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32)] + public void OpenSansJWithNoneZeroShouldntExtendPastGlyphe(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 50); + Color color = Color.Black; + + using Image img = provider.GetImage(); + img.Mutate(ctx => ctx.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, new PointF(-50, 2)), + TestText, + Brushes.Solid(Color.Black), + pen: null))); + + Assert.Equal(Color.White.ToPixel(), img[173, 2]); + } + + [Theory] + [WithSolidFilledImages(20, 50, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, "i")] + [WithSolidFilledImages(200, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.SixLaborsSampleAB, AB)] + [WithSolidFilledImages(900, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, TestText)] + [WithSolidFilledImages(400, 45, "White", PixelTypes.Rgba32, 20, 0, 0, TestFonts.OpenSans, TestText)] + [WithSolidFilledImages(1100, 200, "White", PixelTypes.Rgba32, 50, 150, 50, TestFonts.OpenSans, TestText)] + public void FontShapesAreRenderedCorrectly( + TestImageProvider provider, + int fontSize, + int x, + int y, + string fontName, + string text) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, fontSize); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, new PointF(x, y)), + text, + Brushes.Solid(Color.Black), + pen: null)), + $"{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(50, 50, "White", PixelTypes.Rgba32, 50, 25, 25, TestFonts.OpenSans, "i", 45, 25, 25)] + [WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32, 50, 100, 100, TestFonts.SixLaborsSampleAB, AB, 45, 100, 100)] + [WithSolidFilledImages(1100, 1100, "White", PixelTypes.Rgba32, 50, 550, 550, TestFonts.OpenSans, TestText, 45, 550, 550)] + [WithSolidFilledImages(400, 400, "White", PixelTypes.Rgba32, 20, 200, 200, TestFonts.OpenSans, TestText, 45, 200, 200)] + public void FontShapesAreRenderedCorrectly_WithRotationApplied( + TestImageProvider provider, + int fontSize, + int x, + int y, + string fontName, + string text, + float angle, + float rotationOriginX, + float rotationOriginY) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, fontSize); + float radians = GeometryUtilities.DegreeToRadian(angle); + + RichTextOptions textOptions = new(font) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + Origin = new PointF(x, y) + }; + + Matrix4x4 transform = new(Matrix3x2.CreateRotation(radians, new Vector2(rotationOriginX, rotationOriginY))); + DrawingOptions drawingOptions = new() { Transform = transform }; + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.Black), + pen: null)), + $"F({fontName})-S({fontSize})-A({angle})-{ToTestOutputDisplayText(text)}-({x},{y})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(50, 50, "White", PixelTypes.Rgba32, 50, 25, 25, TestFonts.OpenSans, "i", -12, 0, 25, 25)] + [WithSolidFilledImages(200, 200, "White", PixelTypes.Rgba32, 50, 100, 100, TestFonts.SixLaborsSampleAB, AB, 10, 0, 100, 100)] + [WithSolidFilledImages(1100, 1100, "White", PixelTypes.Rgba32, 50, 550, 550, TestFonts.OpenSans, TestText, 0, 10, 550, 550)] + [WithSolidFilledImages(400, 400, "White", PixelTypes.Rgba32, 20, 200, 200, TestFonts.OpenSans, TestText, 0, -10, 200, 200)] + public void FontShapesAreRenderedCorrectly_WithSkewApplied( + TestImageProvider provider, + int fontSize, + int x, + int y, + string fontName, + string text, + float angleX, + float angleY, + float rotationOriginX, + float rotationOriginY) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, fontSize); + float radianX = GeometryUtilities.DegreeToRadian(angleX); + float radianY = GeometryUtilities.DegreeToRadian(angleY); + + RichTextOptions textOptions = new(font) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + TextAlignment = TextAlignment.Center, + Origin = new PointF(x, y) + }; + + Matrix4x4 transform = new(Matrix3x2.CreateSkew(radianX, radianY, new Vector2(rotationOriginX, rotationOriginY))); + DrawingOptions drawingOptions = new() { Transform = transform }; + + provider.RunValidatingProcessorTest( + ctx => ctx.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.Black), + pen: null)), + $"F({fontName})-S({fontSize})-A({angleX},{angleY})-{ToTestOutputDisplayText(text)}-({x},{y})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + /// + /// Based on: + /// https://github.com/SixLabors/ImageSharp/issues/572 + /// + [Theory] + [WithSolidFilledImages(2480, 3508, "White", PixelTypes.Rgba32)] + public void FontShapesAreRenderedCorrectly_LargeText( + TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 36); + + StringBuilder sb = new(); + string str = Repeat(" ", 78) + "THISISTESTWORDSTHISISTESTWORDSTHISISTESTWORDSTHISISTESTWORDSTHISISTESTWORDS"; + sb.Append(str); + + string newLines = Repeat("\r\n", 61); + sb.Append(newLines); + + for (int i = 0; i < 10; i++) + { + sb.AppendLine(str); + } + + // Strict comparer, because the image is sparse: + ImageComparer comparer = ImageComparer.TolerantPercentage(0.0001F); + + provider.VerifyOperation( + comparer, + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, new PointF(10, 1)), + sb.ToString(), + Brushes.Solid(Color.Black), + pen: null))), + false, + false); + } + + [Theory] + [WithSolidFilledImages(400, 550, "White", PixelTypes.Rgba32, 1, 5, true)] + [WithSolidFilledImages(400, 550, "White", PixelTypes.Rgba32, 1.5, 3, true)] + [WithSolidFilledImages(400, 550, "White", PixelTypes.Rgba32, 2, 2, true)] + [WithSolidFilledImages(400, 100, "White", PixelTypes.Rgba32, 1, 5, false)] + [WithSolidFilledImages(400, 100, "White", PixelTypes.Rgba32, 1.5, 3, false)] + [WithSolidFilledImages(400, 100, "White", PixelTypes.Rgba32, 2, 2, false)] + public void FontShapesAreRenderedCorrectly_WithLineSpacing( + TestImageProvider provider, + float lineSpacing, + int lineCount, + bool wrap) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 16); + + StringBuilder sb = new(); + string str = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna."; + + for (int i = 0; i < lineCount; i++) + { + sb.AppendLine(str); + } + + RichTextOptions textOptions = new(font) + { + KerningMode = KerningMode.Standard, + VerticalAlignment = VerticalAlignment.Top, + HorizontalAlignment = HorizontalAlignment.Left, + LineSpacing = lineSpacing, + Origin = new PointF(10, 1) + }; + + if (wrap) + { + textOptions.WrappingLength = 300; + } + + Color color = Color.Black; + + // NET472 is 0.0045 different. + ImageComparer comparer = ImageComparer.TolerantPercentage(0.0046F); + + provider.VerifyOperation( + comparer, + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + textOptions, + sb.ToString(), + Brushes.Solid(color), + pen: null))), + $"linespacing_{lineSpacing}_linecount_{lineCount}_wrap_{wrap}", + false, + false); + } + + [Theory] + [WithSolidFilledImages(200, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.SixLaborsSampleAB, AB)] + [WithSolidFilledImages(900, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, TestText)] + [WithSolidFilledImages(1100, 200, "White", PixelTypes.Rgba32, 50, 150, 50, TestFonts.OpenSans, TestText)] + public void FontShapesAreRenderedCorrectlyWithAPen( + TestImageProvider provider, + int fontSize, + int x, + int y, + string fontName, + string text) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, fontSize); + Color color = Color.Black; + + provider.VerifyOperation( + OutlinedTextDrawingComparer, + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(new Font(font, fontSize), new PointF(x, y)), + text, + brush: null, + pen: Pens.Solid(color, 1)))), + $"pen_{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(200, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.SixLaborsSampleAB, AB)] + [WithSolidFilledImages(900, 150, "White", PixelTypes.Rgba32, 50, 0, 0, TestFonts.OpenSans, TestText)] + [WithSolidFilledImages(1100, 200, "White", PixelTypes.Rgba32, 50, 150, 50, TestFonts.OpenSans, TestText)] + public void FontShapesAreRenderedCorrectlyWithAPenPatterned( + TestImageProvider provider, + int fontSize, + int x, + int y, + string fontName, + string text) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, fontSize); + Color color = Color.Black; + + provider.VerifyOperation( + OutlinedTextDrawingComparer, + img => img.Mutate(c => c.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(new Font(font, fontSize), new PointF(x, y)), + text, + brush: null, + pen: Pens.DashDot(color, 3)))), + $"pen_{fontName}-{fontSize}-{ToTestOutputDisplayText(text)}-({x},{y})", + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(1000, 1500, "White", PixelTypes.Rgba32, TestFonts.OpenSans)] + public void TextPositioningIsRobust(TestImageProvider provider, string fontName) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, 30); + + string text = Repeat( + "Beware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch!\n", + 20); + + RichTextOptions textOptions = new(font) + { + WrappingLength = 1000, + Origin = new PointF(10, 50) + }; + + string details = fontName.Replace(" ", string.Empty); + + // Based on the reported 0.1755% difference with AccuracyMultiple = 8 + // We should avoid quality regressions leading to higher difference! + ImageComparer comparer = ImageComparer.TolerantPercentage(0.2f); + + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null)), + details, + comparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } + + [Fact] + public void CanDrawTextWithEmptyPath() + { + // The following font/text combination generates an empty path. + Font font = CreateFont(TestFonts.WendyOne, 72); + const string text = "Hello\0World"; + RichTextOptions textOptions = new(font); + FontRectangle textSize = TextMeasurer.MeasureSize(text, textOptions); + + Assert.NotEqual(FontRectangle.Empty, textSize); + + using Image image = new(Configuration.Default, (int)textSize.Width + 20, (int)textSize.Height + 20); + image.Mutate(x => x.ProcessWithCanvas(canvas => canvas.DrawText( + CreateTextOptionsAt(font, Vector2.Zero), + text, + Brushes.Solid(Color.Black), + pen: null))); + } + + [Theory] + [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 32, 75F)] + [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 40, 90F)] + public void CanRotateFilledFont_Issue175( + TestImageProvider provider, + string fontName, + int fontSize, + float angle) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, fontSize); + const string text = "QuickTYZ"; + AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle); + + RichTextOptions textOptions = new(font); + FontRectangle renderable = TextMeasurer.MeasureRenderableBounds(text, textOptions); + Matrix4x4 transform = new(builder.BuildMatrix(Rectangle.Round(new RectangleF(renderable.X, renderable.Y, renderable.Width, renderable.Height)))); + + DrawingOptions drawingOptions = new() { Transform = transform }; + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + Brushes.Solid(Color.Black), + pen: null)), + $"F({fontName})-S({fontSize})-A({angle})-{ToTestOutputDisplayText(text)})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 32, 75F, 1)] + [WithSolidFilledImages(300, 200, nameof(Color.White), PixelTypes.Rgba32, TestFonts.OpenSans, 40, 90F, 2)] + public void CanRotateOutlineFont_Issue175( + TestImageProvider provider, + string fontName, + int fontSize, + float angle, + int strokeWidth) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(fontName, fontSize); + const string text = "QuickTYZ"; + AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle); + + RichTextOptions textOptions = new(font); + FontRectangle renderable = TextMeasurer.MeasureRenderableBounds(text, textOptions); + Matrix4x4 transform = new(builder.BuildMatrix(Rectangle.Round(new RectangleF(renderable.X, renderable.Y, renderable.Width, renderable.Height)))); + + DrawingOptions drawingOptions = new() { Transform = transform }; + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(drawingOptions, canvas => canvas.DrawText( + textOptions, + text, + brush: null, + pen: Pens.Solid(Color.Black, strokeWidth))), + $"F({fontName})-S({fontSize})-A({angle})-STR({strokeWidth})-{ToTestOutputDisplayText(text)})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(500, 200, nameof(Color.Black), PixelTypes.Rgba32, 32)] + [WithSolidFilledImages(500, 300, nameof(Color.Black), PixelTypes.Rgba32, 40)] + public void DrawRichText( + TestImageProvider provider, + int fontSize) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, fontSize); + Font font2 = CreateFont(TestFonts.OpenSans, fontSize * 1.5f); + const string text = "The quick brown fox jumps over the lazy dog"; + + RichTextOptions textOptions = new(font) + { + Origin = new Vector2(15), + WrappingLength = 400, + TextRuns = + [ + new RichTextRun + { + Start = 0, + End = 3, + OverlinePen = Pens.Solid(Color.Yellow, 1), + StrikeoutPen = Pens.Solid(Color.HotPink, 5), + }, + + new RichTextRun + { + Start = 4, + End = 10, + TextDecorations = TextDecorations.Strikeout, + StrikeoutPen = Pens.Solid(Color.Red), + OverlinePen = Pens.Solid(Color.Green, 9), + Brush = Brushes.Solid(Color.Red), + }, + + new RichTextRun + { + Start = 10, + End = 13, + Font = font2, + TextDecorations = TextDecorations.Strikeout, + StrikeoutPen = Pens.Solid(Color.White, 6), + OverlinePen = Pens.Solid(Color.Orange, 2), + }, + + new RichTextRun + { + Start = 19, + End = 23, + TextDecorations = TextDecorations.Underline, + UnderlinePen = Pens.Dot(Color.Fuchsia, 5), + Brush = Brushes.Solid(Color.Blue), + }, + + new RichTextRun + { + Start = 23, + End = 25, + TextDecorations = TextDecorations.Underline, + UnderlinePen = Pens.Solid(Color.White), + } + ] + }; + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), + $"RichText-F({fontSize})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(500, 200, nameof(Color.Black), PixelTypes.Rgba32, 32)] + [WithSolidFilledImages(500, 300, nameof(Color.Black), PixelTypes.Rgba32, 40)] + public void DrawRichTextArabic( + TestImageProvider provider, + int fontSize) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.MeQuranVolyNewmet, fontSize); + string text = "بِسْمِ ٱللَّهِ ٱلرَّحْمَٟنِ ٱلرَّحِيمِ"; + + RichTextOptions textOptions = new(font) + { + Origin = new Vector2(15), + WrappingLength = 400, + TextRuns = + [ + new RichTextRun { Start = 0, End = CodePoint.GetCodePointCount(text.AsSpan()), TextDecorations = TextDecorations.Underline } + ] + }; + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), + $"RichText-Arabic-F({fontSize})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(500, 200, nameof(Color.Black), PixelTypes.Rgba32, 32)] + [WithSolidFilledImages(500, 300, nameof(Color.Black), PixelTypes.Rgba32, 40)] + public void DrawRichTextRainbow( + TestImageProvider provider, + int fontSize) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, fontSize); + const string text = "The quick brown fox jumps over the lazy dog"; + + SolidPen[] colors = + [ + new SolidPen(Color.Red), + new SolidPen(Color.Orange), + new SolidPen(Color.Yellow), + new SolidPen(Color.Green), + new SolidPen(Color.Blue), + new SolidPen(Color.Indigo), + new SolidPen(Color.Violet) + ]; + + List runs = []; + for (int i = 0; i < text.Length; i++) + { + SolidPen pen = colors[i % colors.Length]; + runs.Add(new RichTextRun + { + Start = i, + End = i + 1, + UnderlinePen = pen + }); + } + + RichTextOptions textOptions = new(font) + { + Origin = new Vector2(15), + WrappingLength = 400, + TextRuns = runs, + }; + + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), + $"RichText-Rainbow-F({fontSize})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, "M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50", "spiral")] + [WithSolidFilledImages(350, 350, nameof(Color.Black), PixelTypes.Rgba32, "M275 175 A100 100 0 1 1 275 174", "circle")] + [WithSolidFilledImages(120, 120, nameof(Color.Black), PixelTypes.Rgba32, "M50,10 L 90 90 L 10 90 L50 10", "triangle")] + public void CanDrawRichTextAlongPathHorizontal(TestImageProvider provider, string svgPath, string exampleImageKey) + where TPixel : unmanaged, IPixel + { + bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); + Assert.True(parsed); + + Font font = CreateFont(TestFonts.OpenSans, 13); + + const string text = "Quick brown fox jumps over the lazy dog."; + RichTextRun run = new() + { + Start = 0, + End = text.GetGraphemeCount(), + StrikeoutPen = new SolidPen(Color.Red) + }; + + RichTextOptions textOptions = new(font) + { + WrappingLength = path.ComputeLength(), + VerticalAlignment = VerticalAlignment.Bottom, + HorizontalAlignment = HorizontalAlignment.Left, + Path = path, + TextRuns = [run] + }; + + provider.RunValidatingProcessorTest( + x => x.ProcessWithCanvas(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.White), pen: null)), + $"RichText-Path-({exampleImageKey})", + TextDrawingComparer, + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: true); + } + + [Theory] + [WithBlankImage(100, 100, PixelTypes.Rgba32, "M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50", "spiral")] + [WithBlankImage(350, 350, PixelTypes.Rgba32, "M275 175 A100 100 0 1 1 275 174", "circle")] + [WithBlankImage(120, 120, PixelTypes.Rgba32, "M50,10 L 90 90 L 10 90 L50 10", "triangle")] + public void CanDrawTextAlongPathHorizontal(TestImageProvider provider, string svgPath, string exampleImageKey) + where TPixel : unmanaged, IPixel + { + bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); + Assert.True(parsed); + + const string text = "Quick brown fox jumps over the lazy dog."; + + Font font = CreateFont(TestFonts.OpenSans, 13); + RichTextOptions textOptions = new(font) + { + WrappingLength = path.ComputeLength(), + VerticalAlignment = VerticalAlignment.Bottom, + HorizontalAlignment = HorizontalAlignment.Left, + TextRuns = [new RichTextRun { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Strikeout }], + }; + + IPathCollection glyphs = TextBuilder.GeneratePaths(text, path, textOptions); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.Red, 1), path); + canvas.Fill(Brushes.Solid(Color.Black), glyphs); + }), + new { type = exampleImageKey }, + comparer: ImageComparer.TolerantPercentage(0.0025f)); + } + + [Theory] + [WithBlankImage(350, 350, PixelTypes.Rgba32, "M225 175 A50 50 0 1 1 225 174", "circle")] + [WithBlankImage(250, 250, PixelTypes.Rgba32, "M100,60 L 140 140 L 60 140 L100 60", "triangle")] + public void CanDrawTextAlongPathVertical(TestImageProvider provider, string svgPath, string exampleImageKey) + where TPixel : unmanaged, IPixel + { + bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); + Assert.True(parsed); + + Font font = CreateFont(TestFonts.OpenSans, 13); + RichTextOptions textOptions = new(font) + { + WrappingLength = path.ComputeLength() / 4, + VerticalAlignment = VerticalAlignment.Bottom, + HorizontalAlignment = HorizontalAlignment.Left, + LayoutMode = LayoutMode.VerticalLeftRight + }; + + const string text = "Quick brown fox jumps over the lazy dog."; + IPathCollection glyphs = TextBuilder.GeneratePaths(text, path, textOptions); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Draw(Pens.Solid(Color.Red, 1), path); + canvas.Fill(Brushes.Solid(Color.Black), glyphs); + }), + new { type = exampleImageKey }, + comparer: ImageComparer.TolerantPercentage(0.002f)); + } + + [Theory] + [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] + public void PathAndTextDrawingMatch(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // https://github.com/SixLabors/ImageSharp.Drawing/issues/234 + Font font = CreateFont(TestFonts.NettoOffc, 300); + const string text = "all"; + + provider.VerifyOperation( + TextDrawingComparer, + img => + { + foreach (HorizontalAlignment ha in (HorizontalAlignment[])Enum.GetValues(typeof(HorizontalAlignment))) + { + foreach (VerticalAlignment va in (VerticalAlignment[])Enum.GetValues(typeof(VerticalAlignment))) + { + TextOptions to = new(font) + { + HorizontalAlignment = ha, + VerticalAlignment = va, + }; + + FontRectangle bounds = TextMeasurer.MeasureBounds(text, to); + float x = (img.Size.Width - bounds.Width) / 2; + PointF[] pathLine = + [ + new PointF(x, 500), + new PointF(x + bounds.Width, 500) + ]; + + IPath path = new PathBuilder().AddLine(pathLine[0], pathLine[1]).Build(); + + RichTextOptions rto = new(font) + { + Origin = pathLine[0], + HorizontalAlignment = ha, + VerticalAlignment = va, + }; + + IPathCollection tb = TextBuilder.GeneratePaths(text, path, to); + + img.Mutate(i => i.ProcessWithCanvas(canvas => + { + canvas.DrawLine(new SolidPen(Color.Red, 30), pathLine); + canvas.DrawText(rto, text, Brushes.Solid(Color.Black), pen: null); + canvas.Fill(Brushes.ForwardDiagonal(Color.HotPink), tb); + })); + } + } + }); + } + + [Theory] + [WithBlankImage(500, 400, PixelTypes.Rgba32)] + public void CanFillTextVertical(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 36); + Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); + + const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; + RichTextOptions textOptions = new(font) + { + Origin = new Vector2(0, 0), + FallbackFontFamilies = [fallback.Family], + WrappingLength = 300, + LayoutMode = LayoutMode.VerticalLeftRight, + TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } + ] + }; + + IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, textOptions); + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawGlyphs(Brushes.Solid(Color.Black), Pens.Solid(Color.Black, 1F), glyphs); + }), + comparer: ImageComparer.TolerantPercentage(0.002f)); + } + + [Theory] + [WithBlankImage(500, 400, PixelTypes.Rgba32)] + public void CanFillTextVerticalMixed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 36); + Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); + + const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; + RichTextOptions textOptions = new(font) + { + FallbackFontFamilies = [fallback.Family], + WrappingLength = 400, + LayoutMode = LayoutMode.VerticalMixedLeftRight, + TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } + ] + }; + + IPathCollection glyphs = TextBuilder.GeneratePaths(text, textOptions); + + DrawingOptions options = new() { ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } }; + + provider.RunValidatingProcessorTest( + c => + { + c.ProcessWithCanvas(canvas => canvas.Fill(Brushes.Solid(Color.White))); + c.ProcessWithCanvas(options, canvas => canvas.Fill(Brushes.Solid(Color.Black), glyphs)); + }, + comparer: ImageComparer.TolerantPercentage(0.002f)); + } + + [Theory] + [WithBlankImage(500, 400, PixelTypes.Rgba32)] + public void CanDrawTextVertical(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 36); + Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); + + const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; + RichTextOptions textOptions = new(font) + { + FallbackFontFamilies = [fallback.Family], + WrappingLength = 400, + LayoutMode = LayoutMode.VerticalLeftRight, + LineSpacing = 1.4F, + TextRuns = [ + new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } + ] + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), + comparer: ImageComparer.TolerantPercentage(0.002f)); + } + + [Theory] + [WithBlankImage(48, 935, PixelTypes.Rgba32)] + public void CanDrawTextVertical2(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (SystemFonts.TryGet("Yu Gothic", out FontFamily fontFamily)) + { + Font font = fontFamily.CreateFont(30F); + const string text = "あいうえお、「こんにちはー」。もしもし。ABCDEFG 日本語"; + RichTextOptions textOptions = new(font) + { + LayoutMode = LayoutMode.VerticalLeftRight, + LineSpacing = 1.4F, + TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline }] + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), + comparer: ImageComparer.TolerantPercentage(0.002f)); + } + } + + [Theory] + [WithBlankImage(500, 400, PixelTypes.Rgba32)] + public void CanDrawTextVerticalMixed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 36); + Font fallback = CreateFont(TestFonts.NotoSansKRRegular, 36); + + const string text = "한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo 한국어 hangugeo"; + RichTextOptions textOptions = new(font) + { + FallbackFontFamilies = [fallback.Family], + WrappingLength = 400, + LayoutMode = LayoutMode.VerticalMixedLeftRight, + LineSpacing = 1.4F, + TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline }] + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), + comparer: ImageComparer.TolerantPercentage(0.002f)); + } + + [Theory] + [WithBlankImage(48, 839, PixelTypes.Rgba32)] + public void CanDrawTextVerticalMixed2(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (SystemFonts.TryGet("Yu Gothic", out FontFamily fontFamily)) + { + Font font = fontFamily.CreateFont(30F); + const string text = "あいうえお、「こんにちはー」。もしもし。ABCDEFG 日本語"; + RichTextOptions textOptions = new(font) + { + LayoutMode = LayoutMode.VerticalMixedLeftRight, + LineSpacing = 1.4F, + TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } + ] + }; + + provider.RunValidatingProcessorTest( + c => c.ProcessWithCanvas(canvas => + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + }), + comparer: ImageComparer.TolerantPercentage(0.002f)); + } + } + + [Theory] + [WithBlankImage(200, 200, PixelTypes.Rgba32)] + public void CanRenderTextOutOfBoundsIssue301(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => provider.VerifyOperation( + ImageComparer.TolerantPercentage(0.01f), + img => + { + Font font = CreateFont(TestFonts.OpenSans, 70); + + const string txt = "V"; + FontRectangle size = TextMeasurer.MeasureBounds(txt, new TextOptions(font)); + + img.Mutate(x => x.Resize((int)size.Width, (int)size.Height)); + + RichTextOptions options = new(font) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Origin = new Vector2(size.Width / 2, size.Height / 2) + }; + + LinearGradientBrush brush = new( + new PointF(0, 0), + new PointF(20, 20), + GradientRepetitionMode.Repeat, + new ColorStop(0, Color.Red), + new ColorStop(0.5f, Color.Green), + new ColorStop(0.5f, Color.Yellow), + new ColorStop(1f, Color.Blue)); + + img.Mutate(m => m.ProcessWithCanvas(canvas => canvas.DrawText(options, txt, brush, pen: null))); + }, + false, + false); + + private static RichTextOptions CreateTextOptionsAt(Font font, PointF origin) + => new(font) { Origin = origin }; + + private static RichTextOptions CreateTextOptionsAt(Font font, Vector2 origin) + => new(font) { Origin = new PointF(origin.X, origin.Y) }; + + private static string Repeat(string str, int times) => string.Concat(Enumerable.Repeat(str, times)); + + private static string ToTestOutputDisplayText(string text) + { + string fnDisplayText = text.Replace("\n", string.Empty); + return fnDisplayText[..Math.Min(fnDisplayText.Length, 4)]; + } + + private static Font CreateFont(string fontName, float size) + => TestFontUtilities.GetFont(fontName, size); +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.cs new file mode 100644 index 000000000..87de3c0a5 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +[GroupOutput("Drawing")] +public partial class ProcessWithDrawingCanvasTests +{ +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index ffea506c6..9f5a910a9 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -3,7 +3,6 @@ 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; @@ -11,18 +10,6 @@ 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() { @@ -32,43 +19,7 @@ public void GetDefaultDrawingBackendFromConfiguration_AlwaysReturnsDefaultInstan 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()); + Assert.Same(DefaultDrawingBackend.Instance, first); } [Fact] @@ -94,40 +45,22 @@ public void SetDrawingBackendOnProcessingContext_RoundTrips() 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( + public void FlushCompositions( Configuration configuration, - ImageFrame source, - IPath path, - Brush brush, - in GraphicsOptions graphicsOptions, - in RasterizerOptions rasterizerOptions, - Rectangle brushBounds, - MemoryAllocator allocator) + ICanvasFrame target, + CompositionScene compositionScene) where TPixel : unmanaged, IPixel { } - public void RasterizeCoverage( - IPath path, - in RasterizerOptions rasterizerOptions, - MemoryAllocator allocator, - Buffer2D destination) - { - } + public bool TryReadRegion( + Configuration configuration, + ICanvasFrame target, + Rectangle sourceRectangle, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel + => false; } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/IntersectionsGenerator.py b/tests/ImageSharp.Drawing.Tests/Rasterization/IntersectionsGenerator.py similarity index 100% rename from tests/ImageSharp.Drawing.Tests/Shapes/Scan/IntersectionsGenerator.py rename to tests/ImageSharp.Drawing.Tests/Rasterization/IntersectionsGenerator.py diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/NumericCornerCases.jpg b/tests/ImageSharp.Drawing.Tests/Rasterization/NumericCornerCases.jpg new file mode 100644 index 000000000..4d47bc457 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/NumericCornerCases.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14120aec3ece697f2e69559b542c59fb4008083d1e9300e9174700223551645e +size 623151 diff --git a/tests/ImageSharp.Drawing.Tests/Rasterization/SimplePolygon_AllEmitCases.png b/tests/ImageSharp.Drawing.Tests/Rasterization/SimplePolygon_AllEmitCases.png new file mode 100644 index 000000000..fb6d09308 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Rasterization/SimplePolygon_AllEmitCases.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99b523663db7a64a03a836719b54cf64ded5a1128317a95282e866cb9f368ab4 +size 28411 diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs index 3ed572163..846c14de0 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; /// diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs index 1968ec8af..a292c0d08 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PathBuilderTests.cs @@ -164,7 +164,7 @@ public void DefaultTransform() Vector2 point1 = new(10, 10); Vector2 point2 = new(10, 90); Vector2 point3 = new(50, 50); - Matrix3x2 matrix = Matrix3x2.CreateTranslation(new Vector2(5, 5)); + Matrix4x4 matrix = Matrix4x4.CreateTranslation(5, 5, 0); PathBuilder builder = new(matrix); builder.AddLines(point1, point2, point3); @@ -178,7 +178,7 @@ public void SetTransform() Vector2 point1 = new(10, 10); Vector2 point2 = new(10, 90); Vector2 point3 = new(50, 50); - Matrix3x2 matrix = Matrix3x2.CreateTranslation(new Vector2(100, 100)); + Matrix4x4 matrix = Matrix4x4.CreateTranslation(100, 100, 0); PathBuilder builder = new(); builder.AddLines(point1, point2, point3); @@ -202,7 +202,7 @@ public void SetOriginLeaveMatrix() Vector2 point2 = new(10, 90); Vector2 point3 = new(50, 50); Vector2 origin = new(-50, -100); - PathBuilder builder = new(Matrix3x2.CreateScale(10)); + PathBuilder builder = new(Matrix4x4.CreateScale(10)); builder.AddLines(point1, point2, point3); diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs index 77314de9e..688a0a3e7 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PathExtentionTests.cs @@ -23,18 +23,18 @@ public void RotateInRadians() { const float Angle = (float)Math.PI; - this.mockPath.Setup(x => x.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(x => x.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here - Matrix3x2 targetMatrix = Matrix3x2.CreateRotation(Angle, RectangleF.Center(this.bounds)); + Matrix4x4 targetMatrix = new(Matrix3x2.CreateRotation(Angle, RectangleF.Center(this.bounds))); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.Rotate(Angle); - this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); } [Fact] @@ -42,20 +42,20 @@ public void RotateInDegrees() { const float Angle = 90; - this.mockPath.Setup(x => x.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(x => x.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here const float Radians = (float)(Math.PI * Angle / 180.0); - Matrix3x2 targetMatrix = Matrix3x2.CreateRotation(Radians, RectangleF.Center(this.bounds)); + Matrix4x4 targetMatrix = new(Matrix3x2.CreateRotation(Radians, RectangleF.Center(this.bounds))); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.RotateDegree(Angle); - this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); } [Fact] @@ -63,18 +63,18 @@ public void TranslateVector() { Vector2 point = new(98, 120); - this.mockPath.Setup(x => x.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(x => x.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here - Matrix3x2 targetMatrix = Matrix3x2.CreateTranslation(point); + Matrix4x4 targetMatrix = Matrix4x4.CreateTranslation(point.X, point.Y, 0); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.Translate(point); - this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(x => x.Transform(It.IsAny()), Times.Once); } [Fact] @@ -83,17 +83,17 @@ public void TranslateXY() const float X = 76; const float Y = 7; - this.mockPath.Setup(p => p.Transform(It.IsAny())) - .Callback(m => + this.mockPath.Setup(p => p.Transform(It.IsAny())) + .Callback(m => { // validate matrix in here - Matrix3x2 targetMatrix = Matrix3x2.CreateTranslation(new Vector2(X, Y)); + Matrix4x4 targetMatrix = Matrix4x4.CreateTranslation(X, Y, 0); Assert.Equal(targetMatrix, m); }).Returns(this.mockPath.Object); this.mockPath.Object.Translate(X, Y); - this.mockPath.Verify(p => p.Transform(It.IsAny()), Times.Once); + this.mockPath.Verify(p => p.Transform(It.IsAny()), Times.Once); } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs deleted file mode 100644 index 7943ac2df..000000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonClipper/ClipperTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Shapes.PolygonGeometry; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.PolygonClipper; - -public class ClipperTests -{ - private readonly RectangularPolygon bigSquare = new(10, 10, 40, 40); - private readonly RectangularPolygon hole = new(20, 20, 10, 10); - private readonly RectangularPolygon topLeft = new(0, 0, 20, 20); - private readonly RectangularPolygon topRight = new(30, 0, 20, 20); - private readonly RectangularPolygon topMiddle = new(20, 0, 10, 20); - - private readonly Polygon bigTriangle = new(new LinearLineSegment( - new Vector2(10, 10), - new Vector2(200, 150), - new Vector2(50, 300))); - - private readonly Polygon littleTriangle = new(new LinearLineSegment( - new Vector2(37, 85), - new Vector2(130, 40), - new Vector2(65, 137))); - - private static ComplexPolygon Clip(IPath shape, params IPath[] hole) - => ClippedShapeGenerator.GenerateClippedShapes(BooleanOperation.Difference, shape, hole); - - [Fact] - public void OverlappingTriangleCutRightSide() - { - Polygon triangle = new(new LinearLineSegment( - new Vector2(0, 50), - new Vector2(70, 0), - new Vector2(50, 100))); - - Polygon cutout = new(new LinearLineSegment( - new Vector2(20, 0), - new Vector2(70, 0), - new Vector2(70, 100), - new Vector2(20, 100))); - - ComplexPolygon shapes = Clip(triangle, cutout); - Assert.Single(shapes.Paths); - Assert.DoesNotContain(triangle, shapes.Paths); - } - - [Fact] - public void OverlappingTriangles() - { - ComplexPolygon shapes = Clip(this.bigTriangle, this.littleTriangle); - Assert.Single(shapes.Paths); - PointF[] path = shapes.Paths.Single().Flatten().First().Points.ToArray(); - - Assert.Equal(7, path.Length); - foreach (Vector2 p in this.bigTriangle.Flatten().First().Points.ToArray()) - { - Assert.Contains(p, path, new ApproximateFloatComparer(RectangularPolygonValueComparer.DefaultTolerance)); - } - } - - [Fact] - public void NonOverlapping() - { - IEnumerable shapes = Clip(this.topLeft, this.topRight).Paths - .OfType().Select(x => (RectangularPolygon)x); - - Assert.Single(shapes); - Assert.Contains( - shapes, x => RectangularPolygonValueComparer.Equals(this.topLeft, x)); - - Assert.DoesNotContain(this.topRight, shapes); - } - - [Fact] - public void OverLappingReturns1NewShape() - { - ComplexPolygon shapes = Clip(this.bigSquare, this.topLeft); - - 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 = Clip(this.bigSquare, this.hole).Paths - .OfType().Select(x => (RectangularPolygon)x); - - Assert.Equal(2, shapes.Count()); - - Assert.Contains(shapes, x => RectangularPolygonValueComparer.Equals(this.bigSquare, x)); - Assert.Contains(shapes, x => RectangularPolygonValueComparer.Equals(this.hole, x)); - } - - [Fact] - public void TouchingButNotOverlapping() - { - 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] - public void ClippingRectanglesCreateCorrectNumberOfPoints() - { - IEnumerable paths = new RectangularPolygon(10, 10, 40, 40) - .Clip(new RectangularPolygon(20, 0, 20, 20)) - .Flatten(); - - Assert.Single(paths); - PointF[] points = paths.First().Points.ToArray(); - - Assert.Equal(8, points.Length); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs index 3af46a7a8..e1c4a5f89 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PolygonTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; + namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; public class PolygonTests diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs index 9ea049e3e..aa6406b26 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; @@ -144,7 +145,7 @@ public void ShapePaths() public void TransformIdentityReturnsShapeObject() { IPath shape = new RectangularPolygon(0, 0, 200, 60); - IPath transformedShape = shape.Transform(Matrix3x2.Identity); + IPath transformedShape = shape.Transform(Matrix4x4.Identity); Assert.Same(shape, transformedShape); } @@ -154,7 +155,7 @@ public void Transform() { IPath shape = new RectangularPolygon(0, 0, 200, 60); - IPath newShape = shape.Transform(new Matrix3x2(0, 1, 1, 0, 20, 2)); + IPath newShape = shape.Transform(new Matrix4x4(new Matrix3x2(0, 1, 1, 0, 20, 2))); Assert.Equal(new PointF(20, 2), newShape.Bounds.Location); Assert.Equal(new SizeF(60, 200), newShape.Bounds.Size); diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs deleted file mode 100644 index 382d6fa86..000000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/DefaultRasterizerTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -// 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/NumericCornerCases.jpg b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCases.jpg deleted file mode 100644 index 91bdf70fd..000000000 Binary files a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/NumericCornerCases.jpg and /dev/null differ diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs deleted file mode 100644 index 19842362c..000000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SharpBlazeRasterizerTests.cs +++ /dev/null @@ -1,128 +0,0 @@ -// 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/Shapes/Scan/SimplePolygon_AllEmitCases.png b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SimplePolygon_AllEmitCases.png deleted file mode 100644 index 7b188191a..000000000 Binary files a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/SimplePolygon_AllEmitCases.png and /dev/null differ diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TessellatedMultipolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TessellatedMultipolygonTests.cs deleted file mode 100644 index 6dae4fb1d..000000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TessellatedMultipolygonTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Shapes; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; - -public class TessellatedMultipolygonTests -{ - private static MemoryAllocator MemoryAllocator => Configuration.Default.MemoryAllocator; - - private static void VerifyRing(TessellatedMultipolygon.Ring ring, PointF[] originalPoints, bool originalPositive, bool isHole) - { - ReadOnlySpan points = ring.Vertices; - - Assert.Equal(originalPoints.Length + 1, points.Length); - Assert.Equal(points[0], points[points.Length - 1]); - Assert.Equal(originalPoints.Length, ring.VertexCount); - - originalPoints = originalPoints.CloneArray(); - - if ((originalPositive && isHole) || (!originalPositive && !isHole)) - { - originalPoints.AsSpan().Reverse(); - points = points.Slice(1); - } - else - { - points = points.Slice(0, points.Length - 1); - } - - Assert.True(originalPoints.AsSpan().SequenceEqual(points)); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void Create_FromPolygon_Case1(bool reverseOriginal) - { - PointF[] points = PolygonFactory.CreatePointArray((0, 3), (3, 3), (3, 0), (1, 2), (1, 1), (0, 0)); - if (reverseOriginal) - { - points.AsSpan().Reverse(); - } - - Polygon polygon = new(points); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - VerifyRing(multipolygon[0], points, reverseOriginal, false); - Assert.Equal(6, multipolygon.TotalVertexCount); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void Create_FromPolygon_Case2(bool reverseOriginal) - { - PointF[] points = PolygonFactory.CreatePointArray((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)); - if (reverseOriginal) - { - points.AsSpan().Reverse(); - } - - Polygon polygon = new(points); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - - VerifyRing(multipolygon[0], points, !reverseOriginal, false); - } - - [Fact] - public void Create_FromRecangle() - { - RectangularPolygon polygon = new(10, 20, 100, 50); - - PointF[] points = polygon.Flatten().Single().Points.Span.ToArray(); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - VerifyRing(multipolygon[0], points, true, false); - Assert.Equal(4, multipolygon.TotalVertexCount); - } - - [Fact] - public void Create_FromStar() - { - Star polygon = new(100, 100, 5, 30, 60); - PointF[] points = polygon.Flatten().Single().Points.Span.ToArray(); - - using TessellatedMultipolygon multipolygon = TessellatedMultipolygon.Create(polygon, MemoryAllocator); - VerifyRing(multipolygon[0], points, true, false); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TopologyUtilitiesTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TopologyUtilitiesTests.cs deleted file mode 100644 index 729274f58..000000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Scan/TopologyUtilitiesTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Shapes.Helpers; - -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes.Scan; - -public class TopologyUtilitiesTests -{ - private static PointF[] CreateTestPoints() - => PolygonFactory.CreatePointArray( - (10, 0), - (20, 0), - (20, 30), - (10, 30), - (10, 20), - (0, 20), - (0, 10), - (10, 10), - (10, 0)); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnsureOrientation_Positive(bool isPositive) - { - PointF[] expected = CreateTestPoints(); - PointF[] polygon = expected.CloneArray(); - - if (!isPositive) - { - polygon.AsSpan().Reverse(); - } - - TopologyUtilities.EnsureOrientation(polygon, 1); - - Assert.Equal(expected, polygon); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnsureOrientation_Negative(bool isNegative) - { - PointF[] expected = CreateTestPoints(); - expected.AsSpan().Reverse(); - - PointF[] polygon = expected.CloneArray(); - - if (!isNegative) - { - polygon.AsSpan().Reverse(); - } - - TopologyUtilities.EnsureOrientation(polygon, -1); - - Assert.Equal(expected, polygon); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs b/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs deleted file mode 100644 index 3b8ed47a5..000000000 --- a/tests/ImageSharp.Drawing.Tests/Shapes/SvgPath.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; - -public class SvgPath -{ - [Theory] - [WithBlankImage(110, 70, PixelTypes.Rgba32, "M20,30 L40,5 L60,30 L80, 55 L100, 30", "zag")] - [WithBlankImage(110, 50, PixelTypes.Rgba32, "M20,30 Q40,5 60,30 T100,30", "wave")] - [WithBlankImage(500, 400, PixelTypes.Rgba32, "M10,350 l 50,-25 a25,25 -30 0,1 50,-25 l 50,-25 a25,50 -30 0,1 50,-25 l 50,-25 a25,75 -30 0,1 50,-25 l 50,-25 a25,100 -30 0,1 50,-25 l 50,-25", "bumpy")] - [WithBlankImage(500, 400, PixelTypes.Rgba32, "M300,200 h-150 a150,150 0 1,0 150,-150 z", "pie_small")] - [WithBlankImage(500, 400, PixelTypes.Rgba32, "M275,175 v-150 a150,150 0 0,0 -150,150 z", "pie_big")] - [WithBlankImage(100, 100, PixelTypes.Rgba32, "M50,50 L50,20 L80,50 z M40,60 L40,90 L10,60 z", "arrows")] - [WithBlankImage(500, 400, PixelTypes.Rgba32, "M 10 315 L 110 215 A 30 50 0 0 1 162.55 162.45 L 172.55 152.45 A 30 50 -45 0 1 215.1 109.9 L 315 10", "chopped_oval")] - public void RenderSvgPath(TestImageProvider provider, string svgPath, string exampleImageKey) - where TPixel : unmanaged, IPixel - { - bool parsed = Path.TryParseSvgPath(svgPath, out IPath path); - Assert.True(parsed); - - provider.RunValidatingProcessorTest( - c => c.Fill(Color.White).Draw(Color.Red, 5, path), - new { type = exampleImageKey }, - comparer: ImageComparer.TolerantPercentage(0.0035F)); // NET 472 x86 requires higher percentage - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs b/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs index a9f82b028..7eb1cfb80 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/TestShapes.cs @@ -20,7 +20,7 @@ public static IPath IrisSegment(int rotationPos) new Vector2(78.26f, 97.0461f))).Translate(center - segmentRotationCenter); float angle = rotationPos * ((float)Math.PI / 3); - return segment.Transform(Matrix3x2.CreateRotation(angle, center)); + return segment.Transform(new Matrix4x4(Matrix3x2.CreateRotation(angle, center))); } public static IPath IrisSegment(float size, int rotationPos) @@ -39,9 +39,9 @@ public static IPath IrisSegment(float size, int rotationPos) float angle = rotationPos * ((float)Math.PI / 3); - IPath rotated = segment.Transform(Matrix3x2.CreateRotation(angle, center)); + IPath rotated = segment.Transform(new Matrix4x4(Matrix3x2.CreateRotation(angle, center))); - Matrix3x2 scaler = Matrix3x2.CreateScale(scalingFactor, Vector2.Zero); + Matrix4x4 scaler = Matrix4x4.CreateScale(scalingFactor); IPath scaled = rotated.Transform(scaler); return scaled; diff --git a/tests/ImageSharp.Drawing.Tests/TestFile.cs b/tests/ImageSharp.Drawing.Tests/TestFile.cs index 48db7f854..345ad6791 100644 --- a/tests/ImageSharp.Drawing.Tests/TestFile.cs +++ b/tests/ImageSharp.Drawing.Tests/TestFile.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.PixelFormats; using IOPath = System.IO.Path; +#pragma warning disable IDE0032 namespace SixLabors.ImageSharp.Drawing.Tests; /// @@ -13,20 +14,8 @@ namespace SixLabors.ImageSharp.Drawing.Tests; /// public sealed class TestFile { - /// - /// The test file cache. - /// private static readonly ConcurrentDictionary Cache = new(); - - /// - /// The "Formats" directory, as lazy value - /// - // ReSharper disable once InconsistentNaming private static readonly Lazy InputImagesDirectoryValue = new(() => TestEnvironment.InputImagesDirectoryFullPath); - - /// - /// The image bytes - /// private byte[] bytes; /// @@ -80,7 +69,7 @@ public static string GetInputFileFullPath(string file) /// The . /// public static TestFile Create(string file) - => Cache.GetOrAdd(file, (string fileName) => new TestFile(GetInputFileFullPath(fileName))); + => Cache.GetOrAdd(file, fileName => new TestFile(GetInputFileFullPath(fileName))); /// /// Gets the file name. diff --git a/tests/ImageSharp.Drawing.Tests/TestFontUtilities.cs b/tests/ImageSharp.Drawing.Tests/TestFontUtilities.cs index 955ea397a..bd7fb30ba 100644 --- a/tests/ImageSharp.Drawing.Tests/TestFontUtilities.cs +++ b/tests/ImageSharp.Drawing.Tests/TestFontUtilities.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Globalization; using System.Reflection; using SixLabors.Fonts; using IOPath = System.IO.Path; @@ -47,7 +46,7 @@ public static Font GetFont(FontCollection collection, string name, float size) /// The name of the font. /// The public static FontFamily GetFontFamily(FontCollection collection, string name) - => collection.Add(GetPath(name), CultureInfo.InvariantCulture); + => collection.Add(GetPath(name)); /// /// The formats directory. @@ -79,10 +78,7 @@ private static string GetFontsDirectory() "../../../../TestFonts/" ]; - directories = directories.SelectMany(x => new[] - { - IOPath.GetFullPath(x) - }).ToList(); + directories = [.. directories.SelectMany(x => new[] { IOPath.GetFullPath(x) })]; AddFormatsDirectoryFromTestAssemblyPath(directories); @@ -93,7 +89,7 @@ private static string GetFontsDirectory() return directory; } - throw new System.Exception($"Unable to find Fonts directory at any of these locations [{string.Join(", ", directories)}]"); + throw new IOException($"Unable to find Fonts directory at any of these locations [{string.Join(", ", directories)}]"); } /// diff --git a/tests/ImageSharp.Drawing.Tests/TestImages.cs b/tests/ImageSharp.Drawing.Tests/TestImages.cs index 894f92472..99a1e6ef7 100644 --- a/tests/ImageSharp.Drawing.Tests/TestImages.cs +++ b/tests/ImageSharp.Drawing.Tests/TestImages.cs @@ -394,4 +394,10 @@ public static class GeoJson { public const string States = "GeoJson/States.json"; } + + public static class Svg + { + public const string GhostscriptTiger = "Svg/Ghostscript_Tiger.svg"; + public const string Paris30k = "Svg/paris-30k.svg"; + } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs new file mode 100644 index 000000000..18eac23f1 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Attributes/WebGPUFactAttribute.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; + +/// +/// A that skips when WebGPU compute is not available on the current system. +/// +public class WebGPUFactAttribute : FactAttribute +{ + public WebGPUFactAttribute() + { + if (!WebGPUProbe.IsComputeSupported) + { + this.Skip = "WebGPU compute is not available on this system."; + } + } +} + +/// +/// A that skips when WebGPU compute is not available on the current system. +/// +public class WebGPUTheoryAttribute : TheoryAttribute +{ + public WebGPUTheoryAttribute() + { + if (!WebGPUProbe.IsComputeSupported) + { + this.Skip = "WebGPU compute is not available on this system."; + } + } +} + +/// +/// Caches the result of the WebGPU compute pipeline probe. +/// The backend's already performs +/// a full out-of-process probe via the internal RemoteExecutor, so we simply +/// instantiate the backend and check its result. +/// +internal static class WebGPUProbe +{ + private static bool? computeSupported; + + internal static bool IsComputeSupported => computeSupported ??= Probe(); + + private static bool Probe() + { + try + { + using WebGPUDrawingBackend backend = new(); + return backend.IsSupported; + } + catch + { + return false; + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs index 53c15901b..2e0de6699 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/DebugDraw.cs @@ -27,12 +27,16 @@ public void Polygon(IPath path, float gridSize = 10f, float scale = 10f, [Caller return; } - path = path.Transform(Matrix3x2.CreateScale(scale) * Matrix3x2.CreateTranslation(gridSize, gridSize)); + path = path.Transform(Matrix4x4.CreateScale(scale) * Matrix4x4.CreateTranslation(gridSize, gridSize, 0)); RectangleF bounds = path.Bounds; gridSize *= scale; using Image img = new Image((int)(bounds.Right + (2 * gridSize)), (int)(bounds.Bottom + (2 * gridSize))); - img.Mutate(ctx => DrawGrid(ctx.Fill(TestBrush, path), bounds, gridSize)); + img.Mutate(ctx => ctx.ProcessWithCanvas(canvas => + { + canvas.Fill(TestBrush, path); + DrawGrid(canvas, bounds, gridSize); + })); string outDir = TestEnvironment.CreateOutputDirectory(this.outputDir); string outFile = System.IO.Path.Combine(outDir, testMethod + ".png"); @@ -41,18 +45,18 @@ public void Polygon(IPath path, float gridSize = 10f, float scale = 10f, [Caller private static PointF P(float x, float y) => new(x, y); - private static void DrawGrid(IImageProcessingContext ctx, RectangleF rect, float gridSize) + private static void DrawGrid(IDrawingCanvas canvas, RectangleF rect, float gridSize) { for (float x = rect.Left; x <= rect.Right; x += gridSize) { PointF[] line = [P(x, rect.Top), P(x, rect.Bottom)]; - ctx.DrawLine(GridPen, line); + canvas.DrawLine(GridPen, line); } for (float y = rect.Top; y <= rect.Bottom; y += gridSize) { PointF[] line = [P(rect.Left, y), P(rect.Right, y)]; - ctx.DrawLine(GridPen, line); + canvas.DrawLine(GridPen, line); } } } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs index fcdb3f9c2..c58fafc40 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/GraphicsOptionsComparer.cs @@ -16,6 +16,7 @@ public bool Equals(GraphicsOptions x, GraphicsOptions y) return x.AlphaCompositionMode == y.AlphaCompositionMode && x.Antialias == y.Antialias + && x.AntialiasThreshold == y.AntialiasThreshold && x.BlendPercentage == y.BlendPercentage && x.ColorBlendingMode == y.ColorBlendingMode; } diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs index e0bdfa02f..5d4920e61 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/PolygonFactory.cs @@ -19,7 +19,7 @@ internal static class PolygonFactory // based on: // https://github.com/SixLabors/ImageSharp.Drawing/issues/15#issuecomment-521061283 - public static IReadOnlyList GetGeoJsonPoints(Feature geometryOwner, Matrix3x2 transform) + public static IReadOnlyList GetGeoJsonPoints(Feature geometryOwner, Matrix4x4 transform) { List result = []; IGeometryObject geometry = geometryOwner.Geometry; @@ -64,14 +64,14 @@ PointF PositionToPointF(IPosition pos) } } - public static PointF[][] GetGeoJsonPoints(string geoJsonContent, Matrix3x2 transform) + public static PointF[][] GetGeoJsonPoints(string geoJsonContent, Matrix4x4 transform) { FeatureCollection features = JsonConvert.DeserializeObject(geoJsonContent); return features.Features.SelectMany(f => GetGeoJsonPoints(f, transform)).ToArray(); } public static PointF[][] GetGeoJsonPoints(string geoJsonContent) => - GetGeoJsonPoints(geoJsonContent, Matrix3x2.Identity); + GetGeoJsonPoints(geoJsonContent, Matrix4x4.Identity); public static Polygon CreatePolygon(params (float X, float Y)[] coords) => new(new LinearLineSegment(CreatePointArray(coords))) diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs deleted file mode 100644 index f1e9a7cbc..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.InteropServices; -using ImageMagick; -using ImageMagick.Formats; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Tiff; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -public class MagickReferenceDecoder : ImageDecoder -{ - private readonly IImageFormat imageFormat; - private readonly bool validate; - - public MagickReferenceDecoder(IImageFormat imageFormat) - : this(imageFormat, true) - { - } - - public MagickReferenceDecoder(IImageFormat imageFormat, bool validate) - { - this.imageFormat = imageFormat; - this.validate = validate; - } - - public static MagickReferenceDecoder Png { get; } = new(PngFormat.Instance); - - public static MagickReferenceDecoder Bmp { get; } = new(BmpFormat.Instance); - - public static MagickReferenceDecoder Jpeg { get; } = new(JpegFormat.Instance); - - public static MagickReferenceDecoder Tiff { get; } = new(TiffFormat.Instance); - - public static MagickReferenceDecoder WebP { get; } = new(WebpFormat.Instance); - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - ImageMetadata metadata = new(); - - Configuration configuration = options.Configuration; - BmpReadDefines bmpReadDefines = new() - { - IgnoreFileSize = !this.validate - }; - PngReadDefines pngReadDefines = new() - { - IgnoreCrc = !this.validate - }; - - MagickReadSettings settings = new() - { - FrameCount = (int)options.MaxFrames - }; - settings.SetDefines(bmpReadDefines); - settings.SetDefines(pngReadDefines); - - using MagickImageCollection magickImageCollection = new(stream, settings); - List> framesList = []; - foreach (IMagickImage magicFrame in magickImageCollection) - { - ImageFrame frame = new(configuration, (int)magicFrame.Width, (int)magicFrame.Height); - framesList.Add(frame); - - MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; - - using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe(); - if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - FromRgba32Bytes(configuration, data, framePixels); - } - else if (magicFrame.Depth is 16 or 14) - { - if (this.imageFormat is PngFormat png) - { - metadata.GetPngMetadata().BitDepth = PngBitDepth.Bit16; - } - - ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); - Span bytes = MemoryMarshal.Cast(data.AsSpan()); - FromRgba64Bytes(configuration, bytes, framePixels); - } - else - { - throw new InvalidOperationException(); - } - } - - return ReferenceCodecUtilities.EnsureDecodedMetadata(new Image(configuration, metadata, framesList), this.imageFormat); - } - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(options, stream, cancellationToken); - - protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - using Image image = this.Decode(options, stream, cancellationToken); - ImageMetadata metadata = image.Metadata; - return new ImageInfo(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) - { - PixelType = metadata.GetDecodedPixelTypeInfo() - }; - } - - private static void FromRgba32Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) - where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel - { - Span sourcePixels = MemoryMarshal.Cast(rgbaBytes); - foreach (Memory m in destinationGroup) - { - Span destBuffer = m.Span; - PixelOperations.Instance.FromRgba32( - configuration, - sourcePixels[..destBuffer.Length], - destBuffer); - sourcePixels = sourcePixels[destBuffer.Length..]; - } - } - - private static void FromRgba64Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) - where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel - { - foreach (Memory m in destinationGroup) - { - Span destBuffer = m.Span; - PixelOperations.Instance.FromRgba64Bytes( - configuration, - rgbaBytes, - destBuffer, - destBuffer.Length); - rgbaBytes = rgbaBytes[(destBuffer.Length * 8)..]; - } - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs deleted file mode 100644 index a4df368b9..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/ReferenceCodecUtilities.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Pbm; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Qoi; -using SixLabors.ImageSharp.Formats.Tga; -using SixLabors.ImageSharp.Formats.Tiff; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -internal static class ReferenceCodecUtilities -{ - /// - /// Ensures that the metadata is properly initialized for reference and test encoders which cannot initialize - /// metadata in the same manner as our built in decoders. - /// - /// The type of pixel format. - /// The decoded image. - /// The image format - /// The format is unknown. - public static Image EnsureDecodedMetadata(Image image, IImageFormat format) - where TPixel : unmanaged, IPixel - { - if (image.Metadata.DecodedImageFormat is null) - { - image.Metadata.DecodedImageFormat = format; - } - - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.DecodedImageFormat = format; - } - - switch (format) - { - case BmpFormat: - image.Metadata.GetBmpMetadata(); - break; - case GifFormat: - image.Metadata.GetGifMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetGifMetadata(); - } - - break; - case JpegFormat: - image.Metadata.GetJpegMetadata(); - break; - case PbmFormat: - image.Metadata.GetPbmMetadata(); - break; - case PngFormat: - image.Metadata.GetPngMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetPngMetadata(); - } - - break; - case QoiFormat: - image.Metadata.GetQoiMetadata(); - break; - case TgaFormat: - image.Metadata.GetTgaMetadata(); - break; - case TiffFormat: - image.Metadata.GetTiffMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetTiffMetadata(); - } - - break; - case WebpFormat: - image.Metadata.GetWebpMetadata(); - foreach (ImageFrame frame in image.Frames) - { - frame.Metadata.GetWebpMetadata(); - } - - break; - } - - return image; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs deleted file mode 100644 index ab9b698f7..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Buffers; -using System.Drawing; -using System.Drawing.Imaging; -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -/// -/// Provides methods to convert to/from System.Drawing bitmaps. -/// -public static class SystemDrawingBridge -{ - /// - /// Returns an image from the given System.Drawing bitmap. - /// - /// The pixel format. - /// The input bitmap. - /// Thrown if the image pixel format is not of type - internal static unsafe Image From32bppArgbSystemDrawingBitmap(Bitmap bmp) - where TPixel : unmanaged, IPixel - { - int w = bmp.Width; - int h = bmp.Height; - - System.Drawing.Rectangle fullRect = new(0, 0, w, h); - - if (bmp.PixelFormat != PixelFormat.Format32bppArgb) - { - throw new ArgumentException( - $"{nameof(From32bppArgbSystemDrawingBitmap)} : pixel format should be {PixelFormat.Format32bppArgb}!", - nameof(bmp)); - } - - BitmapData data = bmp.LockBits(fullRect, ImageLockMode.ReadWrite, bmp.PixelFormat); - Image image = new(w, h); - try - { - byte* sourcePtrBase = (byte*)data.Scan0; - - long sourceRowByteCount = data.Stride; - long destRowByteCount = w * sizeof(Bgra32); - - Configuration configuration = image.Configuration; - image.ProcessPixelRows(accessor => - { - using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w); - fixed (Bgra32* destPtr = &workBuffer.GetReference()) - { - for (int y = 0; y < h; y++) - { - Span row = accessor.GetRowSpan(y); - - byte* sourcePtr = sourcePtrBase + (data.Stride * y); - - Buffer.MemoryCopy(sourcePtr, destPtr, destRowByteCount, sourceRowByteCount); - PixelOperations.Instance.FromBgra32( - configuration, - workBuffer.GetSpan().Slice(0, w), - row); - } - } - }); - } - finally - { - bmp.UnlockBits(data); - } - - return image; - } - - /// - /// Returns an image from the given System.Drawing bitmap. - /// - /// The pixel format. - /// The input bitmap. - /// Thrown if the image pixel format is not of type - internal static unsafe Image From24bppRgbSystemDrawingBitmap(Bitmap bmp) - where TPixel : unmanaged, IPixel - { - int w = bmp.Width; - int h = bmp.Height; - - System.Drawing.Rectangle fullRect = new(0, 0, w, h); - - if (bmp.PixelFormat != PixelFormat.Format24bppRgb) - { - throw new ArgumentException( - $"{nameof(From24bppRgbSystemDrawingBitmap)}: pixel format should be {PixelFormat.Format24bppRgb}!", - nameof(bmp)); - } - - BitmapData data = bmp.LockBits(fullRect, ImageLockMode.ReadWrite, bmp.PixelFormat); - Image image = new(w, h); - try - { - byte* sourcePtrBase = (byte*)data.Scan0; - - long sourceRowByteCount = data.Stride; - long destRowByteCount = w * sizeof(Bgr24); - - Configuration configuration = image.Configuration; - Buffer2D imageBuffer = image.GetRootFramePixelBuffer(); - - using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w); - fixed (Bgr24* destPtr = &workBuffer.GetReference()) - { - for (int y = 0; y < h; y++) - { - Span row = imageBuffer.DangerousGetRowSpan(y); - - byte* sourcePtr = sourcePtrBase + (data.Stride * y); - - Buffer.MemoryCopy(sourcePtr, destPtr, destRowByteCount, sourceRowByteCount); - PixelOperations.Instance.FromBgr24(configuration, workBuffer.GetSpan().Slice(0, w), row); - } - } - } - finally - { - bmp.UnlockBits(data); - } - - return image; - } - - internal static unsafe Bitmap To32bppArgbSystemDrawingBitmap(Image image) - where TPixel : unmanaged, IPixel - { - Configuration configuration = image.Configuration; - int w = image.Width; - int h = image.Height; - - Bitmap resultBitmap = new(w, h, PixelFormat.Format32bppArgb); - System.Drawing.Rectangle fullRect = new(0, 0, w, h); - BitmapData data = resultBitmap.LockBits(fullRect, ImageLockMode.ReadWrite, resultBitmap.PixelFormat); - try - { - byte* destPtrBase = (byte*)data.Scan0; - - long destRowByteCount = data.Stride; - long sourceRowByteCount = w * sizeof(Bgra32); - image.ProcessPixelRows(accessor => - { - using IMemoryOwner workBuffer = image.Configuration.MemoryAllocator.Allocate(w); - fixed (Bgra32* sourcePtr = &workBuffer.GetReference()) - { - for (int y = 0; y < h; y++) - { - Span row = accessor.GetRowSpan(y); - PixelOperations.Instance.ToBgra32(configuration, row, workBuffer.GetSpan()); - byte* destPtr = destPtrBase + (data.Stride * y); - - Buffer.MemoryCopy(sourcePtr, destPtr, destRowByteCount, sourceRowByteCount); - } - } - }); - } - finally - { - resultBitmap.UnlockBits(data); - } - - return resultBitmap; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs deleted file mode 100644 index afb0d38f2..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -#pragma warning disable CA1416 // Validate platform compatibility -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.PixelFormats; -using SDBitmap = System.Drawing.Bitmap; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -public class SystemDrawingReferenceDecoder : ImageDecoder -{ - private readonly IImageFormat imageFormat; - - public SystemDrawingReferenceDecoder(IImageFormat imageFormat) - => this.imageFormat = imageFormat; - - public static SystemDrawingReferenceDecoder Png { get; } = new(PngFormat.Instance); - - public static SystemDrawingReferenceDecoder Bmp { get; } = new(BmpFormat.Instance); - - protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - using Image image = this.Decode(options, stream, cancellationToken); - ImageMetadata metadata = image.Metadata; - return new ImageInfo(image.Size, metadata, new List(image.Frames.Select(x => x.Metadata))) - { - PixelType = metadata.GetDecodedPixelTypeInfo() - }; - } - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - { - using SDBitmap sourceBitmap = new(stream); - if (sourceBitmap.PixelFormat == System.Drawing.Imaging.PixelFormat.Format32bppArgb) - { - return SystemDrawingBridge.From32bppArgbSystemDrawingBitmap(sourceBitmap); - } - - using SDBitmap convertedBitmap = new( - sourceBitmap.Width, - sourceBitmap.Height, - System.Drawing.Imaging.PixelFormat.Format32bppArgb); - using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(convertedBitmap)) - { - g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; - g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; - g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; - g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; - - g.DrawImage(sourceBitmap, 0, 0, sourceBitmap.Width, sourceBitmap.Height); - } - - return ReferenceCodecUtilities.EnsureDecodedMetadata( - SystemDrawingBridge.From32bppArgbSystemDrawingBitmap(convertedBitmap), - this.imageFormat); - } - - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(options, stream, cancellationToken); -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs deleted file mode 100644 index b3cf88c96..000000000 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceEncoder.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Drawing; -using System.Drawing.Imaging; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; - -public class SystemDrawingReferenceEncoder : IImageEncoder -{ - private readonly ImageFormat imageFormat; - - public SystemDrawingReferenceEncoder(ImageFormat imageFormat) - => this.imageFormat = imageFormat; - - public static SystemDrawingReferenceEncoder Png { get; } = new(ImageFormat.Png); - - public static SystemDrawingReferenceEncoder Bmp { get; } = new(ImageFormat.Bmp); - - public bool SkipMetadata { get; init; } - - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - using Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image); - sdBitmap.Save(stream, this.imageFormat); - } - - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - using (Bitmap sdBitmap = SystemDrawingBridge.To32bppArgbSystemDrawingBitmap(image)) - { - sdBitmap.Save(stream, this.imageFormat); - } - - return Task.CompletedTask; - } -} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/SvgBenchmarkHelper.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/SvgBenchmarkHelper.cs new file mode 100644 index 000000000..4d63cd77d --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/SvgBenchmarkHelper.cs @@ -0,0 +1,1140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Globalization; +using System.Numerics; +using System.Xml.Linq; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; +using SkiaSharp; +using ISColor = SixLabors.ImageSharp.Color; +using ISDrawingProcessing = SixLabors.ImageSharp.Drawing.Processing; +using SDColor = System.Drawing.Color; +using SDPen = System.Drawing.Pen; +using SDSolidBrush = System.Drawing.SolidBrush; + +namespace SixLabors.ImageSharp.Drawing.Tests; + +/// +/// Shared SVG parsing and per-backend setup for SVG rendering benchmarks. +/// +internal static class SvgBenchmarkHelper +{ + private const float NeighborhoodPadding = 12F; + + /// + /// A single parsed SVG path element with fill, stroke, and per-element transform. + /// + internal readonly record struct SvgElement( + string PathData, + ISColor Fill, + ISColor Stroke, + float StrokeWidth, + Matrix4x4? Transform); + + /// + /// Parses an SVG file into a list of s. + /// Handles fill, fill-opacity, stroke, stroke-width, opacity, and per-path transform attributes. + /// Group-level transforms are composed with per-path transforms. + /// + internal static List ParseSvg(string filePath) + { + XDocument doc = XDocument.Load(filePath); + XNamespace ns = "http://www.w3.org/2000/svg"; + List result = []; + + // Collect group transforms into a stack. + // For simplicity, iterate all path elements and walk up to resolve inherited transforms. + foreach (XElement pathEl in doc.Descendants(ns + "path")) + { + string d = pathEl.Attribute("d")?.Value; + if (string.IsNullOrWhiteSpace(d)) + { + continue; + } + + // Parse fill color (default black per SVG spec). + string fillStr = ResolveInheritedPresentationValue(pathEl, "fill"); + ISColor fill; + if (fillStr is null) + { + fill = ISColor.Black; + } + else if (fillStr == "none") + { + fill = ISColor.Transparent; + } + else if (ISColor.TryParse(fillStr, out ISColor parsed)) + { + fill = parsed; + } + else + { + fill = ISColor.Black; + } + + // Apply fill-opacity. + if (float.TryParse( + ResolveInheritedPresentationValue(pathEl, "fill-opacity"), + NumberStyles.Float, + CultureInfo.InvariantCulture, + out float fillOpacity)) + { + Rgba32 fp = fill.ToPixel(); + fp.A = (byte)(fp.A * Math.Clamp(fillOpacity, 0f, 1f)); + fill = ISColor.FromPixel(fp); + } + + // Apply element-level opacity to fill alpha. + if (float.TryParse( + pathEl.Attribute("opacity")?.Value, + NumberStyles.Float, + CultureInfo.InvariantCulture, + out float opacity)) + { + Rgba32 fp = fill.ToPixel(); + fp.A = (byte)(fp.A * Math.Clamp(opacity, 0f, 1f)); + fill = ISColor.FromPixel(fp); + } + + // Parse stroke. + ISColor stroke = ParseColor(ResolveInheritedPresentationValue(pathEl, "stroke")); + float strokeWidth = float.TryParse( + ResolveInheritedPresentationValue(pathEl, "stroke-width"), + NumberStyles.Float, + CultureInfo.InvariantCulture, + out float sw) ? sw : 0f; + + // Apply element-level opacity to stroke alpha. + if (opacity > 0 && opacity < 1) + { + Rgba32 sp = stroke.ToPixel(); + sp.A = (byte)(sp.A * Math.Clamp(opacity, 0f, 1f)); + stroke = ISColor.FromPixel(sp); + } + + // Resolve transform: compose per-path transform with ancestor group transforms. + Matrix4x4? transform = ResolveTransform(pathEl, ns); + + result.Add(new SvgElement(d, fill, stroke, strokeWidth, transform)); + } + + return result; + } + + /// + /// Builds pre-parsed SkiaSharp elements for benchmarking. + /// + internal static List<(SKPath Path, SKPaint FillPaint, SKPaint StrokePaint)> BuildSkiaElements( + List elements, + float scale) + { + List<(SKPath, SKPaint, SKPaint)> result = []; + foreach (SvgElement el in elements) + { + SKPath skPath = SKPath.ParseSvgPathData(el.PathData); + if (skPath is null) + { + continue; + } + + SKMatrix skMatrix = SKMatrix.CreateScale(scale, scale); + if (el.Transform.HasValue) + { + Matrix4x4 m = el.Transform.Value; + SKMatrix elMatrix = new(m.M11, m.M21, m.M41, m.M12, m.M22, m.M42, 0, 0, 1); + skMatrix = SKMatrix.Concat(skMatrix, elMatrix); + } + + skPath.Transform(skMatrix); + + Rgba32 fillPixel = el.Fill.ToPixel(); + SKPaint fillPaint = fillPixel.A > 0 + ? new SKPaint + { + Style = SKPaintStyle.Fill, + Color = new SKColor(fillPixel.R, fillPixel.G, fillPixel.B, fillPixel.A), + IsAntialias = true, + } + : null; + + Rgba32 strokePixel = el.Stroke.ToPixel(); + SKPaint strokePaint = strokePixel.A > 0 && el.StrokeWidth > 0 + ? new SKPaint + { + Style = SKPaintStyle.Stroke, + Color = new SKColor(strokePixel.R, strokePixel.G, strokePixel.B, strokePixel.A), + StrokeWidth = el.StrokeWidth * scale, + IsAntialias = true, + } + : null; + + result.Add((skPath, fillPaint, strokePaint)); + } + + return result; + } + + /// + /// Builds pre-parsed System.Drawing elements for benchmarking. + /// + internal static List<(GraphicsPath Path, SDSolidBrush Fill, SDPen Stroke)> BuildSystemDrawingElements( + List elements, + float scale) + { + List<(GraphicsPath, SDSolidBrush, SDPen)> result = []; + foreach (SvgElement el in elements) + { + GraphicsPath sdPath = SvgPathDataToGraphicsPath(el.PathData, scale, el.Transform); + + Rgba32 fillPixel = el.Fill.ToPixel(); + SDSolidBrush fill = fillPixel.A > 0 + ? new SDSolidBrush(SDColor.FromArgb(fillPixel.A, fillPixel.R, fillPixel.G, fillPixel.B)) + : null; + + Rgba32 strokePixel = el.Stroke.ToPixel(); + SDPen stroke = strokePixel.A > 0 && el.StrokeWidth > 0 + ? new SDPen(SDColor.FromArgb(strokePixel.A, strokePixel.R, strokePixel.G, strokePixel.B), el.StrokeWidth * scale) + : null; + + result.Add((sdPath, fill, stroke)); + } + + return result; + } + + /// + /// Builds pre-parsed ImageSharp elements for benchmarking. + /// + internal static List<(IPath Path, ISDrawingProcessing.SolidBrush Fill, SolidPen Stroke)> BuildImageSharpElements( + List elements, + float scale) + { + List<(IPath, ISDrawingProcessing.SolidBrush, SolidPen)> result = []; + foreach (SvgElement el in elements) + { + if (!Path.TryParseSvgPath(el.PathData, out IPath isPath)) + { + continue; + } + + Matrix4x4 scaleMatrix = Matrix4x4.CreateScale(scale, scale, 1); + if (el.Transform.HasValue) + { + isPath = isPath.Transform(el.Transform.Value * scaleMatrix); + } + else + { + isPath = isPath.Transform(scaleMatrix); + } + + Rgba32 fillPixel = el.Fill.ToPixel(); + ISDrawingProcessing.SolidBrush fill = fillPixel.A > 0 + ? new ISDrawingProcessing.SolidBrush(el.Fill) + : null; + + Rgba32 strokePixel = el.Stroke.ToPixel(); + SolidPen stroke = strokePixel.A > 0 && el.StrokeWidth > 0 + ? new SolidPen(el.Stroke, el.StrokeWidth * scale) + : null; + + result.Add((isPath, fill, stroke)); + } + + return result; + } + + /// + /// Saves a verification image from each backend. + /// + internal static void VerifyOutput( + string name, + int width, + int height, + SKSurface skSurface, + Bitmap sdBitmap, + Image isImage, + nint webGpuTextureHandle) + { + string outDir = System.IO.Path.Combine(AppContext.BaseDirectory, $"{name}-verify"); + Directory.CreateDirectory(outDir); + + // SkiaSharp + using (SKImage skImage = skSurface.Snapshot()) + using (SKData skData = skImage.Encode(SKEncodedImageFormat.Png, 100)) + using (FileStream fs = File.Create(System.IO.Path.Combine(outDir, $"{name}-skia.png"))) + { + skData.SaveTo(fs); + } + + Console.WriteLine($"Saved {name}-skia.png"); + + // System.Drawing + sdBitmap.Save(System.IO.Path.Combine(outDir, $"{name}-systemdrawing.png")); + Console.WriteLine($"Saved {name}-systemdrawing.png"); + + // ImageSharp (CPU) + isImage.SaveAsPng(System.IO.Path.Combine(outDir, $"{name}-imagesharp.png")); + Console.WriteLine($"Saved {name}-imagesharp.png"); + + // ImageSharp (WebGPU) + if (WebGPUTestNativeSurfaceAllocator.TryReadTexture( + webGpuTextureHandle, + width, + height, + out Image gpuImage, + out string readError)) + { + gpuImage.SaveAsPng(System.IO.Path.Combine(outDir, $"{name}-webgpu.png")); + gpuImage.Dispose(); + Console.WriteLine($"Saved {name}-webgpu.png"); + } + else + { + Console.WriteLine($"WebGPU readback failed: {readError}"); + } + + Console.WriteLine($"Output saved to: {outDir}"); + } + + /// + /// Writes a spatial neighborhood SVG for the requested path using the parsed SVG elements. + /// + internal static void WriteNeighborhoodSvg( + string name, + IReadOnlyList elements, + string targetPathData, + int width, + int height) + { + string outDir = System.IO.Path.Combine(AppContext.BaseDirectory, $"{name}-verify"); + Directory.CreateDirectory(outDir); + + List<(SvgElement Element, RectangleF Bounds)> candidates = []; + (SvgElement Element, RectangleF Bounds)? target = null; + + foreach (SvgElement element in elements) + { + if (!TryGetTransformedBounds(element, out RectangleF bounds)) + { + continue; + } + + candidates.Add((element, bounds)); + + if (target is null && string.Equals(element.PathData, targetPathData, StringComparison.Ordinal)) + { + target = (element, bounds); + } + } + + if (target is null) + { + return; + } + + RectangleF viewport = Inflate(target.Value.Bounds, NeighborhoodPadding); + List neighborhood = []; + foreach ((SvgElement element, RectangleF bounds) in candidates) + { + if (bounds.IntersectsWith(viewport)) + { + neighborhood.Add(element); + viewport = RectangleF.Union(viewport, bounds); + } + } + + viewport = RectangleF.Intersect(Inflate(viewport, NeighborhoodPadding), new RectangleF(0, 0, width, height)); + + XNamespace ns = "http://www.w3.org/2000/svg"; + XElement svg = new( + ns + "svg", + new XAttribute("xmlns", ns.NamespaceName), + new XAttribute("viewBox", FormattableString.Invariant($"{viewport.X} {viewport.Y} {viewport.Width} {viewport.Height}")), + new XAttribute("width", FormattableString.Invariant($"{viewport.Width}")), + new XAttribute("height", FormattableString.Invariant($"{viewport.Height}"))); + + foreach (SvgElement element in neighborhood) + { + svg.Add(CreatePathElement(ns, element)); + } + + XDocument document = new(new XDeclaration("1.0", "utf-8", null), svg); + document.Save(System.IO.Path.Combine(outDir, $"{name}-neighborhood.svg")); + } + + // ---- SVG transform resolution ---- + + private static Matrix4x4? ResolveTransform(XElement element, XNamespace ns) + { + // Walk up the tree, collecting transforms from path → root. + List transforms = null; + XElement current = element; + while (current is not null) + { + string transformStr = current.Attribute("transform")?.Value; + if (transformStr is not null && TryParseTransform(transformStr, out Matrix4x4 m)) + { + transforms ??= []; + transforms.Add(m); + } + + current = current.Parent; + } + + if (transforms is null) + { + return null; + } + + // Compose from root → leaf (reverse of collection order). + Matrix4x4 result = Matrix4x4.Identity; + for (int i = transforms.Count - 1; i >= 0; i--) + { + result *= transforms[i]; + } + + return result; + } + + private static bool TryParseTransform(string value, out Matrix4x4 result) + { + result = Matrix4x4.Identity; + ReadOnlySpan span = value.AsSpan().Trim(); + + if (span.StartsWith("matrix(") && span.EndsWith(")")) + { + span = span[7..^1]; + Span values = stackalloc float[6]; + for (int i = 0; i < 6; i++) + { + span = span.TrimStart(); + if (span.Length > 0 && span[0] == ',') + { + span = span[1..].TrimStart(); + } + + int end = 0; + if (end < span.Length && (span[end] is '-' or '+')) + { + end++; + } + + bool hasDot = false; + while (end < span.Length) + { + char c = span[end]; + if (c == '.' && !hasDot) + { + hasDot = true; + end++; + } + else if (char.IsDigit(c)) + { + end++; + } + else if (c is 'e' or 'E') + { + end++; + if (end < span.Length && span[end] is '+' or '-') + { + end++; + } + + while (end < span.Length && char.IsDigit(span[end])) + { + end++; + } + + break; + } + else + { + break; + } + } + + if (end == 0) + { + return false; + } + + values[i] = float.Parse(span[..end], CultureInfo.InvariantCulture); + span = span[end..]; + } + + // SVG matrix(a,b,c,d,e,f) maps to: + // | a c e | M11=a M21=c M41=e + // | b d f | → M12=b M22=d M42=f + // | 0 0 1 | rest = identity + result = new Matrix4x4( + values[0], values[1], 0, 0, + values[2], values[3], 0, 0, + 0, 0, 1, 0, + values[4], values[5], 0, 1); + return true; + } + + if (span.StartsWith("translate(") && span.EndsWith(")")) + { + span = span[10..^1]; + ReadOnlySpan trimmed = span.Trim(); + int sep = trimmed.IndexOfAny(',', ' '); + float tx = float.Parse(sep < 0 ? trimmed : trimmed[..sep], CultureInfo.InvariantCulture); + float ty = sep < 0 ? 0 : float.Parse(trimmed[(sep + 1)..].Trim(), CultureInfo.InvariantCulture); + result = Matrix4x4.CreateTranslation(tx, ty, 0); + return true; + } + + if (span.StartsWith("scale(") && span.EndsWith(")")) + { + span = span[6..^1]; + ReadOnlySpan trimmed = span.Trim(); + int sep = trimmed.IndexOfAny(',', ' '); + float sx = float.Parse(sep < 0 ? trimmed : trimmed[..sep], CultureInfo.InvariantCulture); + float sy = sep < 0 ? sx : float.Parse(trimmed[(sep + 1)..].Trim(), CultureInfo.InvariantCulture); + result = Matrix4x4.CreateScale(sx, sy, 1); + return true; + } + + return false; + } + + private static ISColor ParseColor(string value) + { + if (string.IsNullOrWhiteSpace(value) || value == "none") + { + return ISColor.Transparent; + } + + return ISColor.TryParse(value, out ISColor color) ? color : ISColor.Transparent; + } + + private static string ResolveInheritedPresentationValue(XElement element, string attributeName) + { + for (XElement current = element; current is not null; current = current.Parent) + { + if (TryGetPresentationValue(current, attributeName, out string value)) + { + return value; + } + } + + return null; + } + + private static bool TryGetPresentationValue(XElement element, string attributeName, out string value) + { + XAttribute attribute = element.Attribute(attributeName); + if (attribute is not null) + { + value = attribute.Value; + return true; + } + + string style = element.Attribute("style")?.Value; + if (TryGetStyleValue(style, attributeName, out value)) + { + return true; + } + + value = null; + return false; + } + + private static bool TryGetStyleValue(string style, string attributeName, out string value) + { + if (string.IsNullOrWhiteSpace(style)) + { + value = null; + return false; + } + + foreach (string entry in style.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + int separatorIndex = entry.IndexOf(':'); + if (separatorIndex <= 0 || separatorIndex >= entry.Length - 1) + { + continue; + } + + if (!entry[..separatorIndex].Equals(attributeName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + value = entry[(separatorIndex + 1)..].Trim(); + return true; + } + + value = null; + return false; + } + + private static XElement CreatePathElement(XNamespace ns, SvgElement element) + { + XElement path = new( + ns + "path", + new XAttribute("d", element.PathData)); + + Rgba32 fillPixel = element.Fill.ToPixel(); + if (fillPixel.A == 0) + { + path.SetAttributeValue("fill", "none"); + } + else + { + path.SetAttributeValue("fill", ToSvgColor(fillPixel)); + if (fillPixel.A < byte.MaxValue) + { + path.SetAttributeValue("fill-opacity", FormattableString.Invariant($"{fillPixel.A / 255F:0.######}")); + } + } + + Rgba32 strokePixel = element.Stroke.ToPixel(); + if (strokePixel.A == 0 || element.StrokeWidth <= 0) + { + path.SetAttributeValue("stroke", "none"); + } + else + { + path.SetAttributeValue("stroke", ToSvgColor(strokePixel)); + path.SetAttributeValue("stroke-width", FormattableString.Invariant($"{element.StrokeWidth:0.######}")); + if (strokePixel.A < byte.MaxValue) + { + path.SetAttributeValue("stroke-opacity", FormattableString.Invariant($"{strokePixel.A / 255F:0.######}")); + } + } + + if (element.Transform is Matrix4x4 transform) + { + path.SetAttributeValue( + "transform", + FormattableString.Invariant( + $"matrix({transform.M11:0.######} {transform.M12:0.######} {transform.M21:0.######} {transform.M22:0.######} {transform.M41:0.######} {transform.M42:0.######})")); + } + + return path; + } + + private static RectangleF Inflate(RectangleF rectangle, float amount) => + new( + rectangle.X - amount, + rectangle.Y - amount, + rectangle.Width + (amount * 2), + rectangle.Height + (amount * 2)); + + private static string ToSvgColor(Rgba32 pixel) => + FormattableString.Invariant($"#{pixel.R:x2}{pixel.G:x2}{pixel.B:x2}"); + + private static bool TryGetTransformedBounds(SvgElement element, out RectangleF bounds) + { + bounds = default; + if (!Path.TryParseSvgPath(element.PathData, out IPath path)) + { + return false; + } + + if (element.Transform is Matrix4x4 transform) + { + path = path.Transform(transform); + } + + bounds = path.Bounds; + return true; + } + + // ---- System.Drawing SVG path parser ---- + + internal static GraphicsPath SvgPathDataToGraphicsPath(string pathData, float scale, Matrix4x4? elementTransform) + { + GraphicsPath gp = new(FillMode.Winding); + float cx = 0, cy = 0; + float sx = 0, sy = 0; + float lcx = 0, lcy = 0; + + ReadOnlySpan span = pathData.AsSpan().Trim(); + char lastCmd = '\0'; + + while (span.Length > 0) + { + span = span.TrimStart(); + if (span.Length == 0) + { + break; + } + + char ch = span[0]; + char cmd; + if (char.IsLetter(ch)) + { + cmd = ch; + span = span[1..].TrimStart(); + lastCmd = cmd; + } + else + { + cmd = lastCmd; + } + + bool rel = char.IsLower(cmd); + char op = char.ToUpperInvariant(cmd); + + switch (op) + { + case 'M': + { + float x = ReadFloat(ref span); + TrimComma(ref span); + float y = ReadFloat(ref span); + if (rel) + { + x += cx; + y += cy; + } + + gp.StartFigure(); + cx = x; + cy = y; + sx = cx; + sy = cy; + lcx = cx; + lcy = cy; + lastCmd = rel ? 'l' : 'L'; + break; + } + + case 'L': + { + float x = ReadFloat(ref span); + TrimComma(ref span); + float y = ReadFloat(ref span); + if (rel) + { + x += cx; + y += cy; + } + + gp.AddLine(cx * scale, cy * scale, x * scale, y * scale); + cx = x; + cy = y; + lcx = cx; + lcy = cy; + break; + } + + case 'H': + { + float x = ReadFloat(ref span); + if (rel) + { + x += cx; + } + + gp.AddLine(cx * scale, cy * scale, x * scale, cy * scale); + cx = x; + lcx = cx; + lcy = cy; + break; + } + + case 'V': + { + float y = ReadFloat(ref span); + if (rel) + { + y += cy; + } + + gp.AddLine(cx * scale, cy * scale, cx * scale, y * scale); + cy = y; + lcx = cx; + lcy = cy; + break; + } + + case 'C': + { + float x1 = ReadFloat(ref span); + TrimComma(ref span); + float y1 = ReadFloat(ref span); + TrimComma(ref span); + float x2 = ReadFloat(ref span); + TrimComma(ref span); + float y2 = ReadFloat(ref span); + TrimComma(ref span); + float x = ReadFloat(ref span); + TrimComma(ref span); + float y = ReadFloat(ref span); + if (rel) + { + x1 += cx; + y1 += cy; + x2 += cx; + y2 += cy; + x += cx; + y += cy; + } + + gp.AddBezier( + cx * scale, cy * scale, + x1 * scale, y1 * scale, + x2 * scale, y2 * scale, + x * scale, y * scale); + lcx = x2; + lcy = y2; + cx = x; + cy = y; + break; + } + + case 'S': + { + float x2 = ReadFloat(ref span); + TrimComma(ref span); + float y2 = ReadFloat(ref span); + TrimComma(ref span); + float x = ReadFloat(ref span); + TrimComma(ref span); + float y = ReadFloat(ref span); + if (rel) + { + x2 += cx; + y2 += cy; + x += cx; + y += cy; + } + + float x1 = (2 * cx) - lcx; + float y1 = (2 * cy) - lcy; + + gp.AddBezier( + cx * scale, cy * scale, + x1 * scale, y1 * scale, + x2 * scale, y2 * scale, + x * scale, y * scale); + lcx = x2; + lcy = y2; + cx = x; + cy = y; + break; + } + + case 'Q': + { + float qx1 = ReadFloat(ref span); + TrimComma(ref span); + float qy1 = ReadFloat(ref span); + TrimComma(ref span); + float x = ReadFloat(ref span); + TrimComma(ref span); + float y = ReadFloat(ref span); + if (rel) + { + qx1 += cx; + qy1 += cy; + x += cx; + y += cy; + } + + float cx1 = cx + (2f / 3f * (qx1 - cx)); + float cy1 = cy + (2f / 3f * (qy1 - cy)); + float cx2 = x + (2f / 3f * (qx1 - x)); + float cy2 = y + (2f / 3f * (qy1 - y)); + + gp.AddBezier( + cx * scale, cy * scale, + cx1 * scale, cy1 * scale, + cx2 * scale, cy2 * scale, + x * scale, y * scale); + lcx = qx1; + lcy = qy1; + cx = x; + cy = y; + break; + } + + case 'T': + { + float x = ReadFloat(ref span); + TrimComma(ref span); + float y = ReadFloat(ref span); + if (rel) + { + x += cx; + y += cy; + } + + float qx1 = (2 * cx) - lcx; + float qy1 = (2 * cy) - lcy; + + float cx1 = cx + (2f / 3f * (qx1 - cx)); + float cy1 = cy + (2f / 3f * (qy1 - cy)); + float cx2 = x + (2f / 3f * (qx1 - x)); + float cy2 = y + (2f / 3f * (qy1 - y)); + + gp.AddBezier( + cx * scale, cy * scale, + cx1 * scale, cy1 * scale, + cx2 * scale, cy2 * scale, + x * scale, y * scale); + lcx = qx1; + lcy = qy1; + cx = x; + cy = y; + break; + } + + case 'A': + { + float rx = ReadFloat(ref span); + TrimComma(ref span); + float ry = ReadFloat(ref span); + TrimComma(ref span); + float xRotation = ReadFloat(ref span); + TrimComma(ref span); + float largeArcFlag = ReadFloat(ref span); + TrimComma(ref span); + float sweepFlag = ReadFloat(ref span); + TrimComma(ref span); + float x = ReadFloat(ref span); + TrimComma(ref span); + float y = ReadFloat(ref span); + if (rel) + { + x += cx; + y += cy; + } + + AddSvgArc(gp, cx, cy, rx, ry, xRotation, largeArcFlag != 0, sweepFlag != 0, x, y, scale); + cx = x; + cy = y; + lcx = cx; + lcy = cy; + break; + } + + case 'Z': + { + gp.CloseFigure(); + cx = sx; + cy = sy; + lcx = cx; + lcy = cy; + break; + } + + default: + if (span.Length > 0) + { + span = span[1..]; + } + + break; + } + + TrimComma(ref span); + } + + // Apply per-element transform via System.Drawing matrix. + if (elementTransform.HasValue) + { + Matrix4x4 m = elementTransform.Value; + using Matrix sdMatrix = new(m.M11, m.M12, m.M21, m.M22, m.M41 * scale, m.M42 * scale); + gp.Transform(sdMatrix); + } + + return gp; + } + + private static float ReadFloat(ref ReadOnlySpan span) + { + span = span.TrimStart(); + int len = 0; + if (len < span.Length && span[len] is '-' or '+') + { + len++; + } + + bool hasDot = false; + while (len < span.Length) + { + char c = span[len]; + if (c == '.' && !hasDot) + { + hasDot = true; + len++; + } + else if (char.IsDigit(c)) + { + len++; + } + else if (c is 'e' or 'E') + { + len++; + if (len < span.Length && span[len] is '+' or '-') + { + len++; + } + + while (len < span.Length && char.IsDigit(span[len])) + { + len++; + } + + break; + } + else + { + break; + } + } + + float result = float.Parse(span[..len], NumberStyles.Float, CultureInfo.InvariantCulture); + span = span[len..]; + return result; + } + + private static void TrimComma(ref ReadOnlySpan span) + { + span = span.TrimStart(); + if (span.Length > 0 && span[0] == ',') + { + span = span[1..].TrimStart(); + } + } + + private static void AddSvgArc( + GraphicsPath gp, + float x1, + float y1, + float rx, + float ry, + float xRotationDeg, + bool largeArc, + bool sweep, + float x2, + float y2, + float scale) + { + float dx = x2 - x1; + float dy = y2 - y1; + if ((dx * dx) + (dy * dy) < 1e-10f) + { + return; + } + + rx = MathF.Abs(rx); + ry = MathF.Abs(ry); + if (rx < 1e-5f || ry < 1e-5f) + { + gp.AddLine(x1 * scale, y1 * scale, x2 * scale, y2 * scale); + return; + } + + float xRot = xRotationDeg * MathF.PI / 180f; + float cosR = MathF.Cos(xRot); + float sinR = MathF.Sin(xRot); + + float dx2 = (x1 - x2) / 2f; + float dy2 = (y1 - y2) / 2f; + float x1p = (cosR * dx2) + (sinR * dy2); + float y1p = (-sinR * dx2) + (cosR * dy2); + + float rxSq = rx * rx; + float rySq = ry * ry; + float x1pSq = x1p * x1p; + float y1pSq = y1p * y1p; + + float cr = (x1pSq / rxSq) + (y1pSq / rySq); + if (cr > 1) + { + float s = MathF.Sqrt(cr); + rx *= s; + ry *= s; + rxSq = rx * rx; + rySq = ry * ry; + } + + float dq = (rxSq * y1pSq) + (rySq * x1pSq); + float pq = MathF.Max(0, ((rxSq * rySq) - dq) / dq); + float q = MathF.Sqrt(pq); + if (largeArc == sweep) + { + q = -q; + } + + float cxp = q * rx * y1p / ry; + float cyp = -q * ry * x1p / rx; + + float arcCx = (cosR * cxp) - (sinR * cyp) + ((x1 + x2) / 2f); + float arcCy = (sinR * cxp) + (cosR * cyp) + ((y1 + y2) / 2f); + + float theta = SvgAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry); + float delta = SvgAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry); + delta %= MathF.PI * 2; + + if (!sweep && delta > 0) + { + delta -= 2 * MathF.PI; + } + + if (sweep && delta < 0) + { + delta += 2 * MathF.PI; + } + + float t = theta; + float remain = MathF.Abs(delta); + float sign = delta < 0 ? -1f : 1f; + float prevX = x1, prevY = y1; + + while (remain > 1e-5f) + { + float step = MathF.Min(remain, MathF.PI / 4f); + float signStep = step * sign; + float alphaT = MathF.Tan(signStep / 2f); + float alpha = MathF.Sin(signStep) * (MathF.Sqrt(4f + (3f * alphaT * alphaT)) - 1f) / 3f; + + float p2x = arcCx + (rx * MathF.Cos(xRot) * MathF.Cos(t + signStep)) - (ry * MathF.Sin(xRot) * MathF.Sin(t + signStep)); + float p2y = arcCy + (rx * MathF.Sin(xRot) * MathF.Cos(t + signStep)) + (ry * MathF.Cos(xRot) * MathF.Sin(t + signStep)); + + float d1x = (-rx * MathF.Cos(xRot) * MathF.Sin(t)) - (ry * MathF.Sin(xRot) * MathF.Cos(t)); + float d1y = (-rx * MathF.Sin(xRot) * MathF.Sin(t)) + (ry * MathF.Cos(xRot) * MathF.Cos(t)); + float d2x = (-rx * MathF.Cos(xRot) * MathF.Sin(t + signStep)) - (ry * MathF.Sin(xRot) * MathF.Cos(t + signStep)); + float d2y = (-rx * MathF.Sin(xRot) * MathF.Sin(t + signStep)) + (ry * MathF.Cos(xRot) * MathF.Cos(t + signStep)); + + float cp1x = prevX + (alpha * d1x); + float cp1y = prevY + (alpha * d1y); + float cp2x = p2x - (alpha * d2x); + float cp2y = p2y - (alpha * d2y); + + gp.AddBezier( + prevX * scale, prevY * scale, + cp1x * scale, cp1y * scale, + cp2x * scale, cp2y * scale, + p2x * scale, p2y * scale); + + prevX = p2x; + prevY = p2y; + t += signStep; + remain -= step; + } + } + + private static float SvgAngle(float ux, float uy, float vx, float vy) + { + float dot = (ux * vx) + (uy * vy); + float len = MathF.Sqrt(((ux * ux) + (uy * uy)) * ((vx * vx) + (vy * vy))); + float ang = MathF.Acos(Math.Clamp(dot / len, -1f, 1f)); + if ((ux * vy) - (uy * vx) < 0) + { + ang = -ang; + } + + return ang; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs index 01598903b..515732a68 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -1,22 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Gif; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Formats.Tga; using IOPath = System.IO.Path; namespace SixLabors.ImageSharp.Drawing.Tests; public static partial class TestEnvironment { - private static readonly Lazy ConfigurationLazy = new(CreateDefaultConfiguration); - - internal static Configuration Configuration => ConfigurationLazy.Value; + internal static Configuration Configuration => Configuration.Default; internal static IImageDecoder GetReferenceDecoder(string filePath) { @@ -38,42 +30,4 @@ internal static IImageFormat GetImageFormat(string filePath) return format; } - - private static void ConfigureCodecs( - this Configuration cfg, - IImageFormat imageFormat, - IImageDecoder decoder, - IImageEncoder encoder, - IImageFormatDetector detector) - { - cfg.ImageFormatsManager.SetDecoder(imageFormat, decoder); - cfg.ImageFormatsManager.SetEncoder(imageFormat, encoder); - cfg.ImageFormatsManager.AddImageFormatDetector(detector); - } - - private static Configuration CreateDefaultConfiguration() - { - Configuration cfg = new( - new JpegConfigurationModule(), - new GifConfigurationModule(), - new TgaConfigurationModule()); - - // Magick codecs should work on all platforms - IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new PngEncoder(); - IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); - - cfg.ConfigureCodecs( - PngFormat.Instance, - MagickReferenceDecoder.Png, - pngEncoder, - new PngImageFormatDetector()); - - cfg.ConfigureCodecs( - BmpFormat.Instance, - IsWindows ? SystemDrawingReferenceDecoder.Bmp : MagickReferenceDecoder.Bmp, - bmpEncoder, - new BmpImageFormatDetector()); - - return cfg; - } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestPoint.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPoint.cs similarity index 94% rename from tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestPoint.cs rename to tests/ImageSharp.Drawing.Tests/TestUtilities/TestPoint.cs index 3606891ae..74710b3aa 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestPoint.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestPoint.cs @@ -4,7 +4,7 @@ using System.Numerics; using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; [Serializable] public class TestPoint : IXunitSerializable diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestSize.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestSize.cs similarity index 94% rename from tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestSize.cs rename to tests/ImageSharp.Drawing.Tests/TestUtilities/TestSize.cs index a5dcb4880..3a2882ecd 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/Structs/TestSize.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/TestSize.cs @@ -3,7 +3,7 @@ using Xunit.Abstractions; -namespace SixLabors.ImageSharp.Drawing.Tests; +namespace SixLabors.ImageSharp.Drawing.Tests.TestUtilities; [Serializable] public class TestSize : IXunitSerializable diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs index 8aa1f6a8a..e1c7cf166 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestEnvironmentTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ReferenceCodecs; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; @@ -48,66 +47,24 @@ public void GetReferenceOutputFileName() Assert.Contains(TestEnvironment.ReferenceOutputDirectoryFullPath, expected); } - [Theory] - [InlineData("lol/foo.png", typeof(SystemDrawingReferenceEncoder))] - [InlineData("lol/Rofl.bmp", typeof(SystemDrawingReferenceEncoder))] - [InlineData("lol/Baz.JPG", typeof(JpegEncoder))] - [InlineData("lol/Baz.gif", typeof(GifEncoder))] - public void GetReferenceEncoder_ReturnsCorrectEncoders_Windows(string fileName, Type expectedEncoderType) - { - if (!TestEnvironment.IsWindows) - { - return; - } - - IImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName); - Assert.IsType(expectedEncoderType, encoder); - } - - [Theory] - [InlineData("lol/foo.png", typeof(MagickReferenceDecoder))] - [InlineData("lol/Rofl.bmp", typeof(SystemDrawingReferenceDecoder))] - [InlineData("lol/Baz.JPG", typeof(JpegDecoder))] - [InlineData("lol/Baz.gif", typeof(GifDecoder))] - public void GetReferenceDecoder_ReturnsCorrectDecoders_Windows(string fileName, Type expectedDecoderType) - { - if (!TestEnvironment.IsWindows) - { - return; - } - - IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); - Assert.IsType(expectedDecoderType, decoder); - } - [Theory] [InlineData("lol/foo.png", typeof(PngEncoder))] [InlineData("lol/Rofl.bmp", typeof(BmpEncoder))] [InlineData("lol/Baz.JPG", typeof(JpegEncoder))] [InlineData("lol/Baz.gif", typeof(GifEncoder))] - public void GetReferenceEncoder_ReturnsCorrectEncoders_Linux(string fileName, Type expectedEncoderType) + public void GetReferenceEncoder_ReturnsCorrectEncoders(string fileName, Type expectedEncoderType) { - if (!TestEnvironment.IsLinux) - { - return; - } - IImageEncoder encoder = TestEnvironment.GetReferenceEncoder(fileName); Assert.IsType(expectedEncoderType, encoder); } [Theory] - [InlineData("lol/foo.png", typeof(MagickReferenceDecoder))] - [InlineData("lol/Rofl.bmp", typeof(MagickReferenceDecoder))] + [InlineData("lol/foo.png", typeof(PngDecoder))] + [InlineData("lol/Rofl.bmp", typeof(BmpDecoder))] [InlineData("lol/Baz.JPG", typeof(JpegDecoder))] [InlineData("lol/Baz.gif", typeof(GifDecoder))] - public void GetReferenceDecoder_ReturnsCorrectDecoders_Linux(string fileName, Type expectedDecoderType) + public void GetReferenceDecoder_ReturnsCorrectDecoders(string fileName, Type expectedDecoderType) { - if (!TestEnvironment.IsLinux) - { - return; - } - IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(fileName); Assert.IsType(expectedDecoderType, decoder); } diff --git a/tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs b/tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs deleted file mode 100644 index 485413515..000000000 --- a/tests/ImageSharp.Drawing.Tests/Utilities/IntersectTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using SixLabors.ImageSharp.Drawing.Utilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Utils; - -public class IntersectTests -{ - public static TheoryData<(float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y), (float X, float Y)?> LineSegmentToLineSegment_Data = - new() - { - { (0, 0), (2, 3), (1, 3), (1, 0), (1, 1.5f) }, - { (3, 1), (3, 3), (3, 2), (4, 2), (3, 2) }, - { (1, -3), (3, -1), (3, -4), (2, -2), (2, -2) }, - { (0, 0), (2, 1), (2, 1.0001f), (5, 2), (2, 1) }, // Robust to inaccuracies - { (0, 0), (2, 3), (1, 3), (1, 2), null }, - { (-3, 3), (-1, 3), (-3, 2), (-1, 2), null }, - { (-4, 3), (-4, 1), (-5, 3), (-5, 1), null }, - { (0, 0), (4, 1), (4, 1), (8, 2), null }, // Collinear intersections are ignored - { (0, 0), (4, 1), (4, 1.0001f), (8, 2), null }, // Collinear intersections are ignored - }; - - [Theory] - [MemberData(nameof(LineSegmentToLineSegment_Data))] - public void LineSegmentToLineSegmentNoCollinear( - (float X, float Y) a0, - (float X, float Y) a1, - (float X, float Y) b0, - (float X, float Y) b1, - (float X, float Y)? expected) - { - Vector2 ip = default; - - bool result = Intersect.LineSegmentToLineSegmentIgnoreCollinear(P(a0), P(a1), P(b0), P(b1), ref ip); - Assert.Equal(result, expected.HasValue); - if (expected.HasValue) - { - Assert.Equal(P(expected.Value), ip, new ApproximateFloatComparer(1e-3f)); - } - - static Vector2 P((float X, float Y) p) => new(p.X, p.Y); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Utilities/NumericUtilitiesTests.cs b/tests/ImageSharp.Drawing.Tests/Utilities/NumericUtilitiesTests.cs deleted file mode 100644 index 206985699..000000000 --- a/tests/ImageSharp.Drawing.Tests/Utilities/NumericUtilitiesTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Utilities; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Utils; - -public class NumericUtilitiesTests -{ - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(3)] - [InlineData(7)] - [InlineData(8)] - [InlineData(13)] - [InlineData(130)] - public void AddToAllElements(int length) - { - float[] values = Enumerable.Range(0, length).Select(v => (float)v).ToArray(); - - const float val = 13.4321f; - float[] expected = values.Select(x => x + val).ToArray(); - values.AsSpan().AddToAllElements(val); - - Assert.Equal(expected, values); - } -} diff --git a/tests/ImageSharp.Drawing.Tests/Utilities/ThreadLocalBlenderBuffersTests.cs b/tests/ImageSharp.Drawing.Tests/Utilities/ThreadLocalBlenderBuffersTests.cs deleted file mode 100644 index 42d7e5983..000000000 --- a/tests/ImageSharp.Drawing.Tests/Utilities/ThreadLocalBlenderBuffersTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Drawing.Utilities; -using SixLabors.ImageSharp.PixelFormats; - -namespace SixLabors.ImageSharp.Drawing.Tests.Drawing.Utils; - -public class ThreadLocalBlenderBuffersTests -{ - private readonly TestMemoryAllocator memoryAllocator = new(); - - [Fact] - public void CreatesPerThreadUniqueInstances() - { - using ThreadLocalBlenderBuffers buffers = new(this.memoryAllocator, 100); - - SemaphoreSlim allSetSemaphore = new(2); - - Thread thread1 = new(() => - { - Span ams = buffers.AmountSpan; - Span overlays = buffers.OverlaySpan; - - ams[0] = 10; - overlays[0] = new Rgb24(10, 10, 10); - - allSetSemaphore.Release(1); - allSetSemaphore.Wait(); - - Assert.Equal(10, buffers.AmountSpan[0]); - Assert.Equal(10, buffers.OverlaySpan[0].R); - }); - - Thread thread2 = new(() => - { - Span ams = buffers.AmountSpan; - Span overlays = buffers.OverlaySpan; - - ams[0] = 20; - overlays[0] = new Rgb24(20, 20, 20); - - allSetSemaphore.Release(1); - allSetSemaphore.Wait(); - - Assert.Equal(20, buffers.AmountSpan[0]); - Assert.Equal(20, buffers.OverlaySpan[0].R); - }); - - thread1.Start(); - thread2.Start(); - thread1.Join(); - thread2.Join(); - } - - [Theory] - [InlineData(false, 1)] - [InlineData(false, 3)] - [InlineData(true, 1)] - [InlineData(true, 3)] - public void Dispose_ReturnsAllBuffers(bool amountBufferOnly, int threadCount) - { - ThreadLocalBlenderBuffers buffers = new(this.memoryAllocator, 100, amountBufferOnly); - - void RunThread() - { - buffers.AmountSpan[0] = 42; - } - - Thread[] threads = new Thread[threadCount]; - for (int i = 0; i < threadCount; i++) - { - threads[i] = new Thread(RunThread); - threads[i].Start(); - } - - foreach (Thread thread in threads) - { - thread.Join(); - } - - buffers.Dispose(); - - int expectedReturnCount = amountBufferOnly ? threadCount : 2 * threadCount; - Assert.Equal(expectedReturnCount, this.memoryAllocator.ReturnLog.Count); - } -} diff --git a/tests/Images/Input/Svg/Ghostscript_Tiger.svg b/tests/Images/Input/Svg/Ghostscript_Tiger.svg new file mode 100644 index 000000000..033611d91 --- /dev/null +++ b/tests/Images/Input/Svg/Ghostscript_Tiger.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Images/Input/Svg/paris-30k.svg b/tests/Images/Input/Svg/paris-30k.svg new file mode 100644 index 000000000..82b317017 --- /dev/null +++ b/tests/Images/Input/Svg/paris-30k.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Blue.png deleted file mode 100644 index dad8ece49..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Blue.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Khaki.png deleted file mode 100644 index 3fc305e9f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/ClearAlwaysOverridesPreviousColor_Khaki.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png deleted file mode 100644 index 1410827bc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png deleted file mode 100644 index 1410827bc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png deleted file mode 100644 index 1410827bc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank16x7.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank16x7.png deleted file mode 100644 index e20907b99..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank16x7.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank1x1.png deleted file mode 100644 index 4e4ee1ee1..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank1x1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank33x32.png deleted file mode 100644 index 31965cc3a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank33x32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank400x500.png deleted file mode 100644 index f3c6b080b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank400x500.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank7x4.png deleted file mode 100644 index 8914b9c49..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/DoesNotDependOnSize_Blank7x4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png deleted file mode 100644 index 4fdb95635..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f34..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png deleted file mode 100644 index 4fdb95635..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f34..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png deleted file mode 100644 index c5ad73f42..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png deleted file mode 100644 index 4860936c1..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/ClearSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png deleted file mode 100644 index 969d80f9b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_ConstrainsOperationToClipBounds.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:790a9e156bee55ddb3d40dd743eafa2a4b0129c43618fea3e99ffd875bd1d551 -size 39092 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 deleted file mode 100644 index 9a791fde1..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index d60adda71..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-20.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index ee0f3d4fd..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x0_y0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 55715e2aa..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x20_y20.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 3d61682c4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x40_y60.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 13f33766a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A150_T5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 84a84ba79..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_HotPink_A255_T5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 6eaaee087..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_Red_A255_T3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 8c376a110..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T1.5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 8c376a110..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawBezierTests/DrawBeziers_White_A255_T15.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1057cf06d0acc8dd05883c14475210953827b0cf8cde751c8dc2bc8eedc6554d -size 4613 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png deleted file mode 100644 index 0e1070e7b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb552c825e6395c17eebfcc42be5b34cb0912bbbb0ada7689fdc80d1f5a22c9a -size 4466 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png deleted file mode 100644 index 0b71e15ae..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Dashed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 0e639df54..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Overlap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 9a7f7901f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawComplexPolygonTests/DrawComplexPolygon__Transparent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57bd54dc3d42753e9d866785d8efa8ec0a79398de26325913973b005d40cd387 -size 4139 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1).png deleted file mode 100644 index b3859ce0b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:459dcb4b0e81dc7e850babc169510ac2298636c7a65c06802f104dca3ce87a45 -size 165 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png deleted file mode 100644 index a10f7de61..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d2ab1c8fd901a5bede28a3036a4df9cb551e6a13c154a988da1700c89fb67b4 -size 161 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5).png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5).png deleted file mode 100644 index f4d76fb04..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:635053b4ce23dafc080d4d9de9e808819d264461ee5e0543acfefb8c9a04d00e -size 187 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png deleted file mode 100644 index ce158e063..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5482537fd9d8f4ad4c6e03ae1ae0d7f8035ecb29f541778009653ab7796510dd -size 173 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 deleted file mode 100644 index cb1e62505..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1d6be109fd0778212b7c92ea4b83af2c9799d383e7cd674514251a7d1e20f45a -size 1011 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 deleted file mode 100644 index 15adb7c31..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b54785fecd7ddc017876f0bdc0190e6a43987c2d69c5150a29b640172ae0d65 -size 1072 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 deleted file mode 100644 index 863cf7c7e..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff0ce825507b2e95b49c8eebfde000b10c890e090cd7bb533397fbb62767178c -size 903 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 deleted file mode 100644 index a2902c5a1..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 8ec380476..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 78098f2df..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index f81d2f0a6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index bba63ff53..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index a9e1d1018..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 28e62bc17..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 6ae3222d4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 9d82ad2cc..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:281d3c8349ea7e15961d1e0be5c5a0c4aad743295381f89bd3f9f7f43a02ac24 -size 2363 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png deleted file mode 100644 index c6439fdc5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_359.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 96098fbf1..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingAddArc_360.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index ceeae75f2..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_False.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index ceeae75f2..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawCircleUsingArcTo_True.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da8ba7c8929209a833524ff5cfb59ecded92d7a95b3022bbda80816aff313c31 -size 1559 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png deleted file mode 100644 index 3d94259f7..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPathClippedOnTop.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 2bd89ce82..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A150_T5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index c5206d91b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_HotPink_A255_T5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index c667647ef..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_Red_A255_T3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 130ae7039..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T1.5.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index ff7d8b685..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/DrawPath_White_A255_T15.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e5d00ab59f163347567cdafc7b1c37c66475dcc4e84de5685214464097ee87a -size 7863 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png b/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png deleted file mode 100644 index 95b8be0e8..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPathTests/PathExtendingOffEdgeOfImageShouldNotBeCropped.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 141ca9492..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:acfc9b104be88bef18386bd3ef9faad56070f1f808aa4ec162dceec01a3c4352 -size 3841 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 deleted file mode 100644 index 2bbf451ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8be397a2c3ea3aeee259dc407633f0bf3f6146acda86a1d7bd8e75f4ffa42b7 -size 3492 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 deleted file mode 100644 index 609fc3579..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:732040a526d7581c2d2842f0f3c35fed6f2266dd7793128d2bb023b8b986f937 -size 3902 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 deleted file mode 100644 index fb1965988..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba9da410ee320f2de0f95a9b37abb1d9306a19e6e6e50ad8ada02766dbcc78bc -size 1264 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png deleted file mode 100644 index 87e3affc6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 6f8346d15..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawPolygonTests/DrawRectangularPolygon_Transformed_Rgba32_BasicTestPattern100x100.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b39e13b16a16caf2bbb8a086fae6eecab8daf01f0b71cec7b6f6939393f554ac -size 601 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png new file mode 100644 index 000000000..254cd8570 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c671f5365c62b2638a556f31785272946cf726b483e51883e6e335c49be11a0 +size 3661 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png new file mode 100644 index 000000000..b73dd3321 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:991ddf7f4bf3386f4ba658398aa49b8ef6d55ec89c3a8fe4e9b111b9894e77d5 +size 10959 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png new file mode 100644 index 000000000..b5dc80a34 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864e9932a5ec64623a7933b793f970705acdda316ace92944d0d72391cf38cff +size 3186 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png new file mode 100644 index 000000000..418fc1a7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fbf8390767408fbbf6ec26b7025fb6746a64bc42028e5a9869dc739e09677d4 +size 10373 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png new file mode 100644 index 000000000..a9f3d37c9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c25546b20774aacaaedf027ac1aca021c9b60c469d0426385c85775e07787f26 +size 10948 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png new file mode 100644 index 000000000..27a8da0f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33efa67ba681678f2a5ca8ec636e00bdd19e0cd579754b74dae5a818a79d35f5 +size 10948 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png new file mode 100644 index 000000000..d03e9f68b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15482b60ff70e35214f9aaeaf13df544c9a2b38aa439f8d9cea86aaf04fd3213 +size 11088 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithRotationTransform_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithRotationTransform_MatchesReference_Rgba32.png new file mode 100644 index 000000000..df8119121 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithRotationTransform_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ad0bf761220373d893c4d39c22007c8f7dbbe85d6df2b5fc99cca07f0034e61 +size 4487 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png new file mode 100644 index 000000000..e836e72ea --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:431a0e81f68c1052900a104702e139051df38cf2aace11df421e695dae7a1679 +size 627 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png new file mode 100644 index 000000000..6cd7de0d2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d27591f58fefd193cd0679ea2a0ed106c6cba81d4a9a955c48503ad7aeb3ddd +size 9158 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawRectangle_AliasedRendersFullCorners_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawRectangle_AliasedRendersFullCorners_Rgba32.png new file mode 100644 index 000000000..ebe3f2c83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawRectangle_AliasedRendersFullCorners_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8aa50dd3c3e4f5bc32ffc68a7cb326aab7127ca692d00b1826c7d295fdb0fd7 +size 104 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png new file mode 100644 index 000000000..ff8390f94 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf8914243626ec17b850a53d0539610cc1af01a83f1b6956bf8236f5328f91d8 +size 11057 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png new file mode 100644 index 000000000..a1ccbdde6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c27778bc1b6f8374528941bb19af16f6e9dfd7977c6446300b10868a966f8cd +size 21426 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png new file mode 100644 index 000000000..5d2bd4c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06958d4871f7b7c1fb0ce12db821904382b7fcdb9cbffc20916152fb8b0c15c4 +size 39059 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png new file mode 100644 index 000000000..08162169c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04583659a2dc61ede9e844237febd5f91a26c0430a573651c0cbde2d0e87106a +size 7365 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png new file mode 100644 index 000000000..68b57a29d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78dc408e7fe6573ebcaedc82a767a6e75930c62fcc8d8344cd426e3bde939a2f +size 45339 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png new file mode 100644 index 000000000..e5215f34c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_NormalizeOutputFalse_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:413de1431396b47b19d89ad16ba9d3300ecfe01cc788a8645788aed09146b4ee +size 10568 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png new file mode 100644 index 000000000..861c40393 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84cce716d279829904de85d3cd8989f70a8e8e14263ead5195d941be6e003424 +size 3435 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png new file mode 100644 index 000000000..e8b7de969 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d7eb8405802fce9d7f7cf2763f49aaa7cfbc3c345fd45844edd712115b60da1 +size 11154 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/FillRectangle_AliasedRendersFullCorners_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/FillRectangle_AliasedRendersFullCorners_Rgba32.png new file mode 100644 index 000000000..7eefffb6d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/FillRectangle_AliasedRendersFullCorners_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:190520bea522b296e0492bfe68af764b30d211acd7e858e5f0b617440d73b246 +size 95 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png new file mode 100644 index 000000000..5563f68e8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:542c8f2c2950cc0a2427b84e0d8da278521db7b560457e3a83bd93d23905cc1a +size 2748 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png new file mode 100644 index 000000000..737481d41 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d40a706d428a5b54c0a46333488f385f5245aa435078a6a3961704d08ce7b05 +size 8253 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png new file mode 100644 index 000000000..443a2495a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a3e596fc671644f770eb79a031434f31bf4ef5c43c254756b26f5aaeac00758 +size 18943 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png new file mode 100644 index 000000000..479eeaffe --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_WithReadbackCapability_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d855bec452658748b0e5cf9d3f7efc16febb367418caa74c35a535ce9259dcbe +size 12938 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png new file mode 100644 index 000000000..5b27e09b8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735603a5db6aa95764a39cca1201075e23aa432738e81d732e39ed5ed764f986 +size 12976 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png new file mode 100644 index 000000000..479eeaffe --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d855bec452658748b0e5cf9d3f7efc16febb367418caa74c35a535ce9259dcbe +size 12938 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png new file mode 100644 index 000000000..42839c543 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce878e13abc2cf4030f099072180380f5d675b450326a3c2055bceebb1cd09a8 +size 6397 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png new file mode 100644 index 000000000..3a943f96b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4778478b965754799180207ae206c5da7c557e98784fc29da8315339def15211 +size 1342 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png new file mode 100644 index 000000000..7fce9ef32 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae6c395044334c461bb322ac07272aa350443c4017c0201e9a6a7aa228091463 +size 27640 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 deleted file mode 100644 index 652850f5d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index c622d0bfd..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 646002b00..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index a3d1fd999..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 4431a489a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 6ea570a90..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingRobustnessTests/LargeGeoJson_States_Fill.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 8ad0ef2cf..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fd3f480017119bc70192255dc3315f444747d1379bec915e7cc3dd771961cecf -size 2410 diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png deleted file mode 100644 index 689851571..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Overlap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b23186fee34f325c965e4a7dc623caa6e71527b05aafa6089ed0e477993e4f6 -size 2656 diff --git a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png deleted file mode 100644 index 48f1ff7af..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillComplexPolygonTests/ComplexPolygon_SolidFill__Transparent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b970c12a4d204c04b54b9907d7206c5e3036a04e40cc6ef87e986509faa8fa4a -size 2376 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawLandscapeImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawLandscapeImage_Rgba32.png deleted file mode 100644 index ce3f363d5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawLandscapeImage_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f79a9486aee6b3201ba20876028c25b1251e70996af8e4ee4847ac294e87458b -size 59884 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawNegativeOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawNegativeOffsetImage_Rgba32.png deleted file mode 100644 index 3a4538c89..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawNegativeOffsetImage_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2cad09068867e5c56874dd5b44937fd22a386d27ff82e6c5d974444512f1950a -size 100398 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png deleted file mode 100644 index 71a0bc8ff..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20c4f6324712fcc2e6b6cf012c169290a01a9199eb96ba3691550b75b2b2b524 -size 150296 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawPortraitImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawPortraitImage_Rgba32.png deleted file mode 100644 index a0fe867a5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawPortraitImage_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb1807440c0a8abb05eb332b379dabff4b1be83f804cc3e2e65978a758373940 -size 48551 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png deleted file mode 100644 index 5acd7f8fe..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b3e455c552537815ca1d5c0699b5fa36bd1963a0a28a06c8cfcf5fb8c5c884f -size 251984 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png deleted file mode 100644 index be90717e5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2fd4fd80d6bfb522b21d884fc6aba6bb5049d5feeb5a05c8b86e087a7229a440 -size 299061 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Bgra32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Bgra32.png deleted file mode 100644 index c0ce4bad1..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Bgra32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Rgba32.png deleted file mode 100644 index c0ce4bad1..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/UseBrushOfDifferentPixelType_Rgba32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-20).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-20).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-20).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-49).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-49).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-49).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-50).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-50).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-50).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-60).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-60).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_-60).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_0).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-110_0).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-99_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-99_0).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(-99_0).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-50).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-50).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-50).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-60).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-60).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-60).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png deleted file mode 100644 index 39062ac87..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:134d4af8c4ed8c112e5d403eb92e7e215fab0fd881bc0435f8fe164e74c47405 -size 537 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-49).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-49).png deleted file mode 100644 index 04782a9bf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-49).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-50).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-50).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-50).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-60).png b/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-60).png deleted file mode 100644 index 06e28c378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-60).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png deleted file mode 100644 index a44d23649..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathArcToAlternates.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:385a840ce196a34a0c5b85ee77aecd36924e642d7d970ff82a3ac981e32649bd -size 1796 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png deleted file mode 100644 index 2180c82a6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathCanvasArcs.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d096fcea8556aaf91eea17a31896527f10285c5d17e073dbe2715aadfa3bdcd5 -size 1500 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png b/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png deleted file mode 100644 index 8d8db722d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPathTests/FillPathSVGArcs.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:94d1f65c198c0e57405459392ffa2b6c36d64f7fe4053960216eb487bc4ed0fa -size 2607 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal.png deleted file mode 100644 index a7ca5cb2a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparent.png deleted file mode 100644 index db76bec8a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparent.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparentx4.png deleted file mode 100644 index c3bd2318e..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonal_Transparentx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonalx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonalx4.png deleted file mode 100644 index a1ed06f0b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/BackwardDiagonalx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal.png deleted file mode 100644 index c5aead13b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparent.png deleted file mode 100644 index f5a69702f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparent.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparentx4.png deleted file mode 100644 index d5e78ba98..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonal_Transparentx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonalx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonalx4.png deleted file mode 100644 index 274f72b11..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/ForwardDiagonalx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal.png deleted file mode 100644 index 482af03e7..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparent.png deleted file mode 100644 index 4dcdf0038..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparent.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparentx4.png deleted file mode 100644 index 76052830b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontal_Transparentx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontalx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontalx4.png deleted file mode 100644 index 1fa5a8973..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Horizontalx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min.png deleted file mode 100644 index 79cf04240..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparent.png deleted file mode 100644 index 1429926cb..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparent.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparentx4.png deleted file mode 100644 index b17dfaaa2..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Min_Transparentx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Minx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Minx4.png deleted file mode 100644 index 93788ac1a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Minx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10.png deleted file mode 100644 index 46e10edf6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparent.png deleted file mode 100644 index c6d2a3132..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparent.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparentx4.png deleted file mode 100644 index feff17b26..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10_Transparentx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10x4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10x4.png deleted file mode 100644 index 02efd3930..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent10x4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20.png deleted file mode 100644 index b508c01ad..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparent.png deleted file mode 100644 index 3522fd2ab..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparent.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparentx4.png deleted file mode 100644 index 47614322e..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20_Transparentx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20x4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20x4.png deleted file mode 100644 index 24d01eb94..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Percent20x4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical.png deleted file mode 100644 index 6f5d24ff9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparent.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparent.png deleted file mode 100644 index 9f1f581ed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparent.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparentx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparentx4.png deleted file mode 100644 index 98c595586..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Vertical_Transparentx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Verticalx4.png b/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Verticalx4.png deleted file mode 100644 index 22b211029..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPatternBrushTests/Verticalx4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png deleted file mode 100644 index 36bc63aa0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5c1275eca572b8121e7f1f3a2f0ebee9684c0f12e85ad32dd9ab11aa1c8bfc9d -size 241 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(Nonzero).png deleted file mode 100644 index 02818fe19..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(Nonzero).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(False).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(False).png deleted file mode 100644 index ff75d7684..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(False).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(True).png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(True).png deleted file mode 100644 index 7b75147f4..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Concave_Reverse(True).png and /dev/null 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 deleted file mode 100644 index 317d43265..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 0945fc432..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 41994df52..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_Car.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:662e4f78996d9c12d6698410543c6ef0aa474337b2d03084924b1c706a1e4916 -size 13462 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png deleted file mode 100644 index d24bd6b86..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_ImageBrush_Rgba32_ducky.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:24f74782275e63f241431410af079089f2bd229ab06071a472de1a360ca934d7 -size 17093 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png deleted file mode 100644 index ad5b15cb6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Pattern_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b08fd85ee90ab448f31ba0eb686142e93cd8d80e0cc6b8abb3a9f615bd5e5cb -size 1687 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 deleted file mode 100644 index 12ecc129c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Bgr24_Yellow_A1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b81ef6b2a5b4f848d740f1ff9de08c90c0dc1e7ef3d11b43c2b704b29546c26d -size 2806 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 deleted file mode 100644 index d6c24cbf4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A0.6.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:145da3c382448615cf0b90f4718b8a79a8b2d7f3dd33042c755e15d5b127d33b -size 2788 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 deleted file mode 100644 index 7c67a4ece..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:779a0d18611eb44bead0f9dbd704c3c9e10fbf92e906fdd4281075f3d5c946f9 -size 2906 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 deleted file mode 100644 index 5be0df234..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 52e6c32c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 417ff62ad..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Difference.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 32da0db02..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Intersection.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d5a7a0a64ff4d9c0d68044155aab68f4cb008b094d95633e55332ef9f0ca40ae -size 2955 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_None.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_None.png deleted file mode 100644 index 8a7568536..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_None.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31e1a9ddb69387f586bafd7d53a75df46103b3f724649b69ab24259cbe8df148 -size 2094 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png deleted file mode 100644 index cb8bd9b88..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Union.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index bab0d07ba..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_StarCircle_AllOperations_Xor.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 85bc516d9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 85bc516d9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(False)_IntersectionRule(Nonzero).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 85bc516d9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 85bc516d9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_EllipsePolygon_Reverse(True)_IntersectionRule(Nonzero).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b122fcb6c408c714a98f85fc0128179caf2de869c4dbbcd0211bba500e61e2cd -size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp deleted file mode 100644 index f4917262c..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_Nonzero_Rgba32_Solid60x60_(0,0,255,255).bmp and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp deleted file mode 100644 index 7c1f28570..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_IntersectionRules_OddEven_Rgba32_Solid60x60_(0,0,255,255).bmp and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Rgba32.png deleted file mode 100644 index f42b72e7a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Rgba32.png and /dev/null differ 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 deleted file mode 100644 index 15431f30a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(50)_Ang(0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:02891cbdc2242395343290bc5403fd161fab49e470f93fd0c5639a93464f274b -size 1754 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 deleted file mode 100644 index 4e29cc25b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(-180).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f95be339c0fc7f9315968001722777d1eebddbf6eea41c8d9d524b8775842763 -size 2024 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 deleted file mode 100644 index 3fe215ed7..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(3)_R(60)_Ang(20).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:86280439d207ed0a74595757af265a313d75f7ff8b7502eb17d2d7184855eb12 -size 2499 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 deleted file mode 100644 index 8ad422f6d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(5)_R(70)_Ang(0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d762246aeec860558a8ee8e5318126937be11a9561a4bd1ff18f90900858bc2b -size 2852 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 deleted file mode 100644 index c7cb00188..000000000 --- a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RegularPolygon_V(7)_R(80)_Ang(-180).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:20457c79f2f5a782088bc4f9a333d0092ba9c5f5307835cdfc3ca7bf527420e4 -size 3247 diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png deleted file mode 100644 index 57cb2e6da..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png deleted file mode 100644 index 346ea8b42..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png deleted file mode 100644 index 2ffa1e7e9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png deleted file mode 100644 index 57cb2e6da..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png deleted file mode 100644 index e93fd5a60..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png deleted file mode 100644 index 2614a2f11..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png deleted file mode 100644 index df75d8d0d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png deleted file mode 100644 index 81e6bb8d8..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png deleted file mode 100644 index 938514381..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png deleted file mode 100644 index db67d9ddd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png deleted file mode 100644 index b428583f3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png deleted file mode 100644 index db67d9ddd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png deleted file mode 100644 index 57cb2e6da..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png deleted file mode 100644 index 346ea8b42..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png deleted file mode 100644 index 2ffa1e7e9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png deleted file mode 100644 index 57cb2e6da..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png deleted file mode 100644 index e93fd5a60..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png deleted file mode 100644 index 2614a2f11..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png deleted file mode 100644 index df75d8d0d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png deleted file mode 100644 index 81e6bb8d8..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png deleted file mode 100644 index 938514381..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png deleted file mode 100644 index db67d9ddd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png deleted file mode 100644 index b428583f3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png deleted file mode 100644 index db67d9ddd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png deleted file mode 100644 index 1410827bc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Argb32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png deleted file mode 100644 index 1410827bc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png deleted file mode 100644 index 1410827bc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSinglePixelType_RgbaVector.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank16x7.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank16x7.png deleted file mode 100644 index e20907b99..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank16x7.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank1x1.png deleted file mode 100644 index 4e4ee1ee1..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank1x1.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank33x32.png deleted file mode 100644 index 31965cc3a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank33x32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank400x500.png deleted file mode 100644 index f3c6b080b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank400x500.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank7x4.png deleted file mode 100644 index 8914b9c49..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/DoesNotDependOnSize_Blank7x4.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png deleted file mode 100644 index 4fdb95635..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f34..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png deleted file mode 100644 index 4fdb95635..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png deleted file mode 100644 index b56cd2f34..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/FillRegion_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png deleted file mode 100644 index c5ad73f42..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Blue.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png deleted file mode 100644 index 4860936c1..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/FillSolidBrushTests/WhenColorIsOpaque_OverridePreviousColor_Khaki.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.10.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.10.png deleted file mode 100644 index fa2315d73..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.10.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.40.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.40.png deleted file mode 100644 index b980bbfe9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.40.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.80.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.80.png deleted file mode 100644 index 9a3758c7b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_0.80.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.00.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.00.png deleted file mode 100644 index 8b93c88ba..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.00.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.20.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.20.png deleted file mode 100644 index 7318e2365..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.20.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.60.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.60.png deleted file mode 100644 index 41683430e..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_1.60.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_2.00.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_2.00.png deleted file mode 100644 index 8f88c760a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/AxisParallelEllipsesWithDifferentRatio_2.00.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png deleted file mode 100644 index fa2315d73..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png deleted file mode 100644 index cf2981f60..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png deleted file mode 100644 index cae5c978f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png deleted file mode 100644 index 0f315979d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png deleted file mode 100644 index b980bbfe9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png deleted file mode 100644 index aa53fe650..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png deleted file mode 100644 index e5c294a42..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png deleted file mode 100644 index 39a82ea75..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png deleted file mode 100644 index 9a3758c7b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png deleted file mode 100644 index ec42d87b3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png deleted file mode 100644 index bac85109c..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png deleted file mode 100644 index 9250a255a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png deleted file mode 100644 index 8b93c88ba..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png deleted file mode 100644 index 0b26dc5be..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png deleted file mode 100644 index 83299be4f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png deleted file mode 100644 index 8b93c88ba..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/RotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png deleted file mode 100644 index fe59554e5..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillEllipticGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png deleted file mode 100644 index 372948bac..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png deleted file mode 100644 index 87ee84ba5..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png deleted file mode 100644 index 42bc4d9fd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png deleted file mode 100644 index fc5c6ed98..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/ArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/BrushApplicatorIsThreadSafeIssue1044.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/BrushApplicatorIsThreadSafeIssue1044.png deleted file mode 100644 index 0f6b4e174..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/BrushApplicatorIsThreadSafeIssue1044.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomLeft.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomLeft.png deleted file mode 100644 index 9d87a3e8b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomLeft.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomRight.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomRight.png deleted file mode 100644 index c990c85f4..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_BottomRight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopLeft.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopLeft.png deleted file mode 100644 index 7b4f2575a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopLeft.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopRight.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopRight.png deleted file mode 100644 index 88fbc2377..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DiagonalReturnsCorrectImages_TopRight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Argb32.png deleted file mode 100644 index e52fe3a25..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Argb32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgb24.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgb24.png deleted file mode 100644 index e52fe3a25..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgb24.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png deleted file mode 100644 index e52fe3a25..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/DoesNotDependOnSinglePixelType_Rgba32.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_DontFill.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_DontFill.png deleted file mode 100644 index 993d56068..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_DontFill.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_None.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_None.png deleted file mode 100644 index 61c620618..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_None.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Reflect.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Reflect.png deleted file mode 100644 index fb9b58378..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Reflect.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Repeat.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Repeat.png deleted file mode 100644 index 22bdf1d3c..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalGradientWithRepMode_Repeat.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalReturnsUnicolorColumns.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalReturnsUnicolorColumns.png deleted file mode 100644 index 55ab3b4c4..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/HorizontalReturnsUnicolorColumns.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/MultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/MultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png deleted file mode 100644 index fea93e74d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/MultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/VerticalBrushReturnsUnicolorRows.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/VerticalBrushReturnsUnicolorRows.png deleted file mode 100644 index 843833d04..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/VerticalBrushReturnsUnicolorRows.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png deleted file mode 100644 index 91a2e5ba2..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png deleted file mode 100644 index 104e80284..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.5.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.5.png deleted file mode 100644 index 2d6c2c25e..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithDoubledStopsProduceDashedPatterns_0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png deleted file mode 100644 index fe59554e5..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillComplex.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillComplex.png deleted file mode 100644 index 145fba142..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillComplex.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73aba67441d29001460d8fab86ba1bc5623ec1196424cb7f30a0e074cdc52525 -size 9396 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors.png deleted file mode 100644 index 4f0b8eebe..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors_Rgba32_Blank10x10.png deleted file mode 100644 index 4f0b8eebe..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillRectangleWithDifferentColors_Rgba32_Blank10x10.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors.png deleted file mode 100644 index edfa65ddd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter.png deleted file mode 100644 index 7069f73dd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png deleted file mode 100644 index 7069f73dd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors_Rgba32_Blank20x20.png deleted file mode 100644 index edfa65ddd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithDifferentColors_Rgba32_Blank20x20.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale.png deleted file mode 100644 index 6b6619a51..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale_HalfSingle_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale_HalfSingle_Blank20x20.png deleted file mode 100644 index 6b6619a51..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillTriangleWithGreyscale_HalfSingle_Blank20x20.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor.png deleted file mode 100644 index 68f88d380..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor_Rgba32_Blank10x10.png deleted file mode 100644 index 2f4392464..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/FillWithCustomCenterColor_Rgba32_Blank10x10.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints.png deleted file mode 100644 index 87515e696..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png deleted file mode 100644 index 87515e696..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillPathGradientBrushTests/ShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(-40,100).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(-40,100).png deleted file mode 100644 index 1ed0a00e7..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(-40,100).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,0).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,0).png deleted file mode 100644 index a9a508e61..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,0).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,100).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,100).png deleted file mode 100644 index e21877e1f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(0,100).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,0).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,0).png deleted file mode 100644 index a181fff1e..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,0).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,100).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,100).png deleted file mode 100644 index 3a2515a1e..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithDifferentCentersReturnsImage_center(100,100).png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png deleted file mode 100644 index d2e3b3ebb..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillRadialGradientBrushTests/WithEqualColorsReturnsUnicolorImage.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png deleted file mode 100644 index 718c00be0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2f06ea31a467d5e59e0622c3834dd962f42f328cdcc8a253fcadbd38c6ea21e5 -size 10623 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png deleted file mode 100644 index 41a7c333b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:99d1013b8b1173c253532ea299ff7bbb13aee0d6bea688cb30edf989359dc2af -size 10732 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png deleted file mode 100644 index 4de5fe103..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b51e8f78f25855e033b0be07ac4568617ce4c3c6a09df03e1ca5105df35f3b53 -size 10685 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png b/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png deleted file mode 100644 index 6edeb18dd..000000000 --- a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillSweepGradientBrushTests/SweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:733a35be9abf41327757907092634424f0901b42a830daad4fb7959b50d129e2 -size 10523 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png new file mode 100644 index 000000000..1c35d0541 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Clear_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4c37fafab2ab35b0307bf336a8b303be9b9f5799ebf82a098fa61fecb30b2aa +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png new file mode 100644 index 000000000..619d2b15e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c89ed62b21034fc29543c6aba93f0732335476240e33151419b7df6fd985089 +size 1286 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png new file mode 100644 index 000000000..5c1be5560 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e9464f040964086dc020037f6c58aae892bae42a39f76b1c9ef03ccd066a45b +size 1285 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png new file mode 100644 index 000000000..619d2b15e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c89ed62b21034fc29543c6aba93f0732335476240e33151419b7df6fd985089 +size 1286 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png new file mode 100644 index 000000000..619d2b15e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c89ed62b21034fc29543c6aba93f0732335476240e33151419b7df6fd985089 +size 1286 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png new file mode 100644 index 000000000..619d2b15e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c89ed62b21034fc29543c6aba93f0732335476240e33151419b7df6fd985089 +size 1286 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Add.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Darken.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-HardLight.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Lighten.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Multiply.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Normal.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Overlay.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Screen.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Subtract.png new file mode 100644 index 000000000..5fa73ecf8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3263442240c2435bdb182450ae59427231b4507da0024478a341230b23cf482 +size 1287 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..1b1bfcced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:111344a27a98dfedec0c149a1ab880e7a48cb94e295f347641263ee6b8bb1f0c +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..bd042664a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1658dd5d7eb5ded891b0562f76f73bc09ddc033cf8c71b3c4af0c77f8168a4c7 +size 2601 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..900f60538 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdc945d3c033d14261788fa45e8a41dbdff8a01189372866f57f872e360054bb +size 1246 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..900f60538 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdc945d3c033d14261788fa45e8a41dbdff8a01189372866f57f872e360054bb +size 1246 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..900f60538 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdc945d3c033d14261788fa45e8a41dbdff8a01189372866f57f872e360054bb +size 1246 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..900f60538 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdc945d3c033d14261788fa45e8a41dbdff8a01189372866f57f872e360054bb +size 1246 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..2674e607c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b37f26e3ebfb068fafb6daffcc7f7c7d60c9076aa6b589f48bbc60168d604729 +size 1244 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png new file mode 100644 index 000000000..0fbf8d9b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ff611b741480f2af40d7a43eea404d69f0a40bd1559bb4d8fa76fe3def6dc9f +size 528 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png new file mode 100644 index 000000000..17108a624 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8baeef1dd14f37b9eef6f157577f220ba8c907fa601222ee54e60b8f034c7590 +size 898 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..2e2577063 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:202515b2738afb62e58f4af3fce2f59e6aed6ff23a6a580950865137b09bcdd8 +size 1283 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..bd042664a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1658dd5d7eb5ded891b0562f76f73bc09ddc033cf8c71b3c4af0c77f8168a4c7 +size 2601 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..e8d0cecc4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9887933e67b9fc826a36381aa68d4bd61edddb3a183812a0b01477384d4398f +size 947 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png new file mode 100644 index 000000000..b35767b71 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Src_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc9ce37a343e09fef3ba6fff4a0165abc0008cb432520cebd6f427fef96169e1 +size 1110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..6fe1fcd24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendBlackEllipse_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49d9d58c02e26e9f142737b65d29f2f42b8aed104ccc0e882fb018399928a327 +size 1361 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png new file mode 100644 index 000000000..f36cc87b8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd388f89ff0049071999a3885d2f5fda8b573a73ddf1ebe02a6ea362bd5a0908 +size 2583 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png new file mode 100644 index 000000000..bf31b07ac --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22ac4feb1f7476c267efd48cba101220796cff915a52c5efa3272ccb59455e46 +size 2696 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png new file mode 100644 index 000000000..81f0cb50f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6957bad85fde68d4880e565eac66d649701566e684c43aa930890844bdd1076d +size 2692 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png new file mode 100644 index 000000000..41fb5b88f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8e22ba57cde86f63ffcebfa3525c9da137a42e09777d48d4442b5a2e3a9cc12 +size 2578 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png new file mode 100644 index 000000000..6b658af7e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7735a1c1b0b291ab6cdcba1b8caf4fb35ec1567d24b641a96c1f4510791fe190 +size 2678 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png new file mode 100644 index 000000000..1f9d151ea --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1663841fd9861973139139c37f6126084c56d078e2795df0203b581f2d512abf +size 2569 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png new file mode 100644 index 000000000..70b30f55a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb62ae6f07484a1533091a7062ec399d0071f4e6be889ad6f6fe4a1ef88ff301 +size 2679 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png new file mode 100644 index 000000000..29362dbd8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:082af0b1088dbc6fe1c6c5b286ef00404e27aecc77d3f483b4c794f957314183 +size 2583 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png new file mode 100644 index 000000000..fba9467a8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19331d2ba1f0531e58b9adc6f9b2c733dfb1820be8b40e255d8b8d3e305573e3 +size 2677 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png new file mode 100644 index 000000000..f6ba0310f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da392886e5c71f708eccb3e582f513530f9dd312916aca55fc016024bae20143 +size 1521 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..7307a790a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a5d633b5cf463a032323da08d9c29e257c20b74591c13d0629bda3cb240d18 +size 1235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..27f68fa7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:061c901d688aae9a7da4c9f3776a38f3fb60570e97a4abf67eda2e9a8411b2ab +size 2019 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..3ffaef7bd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2e110f322a4cbfb57babc181e8c73f56137a2adba450c44ac01459e77ffbfef +size 2699 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..41713f7c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ed8178579ab7645fa0afc9935b3034a45e36fbcb5281b6d4929ac919ffdd4d3 +size 2627 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..ae387faf2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a82dfae9b5fa9d88667691db98f9f3d3ef4fcf25bc059db30b6d4e9e695b0dc +size 2014 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..55b00d367 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbf23b924cdeb8305dc16ec0074dd62e912847c78c7789511bbac578cb8040d3 +size 2684 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..8f97b6166 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df8d96a6b84d517bb9822037f0175cc5502706bea6b9aac49a563a8b2dbca9fa +size 1664 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..e8ba30165 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c974ea37fa8b06c914ed1d7ec5578d2e0d2d71d6d0c547ecaec3400de1591bb8 +size 2845 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..1e44ff74f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e6266af280c96656040ffbc4817d7d1319e84df32de82650c247dbf043bc4f5 +size 2018 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..f69bdbd04 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b6de848e7181310b1f9c4344864ff0708d28fcb0467a7b7ddcec0e05e898450 +size 3051 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..95f4e5c4b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735711542ef4c1d22e35fa72e96c57e7fb024d20fab462b703898db6c7b3888c +size 126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..fe6da0d05 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2b488c94b3905a260526acb6352069e360d484ff82f72bfda4924330e9902af +size 1246 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..5c61391c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c579c96db5b4e1fd4092ee33384e7e554a6f7d47d4dba13428d701e3de34b8e5 +size 1514 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..348eb39af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9be4df716e14ddf1474548007e5044fe33a1d7a154658399166bcfd1c603fd7 +size 1662 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..233a1e923 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df5a41472ca1d36eb9765460f7ff361c70420b24f375ee77017f212dd1cdfcc0 +size 1248 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..863a9f701 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:590604105ff204b34c4b37d4d435f44c338c4a0704815d4c85a5341fd47cf1f0 +size 1501 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..0ee86a1c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:656b249ff4d35d668f55e0042935f53d03727b852bd2f5e48501eaa323843961 +size 1744 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..11b949fe8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22979a807ecfc85220a8b1ceea989328a6d4ca49383f3a1fda716ecc17440bfe +size 1464 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..92c576b5a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38ad0147325ade171a6b670c9c90ede58403fca16c9c880739bb5e5ad26798d5 +size 1250 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..2754dfdb6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb1cd6693a9359d352b3991f0df2bfe13ce1b905b843fb22956646d7d0c541f4 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png new file mode 100644 index 000000000..15b50c225 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acf34c0a4ffffdddb1ce1defaa22e700a06386279879a9697a93bd0d6c2b9b16 +size 1619 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png new file mode 100644 index 000000000..d8e96d4c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c666fd37e860b6bf4fffd2f0f3f8244259cbfb98b85ff1c49fc08c3de42fe318 +size 2178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..27f68fa7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:061c901d688aae9a7da4c9f3776a38f3fb60570e97a4abf67eda2e9a8411b2ab +size 2019 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..3ffaef7bd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2e110f322a4cbfb57babc181e8c73f56137a2adba450c44ac01459e77ffbfef +size 2699 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..e8ba30165 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c974ea37fa8b06c914ed1d7ec5578d2e0d2d71d6d0c547ecaec3400de1591bb8 +size 2845 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..ae387faf2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a82dfae9b5fa9d88667691db98f9f3d3ef4fcf25bc059db30b6d4e9e695b0dc +size 2014 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..55b00d367 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbf23b924cdeb8305dc16ec0074dd62e912847c78c7789511bbac578cb8040d3 +size 2684 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..2f83f906d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3561993df7b8c6270abf07aa8745571314376632cb477691056d88dc0693858 +size 2901 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..41713f7c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ed8178579ab7645fa0afc9935b3034a45e36fbcb5281b6d4929ac919ffdd4d3 +size 2627 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..1e44ff74f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e6266af280c96656040ffbc4817d7d1319e84df32de82650c247dbf043bc4f5 +size 2018 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..91027c85f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a37dab49accc6ec88201c5e33101c67f25e42699f0e0cb68fa28bb57f3e89f2e +size 2017 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png new file mode 100644 index 000000000..a04cad7f2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652ef3e3ed5bd7065574b89f7e0424b10b76f1fa6af6e98472b97906db7945a9 +size 2683 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..84b2c17f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e1a59d78e1d4169703798a10074898591362a5bf8314c203cd07e13dcf93e15 +size 2648 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Clear_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestAtop_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestIn_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..f605fcda3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..35b66a733 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..b5724b84b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..76673a63e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..9d2e596e7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..1ab310b89 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f23c0af3155c58de5594a423ac22f97288ad2d802c22f8ec89a9d32e9b363982 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..e55378a70 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b06af1e79bd0bd98b638bd2cd6cc80a25613723d8c7143abcce95e0b801b089d +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..20e8f35a9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8d16b2057686a61d4be59dbffc052cc03c328c36fcff65ad5ad2b53529d0dbf +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..9ab6cc778 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45283aa3e74d93b384df71feeac379a3911582fabdc0145e94a6eb488afe4d7b +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..2205dcba1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8500bb6fa35ef940302d9aa9d391077af78f8c509c93900f191e2e080b308d7f +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..1c55a5890 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdeac033f313e0151bd177678c697499f8f09c31df65d4e3a9b512f369c90da5 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..adef2c3ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a26c4fb100ccb1026def8738856423e945cb193d8cd1c93365a08f4b3d6dad3 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcIn_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOut_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..f605fcda3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..76673a63e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..b5724b84b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..35b66a733 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..9d2e596e7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..2d42e2a0f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d378094f7ac82529a962ab155ec020c10c51394728f340bf20af17bf6e9bf07 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Add.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Darken.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-HardLight.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Lighten.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Multiply.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Normal.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Overlay.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Screen.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Src_blending-Subtract.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Clear_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png new file mode 100644 index 000000000..f605fcda3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png new file mode 100644 index 000000000..35b66a733 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png new file mode 100644 index 000000000..b5724b84b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png new file mode 100644 index 000000000..76673a63e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png new file mode 100644 index 000000000..9d2e596e7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png new file mode 100644 index 000000000..1ab310b89 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f23c0af3155c58de5594a423ac22f97288ad2d802c22f8ec89a9d32e9b363982 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png new file mode 100644 index 000000000..c05007ce9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77ecd6cab29c9f68f9b1c495fe6465fc2e0f7c83b81f9d136feae3efd8c347c +size 135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png new file mode 100644 index 000000000..f605fcda3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png new file mode 100644 index 000000000..35b66a733 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png new file mode 100644 index 000000000..b5724b84b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png new file mode 100644 index 000000000..76673a63e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png new file mode 100644 index 000000000..9d2e596e7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png new file mode 100644 index 000000000..1ab310b89 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-DestOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f23c0af3155c58de5594a423ac22f97288ad2d802c22f8ec89a9d32e9b363982 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Dest_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png new file mode 100644 index 000000000..e55378a70 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b06af1e79bd0bd98b638bd2cd6cc80a25613723d8c7143abcce95e0b801b089d +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png new file mode 100644 index 000000000..7333d02c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d84ca3aad81a27c5d11fefda37153dbdbc11ff2d84e3c57711f21d5b22ecde8f +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png new file mode 100644 index 000000000..20e8f35a9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8d16b2057686a61d4be59dbffc052cc03c328c36fcff65ad5ad2b53529d0dbf +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png new file mode 100644 index 000000000..9ab6cc778 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45283aa3e74d93b384df71feeac379a3911582fabdc0145e94a6eb488afe4d7b +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png new file mode 100644 index 000000000..2205dcba1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8500bb6fa35ef940302d9aa9d391077af78f8c509c93900f191e2e080b308d7f +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png new file mode 100644 index 000000000..1c55a5890 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdeac033f313e0151bd177678c697499f8f09c31df65d4e3a9b512f369c90da5 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png new file mode 100644 index 000000000..adef2c3ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcAtop_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a26c4fb100ccb1026def8738856423e945cb193d8cd1c93365a08f4b3d6dad3 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png new file mode 100644 index 000000000..f59ea17c0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcIn_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c441d50b1a5f7403cdf86563a38a9e380ae2cb2bb335880d341556f6ce1f3f63 +size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOut_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png new file mode 100644 index 000000000..f605fcda3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46a33f56439bd22e8bac8a53f76a4f6589f1838f2a606fb74b53902fae64952f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png new file mode 100644 index 000000000..49fcf0c57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b047a5333d017d4f872fc01474d6dd4e5be1ad4516a688bc519501cdfdf26cc5 +size 176 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png new file mode 100644 index 000000000..76673a63e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2afc2175477bf2822b6df58feaa87401875e61fc5ca58ece56f55411dfd1bf60 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png new file mode 100644 index 000000000..b5724b84b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:620336bdd922435ee42b83cb25691d9e762e889c9d051dc9248dea6a995d73bc +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png new file mode 100644 index 000000000..35b66a733 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b87e2dfb5166737c6e4d689b163937a948d993203b70b58b95334d0672ef997f +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png new file mode 100644 index 000000000..9d2e596e7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2927660c0739424f62be298ad2da0b3c06b69362c665e52f8efdb281dc6613fa +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png new file mode 100644 index 000000000..2d42e2a0f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-SrcOver_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d378094f7ac82529a962ab155ec020c10c51394728f340bf20af17bf6e9bf07 +size 183 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png new file mode 100644 index 000000000..d61a0f208 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Src_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d166c86a768bff0d93b077342b4e018e5477a9985d3cb26305b1e8c56e5d96b +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Add.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Darken.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-HardLight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Lighten.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Multiply.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Overlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Screen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png new file mode 100644 index 000000000..3f7a7ae6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRect_composition-Xor_blending-Subtract.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f01507f25ec90159d46faa286485950b3cd28aba1d20628390fef3d41539b9 +size 180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png new file mode 100644 index 000000000..419c86358 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ab5c9a58bf74536edd850c3609c5f3a7a4cd0bc4a9e1116daa5439c8e2dd1c6 +size 4166 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png new file mode 100644 index 000000000..beffa4685 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc64d91dd1341ecc423d2baed7f324644887db3beae40454ac99b0bec8f8a17b +size 5893 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png new file mode 100644 index 000000000..099fb59ad --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11f3b611ce3e427a590e0f93f23974c7b8b92353235685b7a0b4e53e436e67ad +size 5252 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png new file mode 100644 index 000000000..b00c89c6a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dae5da0afaff8298206ab5648627bd0b7b64648e305bcf937dfcede146adf24a +size 5307 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png new file mode 100644 index 000000000..9dbebbfb5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7141903694f5d9d7689d57baefe92a0b282ee088d77a213d937b76ed25e1d0a3 +size 4383 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png new file mode 100644 index 000000000..83c1981b8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa514df21cc4dd41a0d40db114d41b44dab0502fb233529ce31c95029a5ee08d +size 9420 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png new file mode 100644 index 000000000..8b31d2b89 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55b0af0e5f9f0b0296a725738e364a725a33a3957af4531da3af6abe06babf20 +size 5210 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png new file mode 100644 index 000000000..66e89b2a7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8338644c7a56c54da3bcf15c5b912ec825714192b4b6fd28e5cc7063575b8141 +size 10036 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png new file mode 100644 index 000000000..d793e1926 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23871e431b2be3d4ce4ca64f1c5832f47b1956d4b4d4e8b04f18c35306659363 +size 5031 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png new file mode 100644 index 000000000..7b8ee3409 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9be53b4257b2c3e5723f1353cd9a090abbf798fa2f2d4c7dac3fb44defc4f0d +size 4949 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png new file mode 100644 index 000000000..8384edee9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35b13315e9aebec1ff2ce00dc73ca60a77432716c6912777a1c40bfb11c53227 +size 14328 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png new file mode 100644 index 000000000..a5c714523 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77cf0bd7da4876d7788a1fcf214b66c212dd788630217fe3f7ea368775806037 +size 29513 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png new file mode 100644 index 000000000..b9d32646f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25d0f2be99b5c07bbb8dd6589ec1e8d89f8770510278e82075ef939ffe7f9324 +size 11117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png new file mode 100644 index 000000000..66a8f12f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32a1a6fbba80344f5864afc08c2bd0c20aa14589d36454cb940ffac0c4612b42 +size 4479 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png new file mode 100644 index 000000000..697329fd6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:234596df14e1468aae4d588891314c6db9e39ee87e5799dc5be419814d169810 +size 1132 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png new file mode 100644 index 000000000..7c86cc5f6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd22b45e179c2a61d61950c3e7273d63a260f8aa5c512008819ec9d777fb6479 +size 1953 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png new file mode 100644 index 000000000..191f2bfa8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a2a6c273dabf7d3a7d38b422da03d5524fe3c721a4b0d66a57e566372431b40 +size 1729 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png new file mode 100644 index 000000000..352c15727 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15c42e90e5982d8f5588911506bce748df0c734d0270bd100ec8fcaac1afe6f7 +size 2595 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png new file mode 100644 index 000000000..25a16f218 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf9aa62ce21cedf71c58cd5fd315d8836a274416e3721b376cdc7eaee2b50bea +size 2502 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Blue.png new file mode 100644 index 000000000..5254df120 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bbd2c59e95ae401a038f69d9de433b56ea89c493bf5d73af54197fadf032393 +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Khaki.png new file mode 100644 index 000000000..4bd589703 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_AlwaysOverridesPreviousColor_Khaki.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27281a193533244c31acb485c7ecacc047b505042a1bbfd3c214016db2b6f7b1 +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Argb32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Argb32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Rgba32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_RgbaVector.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_RgbaVector.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSinglePixelType_RgbaVector.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank16x7.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank16x7.png new file mode 100644 index 000000000..113c9e069 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank16x7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:577cff471034b801e84c2df271946e59e441d0890910b949dcd7b81b25f38d58 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank1x1.png new file mode 100644 index 000000000..d406a3275 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank1x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8df185b0b10595bba92b871646a6b349b308221e63c2ead096e718676716bddd +size 72 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank33x32.png new file mode 100644 index 000000000..4c6092dfc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank33x32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e7483e76c3b65b94b68499f93f07b2c73435353adf46638c2d1fe16d62f6a0 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank400x500.png new file mode 100644 index 000000000..af764cb13 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank400x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed2eeed8c081a355f23062a56ea80f1891745c63f8468d65771e49418b508cd6 +size 119 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank7x4.png new file mode 100644 index 000000000..0a126de11 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_DoesNotDependOnSize_Blank7x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c24b47a3afd5d7185d9722cd8f0bd4274489bedd4e09c8d23a66e49af405dc2 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png new file mode 100644 index 000000000..cf2790c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png new file mode 100644 index 000000000..7631eab46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png new file mode 100644 index 000000000..cf2790c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png new file mode 100644 index 000000000..7631eab46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_Region_WorksOnWrappedMemoryImage_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Blue.png new file mode 100644 index 000000000..8570310b7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c70c09a82dfbb4db1955e417c1f24ea90178f5234bba420e71f74a094218c7c +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png new file mode 100644 index 000000000..3730d9c35 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/Clear_WhenColorIsOpaque_OverridePreviousColor_Khaki.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dfc380297d61413eec1b9a3f634ae0489c22fb310b004e1ecf6ea26ecc28b5f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png new file mode 100644 index 000000000..580577b02 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c7a381cf9d9a6999f30c9198cc7dbe3797ba3a066605c2f4c3562cea7664d9f6 +size 31167 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png new file mode 100644 index 000000000..36f9ab4bd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ebe55ff26e92e0dd36fd808dc97951c95c46d5ac1580aa90535005b3632ce9d +size 4713 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png new file mode 100644 index 000000000..6b87d43b3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x-20_y-20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a6505097a926ca0ff62ecb28cebe587b42c7e9264517d86065304db34e313b9 +size 4993 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png new file mode 100644 index 000000000..64a69e06a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x0_y0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbef83e3f57bec8348f37b109022b3dfa07112faa52b08cb6c46df577cf842a9 +size 5311 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png new file mode 100644 index 000000000..38eb1ad67 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x20_y20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9b83a8a870b65c2e1cdf2cd2230873e05648a84716abe8ba340164f4a7c7e33 +size 5390 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png new file mode 100644 index 000000000..e902cf3ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipOffset_offset_x40_y60.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d64416f7cc96ef43fd743ee4a8281bfc8130cfdac209679d458f9a3e6c73db4b +size 2319 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png new file mode 100644 index 000000000..c2e27be7f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8428807dbc2f1ed17e54a125f4f36d64e6654696ebbd3af7f30c6668c6f3354 +size 11177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png new file mode 100644 index 000000000..56a7febc6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24533ba6a0c9a2abd06e151ea0c698397dbf6763b41412f37f232c0399ab907d +size 4614 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png new file mode 100644 index 000000000..55710d28b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f54ff61ae8a5c265a8938f61a544bbc33d551627f9cca5f3e0e4e17021668e6 +size 4614 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png new file mode 100644 index 000000000..0a1de4f0f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e9718e1d652faf60b67b43f62aa841226a0ef3c730284458bdf597c2aaf17a3 +size 4615 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png new file mode 100644 index 000000000..0a1de4f0f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e9718e1d652faf60b67b43f62aa841226a0ef3c730284458bdf597c2aaf17a3 +size 4615 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png new file mode 100644 index 000000000..f9a5dfa53 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53ddc1e46d0c59a61a7e225a63065f6532c95e79cf3d8421083d0fe9af5a71ba +size 4328 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png new file mode 100644 index 000000000..f3e0eaa7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:260c214b122ffab9fe16c32a8dc0d334d8380373cd6c66933ffdbe0d226f65bf +size 8034 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png new file mode 100644 index 000000000..0fe703304 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9118b0515f4318afb00ee3d3f7d7a77c7d46968a9ae1a4420d22e18d88ca299a +size 5949 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png new file mode 100644 index 000000000..61e7e2d14 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77017e9f42a983c28bce49120add1ea74bc9a67df3fe6a40c04c7ba84b8ce00c +size 4020 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png new file mode 100644 index 000000000..368f44ff6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png new file mode 100644 index 000000000..368f44ff6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png new file mode 100644 index 000000000..b213ccca7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b +size 92 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png new file mode 100644 index 000000000..b213ccca7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b +size 92 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png new file mode 100644 index 000000000..2ea9c6524 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0ad1d0a22afe164100e33eefe58c51e864aa2db12eb76a2b56e076015259060 +size 1011 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png new file mode 100644 index 000000000..a57c7909d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0895fbd030192a18b6877826e5022ff45597ec1f57639f2480b919a2a31a476 +size 1073 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png new file mode 100644 index 000000000..27f4564db --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5809a59acabe48b8cbb66dd3636fe1575856d461e511c78632ef9551c2d3c14 +size 903 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png new file mode 100644 index 000000000..e4452b6d5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53c0b23db1f3067f91712c507ddcc652eda6fd10b3cd813a53c081a477338559 +size 5001 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png new file mode 100644 index 000000000..02db47de1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af8d0844f56649d2af0a231aaf4b26adbd1bab85d0a8de6c6229fe18d31f19a5 +size 4200 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png new file mode 100644 index 000000000..424503275 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dcd6bebe4682e60ba59eb799333b3d447578a325f9863af8620290b6cec44a26 +size 3119 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png new file mode 100644 index 000000000..d3ef750b3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a3ce15c6c0eb498f83bc84f90ffbb0f2ea8e1aaf71a0add5d2d05a7192902bc +size 2381 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png new file mode 100644 index 000000000..e1a3dde6f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d01bf108046776e67353dc02793c6879f5b6b6f872017168cd056da4b62599c7 +size 2408 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png new file mode 100644 index 000000000..77b1898b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da6894cf11f483dcca6f93f8d33f942dc19d807a90f86e5e231f2e19ea6496e4 +size 2386 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png new file mode 100644 index 000000000..b67d7e452 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5606c76bc4cf75ce6a52480eb317224737ddab306c0cb94af78ed3be891b8ce1 +size 2229 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png new file mode 100644 index 000000000..80ef0ca9e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d86a8493eafa23883deb6b64d4fb000d3f800b156b8c7587d8e2c08b9f246310 +size 4276 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png new file mode 100644 index 000000000..e4122b40d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:549b2ee007b7d77f535b79bb62047f84acc4f4e1d0c4b2ab9842f3227df2cb23 +size 2294 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/DrawLinesTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png new file mode 100644 index 000000000..ccd90b66e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:866448b41b6b1235e5088aba2646f000d248388b8d178c794164a27d86f8f73e +size 4494 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png new file mode 100644 index 000000000..50edd62cd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aae1d3f5cfceb6d57492e2a801d469939410eb88b2eb5741332489348d42677 +size 4471 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png new file mode 100644 index 000000000..328a60bf5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb7f33b66433bb143e071e584d21d58bfca3cd6d8489c4c2db19262da7d6b5e1 +size 4448 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png new file mode 100644 index 000000000..88e8513ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b22c78e6a2f298013a7d0fd99116f3bcf6b4b630383f83176dcb2b65c81d3108 +size 4448 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png new file mode 100644 index 000000000..3da386a15 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d83a3f350dd4fec37974ddddb47d450fcced49518941351204be2f64430e876 +size 201 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png new file mode 100644 index 000000000..2cad4e8ea --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6726a8085ddbc5fae97195467806397f436b0a937575c6137549a4238cb748e9 +size 5912 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png new file mode 100644 index 000000000..fd6bfc047 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2337fc9b94c179fc442fe16eb116dcd913a8e9efba087a569fd518c587e198b0 +size 7740 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png new file mode 100644 index 000000000..d34e92656 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4821d9fac4667914c3d188c72282c7531377afbaf61eb205bdf31ebaa11525ae +size 15011 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png new file mode 100644 index 000000000..0caffaa80 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64baf204b983d3693a1e0e22d00c2d5ca5c1a11dcf9403324bc64d9b3b6287dd +size 14446 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png new file mode 100644 index 000000000..10e15c453 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bba187ab4f8855f2a39ebddc8517fedd044ab4978fd6cb6df0e3d1b90d93c18 +size 7271 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png new file mode 100644 index 000000000..1bbd0f4af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:035039c45eb0f635926d9920f8856d475f23eb2d32ef90fb8cc3dce86e01ad05 +size 7981 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png new file mode 100644 index 000000000..a7b1b6251 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b516e8f47ba90e9f4031a886a4aab76035f6df56d47e7f90711cd120b897a9c4 +size 637 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png new file mode 100644 index 000000000..f69b5b405 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4837b2a336496c63da6306eb8ae1e43b682a923e9e79f95857f2a310e0ac40a0 +size 3654 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png new file mode 100644 index 000000000..2fcfd482c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c00fe9486b4a3ab9361bfc8b648910c5fb711df112ccd26af1ab2d3c4ae542ec +size 3467 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png new file mode 100644 index 000000000..66d49f1c6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1d5f2030d455b034514b4de126429a6f7c8b04eeace81fca63aaf124c4a5c7 +size 6499 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png new file mode 100644 index 000000000..e9e3ae1e3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54ea5ce83e31694e5d4813ae10d431bf73bd0c2c1bb3acbb6490dad2b15b74ff +size 1264 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png new file mode 100644 index 000000000..05ef027b2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1aec33dd995f3286fb093302e7f727447390d697bf424c4da2ec0482fb3fe4e0 +size 10511 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png new file mode 100644 index 000000000..e3b70cbf7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d54475500b1b3c8b9ee9e175a00d8e029e97c40feef9ac7bc5c10c44b937f14 +size 3108 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png new file mode 100644 index 000000000..08888409d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:685525a8bfb41b48a9800ee4fafb7b3bc0d3f2ededa7e5c03fbae56ed81c8f19 +size 10154 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png new file mode 100644 index 000000000..0b29f4609 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f482cc7cf211fe02f29e635ac0a4a3647538289c784843eed0e1dcfc0547793 +size 8844 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png new file mode 100644 index 000000000..d3aab7737 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de25459aa64c23ca1ed34d1a14fbb7a70b5e46ff88daba30829e73da7e4d17fd +size 11434 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png new file mode 100644 index 000000000..6e9a95855 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bef21afb2782a50b1f0aa565ba11e49bbdc05b900797bd583b8e028a806ff1c +size 13175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png new file mode 100644 index 000000000..e9d4b25ac --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f6b4b4f483a05e1302f320b34c1db8a55fe0d5c369fb36eb5f53e0965f3b8e0 +size 11795 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png new file mode 100644 index 000000000..283056b24 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ec84f87a66f827fafbbb7862eb9ea51ac4fd7fc0ccec04d1eefc923e74f3573 +size 10041 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png new file mode 100644 index 000000000..47393cc4c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c6e63a50c14c949c963d2c01f804f8c75662725ddc46f925dd4e40792effa20 +size 18510 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png new file mode 100644 index 000000000..403a99150 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a970c680a5e2b24858f137ce68ab052fa413a3b610c06cb023765f2bb3542bde +size 1807 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png new file mode 100644 index 000000000..ae463ca4f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a32d25836eac28d9a0d7d086494eb0ab597ed277a8f34c1199fbda7a8e3f490b +size 6180 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png new file mode 100644 index 000000000..b991f5e58 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Overlap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e50fde7bd9535b317b2202bffce44c14744835115a1df0230a7724db8809227 +size 6650 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png new file mode 100644 index 000000000..98a3e0da3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillComplexPolygon_SolidFill__Transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b16bb433a83f6931c9f7a7898612e0f49984ba7002c0d9ddb2018f27df735962 +size 5982 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png new file mode 100644 index 000000000..43ad24b93 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a7090de2e49df30cbfa51953f77d5e0584438602265ff6a48ad75c53e4e8e6 +size 674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png new file mode 100644 index 000000000..c0decb5c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.40.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0890bc4fde8013d814458a2ec34ed1e51c232e1e99c31b84dc7ae023c8bf62a1 +size 1548 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png new file mode 100644 index 000000000..5b210ff3a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_0.80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6412666ed00fd3b2dcf6ce7645dec3fdfa5fd029651f8166b9e7f34bf5b47d4 +size 1704 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png new file mode 100644 index 000000000..e6f419818 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47225f2e7993b60477676d92f14404a0073a6da793e03fdc9c6de044a428e8f6 +size 1907 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png new file mode 100644 index 000000000..75186d636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba202c83d3bf4a900af8654ea8b231393a4182d7befbbf895fff418e37bcadf1 +size 1996 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png new file mode 100644 index 000000000..0db8ffed5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_1.60.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:804e9a7d31d80705e5005a39edbe28dd8ae766b2885e38e45933a62de2b9509e +size 2114 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png new file mode 100644 index 000000000..4183ebed9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio_2.00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffc51d54e067ac514425d9ea9b19f2fc5f0b4af11451be153c1da29885ede640 +size 2225 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png new file mode 100644 index 000000000..43ad24b93 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a7090de2e49df30cbfa51953f77d5e0584438602265ff6a48ad75c53e4e8e6 +size 674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png new file mode 100644 index 000000000..06c04c71c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e2dc06fdec0dd48bd1e4e29420bf25e094fcf98263634e8efb0b11f85f5892b +size 932 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png new file mode 100644 index 000000000..90d85f0fb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a742a4e1ff6786d4435a3abfae5f3de97b8409434a7fc7c7d8c567cdf523c7b8 +size 796 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png new file mode 100644 index 000000000..27a056b60 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dbe6cef98d0bd165a63aeb282e21ac1ff480171af32d9d2babfe75113a5da57 +size 535 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png new file mode 100644 index 000000000..c0decb5c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0890bc4fde8013d814458a2ec34ed1e51c232e1e99c31b84dc7ae023c8bf62a1 +size 1548 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png new file mode 100644 index 000000000..82fe59ac1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97cf0529ee30218df153d180864938918f9ef118cdae35f6d0d347e088b41776 +size 1445 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png new file mode 100644 index 000000000..a3f902e34 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aa7cda377b908fb4c4cd9ae1b8e2dfc493d82f6771a1ad3e074657c8c431c0f +size 1329 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png new file mode 100644 index 000000000..834baf70c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:559e0f049d6d5b990347e36caafe696fe12e39a9d609755b8d87365cc37e9ac3 +size 1436 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png new file mode 100644 index 000000000..5b210ff3a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6412666ed00fd3b2dcf6ce7645dec3fdfa5fd029651f8166b9e7f34bf5b47d4 +size 1704 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png new file mode 100644 index 000000000..d96dd5716 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd10694b16b98783c3aa16ac9e50501fff4c5c9a5dd9a78ee91dd2af8c949f8c +size 1687 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png new file mode 100644 index 000000000..322d69db7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:405686e70c0f341f219496dd0b34e7ad5373ac43eb38358dee9a5e59d67cc77a +size 1674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png new file mode 100644 index 000000000..f24da8215 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d75fa6b243b87202ec4d7a599378c9b4195c15c772a36c7aeafb64e41263ce3 +size 1597 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png new file mode 100644 index 000000000..e6f419818 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_00deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47225f2e7993b60477676d92f14404a0073a6da793e03fdc9c6de044a428e8f6 +size 1907 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png new file mode 100644 index 000000000..3e9e0fd81 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_30deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7cdc8e25d6545f4d5ce0265dd152e58748f7740d071e257af671abca5f36bd7 +size 1801 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png new file mode 100644 index 000000000..f1b795aab --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_45deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ae44f19a7e2c1aba894bf706a33b2fd533e1718a5c895132007db06eee5ffdc +size 1861 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png new file mode 100644 index 000000000..e6f419818 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_1.00_AT_90deg.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47225f2e7993b60477676d92f14404a0073a6da793e03fdc9c6de044a428e8f6 +size 1907 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png new file mode 100644 index 000000000..5510cbb77 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb27d43cc9608027f87b0b9dfb56404a3c6a7f5de3a86746836bdf1756b01559 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png new file mode 100644 index 000000000..f6793dc92 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawLandscapeImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a772b2e9f117174a54de856c3a9b3ad0e592b6c45d56f0f7930cafe424367370 +size 24695 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png new file mode 100644 index 000000000..eb08e65a4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawNegativeOffsetImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3856dd885f97801efd250f858c6c287b4c0ab5b05dc9f6eca5defc2412b344b +size 100634 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png new file mode 100644 index 000000000..c3a129338 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawOffsetImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3aa1aac6aa2484bf7eae2e6fd08de4c7b6110833e19e7ff4b217e005192933e +size 100593 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png new file mode 100644 index 000000000..74465715e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanDrawPortraitImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c5046daca9f61c66a91e323c2762fa6eb86bcc0a75430d34acacb774319af95 +size 18999 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png new file mode 100644 index 000000000..1fe5826ba --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5267aef9d7cf7f6adef2b84332a814aa8891ecf9060d142cec534d586b3ca55e +size 73970 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png new file mode 100644 index 000000000..1fe5826ba --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushCanOffsetViaBrushImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5267aef9d7cf7f6adef2b84332a814aa8891ecf9060d142cec534d586b3ca55e +size 73970 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png new file mode 100644 index 000000000..109bad9cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Bgra32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64f79e92ad86f3efb9cdbafe15c7fffb04694362d1d5cceaa5ea613c68a940f4 +size 28900 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png new file mode 100644 index 000000000..109bad9cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillImageBrushUseBrushOfDifferentPixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64f79e92ad86f3efb9cdbafe15c7fffb04694362d1d5cceaa5ea613c68a940f4 +size 28900 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png new file mode 100644 index 000000000..bcb3f3844 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;000080FF@0.2;90EE90FF@0.5;90EE90FF@0.9;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da374c22ddaefda0d975a5c50f3efc3738e000314a73c878ad9dbf1455f47e8b +size 4433 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png new file mode 100644 index 000000000..1999238d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,0)_TO_(499,499)__[000080FF@0;90EE90FF@0.5;FF0000FF@1;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03880367b9a9a528f01b4d0a398d48ed069ad1e3f1d3cae7519e298f121b3307 +size 7037 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png new file mode 100644 index 000000000..2989aa05e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(0,499)_TO_(499,0)__[000080FF@0;90EE90FF@0.2;FFFF00FF@0.5;FF0000FF@0.9;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e0ce379320f6b2b225d8d0c26e2e956aa484341b0dac43f8523e51361a56cd1 +size 6978 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png new file mode 100644 index 000000000..348ab784a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushArbitraryGradients_(499,499)_TO_(0,0)__[000080FF@0;90EE90FF@0.7;FFFF00FF@0.8;000080FF@0.9;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19bd1e4651295d02b7635609a029e236a1c2c5a1db96c8191f87fcaf69c4ac0b +size 6858 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png new file mode 100644 index 000000000..714585d94 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushBrushApplicatorIsThreadSafeIssue1044.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbebd336f68bb4232d683ed6ae5e8659cd274ed5ac3c72d41b9e0935c5bd3139 +size 4674 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png new file mode 100644 index 000000000..252662fab --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e826ac8ef1c72a6f2b050ce10a649f905269e47461a72137335fbbcdffd03ff8 +size 1972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png new file mode 100644 index 000000000..c10d3a435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_BottomRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fb2276b9f6c566156011a12a61d6517c7bc2069068a0eb1ea6e3f69a4a081a0 +size 1719 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png new file mode 100644 index 000000000..8bbe40ccd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e06f9d71559cccde2950eef23cfe7041e1e00f66d08cc3013b1489bb8bca161 +size 1719 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png new file mode 100644 index 000000000..4f8cc9dbd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDiagonalReturnsCorrectImages_TopRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b676834eb7a429b08ac4a437f00de60f3511eef118887a3c0650bde750af1983 +size 1971 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png new file mode 100644 index 000000000..8dda79f32 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Argb32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cc7f7d5e5950caa162e7ec82b617129a2683107518e352b5768eb015b240285 +size 87 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png new file mode 100644 index 000000000..8dda79f32 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgb24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cc7f7d5e5950caa162e7ec82b617129a2683107518e352b5768eb015b240285 +size 87 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png new file mode 100644 index 000000000..8dda79f32 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushDoesNotDependOnSinglePixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cc7f7d5e5950caa162e7ec82b617129a2683107518e352b5768eb015b240285 +size 87 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/GradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushGradientsWithTransparencyOnExistingBackground_Rgba32_Blank200x200.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png new file mode 100644 index 000000000..81f4afd17 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_DontFill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62d1f8629981f138a8151010da4b2587dff1991a6bd537b153920703a7873e3e +size 162 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png new file mode 100644 index 000000000..86978ce00 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_None.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff0a58b52f199e1c96255546fae91c3eadc9055786848ec061fe637c94346350 +size 131 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png new file mode 100644 index 000000000..42b48642c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Reflect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ddf6564ea8a8c8c7c8e919cb046b82ed5e7932ea8c2b4a1b23f5ee3914463ea +size 151 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png new file mode 100644 index 000000000..72d55008e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalGradientWithRepMode_Repeat.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:652c64d699abdcf15f858be4479e4837a0310ee3749028fb46a340e736c4080f +size 145 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png new file mode 100644 index 000000000..ba25a6969 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushHorizontalReturnsUnicolorColumns.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69c58759a0fb50eb92431e835a8ac2fc7a97032bbc88f321c56832e6b6271f49 +size 148 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png new file mode 100644 index 000000000..449b5dbed --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushMultiplePointGradients_(0,0)_TO_(199,199)__[000000FF@0;0000FFFF@0.25;FF0000FF@0.5;FFFFFFFF@0.75;00FF00FF@1;].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15351ad42f0cd94694323203d78efdc0619777f8c20be2ca11ffb7ca1e4a7e04 +size 2135 diff --git a/tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/RotatedGradient.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushRotatedGradient.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/GradientBrushes/FillLinearGradientBrushTests/RotatedGradient.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushRotatedGradient.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png new file mode 100644 index 000000000..d62d7aecd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushVerticalBrushReturnsUnicolorRows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3187dbe8408a733123fe1a15f4245002858c70586d0fb1f31f9bdaa9035bee27 +size 168 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png new file mode 100644 index 000000000..3d75bf3e0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.1_0.3_0.6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd27868ebc84bc9614f78540f96ace5082dd4e5f2deacf22d684a9aa58835a5a +size 116 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png new file mode 100644 index 000000000..1ffbe1b0e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.2_0.4_0.6_0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e9edc0787ab2997d19da66c0b59d836c8e0acd9422834a01f60d35de37a2d15 +size 110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png new file mode 100644 index 000000000..842796a9c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithDoubledStopsProduceDashedPatterns_0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f49d7730c8e8c52f2b072bbe2ff2717464aad4eebc14ec0bda20596e08e7f7a5 +size 109 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png new file mode 100644 index 000000000..5510cbb77 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillLinearGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb27d43cc9608027f87b0b9dfb56404a3c6a7f5de3a86746836bdf1756b01559 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-20).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-49).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_-60).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-110_0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(-99_0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-20).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-20).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-20).png diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-49).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(0_-49).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-49).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_-60).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_0).png new file mode 100644 index 000000000..3a265dcef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(0_0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c18390683a7c9f02b504a5913308ef45b4601d9b3054ac43fe52e26dfc18406d +size 540 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_-20).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-20).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-49).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png new file mode 100644 index 000000000..99f68c1f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_-60).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d2c4dfb8665ad8cae68673c8deae206894bbd48b8b00990d6a3104060eafbb5 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(110_0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(110_0).png diff --git a/tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillOutsideBoundsTests/DrawCircleOutsideBoundsDrawingArea_(99_0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillOutsideBoundsDrawCircleOutsideBoundsDrawingArea_(99_0).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png new file mode 100644 index 000000000..386f19532 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adfac6615d147937c5cf6fe138e19939817688d1ecfe7c8a4ede13efb504a0c0 +size 1805 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png new file mode 100644 index 000000000..1e70a2ac5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d63bd76d3b1150cdeb0d16428033ef35e81fdb340d2f1cee075a64f98e98027 +size 1653 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png new file mode 100644 index 000000000..30cec9964 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillComplex.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14841d28a0bc5218d7b5d969d990a6df51757b77b0065f19bae2263aec70e1c0 +size 2783 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png new file mode 100644 index 000000000..9026bab6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33d6a4d77f6d9418dac470876aa6aa2c5bd274e6107b93579daf14f90cbfa854 +size 136 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png new file mode 100644 index 000000000..9026bab6e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillRectangleWithDifferentColors_Rgba32_Blank10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33d6a4d77f6d9418dac470876aa6aa2c5bd274e6107b93579daf14f90cbfa854 +size 136 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png new file mode 100644 index 000000000..ae3660f5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9808622aa1a16df85ec3911f95072815a4f1cada6fdb10ed89ad79d732edecfb +size 332 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png new file mode 100644 index 000000000..ffe949b9e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19d71f16b40889bbd18033a1f591d6530a5c4aa0f0ffcd7b2da882c720d35b9b +size 368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png new file mode 100644 index 000000000..ffe949b9e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColorsCenter_Rgba32_Blank20x20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19d71f16b40889bbd18033a1f591d6530a5c4aa0f0ffcd7b2da882c720d35b9b +size 368 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png new file mode 100644 index 000000000..ae3660f5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithDifferentColors_Rgba32_Blank20x20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9808622aa1a16df85ec3911f95072815a4f1cada6fdb10ed89ad79d732edecfb +size 332 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png new file mode 100644 index 000000000..8d4a81f75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f38baa9ef4e4fa2484a2036f2b03f0aed4f2d82b5ffd42cba01643a12552621c +size 240 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png new file mode 100644 index 000000000..8d4a81f75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithGreyscale_HalfSingle_Blank20x20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f38baa9ef4e4fa2484a2036f2b03f0aed4f2d82b5ffd42cba01643a12552621c +size 240 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png new file mode 100644 index 000000000..772cbc724 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a7e58de9fec685980aecd0811d72c3ff37d15899ea8d9a216da589336f5f627 +size 186 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png new file mode 100644 index 000000000..772cbc724 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillWithCustomCenterColor_Rgba32_Blank10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a7e58de9fec685980aecd0811d72c3ff37d15899ea8d9a216da589336f5f627 +size 186 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png new file mode 100644 index 000000000..6047ba357 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e3c3be6e9a9bc013d871d0f0116e246586dfe3f2ea5341d2f72004614549a53 +size 156 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png new file mode 100644 index 000000000..6047ba357 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushShouldRotateTheColorsWhenThereAreMorePoints_Rgba32_Blank10x10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e3c3be6e9a9bc013d871d0f0116e246586dfe3f2ea5341d2f72004614549a53 +size 156 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png new file mode 100644 index 000000000..85dd542fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathSVGArcs.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:747c51025cee10019752c27682c2f8a1cec892ca02826c971238de5295d2e181 +size 2603 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png new file mode 100644 index 000000000..cc8710ec3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25f45451fe5c6898611cdd7504d5f68a419e3fe8e2614cc5da1b0022b6a8864e +size 103 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png new file mode 100644 index 000000000..f7d162409 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithBackwardDiagonalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01608bed44a5e936807fb77b44ba1d2f4bccd84efd0774d29145775521a90892 +size 103 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png new file mode 100644 index 000000000..bd35802bd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0693514c8034ecc07a6eba1971b7a35343226d757fd66603c9e4d09864747b8a +size 102 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png new file mode 100644 index 000000000..5af54eab3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithForwardDiagonalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23882cab09040361405de161753c0ffe2f27f6d3160495edf3ebff25cc4ca4b8 +size 102 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png new file mode 100644 index 000000000..3823ec42f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87eeebbfd7a24863bf73aa42b544528f7928ff7dc80d698f7650aafa28487d85 +size 93 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png new file mode 100644 index 000000000..66efecdea --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithHorizontalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:87b01b762fa99c54d5a294640344e559768b049e9d1954e2ccd5205f9fb82126 +size 93 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png new file mode 100644 index 000000000..bb9d648d9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b813035f11b0d3abc80360ad38ced07ec0961d0744e438c13650bf76185dfdb1 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png new file mode 100644 index 000000000..2ce615085 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithMinTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1eced1a6acf836a5d8916585c13087c987d6c7ec070d020e2347dd06cdb33ae +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png new file mode 100644 index 000000000..8fa401b3a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:595a11891dca2657e14cc2b906b810f6d6089f00f552193cc597066c5b5de43d +size 99 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png new file mode 100644 index 000000000..731c7682f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent10Transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d69e73b2793eef700ac9bea010e8f40d9c588ebd34428d4bb2505b3ebe91190 +size 99 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png new file mode 100644 index 000000000..96553c88f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a34da0f8310f7d90c8d571d08502794f984d63d74f24e2e0e26a2bc1768b0315 +size 94 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png new file mode 100644 index 000000000..fbca36055 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithPercent20Transparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80c73db0b7f4e6b76a56bcb893cfa65f7da0a113751c505d0b0953618bc1763e +size 94 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png new file mode 100644 index 000000000..dc2a9cabc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVertical.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4c4ecd5a396025b3868859e2ddfcbdd7943b2494bf0b31c35d467097abccf96 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png new file mode 100644 index 000000000..4e26095c1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPatternBrushImageShouldBeFloodFilledWithVerticalTransparent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c624408bb46eacdd6795a4d6738315a457d74f6b3ee8ad24c87802fd58da1029 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(EvenOdd).png diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(Nonzero).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(False)_IntersectionRule(NonZero).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png new file mode 100644 index 000000000..d029a873a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(EvenOdd).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png new file mode 100644 index 000000000..d029a873a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Complex_Reverse(True)_IntersectionRule(NonZero).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:33f8f7a7b8392bba9e4dc9202d7dd6b2d699d925dc6a369c72a574a5818f0921 +size 177 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png new file mode 100644 index 000000000..b5a207f2c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(False).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931c4ccc31543101fdd1196b8784ee939b643477b4272213988d95ef47efe30d +size 223 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png new file mode 100644 index 000000000..b5a207f2c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Concave_Reverse(True).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:931c4ccc31543101fdd1196b8784ee939b643477b4272213988d95ef47efe30d +size 223 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png new file mode 100644 index 000000000..45c600116 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(EvenOdd).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b2b1abc716241f2f4a342847d652280febd515b7a3f3150393a571a8d56c937 +size 2649 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png new file mode 100644 index 000000000..45c600116 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(False)_IntersectionRule(NonZero).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b2b1abc716241f2f4a342847d652280febd515b7a3f3150393a571a8d56c937 +size 2649 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png new file mode 100644 index 000000000..091e5c11b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(EvenOdd).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:328f49c80f91835b100be5747c46f59add01cd2fe9a88bf32698369a59341fc2 +size 2639 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png new file mode 100644 index 000000000..091e5c11b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_EllipsePolygon_Reverse(True)_IntersectionRule(NonZero).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:328f49c80f91835b100be5747c46f59add01cd2fe9a88bf32698369a59341fc2 +size 2639 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png new file mode 100644 index 000000000..edd5d472c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_Car_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff238dcd899564fdb17e4ba7843bac70e28c0a4495d1d482244965d0c851beb0 +size 38411 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png new file mode 100644 index 000000000..ca4d322a1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rect_Rgba32_ducky_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cce93d5686317addd600c6a2cb42bdfa74c5d3abf50e4aaedef6bf9afa0ea89 +size 24126 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png new file mode 100644 index 000000000..e3600d76f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_Car.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:267a72eefde27e0fdbea326bfb887dc5f9545b4a5e46fb1db28fb953a8f04d22 +size 13479 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png new file mode 100644 index 000000000..519bb7ef2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_ImageBrush_Rgba32_ducky.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e49e9a72f8aa2d96a67d9994972ac9204d259ff1b7e261be3c4a88b30860b43 +size 17097 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero.png new file mode 100644 index 000000000..92928f27e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_Nonzero.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce3f3b0f2bf919cf64fd1b01b0be62df81a0ba8221124ee37347a786a60dbd2c +size 110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven.png new file mode 100644 index 000000000..d2bec2891 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_IntersectionRules_OddEven.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7ede75d604ad556527a04488a06085cdba2c4a044a60844a02dcf0c07a6eb58 +size 110 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png new file mode 100644 index 000000000..9d6d9f9f8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Pattern_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efa99879f72863fbbdcbd5fd8de3b7c6500e6c8da2a861c8a2ac6d2b71a9b4b9 +size 1672 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png new file mode 100644 index 000000000..43bc50298 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82278e1de2e0079b3db5f1d9da3939c725081dd7d791d3e9cef2ef9de0f7aef7 +size 268 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_TransformedUsingConfiguration_Rgba32_BasicTestPattern100x100.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/Fill_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RectangularPolygon_Solid_Transformed_Rgba32_BasicTestPattern100x100.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png new file mode 100644 index 000000000..b91676088 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(50)_Ang(0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60cca0928fa7fa2205fd19c36426a0b093b0dec152d549185e512653bd2948d7 +size 1751 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png new file mode 100644 index 000000000..69a7f5300 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(-180).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aef167d7be80289e87330fabbbb6ead45be4896e02d9a657737db4b2953eddfa +size 2022 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png new file mode 100644 index 000000000..86e74f928 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(3)_R(60)_Ang(20).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34dd3123decc569fd15725f8e2e104b07ad586f471279724ee314acbf028ef9b +size 2484 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png new file mode 100644 index 000000000..5d0b357bf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(5)_R(70)_Ang(0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97ccfa39faca6b56ece1bb4655f1eb54d4fe24a0d181fab876521dfac557db6b +size 2847 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png new file mode 100644 index 000000000..22fcc4942 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_RegularPolygon_V(7)_R(80)_Ang(-180).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dd6749d8a76082d2c225ab71f96d194eb0cf3a1ad65bc1e6e38f1bf9665455f +size 3228 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa0.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa0.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa0.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa16.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa16.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa16.png diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Basic_aa8.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Basic_aa8.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png new file mode 100644 index 000000000..1e8a5ceae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Bgr24_Yellow_A1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8fb5a2595b4072ef333b8fd29a93351e4d15c5c6a15acd1eab816326c9b08733 +size 3002 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png new file mode 100644 index 000000000..802a1a213 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A0.6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8086be96f56bd6477ecf52b16c0ad1bd626f49c14af933c77049a971d91611f8 +size 2784 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png new file mode 100644 index 000000000..285b36c4d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c02ed3a0de9ea7fd8408ca6eb4af8bf2936364e1972b9fc1f880181da3d8373 +size 2835 diff --git a/tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/FillPolygonTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Rgba32_White_A1_NoAntialias.png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png new file mode 100644 index 000000000..2c282ebbe --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_Solid_Transformed_Rgba32_BasicTestPattern250x350.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4ff122d2be4e2b65a4fe031eec536f11a221fab7a8a9cabc0fd1778364851b9 +size 5436 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png new file mode 100644 index 000000000..f20027b7a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b2c64dc50f3178cf62ac6995fe5eb1da41b48ba1604800df56ce4f30120083a +size 1006 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png new file mode 100644 index 000000000..560a287bb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Difference.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c841f4e6ec78be093da0169569309b3413dea97aba2b1e1b08eba57fd0a82da +size 2921 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png new file mode 100644 index 000000000..8bf91a3ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Intersection.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95eb0ad70989ab53db21a88937d98e6ef6fce7139b4018a23b680b4517cefe73 +size 2917 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png new file mode 100644 index 000000000..fb9e9616c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Union.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8273fc78d738330b064f10a530544de88405faa35639a397e0fa4e2bac5ddec +size 4288 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png new file mode 100644 index 000000000..38d8422d9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPolygon_StarCircle_AllOperations_Xor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc3fa025068524d8cc0f247a586bb296c2374d2478e9b0cc4745a8a7ef855899 +size 2874 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png new file mode 100644 index 000000000..50e6559e0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(-40,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c92da609a4c66a3775d65fded2472d3ed3a72c5af1fde8c1148e19d0f3b36346 +size 2134 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png new file mode 100644 index 000000000..c6d01e640 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b39304d8ce4df9a5f741f6336486b27e78cf80de07d3cb5fd56d9d2b75c1afd +size 1912 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png new file mode 100644 index 000000000..ccd515477 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(0,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a30fff36046c0b58859f102bcc22b8ff90fd2c19ee71a8814b1c14c1b0032f3 +size 3355 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png new file mode 100644 index 000000000..29f91d6ed --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d27da50076609e7c215fa566e09d88ca98abef487d742870801a056c126d306 +size 3021 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png new file mode 100644 index 000000000..b6b082da6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithDifferentCentersReturnsImage_center(100,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7657e38e341c96ede6e80e929eb1d22dafa04f19692a929a1cf1f4d535a0d889 +size 5171 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png new file mode 100644 index 000000000..2234ebc2f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillRadialGradientBrushWithEqualColorsReturnsUnicolorImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:672f2ab9f185757958192d8c28f94a800706a4f1ad3cfadc042443cac04056f1 +size 100 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png new file mode 100644 index 000000000..918d647a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2daedacd9a16eb276da4fabf41447e568d133a239d69cf7bfa0d1a7132d41e90 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png new file mode 100644 index 000000000..345a61049 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:474135f94f16c3a874b0b8a69b1f244224fa1b3bcbb5fad084eb1dc6eb3bb064 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png new file mode 100644 index 000000000..ee5805b46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd5940c044f15bb6ab4ee9b60e7e11d8d587bba9b510958ac8d0bd1e924c7a21 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png new file mode 100644 index 000000000..1a354b3ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83195d69738bf815d169ac86a0dda8fc3996ed78d46b27c22e572efa64d3a65e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png new file mode 100644 index 000000000..3d04f2ab3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:683565c4a7dd7a20d60e11cf22ba7c178bec285025868d87a9cab7cddbd667d4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png new file mode 100644 index 000000000..6f2a37c7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609f9c6c4fafc8babda5eb4ae5155d1b83e1527ec2af495d82e3ce721c2c9e41 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png new file mode 100644 index 000000000..f1ead9c08 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2e607ec3da2e0c7f3ad1085f7394730246381a2b707c006662dc334871c03d +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png new file mode 100644 index 000000000..d4f6ee246 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d4780e3b175e966618f9d0ba070a6bcc8e8feebfb2be4c572870ad31b61e7c1 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-False_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Add_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png new file mode 100644 index 000000000..918d647a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Multiply_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2daedacd9a16eb276da4fabf41447e568d133a239d69cf7bfa0d1a7132d41e90 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png new file mode 100644 index 000000000..345a61049 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-0.5_blenderMode-Normal_blendPercentage-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:474135f94f16c3a874b0b8a69b1f244224fa1b3bcbb5fad084eb1dc6eb3bb064 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png new file mode 100644 index 000000000..8d329b435 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Add_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:076baf174febccc2a3924c2f891d8486ee8c25c38978ae591261df69ee08e49e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png new file mode 100644 index 000000000..ee5805b46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Multiply_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd5940c044f15bb6ab4ee9b60e7e11d8d587bba9b510958ac8d0bd1e924c7a21 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png new file mode 100644 index 000000000..1a354b3ce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Blue_alpha-1_blenderMode-Normal_blendPercentage-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83195d69738bf815d169ac86a0dda8fc3996ed78d46b27c22e572efa64d3a65e +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png new file mode 100644 index 000000000..3d04f2ab3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Add_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:683565c4a7dd7a20d60e11cf22ba7c178bec285025868d87a9cab7cddbd667d4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png new file mode 100644 index 000000000..6f2a37c7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Multiply_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609f9c6c4fafc8babda5eb4ae5155d1b83e1527ec2af495d82e3ce721c2c9e41 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png new file mode 100644 index 000000000..f1ead9c08 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-Green_alpha-0.5_blenderMode-Normal_blendPercentage-0.3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2e607ec3da2e0c7f3ad1085f7394730246381a2b707c006662dc334871c03d +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Add_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png new file mode 100644 index 000000000..d4f6ee246 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Multiply_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d4780e3b175e966618f9d0ba070a6bcc8e8feebfb2be4c572870ad31b61e7c1 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png new file mode 100644 index 000000000..c57c20a83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_BlendFillColorOverBackground_triggerFillRegion-True_newColorName-HotPink_alpha-0.8_blenderMode-Normal_blendPercentage-0.8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2200755515c19e5fea5040d1116a484c83b1e8e8545875118352fa4e087165e4 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Argb32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_RgbaVector.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_RgbaVector.png new file mode 100644 index 000000000..662dd0037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSinglePixelType_RgbaVector.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25adfbb17267acb56770cf0de92eab85bdb4c6a3bc790b24022b618e64e70f0f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank16x7.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank16x7.png new file mode 100644 index 000000000..113c9e069 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank16x7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:577cff471034b801e84c2df271946e59e441d0890910b949dcd7b81b25f38d58 +size 82 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank1x1.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank1x1.png new file mode 100644 index 000000000..d406a3275 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank1x1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8df185b0b10595bba92b871646a6b349b308221e63c2ead096e718676716bddd +size 72 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank33x32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank33x32.png new file mode 100644 index 000000000..4c6092dfc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank33x32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1e7483e76c3b65b94b68499f93f07b2c73435353adf46638c2d1fe16d62f6a0 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank400x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank400x500.png new file mode 100644 index 000000000..af764cb13 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank400x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed2eeed8c081a355f23062a56ea80f1891745c63f8468d65771e49418b508cd6 +size 119 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank7x4.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank7x4.png new file mode 100644 index 000000000..0a126de11 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_DoesNotDependOnSize_Blank7x4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c24b47a3afd5d7185d9722cd8f0bd4274489bedd4e09c8d23a66e49af405dc2 +size 91 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png new file mode 100644 index 000000000..cf2790c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png new file mode 100644 index 000000000..7631eab46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png new file mode 100644 index 000000000..cf2790c36 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x5,y7,w3,h8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b99d68b7a4004b690bf3e2c03d408c40491926435fd1afdccd93ad23c919b20 +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png new file mode 100644 index 000000000..7631eab46 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_Region_WorksOnWrappedMemoryImage_Rgba32_Solid16x16_(255,0,0,255)_(x8,y5,w6,h4).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4db5130b5c73181a950b9f3f4697a09d9486dba90fa140ace97c368c1e8550f +size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Blue.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Blue.png new file mode 100644 index 000000000..8570310b7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Blue.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c70c09a82dfbb4db1955e417c1f24ea90178f5234bba420e71f74a094218c7c +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png new file mode 100644 index 000000000..3730d9c35 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSolidBrush_WhenColorIsOpaque_OverridePreviousColor_Khaki.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dfc380297d61413eec1b9a3f634ae0489c22fb310b004e1ecf6ea26ecc28b5f +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png new file mode 100644 index 000000000..91d394173 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(0,end360).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ceb08efcce332016f1795e3baf68a918394d0c890590edd257e02e24bcd506c +size 14282 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png new file mode 100644 index 000000000..99effd0c2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(180,end540).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9cbf69d108b3e12564448127f081ae2140bac129a65fcbe0d3f256f3ee79530 +size 14189 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png new file mode 100644 index 000000000..a0d674219 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(270,end630).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91178428c472fd718336706dd61ef2804597fd9d4afb45d5c6d1725ccbc370b1 +size 14282 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png new file mode 100644 index 000000000..5fbd00f8f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillSweepGradientBrush_RendersFullSweep_Every90Degrees_start(90,end450).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c6ece13ed9f676d521ee341f9cf7db6bed82ff108c1bf69c43a94c8fb4aa849 +size 14241 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png new file mode 100644 index 000000000..3f2f875f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86ac9ce27e8f050f46497ea16d4674fddc214e339d78bd24a7880aa87e5de5cb +size 17373 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png new file mode 100644 index 000000000..d97784cce --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d7d57a9c7b1d99e7bc648634e2ed7d335651e9a8fbe5e1f7ab55feaaa467d2e +size 3266 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png new file mode 100644 index 000000000..9f2a3aec4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa98f582880e1491e12ea6aa47754e6b37685f4525090bdab70bc63ca801ed06 +size 16822 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png new file mode 100644 index 000000000..f9cd2a1fa --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6c4a3ff2ba8aec309677bbf64be0d364f861ad9a879ff3c659704ee821c413a +size 15105 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png new file mode 100644 index 000000000..155ef1f90 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7ce98889e124d29e8ec8a4cd00dff08756e4f582f7511f1dcac2b76e84e7780 +size 833 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png new file mode 100644 index 000000000..170ca55c7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8700c5876ad15e5180e59f39049fd7c1e78e2efe8ea56a3156d1ee2bfff652c6 +size 15509 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png new file mode 100644 index 000000000..2758d9b2f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:002373f52cf14e441ed96d4f4098e6a2d78f91d3bec1f31d38b1409c1a050c78 +size 115501 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png new file mode 100644 index 000000000..9b870d68c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:782fea0eefa65b2ed6967fd2754a437cb79e0f0088cf220f54dfc601c850e6a4 +size 11029 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png new file mode 100644 index 000000000..2909b7a67 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c65d0591e60dbf5043502fe6e90301dee66ec8a6304235054db52b3e42c85592 +size 2105 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/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png similarity index 100% rename from tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png rename to tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png new file mode 100644 index 000000000..aa898bc10 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0317107437a8afdfa8f1cdbea88f7e728caff6ac527cb1a2e5b7ddeff95dfa23 +size 7367 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png new file mode 100644 index 000000000..23ee106d0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ad9e1839ba06c9fc510dc0eb622284ee657ce97fa61d71b0b821c0c74cb0216 +size 10624 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png new file mode 100644 index 000000000..4ee46d213 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ec7aca5d49cce2b1995eee7239b2520b277b6424acfb285dd65d495f4642452 +size 2997 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png new file mode 100644 index 000000000..6b54229a8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f1ec94333ce8919238ff0c91ffc9880339c6f1ab8ebcc2b4c2dd2d2149041b4 +size 24842 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png new file mode 100644 index 000000000..929c2d8bf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcb0178508b7d57355bd4b8689dd03ec84b3f849d14f7bbc5a17be2d1072a7cc +size 2984 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png new file mode 100644 index 000000000..24ae92250 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72d7ad59f0005f83e32f651a80d38d3690953099638c1feca346dc5bef82651e +size 35397 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png new file mode 100644 index 000000000..ddaaa54ab --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a46c29e5e9e0d5b259500edd50fe8a35f877f1d66b46c1f00e18b704507f623 +size 2978 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png new file mode 100644 index 000000000..06588b651 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52b440c8b06c31f0996c7966a5c05c2d20ee164c21d57580fc3c1818388b6896 +size 17707 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png new file mode 100644 index 000000000..a1f3f882e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8356bbafcd2d6e336c64a93bebbde8e6859aa2fc72a05f9f60ecbad0d64ccf0 +size 47754 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png new file mode 100644 index 000000000..dbf78fb27 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37c010c48e2b58134460b7a524f8c1976c2ac4dd5fecbedefc05a03628c302cd +size 1094 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png new file mode 100644 index 000000000..969123203 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c23f233c820fdfd5dae4cd9ccb4eac768d8f0c3b1a164ba37459275c7c3fe21a +size 13029 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png new file mode 100644 index 000000000..e300df9b2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c597d476c953f807f85fab0635e6377e07017c0b8e1ae3583a20afcf79b8135 +size 228 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png new file mode 100644 index 000000000..147496a9b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0c2f0f9444f2de88e3b241568ce44e0558b8fc3be9c329c5f5a66195198aca7 +size 35915 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png new file mode 100644 index 000000000..d858e2069 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9895a066b5d6b25a4b6fd151c0b1250610e19e0f2a9fbd6509cdefd93b2ffc8 +size 1010 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png new file mode 100644 index 000000000..424b7dd49 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74b613271357bbf75b616388cfe0a41513a0f4ef5fde9d0d90efd47c642f1863 +size 10326 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/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/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png new file mode 100644 index 000000000..b32058491 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:509e8691cd700301a8c0b7542f55a7069ab9d05db7f52f881f8518d8e5f8a0a4 +size 293 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png new file mode 100644 index 000000000..e417ebe9b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e95dc6e245c6c130b93518def6e0732c1ba2dcf57349fb90c3a302de332c875 +size 243308 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png new file mode 100644 index 000000000..01806ab99 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c71c637402157614880b6c87f4ff3a66964aa2978b9635cb16c66dfe6ccd094 +size 55027 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png new file mode 100644 index 000000000..531013eb9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20ac2c62e73e9799f0176ccdce68d67c4b6df739084d62c6fd9a159f0aff8af6 +size 34721 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png new file mode 100644 index 000000000..306d5abd3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99893a28a3581c9d52bca6c48dcbe2548dbee4f3821aee21216004dbdd18c64b +size 4479 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png new file mode 100644 index 000000000..d4c2089f0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4744bcd789e8731fab124a559f448d07dcfebc1a86ff56c57438eaeeb3b79444 +size 165443 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png new file mode 100644 index 000000000..e523c650f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_States_Fill.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:546bc7f6663ac44d3edbd976bf3ad2bbb56ca961b9ea7b0d40f7cc0d448ef4d7 +size 407893 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png new file mode 100644 index 000000000..463eea6ad --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7939b6c78473cf978d54310307cd8e763b287cf8a71dc661e93fc0374a58ca24 +size 36256 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png new file mode 100644 index 000000000..997b6720d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:453db170043d15c55302819678b2dd43686388196f9307e254a1e9796e247fae +size 306360 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png new file mode 100644 index 000000000..1b7e4c6bf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:608e0ce36b4866c077cb025e5eca78b8e85e4a5b9f2fa4459e97a410f27928cd +size 313445 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png new file mode 100644 index 000000000..265e0f2be --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:553b92cb26ffb752d7ada262c7d918c840a6ca8b91f480714ced7a751259e89a +size 3329 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png new file mode 100644 index 000000000..925389091 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5db7f7974d1d83cea37e88cb7cdb1482563c0264b5dd3b9d3d0f971b8a6c5283 +size 325228 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png new file mode 100644 index 000000000..c48efa24f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2aadb60a463ce878e6f6d03ab44fa8b1ce1f29f166e2ec9619f50b4dfd00ebe +size 4964 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png new file mode 100644 index 000000000..e07ff4945 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/RecolorImage_Rgba32_TestPattern100x100_Red-Blue-0.6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b85840ba2c6c185bc7f979a9e889aae74d5ae4781cc3915e817105f6a6af235f +size 10085 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png new file mode 100644 index 000000000..2aca52b7e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierFilledBezier_Rgba32_Blank500x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f55758580043d2478e73782b0b7e18489129711fbccd1717125c7af3db582af +size 3198 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png new file mode 100644 index 000000000..7d23c4e00 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SolidBezierOverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:292ccae9934f99643f7cbe1ac058b72ac74b771d82d9e4a8695d6e087c8d9ba7 +size 3017 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png new file mode 100644 index 000000000..66b216e54 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4a267fee106276a4d1f20fd1a25852b433929b565d5e614f3245aa5e7bea6f9 +size 402 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png new file mode 100644 index 000000000..a9560f698 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdef336c5e133be0f70ac68e64f238a76a53d7b83d39ef57c01d568e57e9039f +size 687 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png new file mode 100644 index 000000000..cde34bc1f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18f28571933f8a67ab37c8c613d3d758dff98e1cb3181ec37975d5451753ba50 +size 458 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png new file mode 100644 index 000000000..fe9459310 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3be2b2f4ab0ad34cee0ea9d0ba2522e284cb40379fcd38d091a9af9977b7dd1 +size 13249 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png new file mode 100644 index 000000000..66b3eec2c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:235d4a95e77c5421d95232ea19514e6b93980ab58e436190334fd92568b9cc29 +size 2804 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png new file mode 100644 index 000000000..b568e81b3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c1d2c00135ccae607db16363f478f7f3c02773bec31f6c54848fdd27bf52981 +size 2464 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png new file mode 100644 index 000000000..d7d12b832 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:894e0a780d76bf4c631a9e9997ad661423c2765d25e5188fe35247ed67afc956 +size 10842 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png new file mode 100644 index 000000000..53cdcb9ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f426c118d2d3d198d7b527998ae0d73e049f0175516fd37497db2777f076019 +size 452149 diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png deleted file mode 100644 index 0d763c0f2..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png deleted file mode 100644 index 87f594747..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Bgra32_CalliphoraPartial_Yellow-Pink-0.5.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png deleted file mode 100644 index ea1921837..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_InBox_Rgba32_TestPattern100x100_Red-Blue-0.2.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png deleted file mode 100644 index 0472c7b96..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_CalliphoraPartial_Yellow-Pink-0.2.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.2.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.2.png deleted file mode 100644 index 05ecc0ae5..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.2.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.6.png b/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.6.png deleted file mode 100644 index d653c37ad..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/RecolorImageTests/Recolor_Rgba32_TestPattern100x100_Red-Blue-0.6.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png b/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png deleted file mode 100644 index 2ec013d52..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/FilledBezier_Rgba32_Blank500x500.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 266a6d6b9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidBezierTests/OverlayByFilledPolygonOpacity_Rgba32_Blank500x500.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:989c843ed10a31190d812545fff20bb9fa0aeea67ca0053af31fcdb06aa6d4de -size 3004 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Add.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Darken.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-HardLight.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Lighten.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Multiply.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Normal.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Overlay.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Screen.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Subtract.png deleted file mode 100644 index c7ab88be3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Clear_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Add.png deleted file mode 100644 index a5c638505..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Darken.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-HardLight.png deleted file mode 100644 index 131d4dc8a..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Lighten.png deleted file mode 100644 index a5c638505..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Multiply.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Normal.png deleted file mode 100644 index a5c638505..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Overlay.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Screen.png deleted file mode 100644 index a5c638505..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Subtract.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestAtop_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Add.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Darken.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-HardLight.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Lighten.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Multiply.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Normal.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Overlay.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Screen.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Subtract.png deleted file mode 100644 index 438d11255..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestIn_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Add.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Darken.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-HardLight.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Lighten.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Multiply.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Normal.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Overlay.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Screen.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Subtract.png deleted file mode 100644 index aadf1c064..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOut_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Add.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Darken.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-HardLight.png deleted file mode 100644 index e98bf8cec..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Lighten.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Multiply.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Normal.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Overlay.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Screen.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Subtract.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-DestOver_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Add.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Darken.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-HardLight.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Lighten.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Multiply.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Normal.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Overlay.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Screen.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Subtract.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Dest_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Add.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Darken.png deleted file mode 100644 index e551a7302..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-HardLight.png deleted file mode 100644 index e551a7302..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Lighten.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Multiply.png deleted file mode 100644 index e551a7302..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Normal.png deleted file mode 100644 index e551a7302..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Overlay.png deleted file mode 100644 index ff8fddbe9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Screen.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Subtract.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcAtop_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Add.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Darken.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-HardLight.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Lighten.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Multiply.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Normal.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Overlay.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Screen.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Subtract.png deleted file mode 100644 index 6b582928b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcIn_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Add.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Darken.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-HardLight.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Lighten.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Multiply.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Normal.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Overlay.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Screen.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Subtract.png deleted file mode 100644 index e75c117cf..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOut_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Add.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Darken.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-HardLight.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Lighten.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Multiply.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Normal.png deleted file mode 100644 index 6589f4b4b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index e98bf8cec..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Screen.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Subtract.png deleted file mode 100644 index fdf747851..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-SrcOver_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Add.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Darken.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-HardLight.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Lighten.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Multiply.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Normal.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Overlay.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Screen.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Subtract.png deleted file mode 100644 index 0a489f8e3..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Src_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Add.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Darken.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-HardLight.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Lighten.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Multiply.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Normal.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Overlay.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Screen.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Subtract.png deleted file mode 100644 index c9286262f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendBlackEllipse_composition-Xor_blending-Subtract.png and /dev/null 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 deleted file mode 100644 index 562fb674c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e71faafad588f0f458186b236ebe6d12b6cc6c11e3b5a80cf3c311ea6affc5e4 -size 3234 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 deleted file mode 100644 index ef30b2c38..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:482e7c99eab9e2830022d8fc684db024ada5e91a64f9cfc7cb1be7248bc16b4d -size 2631 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 deleted file mode 100644 index 62c650d87..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:825e0926c1ea7c6e5370cb08a0a5a3dd3fa7f29454bf58ca88b18055df29563c -size 3250 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 deleted file mode 100644 index 6cf010a45..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b6bf8b2a153457df53d35c2f489c0d594ba278e597b564ef9da8c5c0c9f33c7 -size 3242 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 deleted file mode 100644 index 7aeebeb3f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b5f596d0bbac2518d109f61dcf638064d5f73f27dd898fdcc628b73132d8508 -size 2755 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 deleted file mode 100644 index c84ad3819..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d7017d581bdf49113e9b21c66d197979b2b9156ca2a2b9a8211b8833fdc92889 -size 2990 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 deleted file mode 100644 index 8cfbd3fa6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:189f945e1b37c22205f905da66544814048d38a69f7b80957873c9b0cf9e0395 -size 2751 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 deleted file mode 100644 index 0d1cf5271..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e677bbd46c6d457538c1c11f140ef372749e32c7af55158fc05af944a00825f9 -size 3251 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 deleted file mode 100644 index 8afcb3f4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:686a28d9e38b3324583d3e0003d7a79cdb4756007c226d2003d3e0b2a3b171ef -size 3199 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 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 deleted file mode 100644 index dd2b6de4f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:378156fc00041f8025393837451b8c66116fa0f1ee716e6bd8e1665ff5392392 -size 1085 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png deleted file mode 100644 index 42f37898f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOut_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png deleted file mode 100644 index 8996f8113..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png deleted file mode 100644 index 6ca91fea5..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png deleted file mode 100644 index 6a173a4d0..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png deleted file mode 100644 index 7c43a67f7..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png deleted file mode 100644 index dee124965..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png deleted file mode 100644 index 12cad227b..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png deleted file mode 100644 index 49945e472..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png deleted file mode 100644 index 87c688054..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png deleted file mode 100644 index 5f3f36f5c..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-DestOver_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png deleted file mode 100644 index 80cfabaed..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Dest_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png deleted file mode 100644 index cd97328f7..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png deleted file mode 100644 index 15bc914ae..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png deleted file mode 100644 index fed2801fc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png deleted file mode 100644 index aa91deacc..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png deleted file mode 100644 index 9fdbbc176..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png deleted file mode 100644 index 35707ec0d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png deleted file mode 100644 index 8eaad1641..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png deleted file mode 100644 index 3beabf2c1..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png deleted file mode 100644 index ad9b2d08f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcAtop_blending-Subtract.png and /dev/null 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index 1431969ed..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26940abafa0ada0b11f413127dda2783ca570cb4e9052634c4fbca049ad873 -size 1231 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 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 deleted file mode 100644 index cb70160c0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23e60b53fd2a54e34323db22f8abd9cdfbedab6da69a0048c754a2ccc67b17dc -size 2803 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png deleted file mode 100644 index 8996f8113..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png deleted file mode 100644 index 6ca91fea5..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png deleted file mode 100644 index 49945e472..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png deleted file mode 100644 index 7c43a67f7..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png deleted file mode 100644 index dee124965..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png deleted file mode 100644 index f45b1a903..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index 6a173a4d0..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png deleted file mode 100644 index 87c688054..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png deleted file mode 100644 index 29cea214f..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-SrcOver_blending-Subtract.png and /dev/null 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Add.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 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 deleted file mode 100644 index caa7c8ba9..000000000 --- a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8e2856bfbd3039832806a7c0fab9c24819932e3cc50aebc0cbd6bb9742dcb8c -size 2753 diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png deleted file mode 100644 index 8939c7b50..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendSemiTransparentRedEllipse_composition-Xor_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Add.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Darken.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-HardLight.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Lighten.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Multiply.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Normal.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Overlay.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Screen.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Subtract.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOut_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Add.png deleted file mode 100644 index ade8cd764..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Darken.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-HardLight.png deleted file mode 100644 index 83c95a638..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Lighten.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Multiply.png deleted file mode 100644 index a46206b47..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Normal.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Overlay.png deleted file mode 100644 index 9aa2cb468..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Screen.png deleted file mode 100644 index 1fed1d6d0..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Subtract.png deleted file mode 100644 index 4e721e269..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-DestOver_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Add.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Darken.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-HardLight.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Lighten.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Multiply.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Normal.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Overlay.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Screen.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Subtract.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Dest_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Add.png deleted file mode 100644 index e022e6db2..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Darken.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png deleted file mode 100644 index de81940d8..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png deleted file mode 100644 index 5f9ec76a7..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Normal.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png deleted file mode 100644 index 5ffb0e92c..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Screen.png deleted file mode 100644 index 47e93cb39..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png deleted file mode 100644 index 8e727d849..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcAtop_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Add.png deleted file mode 100644 index ade8cd764..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Darken.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-HardLight.png deleted file mode 100644 index 9aa2cb468..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Lighten.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Multiply.png deleted file mode 100644 index a46206b47..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Normal.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index 83c95a638..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Screen.png deleted file mode 100644 index 1fed1d6d0..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Subtract.png deleted file mode 100644 index 0843d42fd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-SrcOver_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Add.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Darken.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-HardLight.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Lighten.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Multiply.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Normal.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Overlay.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Screen.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Subtract.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_3BlendTransparentEllipse_composition-Xor_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Add.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Darken.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-HardLight.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Lighten.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Multiply.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Normal.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Overlay.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Screen.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Subtract.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Clear_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Add.png deleted file mode 100644 index ade8cd764..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Darken.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-HardLight.png deleted file mode 100644 index 83c95a638..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Lighten.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Multiply.png deleted file mode 100644 index a46206b47..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Normal.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Overlay.png deleted file mode 100644 index 9aa2cb468..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Screen.png deleted file mode 100644 index 1fed1d6d0..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Subtract.png deleted file mode 100644 index 4e721e269..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestAtop_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Add.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Darken.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-HardLight.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Lighten.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Multiply.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Normal.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Overlay.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Screen.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Subtract.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestIn_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Add.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Darken.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-HardLight.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Lighten.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Multiply.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Normal.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Overlay.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Screen.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Subtract.png deleted file mode 100644 index 8071681d6..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOut_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Add.png deleted file mode 100644 index ade8cd764..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Darken.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-HardLight.png deleted file mode 100644 index 83c95a638..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Lighten.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Multiply.png deleted file mode 100644 index a46206b47..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Normal.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Overlay.png deleted file mode 100644 index 9aa2cb468..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Screen.png deleted file mode 100644 index 1fed1d6d0..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Subtract.png deleted file mode 100644 index 4e721e269..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-DestOver_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Add.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Darken.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-HardLight.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Lighten.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Multiply.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Normal.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Overlay.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Screen.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Subtract.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Dest_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Add.png deleted file mode 100644 index e022e6db2..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Darken.png deleted file mode 100644 index b11a2e63d..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-HardLight.png deleted file mode 100644 index de81940d8..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Lighten.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Multiply.png deleted file mode 100644 index 5f9ec76a7..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Normal.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Overlay.png deleted file mode 100644 index 5ffb0e92c..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Screen.png deleted file mode 100644 index 47e93cb39..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Subtract.png deleted file mode 100644 index 8e727d849..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcAtop_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Add.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Darken.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-HardLight.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Lighten.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Multiply.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Normal.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Overlay.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Screen.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Subtract.png deleted file mode 100644 index ff14eccb9..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcIn_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Add.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Darken.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-HardLight.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Lighten.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Multiply.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Normal.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Overlay.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Screen.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Subtract.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOut_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Add.png deleted file mode 100644 index ade8cd764..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Darken.png deleted file mode 100644 index b4f433274..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-HardLight.png deleted file mode 100644 index 9aa2cb468..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Lighten.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Multiply.png deleted file mode 100644 index a46206b47..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Normal.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Overlay.png deleted file mode 100644 index 83c95a638..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Screen.png deleted file mode 100644 index 1fed1d6d0..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Subtract.png deleted file mode 100644 index 0843d42fd..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-SrcOver_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Add.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Darken.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-HardLight.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Lighten.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Multiply.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Normal.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Overlay.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Screen.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Subtract.png deleted file mode 100644 index 8db71d319..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Src_blending-Subtract.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Add.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Add.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Darken.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Darken.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-HardLight.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-HardLight.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Lighten.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Lighten.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Multiply.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Multiply.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Normal.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Normal.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Overlay.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Overlay.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Screen.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Screen.png and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Subtract.png deleted file mode 100644 index 12e1335aa..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/SolidFillBlendedShapesTests/_1DarkBlueRect_2BlendHotPinkRect_composition-Xor_blending-Subtract.png and /dev/null 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 deleted file mode 100644 index 48458452e..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index a663c5d35..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 7e26d6959..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 5b5dc4529..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 1ab954d5d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index d2d64f8b5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 9e50b2fa0..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index dc6115863..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 1365aa909..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical2_Rgba32_Blank48x935.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 483091b77..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 95806e725..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index cb39952cb..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanDrawTextVertical_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index ebebcb871..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index a9d95b2b6..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanFillTextVertical_Rgba32_Blank500x400.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 2d7907dad..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRenderTextOutOfBoundsIssue301.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 335809eec..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 2b116b146..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 12024df67..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 9b8104f7c..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index d4e7c41a5..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 474f9fd69..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 58256c3f3..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index e962adb7d..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 5120c4629..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index eb9103188..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index f9714e303..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index a09ecc748..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index b8b94d90c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 8dad5340a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 37e3bd5fb..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 0aa68114b..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 864ffbf1c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 12ac94d02..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index d839ae8e1..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 9780a7767..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_LargeText.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index cfd648192..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 3e68f9b77..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f05a32ebdbdca54454ea2624d085cfd4965cf676bbad36f9be9ad199a3b7faa8 -size 604 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 deleted file mode 100644 index 6eed6ea69..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index e243c035f..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 2359d15ee..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 49d87771a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 86c4902ee..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index e99e4a71a..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 8983ed62c..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index d8029d4c4..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index cd00ebe1a..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index ca42e83e7..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 46ca78bc8..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index a1084dd6d..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 6d0f59b16..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index f6c0883a1..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 59db80f48..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 44f4e51a9..000000000 --- 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 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 0ee6e7102..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 6d70f32c2..000000000 --- a/tests/Images/ReferenceOutput/Drawing/Text/DrawTextOnImageTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f839ffbac1b001539912b2759206d2b3de2235f059e487505e5fb6226396c531 -size 184457 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png new file mode 100644 index 000000000..23d453623 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5888b5c209fbbd54bb2f095fbabb8f72ff6e180b07659c688f3b3f6d983b1dd7 +size 32100 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png new file mode 100644 index 000000000..f267e1502 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_StarWarsCrawl_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65a50b6549efdb62424c9608b0bb5b3904420726882d84a9d67c76a41ae13410 +size 32300 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png new file mode 100644 index 000000000..09c1b6d82 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf22c52e855eff992dfb1c365a3e57b05ac0ba01573b2b8aab228e8c3980e8ae +size 930 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_NativeSurface.png new file mode 100644 index 000000000..60874a9e0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Butt_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d50ee61f94ca4032e4d2617f543827421f5a29ecdf38826a760c6184e119879c +size 956 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png new file mode 100644 index 000000000..d306f5864 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af11b3643ea555b174c0ad7a0fb2e4877d617fd9c7f97d2468863140a3c5f26a +size 1052 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b824f8f63 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Round_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c3bce7aa6f789c2faa6152b7ab01d4a293a906698578130755610b39656a6ef +size 1032 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png new file mode 100644 index 000000000..abc714565 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42c38c5800214ba5bcf36d22de6b7501f167e22c87c9e319cff0c1109f61c63b +size 878 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png new file mode 100644 index 000000000..a484be993 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_DrawPath_Stroke_LineCap_Square_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f0b29d354efd8b6ff28b070e62588171cc0a8a06119216481a1289c7ad22b3b +size 869 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png new file mode 100644 index 000000000..80a3f687d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de5c665ea5b73d2909de4f107675686734af3b9a2eaff03a12018148df7f819a +size 3199 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png new file mode 100644 index 000000000..2380a88c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Bevel_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4c88fd902abaa10ecead8a2b14d421cc8e8b113486444de96fe1ac81f0f960a +size 2868 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png new file mode 100644 index 000000000..edb8b1464 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2baae6cba42f3d4333449060f76d2d77d6190c3f31a248751acc084b5c54d68e +size 3365 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png new file mode 100644 index 000000000..5691dcecd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRevert_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d01fe9cda28b598a3461da4ba43145c8568d70c9d1b4d51ef36e4a088e1a3054 +size 2991 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png new file mode 100644 index 000000000..edb8b1464 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2baae6cba42f3d4333449060f76d2d77d6190c3f31a248751acc084b5c54d68e +size 3365 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png new file mode 100644 index 000000000..5691dcecd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_MiterRound_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d01fe9cda28b598a3461da4ba43145c8568d70c9d1b4d51ef36e4a088e1a3054 +size 2991 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png new file mode 100644 index 000000000..edb8b1464 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2baae6cba42f3d4333449060f76d2d77d6190c3f31a248751acc084b5c54d68e +size 3365 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png new file mode 100644 index 000000000..5691dcecd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Miter_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d01fe9cda28b598a3461da4ba43145c8568d70c9d1b4d51ef36e4a088e1a3054 +size 2991 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png new file mode 100644 index 000000000..1771d8a8b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b3f40f34707b72807acd268321739239ea48576aab80be43432ed12b0aa45c44 +size 3297 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_NativeSurface.png new file mode 100644 index 000000000..905e7938e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_DrawPath_Stroke_LineJoin_Round_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a5e6dc213393764b1f1df5fdb62630ed8609e62bc23ae53c33a12802a20fcb5 +size 2956 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png new file mode 100644 index 000000000..772037c86 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5e28e7d0fb9a311ee7b8d65b7b769845515041ad8779097884e8012f5519ff4 +size 2248 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png new file mode 100644 index 000000000..7cd83a297 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_DrawPath_Stroke_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72b6664a1e2fc077f6a4ffa949f1e2226a11e79222fd6ce3cb607e70b1f2acdb +size 1958 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png new file mode 100644 index 000000000..15e15ce73 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38e16e588acd70f95ec7b293d9861a87fafce041ee4befbb6fa7509764021ae4 +size 4707 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png new file mode 100644 index 000000000..a289c9c99 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_RepeatedGlyphs_AfterClear_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83ecec67faad7b98f0c68a5a86db414c98202f078bf8b9e72362c101a06b17dc +size 4785 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png new file mode 100644 index 000000000..c85d5e921 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c3cefee933f84706672b310e84c2a1870b2e29136b630c1caf0d7d296646b31 +size 4885 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png new file mode 100644 index 000000000..2a27d4044 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_RepeatedGlyphs_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88010a55f9ceb55bc5b3396c327663081eacb2d429fcb828870f5e32fa48c050 +size 4859 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png new file mode 100644 index 000000000..a20422ced --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23724aec3a00c7429bbb1302ee80a83107886af8806e353a593d930755ca75ae +size 36512 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png new file mode 100644 index 000000000..17703ec32 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_DrawText_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8775bd145fdd31d33ee974af8bd0c38fa247a03e38ce001bad5db8ce23c66f1 +size 36893 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_Default.png new file mode 100644 index 000000000..71fe4495e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11a4ff84a5a8a142f076a26b9e0066410ac96aba34bc1916abbdb487ad9eb989 +size 523 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_NativeSurface.png new file mode 100644 index 000000000..42b432385 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_AliasedWithThreshold_MatchesDefaultOutput_FillPath_AliasedThreshold_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3253e369f745389b46221df9bad5ba07e4e2d5f37dc588a099afb7f156074cb1 +size 1932 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_Default.png new file mode 100644 index 000000000..09adf1c65 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b44c8cc975defeb5235bd8c84eb03726a156a2c744c9e0b6289a705179fad1 +size 138 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_NativeSurface.png new file mode 100644 index 000000000..09adf1c65 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_EvenOddRule_MatchesDefaultOutput_FillPath_EvenOdd_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b44c8cc975defeb5235bd8c84eb03726a156a2c744c9e0b6289a705179fad1 +size 138 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_Default.png new file mode 100644 index 000000000..add1befee --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:034ca79bcca2afd5c38a038974b1a5e5d65bcdcbfa05ab993dbcf12a13dea30b +size 6088 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_NativeSurface.png new file mode 100644 index 000000000..ca1b8301b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_LargeTileCount_MatchesDefaultOutput_FillPath_LargeTileCount_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c325ac506f80265d8bd4471327ef3755a2887a7ffd2b856056d4fcd06228c00 +size 5855 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_Default.png new file mode 100644 index 000000000..3b25b6ccb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9aa3913abd295949b3d22e8bd2031a406a11dcc9a302f3856eaa9e37cbe112d +size 336 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png new file mode 100644 index 000000000..3b25b6ccb --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_MultipleSeparatePaths_MatchesDefaultOutput_FillPath_MultipleSeparate_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9aa3913abd295949b3d22e8bd2031a406a11dcc9a302f3856eaa9e37cbe112d +size 336 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png new file mode 100644 index 000000000..0312b3f84 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37df4c3c1e2174e4ce5ccc1cf56463e0232e8147106e996c432233eb56f31b48 +size 4349 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png new file mode 100644 index 000000000..bce2f0949 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_MatchesDefaultOutput_FillPath_EllipticGradient_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec7c9b335c5cc1d147bad5bd3bf7adc1899c2152a9c16db65fd959e0368372ce +size 4461 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png new file mode 100644 index 000000000..068a89979 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13405f754723b9cff04307e94079a2826a25c20e16957bb5abea556aceea4399 +size 38897 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png new file mode 100644 index 000000000..c250aeded --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_FillPath_EllipticGradient_Reflect_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb4aa2e84a6133afff33ccbab87791eef154cef4ddae2ae10bd06e1d103df7b6 +size 39875 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_Default.png new file mode 100644 index 000000000..1a905ff83 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13062ce218d198269d6b2f130182c0ea30bf12a3460e72d6dcb57a2975bdf719 +size 566 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_NativeSurface.png new file mode 100644 index 000000000..fe08bc009 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Add_Src_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e654aefa664b8b3cc8162cf9b81117fd6bf6c667dd9df9dff341065b5a16b2ad +size 1183 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_Default.png new file mode 100644 index 000000000..1b1ed3e3b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c +size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1b1ed3e3b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Darken_DestAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c +size 826 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_Default.png new file mode 100644 index 000000000..0d51f838c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0b811939ce1323656bb91c88841c9c33419ecbe511cc3ff623f5a3e117035bd +size 804 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png new file mode 100644 index 000000000..9ba1662bc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:054e3444ae48043cc6b0fb904bbbd2e46bd7efb2fdc9a157c9926a651c234a71 +size 1166 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_Default.png new file mode 100644 index 000000000..00a793ec2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 +size 798 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..68290eedc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Lighten_DestIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25d0df59e4bccd58eb214adb0039157fb70d93e7f2c4a8e6f534697f46dc2318 +size 1166 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_Default.png new file mode 100644 index 000000000..443c5e78e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c78b60cfef6fca9cf9c1f1bd1b238c659a307a33693d12ccfc86a9a520b65de +size 781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..7b6647ade --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2355ff2e5dfb1fecbbdf6207c73d9fd7c2db2d9160a569eb8ee81ffa5fc3327 +size 1166 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_Default.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b81d85a1f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_Clear_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dbc715aab6d6fed34115634e69481e89a43fda846948e5044c9158b8cfc13a5 +size 1166 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_Default.png new file mode 100644 index 000000000..0627f844b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06502181f6cef0bb53cd4c142009a7da9093533f2cb6188f78e75036db4fbe7f +size 828 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..0627f844b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Normal_SrcOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06502181f6cef0bb53cd4c142009a7da9093533f2cb6188f78e75036db4fbe7f +size 828 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_Default.png new file mode 100644 index 000000000..71d06c28f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a +size 533 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..71d06c28f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Overlay_SrcIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a +size 533 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_Default.png new file mode 100644 index 000000000..d8b6ebb18 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf632398801d9696749701016ab1c35341c62ca87f8f9ffa1b634a03e518a6c9 +size 834 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d8b6ebb18 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Screen_DestOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf632398801d9696749701016ab1c35341c62ca87f8f9ffa1b634a03e518a6c9 +size 834 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_Default.png new file mode 100644 index 000000000..212ff2e1a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:448effa8e8cac551ffb3f849109f07291e575cbcd0c6e8bb4cc7b8c514772312 +size 793 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_NativeSurface.png new file mode 100644 index 000000000..9fa3d7278 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_ImageBrush_Subtract_DestOut_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c6bbdfe46e2fb0c5866c5eb07e0a23bd9ccea5063fb84725105dcbd6116466c +size 1166 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_Default.png new file mode 100644 index 000000000..bcc59e5ae --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4215d621ff15138795a72651e8aba14fca5aea4356b1d3a1687d78e2306e71f8 +size 472 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_NativeSurface.png new file mode 100644 index 000000000..a65a522b4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Add_Src_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c35594b8a5053845175a57c0d880e05bb58ae36edfcd94baebbf6759511773cd +size 1105 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_Default.png new file mode 100644 index 000000000..ff3590331 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b88bdda75c9f2addee9d898b9d9dcbfa45f247de2d9f4f771b3d31051fc8dd88 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..2f974a1a8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Darken_DestAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54f6aa02b6b856d6780bd32f6076d272639fb76eac9654d0d56e97bc190dd5b2 +size 1106 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_Default.png new file mode 100644 index 000000000..c561128ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png new file mode 100644 index 000000000..c561128ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_HardLight_Xor_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b228b04cbfabb613782ce0569aecae88ab8de33ce5f853bb10016b266f8cfa30 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_Default.png new file mode 100644 index 000000000..43394d294 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d274697d9d07a0f27e610e796452aa09db103a96473e7bac8decd0c656ee0d5 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..28f0fae92 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Lighten_DestIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3cb69cbc090ed8eb097b31f387d64b85596427bc28161d901f8477b075c803bb +size 1105 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_Default.png new file mode 100644 index 000000000..31b07cb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630a5530b5484317a46507404825943321840e7803172a0895cc2c10d40a4338 +size 444 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png new file mode 100644 index 000000000..31b07cb56 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Multiply_SrcAtop_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:630a5530b5484317a46507404825943321840e7803172a0895cc2c10d40a4338 +size 444 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_Default.png new file mode 100644 index 000000000..d835b86af --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 +size 446 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_NativeSurface.png new file mode 100644 index 000000000..9c7ed03fc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_Clear_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfdf944219c342c70f99db07c55ddc6d66421950f64e29e9d85e4f368a6b225d +size 1088 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_Default.png new file mode 100644 index 000000000..1ad01578b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34cfa0616b966a9f675fa61c2cd9ff5b9637e452e2c6ff59f36f790314213a24 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..94dcbbec0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Normal_SrcOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af85b841801113f0c67f6c3096d96c6bdcc2e959308065ca0c71e0ce8af10585 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_Default.png new file mode 100644 index 000000000..9f6074d7f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1433e8e3d4c0cf4f1a67080b5ceef482980177b4a6828048d05dea98e682697b +size 474 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_NativeSurface.png new file mode 100644 index 000000000..67e70e51a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Overlay_SrcIn_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae8272b62ce641ebe5c186f5cca39d9d19142d117b0319aa8563e5cf8be39218 +size 1111 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_Default.png new file mode 100644 index 000000000..c76cbf48c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90784f2523c8e7d6680cc17e618893ebf040033642a3cbaad73918c2f5d6b2f8 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_NativeSurface.png new file mode 100644 index 000000000..6b3b68980 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Screen_DestOver_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d1bf13431d9f5796350906b85b9b69814fcf13581cc7ca7a2fc0bc38710263f7 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_Default.png new file mode 100644 index 000000000..b9ac5f192 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6392bded60931b04bd5044a2e789405506bb8c98f4ac271e04af7698696c929 +size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d9346703d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_FillPath_GraphicsOptions_SolidBrush_Subtract_DestOut_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcb8489689d8f2377d65f5dc3c82ada44d89a9db08edbd3fa97cafe5f2680dbb +size 1088 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_Default.png new file mode 100644 index 000000000..55694d401 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74b4e0b213dd604413745b05195fb9bbf5eacac1883ade35b73f4985a800b69b +size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1eeb01770 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrush_MatchesDefaultOutput_FillPath_ImageBrush_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3bfb3cb3510c77beb21625d4d45bb3c10629f5469b4b4910d202e71967dce94 +size 363 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_Default.png new file mode 100644 index 000000000..f21f70de5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1084533b99ec908fa584bdacca0163a200210291a0634b9b1ba5c7d451af76f9 +size 5452 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_NativeSurface.png new file mode 100644 index 000000000..40a025d84 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_MatchesDefaultOutput_FillPath_LinearGradient_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e40c933b0ed69579760861f20277ebb3ede47feb331ba7b7d562f8b6c5a4698 +size 5361 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_Default.png new file mode 100644 index 000000000..4c8c85716 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6831aad3db662c097dd6bc163eeeb80f90e802176dcbf7f29aa3696efae42d40 +size 1643 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png new file mode 100644 index 000000000..362a0651a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput_FillPath_LinearGradient_Repeat_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb1337056126bea1d569742309a125713ffb3a03da6c1fc2478494b3f6964133 +size 1643 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_Default.png new file mode 100644 index 000000000..ab47e6ae3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f670831068c21e3151e5ff2f1a985bf9ec26445b74b815de5aba50316995d20a +size 1370 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_NativeSurface.png new file mode 100644 index 000000000..5fe35068c --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_FillPath_LinearGradient_ThreePoint_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2cec37aa744945b7c075253238778b0092e6fa04e4a244a460a58b328f945dc +size 1367 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_Default.png new file mode 100644 index 000000000..516e2f405 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56e326664a279ba7e03c5439fb87fdea3065ce68b8407971c307df7af6e5c96c +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_NativeSurface.png new file mode 100644 index 000000000..516e2f405 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithNonZeroNestedContours_MatchesDefaultOutput_FillPath_NonZeroNestedContours_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56e326664a279ba7e03c5439fb87fdea3065ce68b8407971c307df7af6e5c96c +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_Default.png new file mode 100644 index 000000000..30c3e9349 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a0ded646cdc61bfff585b39f1beaa3b444e25c9866474fff335fc1b828526ac +size 2209 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_NativeSurface.png new file mode 100644 index 000000000..880c5bd34 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_FillPath_PatternBrush_Diagonal_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecc01dbecdf40cd262d7db2f84befa5eff4f319c9de80e897c4d10cd788c8e96 +size 6251 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_Default.png new file mode 100644 index 000000000..82c778d4a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd8a2d93a0306cf23bb1138b31d8fac263cf6bca49dea577d4caf2ff4bb52cbb +size 146 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_NativeSurface.png new file mode 100644 index 000000000..82c778d4a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_FillPath_PatternBrush_Horizontal_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd8a2d93a0306cf23bb1138b31d8fac263cf6bca49dea577d4caf2ff4bb52cbb +size 146 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_Default.png new file mode 100644 index 000000000..23b687e57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af2824eed2bb429f76270556cbb939a7f32558fa5eb9d7ada891ab3c888f45b2 +size 10237 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_NativeSurface.png new file mode 100644 index 000000000..86da5bda0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_FillPath_RadialGradient_Single_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63bd36716166044824a024b097a998333e20b7ab99e05516428fc5eaa392c1d1 +size 10370 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_Default.png new file mode 100644 index 000000000..0b5a90037 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:109be2e6cf31cc56d54993f66cc88372d6c6faed807a93d38c67dd8d33c2c3d2 +size 10997 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_NativeSurface.png new file mode 100644 index 000000000..e114907cc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_FillPath_RadialGradient_TwoCircle_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9912f3fe725924a302683afd6d89178e518ba5b28540eecd923d17fc74424356 +size 8833 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_Default.png new file mode 100644 index 000000000..25a223a26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd31a65567b5a4a498604fe0089b57d89b09640938731279c1cb14abb25cd830 +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_NativeSurface.png new file mode 100644 index 000000000..25a223a26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRecolorBrush_MatchesDefaultOutput_FillPath_RecolorBrush_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd31a65567b5a4a498604fe0089b57d89b09640938731279c1cb14abb25cd830 +size 127 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_Default.png new file mode 100644 index 000000000..8007122cf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e012dd084fd10a6c55bac818e9b26cb700f41b4a9d0f0afc40a67f89d0e74efc +size 15258 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d9cc7c0e0 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_FillPath_SweepGradient_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:863826416e252b3d6ee0d3f737d20c8d0f25563fa228fea69c5efbb269df7fe9 +size 15816 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_Default.png new file mode 100644 index 000000000..2fcf50f05 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f12596ccf2f20c8845a5dbf4bafb0bc5827bd058b4596629213b676da45f317 +size 14294 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_NativeSurface.png new file mode 100644 index 000000000..34c1b4cd1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput_FillPath_SweepGradient_PartialArc_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac2d2d379102ee3725918613c8e954691ee2aaff4f98fb245538596f3fd0aea3 +size 12283 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_Default.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_NativeSurface.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput_FillPath_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_Default.png new file mode 100644 index 000000000..55a946401 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 +size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..55a946401 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurfaceSubregion_MatchesDefaultOutput_FillPath_NativeSurfaceSubregionParity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f02ab5aef4c00977bc766e4a03b16efd08da105faf1a1495f33087bc882cd370 +size 491 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_Default.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..883df5636 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithWebGPUCoverageBackend_NativeSurface_MatchesDefaultOutput_FillPath_NativeSurfaceParity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05e94f0d3fe81b28eb21796321e73dcc5ec8b94a965af761107d13b0bb2ff920 +size 714 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_Default.png new file mode 100644 index 000000000..ced935c37 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1baf8a477ff132f73d6ea5c116146729cc8d693d33422f0beb21432c34798ac5 +size 158 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_NativeSurface.png new file mode 100644 index 000000000..ced935c37 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/MultipleFlushes_OnSameBackend_ProduceCorrectResults_MultipleFlushes_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1baf8a477ff132f73d6ea5c116146729cc8d693d33422f0beb21432c34798ac5 +size 158 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png new file mode 100644 index 000000000..479eeaffe --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d855bec452658748b0e5cf9d3f7efc16febb367418caa74c35a535ce9259dcbe +size 12938 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b5fbb40ef --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Process_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae634a34f9a65f14827e08442e55b99b08fe79b14d0ec290e65a759a7f7b2544 +size 12887 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_Default.png new file mode 100644 index 000000000..27ae8e895 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b60b22409f9dba870c52f04b39ad0051bb4397c00321fe935431558243d4812b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..27ae8e895 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_FullOpacity_MatchesDefaultOutput_SaveLayer_FullOpacity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b60b22409f9dba870c52f04b39ad0051bb4397c00321fe935431558243d4812b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_Default.png new file mode 100644 index 000000000..f034e374b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8ce1ab20efc7f8ab17b614e548a7169f1070a8b46b273e10ae0cd51b13f66b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..f034e374b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_HalfOpacity_MatchesDefaultOutput_SaveLayer_HalfOpacity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8ce1ab20efc7f8ab17b614e548a7169f1070a8b46b273e10ae0cd51b13f66b +size 108 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_Default.png new file mode 100644 index 000000000..15e58f4c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06c54b58d35e349cfd18aeaff9614ce5348e696ebd3da5f330b03a6d0a4ab4f +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_WebGPU_NativeSurface.png new file mode 100644 index 000000000..15e58f4c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_MixedSaveAndSaveLayer_MatchesDefaultOutput_SaveLayer_MixedSaveAndSaveLayer_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06c54b58d35e349cfd18aeaff9614ce5348e696ebd3da5f330b03a6d0a4ab4f +size 96 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_Default.png new file mode 100644 index 000000000..fbecf9946 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1e8240f4bf43453704c3a7ac27b1f21174097048a94793c6bd900899175963 +size 107 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_WebGPU_NativeSurface.png new file mode 100644 index 000000000..fbecf9946 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_NestedLayers_MatchesDefaultOutput_SaveLayer_NestedLayers_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee1e8240f4bf43453704c3a7ac27b1f21174097048a94793c6bd900899175963 +size 107 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_Default.png new file mode 100644 index 000000000..c8c5208e6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528d775147faaf132df52f78b80356f3df98ef519779ade404a2d6f819d265cc +size 131 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_WebGPU_NativeSurface.png new file mode 100644 index 000000000..c8c5208e6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBlendMode_MatchesDefaultOutput_SaveLayer_WithBlendMode_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528d775147faaf132df52f78b80356f3df98ef519779ade404a2d6f819d265cc +size 131 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_Default.png new file mode 100644 index 000000000..b4f2bc648 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f74fc36ceebd139d08347771b75dd525bc7bb50fa78ae66cfc0480d25bc647a2 +size 107 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b4f2bc648 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_WithBounds_MatchesDefaultOutput_SaveLayer_WithBounds_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f74fc36ceebd139d08347771b75dd525bc7bb50fa78ae66cfc0480d25bc647a2 +size 107 diff --git a/tests/Images/ReferenceOutput/Drawing/optimize-all.cmd b/tests/Images/ReferenceOutput/Drawing/optimize-all.cmd deleted file mode 100644 index 98b5eb6f2..000000000 --- a/tests/Images/ReferenceOutput/Drawing/optimize-all.cmd +++ /dev/null @@ -1 +0,0 @@ -optipng.exe -o 7 ./**/*.png \ No newline at end of file diff --git a/tests/Images/ReferenceOutput/Drawing/optipng.exe b/tests/Images/ReferenceOutput/Drawing/optipng.exe deleted file mode 100644 index 49f9dee09..000000000 Binary files a/tests/Images/ReferenceOutput/Drawing/optipng.exe and /dev/null differ diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png new file mode 100644 index 000000000..adef79dcd --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_False.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67515a55bc0bed27054d344d430e0ea0ab0ff15a5038b6b33e8f6118f2d84d42 +size 176 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png new file mode 100644 index 000000000..c2628e2ad --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19039af8089b9ba3544553e7bd1248c135a8d9c17bdfc46728093bff7eef4bbf +size 675 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png index 1770f1516..29685ad13 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.003.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc86c51ad4946fb8a314c8d869a83cc2496d30468036729c3827c2c121cae69c -size 1068 +oid sha256:35060ebe93aba15a6a0982ded5a13e6d3b01bc3d22a6cf65498d775aff721c88 +size 262 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png deleted file mode 100644 index a60e07711..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:62f86685f6f2326e629b8b84340bb1b3cbcf6ffe75facff0931b613337772345 -size 3296 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png deleted file mode 100644 index 5bf6378e5..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:534d3fece38b94386b6ba20aa121addda1beea61d38cb34b9d2ae09b662fd38b -size 3585 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png deleted file mode 100644 index 61df79c32..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4fff4ab1fec04c432529fd67d9c50934ce083fa6e7c0c4432fd6520eac2e53ac -size 3625 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png deleted file mode 100644 index d5630da20..000000000 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92a356847e8aa36b361a2208021a0c0e2a3287d615f0b948bdbcc0bc3b336bc4 -size 4300 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 d93b91a30..29685ad13 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:84eff105c799ed23497870d1a13d2e69986cf7240da2d508794b2974bee1c5b6 -size 254 +oid sha256:35060ebe93aba15a6a0982ded5a13e6d3b01bc3d22a6cf65498d775aff721c88 +size 262 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 f0cad6422..4bd17cf3b 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:3581673cd0c053326d7c6947d232f62a7c0c61f3b86aa881be40ae609278100c -size 1188 +oid sha256:3f5c71327671428fabd8570ac86a823fd723eca816a75106424e1e0b419b49c6 +size 1039 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 3e0bec9ce..90b91a6f2 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:0ccc8f2a14e5a3c8f7aca9c9411dbff01d8e1a3c7dbd66d515881bd753ebf922 -size 1284 +oid sha256:8b01aa86c8f97d3529576b18614b8b6243e7d9333a57565dc1b9d65b18926257 +size 1257 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 78e2037c1..08c7c19ba 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:5befd1a942a8b50b40b1f4e2938699b84239d028bd83e31445271ec5f2043c64 -size 1238 +oid sha256:28eb1390316c9a49cfe540265ff1ba3b661be7e52c791455a3d40a01cf96f133 +size 1248 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 a4f0a8bfa..86d2a1f65 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:5e491edd79708d65a78d7eec5e32f4ec108d2b73fdc19ea614f09ea02f8e4183 -size 1373 +oid sha256:12ede4814260c5fa14d9392dddcb4784e6c7191dc8d8faf295d91ff98fe2d7f9 +size 1384 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 473293933..1412149b5 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:69c50b96bfc9c30b3d53ca17503ed5072f0e83a0541cfe0ef5570f3549d5b1e4 -size 116690 +oid sha256:2d4694067cedd250f54f716e9c15ee6d1311818c906f20ce9c5a6d950bf0f504 +size 116706 diff --git a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png new file mode 100644 index 000000000..015bd5aa2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6628022994715b9a00cc6410236b8c2b26b2d1bbf50e1b9163a5b87a8a7d2bd0 +size 111 diff --git a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png new file mode 100644 index 000000000..015bd5aa2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6628022994715b9a00cc6410236b8c2b26b2d1bbf50e1b9163a5b87a8a7d2bd0 +size 111 diff --git a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png new file mode 100644 index 000000000..78f83c2bd --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:516becb76b8c3293e923443c7b8cdff2fbfa5e6091b3c2dfa6b7e2f1d2bcb87f +size 8904 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 6dd59fe24..b3a7b47eb 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:742e4bd37428a4402b097eb2e33c0cc2611cb17040a34ee1457508b630705f62 -size 31937 +oid sha256:8300ebcd5ba20759fdd0b451b6dfdf79531c421b2d75dcd5b51c04c951a1fa60 +size 32026 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 462ffcfc5..a9f3d37c9 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:919a6c8b5be40aa3894050f033d487f90d6bd2621cfb2f337874bd20904d9603 -size 10646 +oid sha256:c25546b20774aacaaedf027ac1aca021c9b60c469d0426385c85775e07787f26 +size 10948 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 8cc405e45..845a582ca 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:48b6a904ad0557908dd053ff357b8c10d4e279eeaf6dd9d0df40aee653ecca72 -size 31954 +oid sha256:76a9f105ad5fdd5aa84e4c6df3d3434a9d54d025da1f61321b1c8578796c4890 +size 32066 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 f3deebc62..27a8da0f0 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:1a0df948f516294d3499aaab857729635b160f6ad15adc93c81fbade0fecfce7 -size 10640 +oid sha256:33efa67ba681678f2a5ca8ec636e00bdd19e0cd579754b74dae5a818a79d35f5 +size 10948 diff --git a/tests/Images/ReferenceOutput/RasterizerExtensionsTests/AntialiasingIsAntialiased_Rgba32_Solid400x75_(255,255,255,255).png b/tests/Images/ReferenceOutput/RasterizerExtensionsTests/AntialiasingIsAntialiased_Rgba32_Solid400x75_(255,255,255,255).png deleted file mode 100644 index b18af7e56..000000000 --- a/tests/Images/ReferenceOutput/RasterizerExtensionsTests/AntialiasingIsAntialiased_Rgba32_Solid400x75_(255,255,255,255).png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9950be5c4b9024969a96b8b30be5b30b6252366e492a14dd7f33db692453188d -size 397 diff --git a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png deleted file mode 100644 index 9993d5d5a..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank100x100_type-arrows.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index f61f6ff2c..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x50_type-wave.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index c1a2333a9..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank110x70_type-zag.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 7fea71a75..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-bumpy.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 429f4440c..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index 00af7f35d..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_big.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -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 deleted file mode 100644 index cfbbe58a6..000000000 --- a/tests/Images/ReferenceOutput/SvgPath/RenderSvgPath_Rgba32_Blank500x400_type-pie_small.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:576d8476345f085444183cbcb8c61fd03c082113dfb32cc5d9f4859d86fc5be2 -size 4765 diff --git a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png index 5d808e14f..1927b2a43 100644 --- a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png +++ b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_DoNotAppendPixelType_Solid10x10_(0,0,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78036e25b5bfb3211e8794ec11d71e385401c8ff569e598a32152e7a8023eac9 -size 118 +oid sha256:2ccaca38823033a6aacde86619c7c959a79e65d36b5e8e94f42b762b47344f10 +size 82 diff --git a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png index 5d808e14f..1927b2a43 100644 --- a/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png +++ b/tests/Images/ReferenceOutput/TestImageExtensionsTests/CompareToReferenceOutput_WhenReferenceOutputMatches_ShouldNotThrow_Rgba32_Solid10x10_(0,0,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78036e25b5bfb3211e8794ec11d71e385401c8ff569e598a32152e7a8023eac9 -size 118 +oid sha256:2ccaca38823033a6aacde86619c7c959a79e65d36b5e8e94f42b762b47344f10 +size 82 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/00.png b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/00.png index e9052e9f5..9b752b4a7 100644 Binary files a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/00.png and b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/00.png differ diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/01.png b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/01.png index 4dc2e45d8..261476218 100644 Binary files a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/01.png and b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/01.png differ diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/02.png b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/02.png index b538aeaec..71a2732d8 100644 Binary files a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/02.png and b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/02.png differ diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/03.png b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/03.png index 18c3d2d59..9bb973b1e 100644 Binary files a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/03.png and b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/03.png differ diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/04.png b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/04.png index ed84340cb..540c0fe47 100644 Binary files a/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/04.png and b/tests/Images/ReferenceOutput/TestImageProviderTests/SaveTestOutputFileMultiFrame_Rgba32_giphy.gif/04.png differ diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png index 381ff3db9..a57a541ad 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern20x10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:883a948b8920fc6930f82b4985ab4fdf7d046a508221df88811a646a52036f11 -size 135 +oid sha256:35a69eb51a6954642789a80105a6133877f66c685c1564ae328fd18efceb2009 +size 121 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png index f2d513996..209a13c0b 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern49x17.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e8795374bf53ed7585950437af71cbe6ab3ce5c6fda2c4da35566cb398333ac -size 155 +oid sha256:b541dadd48b1a136fed30442fd57fcb94f554c51f7869c2803e0ac72b98e97dc +size 122 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png index 74de36cb4..a56e0e6d7 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithBasicTestPatternImages_Rgba32_BasicTestPattern50x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:015529debc61dd2eff6b61de0c107e6bdd87eb15bebe45b99f1d38b13242cec9 -size 219 +oid sha256:6ffc4784ea34bc5424363c9a5e9229bb2c1d330c49bac2a324db16c40b2f52b1 +size 132 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png index 464b8f0b0..69e8495e2 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_F.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6773e8bfdcd78eea58c95c773a13a76b9e23dd0f16058675ade0d50786437fd1 -size 483 +oid sha256:c66b3009b2527c13e519c6cb9c86733e103a2b719b556c098380244d15a9a9e1 +size 183 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png index 3595cd1cc..81383beb7 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Argb32_test8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bf11d42f98951eb78873103c2bc51c690da6ff292e4f4ce25d9e30856b0f61c -size 404 +oid sha256:d3d0639717ff52db04200e00a75cf06490e55a661059511572954bec3d60dce9 +size 384 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png index 464b8f0b0..69e8495e2 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_F.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6773e8bfdcd78eea58c95c773a13a76b9e23dd0f16058675ade0d50786437fd1 -size 483 +oid sha256:c66b3009b2527c13e519c6cb9c86733e103a2b719b556c098380244d15a9a9e1 +size 183 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png index 3595cd1cc..81383beb7 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithFileCollection_Rgba32_test8.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bf11d42f98951eb78873103c2bc51c690da6ff292e4f4ce25d9e30856b0f61c -size 404 +oid sha256:d3d0639717ff52db04200e00a75cf06490e55a661059511572954bec3d60dce9 +size 384 diff --git a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png index 211f4f24d..c3ee13817 100644 --- a/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png +++ b/tests/Images/ReferenceOutput/TestImageProviderTests/Use_WithTestPatternImages_Rgba32_TestPattern49x20.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:952e58d9b70c65831d8d5c0aa151a7842e859e3519534d60d9e803492262aee6 -size 283 +oid sha256:3f809bac08fea0254309757288583ee82b8f7aa756e126e12b83730482fd8c03 +size 211 diff --git a/tests/coverlet.runsettings b/tests/coverlet.runsettings index 907d91489..455b7fe84 100644 --- a/tests/coverlet.runsettings +++ b/tests/coverlet.runsettings @@ -6,10 +6,11 @@ lcov [SixLabors.*]* - - - ^Clipper2Lib\..* - + + [SixLabors.ImageSharp.Drawing.WebGPU*]* true