Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
4561a3c
Add hacked AGG stroker
JimBobSquarePants Jan 27, 2024
52c5312
Update PolygonStroker.cs
JimBobSquarePants Jan 27, 2024
bed3722
Actually use the width
JimBobSquarePants Jan 27, 2024
558de97
Bad hack to attempt to close path
JimBobSquarePants Jan 27, 2024
7527241
Close the polygons on stroking.
JimBobSquarePants Jan 29, 2024
3c8b5d3
Clip dashed paths
JimBobSquarePants Jan 30, 2024
319b80d
Wire up options
JimBobSquarePants Jan 30, 2024
0a70463
Cleanup
JimBobSquarePants Feb 8, 2024
2152cec
Use latest ImageSharp build
JimBobSquarePants Mar 7, 2024
4fdddc4
Added Blaze to some benchmarks
TechPizzaDev Apr 7, 2024
99a6283
Optimized ArrayBuilder
TechPizzaDev Apr 8, 2024
4569fe7
Cleaned up PolygonStroker.Accumulate
TechPizzaDev Apr 8, 2024
0285e5d
Reduce allocs in InternalPath
TechPizzaDev Apr 8, 2024
4bfa88b
Add methods on PolygonStroker for manually composing (line) paths
TechPizzaDev Apr 8, 2024
0dbfc38
Lazy-init bezier drawing points
TechPizzaDev May 3, 2024
5703f58
Access list of paths directly in RichTextGlyphRenderer
TechPizzaDev May 3, 2024
0e0d9bf
Reduce ILineSegment[] copies for Path classes
TechPizzaDev May 3, 2024
975c453
Derive EllipsePolygon from Polygon
TechPizzaDev May 3, 2024
5448713
Lazy-init InternalPaths and Bounds in ComplexPolygon
TechPizzaDev May 3, 2024
9b91ee4
Cleanup DrawPolygon
TechPizzaDev May 3, 2024
d3ec07c
Improve copying in ClipperOffset
TechPizzaDev May 23, 2024
d819f81
Merge main into blaze
TechPizzaDev May 23, 2024
70da339
Merge branch 'main' into blaze
JimBobSquarePants Oct 20, 2025
f8a963b
Update ImageSharp.Drawing.csproj
JimBobSquarePants Oct 20, 2025
c5c823c
Fix merge conflicts and missing test output
JimBobSquarePants Oct 20, 2025
018962b
Get solution building.
JimBobSquarePants Oct 20, 2025
9e8c991
Fix benchmark csproj file.
JimBobSquarePants Oct 30, 2025
72bd892
Strip out blaze so we can port back improvements.
JimBobSquarePants Oct 30, 2025
c40f450
Remove DebugFast
JimBobSquarePants Oct 30, 2025
1e06414
Remove DebugFast
JimBobSquarePants Oct 30, 2025
539e09d
Remove bad target framework declaration
JimBobSquarePants Oct 30, 2025
f71184b
Merge branch 'blaze' into js/updates-to-backport
JimBobSquarePants Oct 30, 2025
4cf8fa3
Remove more bad target framework declarations
JimBobSquarePants Oct 30, 2025
b976617
Merge branch 'blaze' into js/updates-to-backport
JimBobSquarePants Oct 30, 2025
ea766fd
Try matching build process with ImageSharp
JimBobSquarePants Oct 30, 2025
fdbb390
Update build-and-test.yml
JimBobSquarePants Oct 30, 2025
9c877af
Try using the no-deps package
JimBobSquarePants Oct 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ on:
push:
branches:
- main
- release/*
tags:
- "v*"
pull_request:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]

jobs:
# Prime a single LFS cache and expose the exact key for the matrix
WarmLFS:
Expand Down Expand Up @@ -112,14 +115,14 @@ jobs:
options:
os: buildjet-4vcpu-ubuntu-2204-arm

runs-on: ${{matrix.options.os}}
runs-on: ${{ matrix.options.os }}

steps:
- name: Install libgdi+, which is required for tests running on ubuntu
if: ${{ contains(matrix.options.os, 'ubuntu') }}
run: |
sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
sudo apt-get update
sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev

- name: Git Config
shell: bash
Expand All @@ -141,6 +144,7 @@ jobs:
key: ${{ needs.WarmLFS.outputs.lfs_key }}

- name: Git Pull LFS
shell: bash
run: git lfs pull

- name: NuGet Install
Expand Down Expand Up @@ -211,14 +215,10 @@ jobs:
with:
flags: unittests


Publish:
needs: [Build]

runs-on: ubuntu-latest

if: (github.event_name == 'push')

steps:
- name: Git Config
shell: bash
Expand Down Expand Up @@ -259,4 +259,3 @@ jobs:
run: |
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate

12 changes: 10 additions & 2 deletions ImageSharp.Drawing.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
# Visual Studio Version 18
VisualStudioVersion = 18.0.11123.170 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1-D75E-4C6D-83EB-80367343E0D7}"
ProjectSection(SolutionItems) = preProject
Expand Down Expand Up @@ -359,6 +359,14 @@ Global
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5493F024-0A3F-420C-AC2D-05B77A36025B}.Release|Any CPU.Build.0 = Release|Any CPU
{FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCEDD229-22BC-4B82-87DE-786BBFC52EDE}.Release|Any CPU.Build.0 = Release|Any CPU
{5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5490DFAF-0891-535F-08B4-2BF03C2BB778}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5490DFAF-0891-535F-08B4-2BF03C2BB778}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;

namespace SixLabors.ImageSharp.Drawing;

/// <summary>
Expand All @@ -28,20 +26,19 @@ public static bool IsOpaqueColorWithoutBlending(this GraphicsOptions options, Co
return false;
}

if (options.AlphaCompositionMode != PixelAlphaCompositionMode.SrcOver
&& options.AlphaCompositionMode != PixelAlphaCompositionMode.Src)
if (options.AlphaCompositionMode is not PixelAlphaCompositionMode.SrcOver and not PixelAlphaCompositionMode.Src)
{
return false;
}

const float Opaque = 1F;
const float opaque = 1f;

if (options.BlendPercentage != Opaque)
if (options.BlendPercentage != opaque)
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equality checks on floating point values can yield unexpected results.

Copilot uses AI. Check for mistakes.
{
return false;
}

if (color.ToScaledVector4().W != Opaque)
if (color.ToScaledVector4().W != opaque)
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equality checks on floating point values can yield unexpected results.

Copilot uses AI. Check for mistakes.
{
return false;
}
Expand Down
1 change: 0 additions & 1 deletion src/ImageSharp.Drawing/ImageSharp.Drawing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
<Otherwise>
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
</Otherwise>
</Choose>
Expand Down
4 changes: 2 additions & 2 deletions src/ImageSharp.Drawing/Processing/PatternBrush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ public PatternBrushApplicator(
public override void Apply(Span<float> scanline, int x, int y)
{
int patternY = y % this.pattern.Rows;
Span<float> amounts = this.blenderBuffers.AmountSpan.Slice(0, scanline.Length);
Span<TPixel> overlays = this.blenderBuffers.OverlaySpan.Slice(0, scanline.Length);
Span<float> amounts = this.blenderBuffers.AmountSpan[..scanline.Length];
Span<TPixel> overlays = this.blenderBuffers.OverlaySpan[..scanline.Length];

for (int i = 0; i < scanline.Length; i++)
{
Expand Down
1 change: 1 addition & 0 deletions src/ImageSharp.Drawing/Processing/Pen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ protected Pen(Brush strokeFill, float strokeWidth)
protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern)
{
Guard.NotNull(strokeFill, nameof(strokeFill));

Guard.MustBeGreaterThan(strokeWidth, 0, nameof(strokeWidth));
Guard.NotNull(strokePattern, nameof(strokePattern));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ protected override void OnFrameApply(ImageFrame<TPixel> source)

// We need to offset the pixel grid to account for when we outline a path.
// basically if the line is [1,2] => [3,2] then when outlining at 1 we end up with a region of [0.5,1.5],[1.5, 1.5],[3.5,2.5],[2.5,2.5]
// and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the#
// and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the
// region to align with the pixel grid.
if (graphicsOptions.Antialias)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ protected override void EndGlyph()
}

// Path has already been added to the collection via the base class.
IPath path = this.Paths.Last();
IPath path = this.PathList[^1];
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local scope variable 'path' shadows RichTextGlyphRenderer.path.

Copilot uses AI. Check for mistakes.
Point renderLocation = ClampToPixel(path.Bounds.Location);
if (this.noCache || this.rasterizationRequired)
{
Expand Down
3 changes: 2 additions & 1 deletion src/ImageSharp.Drawing/Processing/RecolorBrush.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ public RecolorBrushApplicator(
float threshold)
: base(configuration, options, source)
{
this.sourceColor = sourceColor.ToVector4();
this.sourceColor = sourceColor.ToScaledVector4();
this.targetColorPixel = targetColor;

// TODO: Review this. We can skip the conversion from/to Vector4.
// Lets hack a min max extremes for a color space by letting the IPackedPixel clamp our values to something in the correct spaces :)
TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue));
TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue));
Expand Down
7 changes: 1 addition & 6 deletions src/ImageSharp.Drawing/Shapes/ArcLineSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ public ArcLineSegment(PointF from, PointF to, SizeF radius, float rotation, bool
{
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
}

this.EndPoint = this.linePoints[^1];
}

/// <summary>
Expand Down Expand Up @@ -80,18 +78,15 @@ public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAn
{
this.linePoints = EllipticArcFromEndParams(from, to, radius, rotation, largeArc, sweep);
}

this.EndPoint = this.linePoints[^1];
}

private ArcLineSegment(PointF[] linePoints)
{
this.linePoints = linePoints;
this.EndPoint = this.linePoints[^1];
}

/// <inheritdoc/>
public PointF EndPoint { get; }
public PointF EndPoint => this.linePoints[^1];

/// <inheritdoc/>
public ReadOnlyMemory<PointF> Flatten() => this.linePoints;
Expand Down
110 changes: 58 additions & 52 deletions src/ImageSharp.Drawing/Shapes/ComplexPolygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.

using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;

namespace SixLabors.ImageSharp.Drawing;
Expand All @@ -14,8 +15,9 @@ namespace SixLabors.ImageSharp.Drawing;
public sealed class ComplexPolygon : IPath, IPathInternals, IInternalPathOwner
{
private readonly IPath[] paths;
private readonly List<InternalPath> internalPaths;
private readonly float length;
private List<InternalPath>? internalPaths;
private float length;
private RectangleF? bounds;

/// <summary>
/// Initializes a new instance of the <see cref="ComplexPolygon"/> class.
Expand Down Expand Up @@ -45,53 +47,10 @@ public ComplexPolygon(params IPath[] paths)
Guard.NotNull(paths, nameof(paths));

this.paths = paths;
this.internalPaths = new List<InternalPath>(this.paths.Length);

if (paths.Length > 0)
{
float minX = float.MaxValue;
float maxX = float.MinValue;
float minY = float.MaxValue;
float maxY = float.MinValue;
float length = 0;

foreach (IPath p in this.paths)
{
if (p.Bounds.Left < minX)
{
minX = p.Bounds.Left;
}

if (p.Bounds.Right > maxX)
{
maxX = p.Bounds.Right;
}

if (p.Bounds.Top < minY)
{
minY = p.Bounds.Top;
}

if (p.Bounds.Bottom > maxY)
{
maxY = p.Bounds.Bottom;
}

foreach (ISimplePath s in p.Flatten())
{
InternalPath ip = new(s.Points, s.IsClosed);
length += ip.Length;
this.internalPaths.Add(ip);
}
}

this.length = length;
this.Bounds = new RectangleF(minX, minY, maxX - minX, maxY - minY);
}
else
if (paths.Length == 0)
{
this.length = 0;
this.Bounds = RectangleF.Empty;
this.bounds = RectangleF.Empty;
}

this.PathType = PathTypes.Mixed;
Expand All @@ -106,7 +65,7 @@ public ComplexPolygon(params IPath[] paths)
public IEnumerable<IPath> Paths => this.paths;

/// <inheritdoc/>
public RectangleF Bounds { get; }
public RectangleF Bounds => this.bounds ??= this.CalcBounds();

/// <inheritdoc/>
public IPath Transform(Matrix3x2 matrix)
Expand All @@ -118,10 +77,10 @@ public IPath Transform(Matrix3x2 matrix)
}

IPath[] shapes = new IPath[this.paths.Length];
int i = 0;
foreach (IPath s in this.Paths)

for (int i = 0; i < shapes.Length; i++)
{
shapes[i++] = s.Transform(matrix);
shapes[i] = this.paths[i].Transform(matrix);
}

return new ComplexPolygon(shapes);
Expand Down Expand Up @@ -159,6 +118,11 @@ public IPath AsClosedPath()
/// <inheritdoc/>
SegmentInfo IPathInternals.PointAlongPath(float distance)
{
if (this.internalPaths == null)
{
this.InitInternalPaths();
}

distance %= this.length;
foreach (InternalPath p in this.internalPaths)
{
Expand All @@ -177,7 +141,49 @@ SegmentInfo IPathInternals.PointAlongPath(float distance)

/// <inheritdoc/>
IReadOnlyList<InternalPath> IInternalPathOwner.GetRingsAsInternalPath()
=> this.internalPaths;
{
this.InitInternalPaths();
return this.internalPaths;
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable this.internalPaths may be null at this access because it has a nullable type.

Suggested change
return this.internalPaths;
return this.internalPaths!;

Copilot uses AI. Check for mistakes.
}

/// <summary>
/// Initializes <see cref="internalPaths"/> and <see cref="length"/>.
/// </summary>
[MemberNotNull(nameof(internalPaths))]
private void InitInternalPaths()
{
this.internalPaths = new List<InternalPath>(this.paths.Length);

foreach (IPath p in this.paths)
{
foreach (ISimplePath s in p.Flatten())
{
InternalPath ip = new(s.Points, s.IsClosed);
this.length += ip.Length;
this.internalPaths.Add(ip);
}
}
}

private RectangleF CalcBounds()
{
float minX = float.MaxValue;
float maxX = float.MinValue;
float minY = float.MaxValue;
float maxY = float.MinValue;

foreach (IPath p in this.paths)
{
RectangleF pBounds = p.Bounds;

minX = MathF.Min(minX, pBounds.Left);
maxX = MathF.Max(maxX, pBounds.Right);
minY = MathF.Min(minY, pBounds.Top);
maxY = MathF.Max(maxY, pBounds.Bottom);
}
Comment on lines +175 to +183
Copy link

Copilot AI Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop immediately maps its iteration variable to another variable - consider mapping the sequence explicitly using '.Select(...)'.

Copilot uses AI. Check for mistakes.

return new RectangleF(minX, minY, maxX - minX, maxY - minY);
}

private static InvalidOperationException ThrowOutOfRange() => new("Should not be possible to reach this line");
}
Loading
Loading