Skip to content

Commit d819f81

Browse files
committed
Merge main into blaze
2 parents d3ec07c + db32b13 commit d819f81

File tree

26 files changed

+263
-103
lines changed

26 files changed

+263
-103
lines changed

.github/workflows/build-and-test.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
sdk-preview: true
2424
runtime: -x64
2525
codecov: false
26-
- os: macos-latest
26+
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
2727
framework: net7.0
2828
sdk: 7.0.x
2929
sdk-preview: true
@@ -46,7 +46,7 @@ jobs:
4646
sdk: 6.0.x
4747
runtime: -x64
4848
codecov: false
49-
- os: macos-latest
49+
- os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
5050
framework: net6.0
5151
sdk: 6.0.x
5252
runtime: -x64
@@ -85,7 +85,7 @@ jobs:
8585
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
8686

8787
- name: Git Setup LFS Cache
88-
uses: actions/cache@v3
88+
uses: actions/cache@v4
8989
id: lfs-cache
9090
with:
9191
path: .git/lfs
@@ -95,10 +95,10 @@ jobs:
9595
run: git lfs pull
9696

9797
- name: NuGet Install
98-
uses: NuGet/setup-nuget@v1
98+
uses: NuGet/setup-nuget@v2
9999

100100
- name: NuGet Setup Cache
101-
uses: actions/cache@v3
101+
uses: actions/cache@v4
102102
id: nuget-cache
103103
with:
104104
path: ~/.nuget
@@ -150,14 +150,14 @@ jobs:
150150
XUNIT_PATH: .\tests\ImageSharp.Drawing.Tests # Required for xunit
151151

152152
- name: Export Failed Output
153-
uses: actions/upload-artifact@v3
153+
uses: actions/upload-artifact@v4
154154
if: failure()
155155
with:
156156
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
157157
path: tests/Images/ActualOutput/
158158

159159
- name: Codecov Update
160-
uses: codecov/codecov-action@v3
160+
uses: codecov/codecov-action@v4
161161
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
162162
with:
163163
flags: unittests
@@ -184,10 +184,10 @@ jobs:
184184
submodules: recursive
185185

186186
- name: NuGet Install
187-
uses: NuGet/setup-nuget@v1
187+
uses: NuGet/setup-nuget@v2
188188

189189
- name: NuGet Setup Cache
190-
uses: actions/cache@v3
190+
uses: actions/cache@v4
191191
id: nuget-cache
192192
with:
193193
path: ~/.nuget

src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp.drawing\sixlabors.imagesharp.drawing.128.png" Pack="true" PackagePath="" />
4646
</ItemGroup>
4747
<ItemGroup>
48-
<PackageReference Include="SixLabors.Fonts" Version="2.0.2" />
48+
<PackageReference Include="SixLabors.Fonts" Version="2.0.3" />
4949
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0-alpha.0.4" />
5050
</ItemGroup>
5151
<Import Project="..\..\shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems" Label="Shared" />

src/ImageSharp.Drawing/Processing/Processors/Text/RichTextGlyphRenderer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,10 @@ protected override void EndGlyph()
367367

368368
if (renderData.OutlineMap != null)
369369
{
370+
int offset = (int)((this.currentPen?.StrokeWidth ?? 0) / 2);
370371
this.DrawingOperations.Add(new DrawingOperation
371372
{
372-
RenderLocation = renderLocation,
373+
RenderLocation = renderLocation - new Size(offset, offset),
373374
Map = renderData.OutlineMap,
374375
Brush = this.currentPen?.StrokeFill ?? this.currentBrush!,
375376
RenderPass = RenderOrderOutline

src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs

Lines changed: 58 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,35 @@ namespace SixLabors.ImageSharp.Drawing;
1212
/// </summary>
1313
public static class OutlinePathExtensions
1414
{
15+
private const float MiterOffsetDelta = 20;
1516
private const JointStyle DefaultJointStyle = JointStyle.Square;
1617
private const EndCapStyle DefaultEndCapStyle = EndCapStyle.Butt;
1718

19+
/// <summary>
20+
/// Calculates the scaling matrixes tha tmust be applied to the inout and output paths of for successful clipping.
21+
/// </summary>
22+
/// <param name="width">the requested width</param>
23+
/// <param name="scaleUpMartrix">The matrix to apply to the input path</param>
24+
/// <param name="scaleDownMartrix">The matrix to apply to the output path</param>
25+
/// <returns>The final width to use internally to outlining</returns>
26+
private static float CalculateScalingMatrix(float width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix)
27+
{
28+
// when the thickness is below a 0.5 threshold we need to scale
29+
// the source path (up) and result path (down) by a factor to ensure
30+
// the offest is greater than 0.5 to ensure offsetting isn't skipped.
31+
scaleUpMartrix = Matrix3x2.Identity;
32+
scaleDownMartrix = Matrix3x2.Identity;
33+
if (width < 0.5)
34+
{
35+
float scale = 1 / width;
36+
scaleUpMartrix = Matrix3x2.CreateScale(scale);
37+
scaleDownMartrix = Matrix3x2.CreateScale(width);
38+
width = 1;
39+
}
40+
41+
return width;
42+
}
43+
1844
/// <summary>
1945
/// Generates an outline of the path.
2046
/// </summary>
@@ -41,15 +67,14 @@ public static IPath GenerateOutline(this IPath path, float width, JointStyle joi
4167
return Path.Empty;
4268
}
4369

44-
List<Polygon> stroked = [];
70+
width = CalculateScalingMatrix(width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix);
4571

46-
PolygonStroker stroker = new() { Width = width, LineJoin = GetLineJoin(jointStyle), LineCap = GetLineCap(endCapStyle) };
47-
foreach (ISimplePath simplePath in path.Flatten())
48-
{
49-
stroked.Add(new Polygon(stroker.ProcessPath(simplePath.Points.Span, simplePath.IsClosed || endCapStyle is EndCapStyle.Polygon or EndCapStyle.Joined).ToArray()));
50-
}
72+
ClipperOffset offset = new(MiterOffsetDelta);
5173

52-
return new ComplexPolygon(stroked);
74+
// transform is noop for Matrix3x2.Identity
75+
offset.AddPath(path.Transform(scaleUpMartrix), jointStyle, endCapStyle);
76+
77+
return offset.Execute(width).Transform(scaleDownMartrix);
5378
}
5479

5580
/// <summary>
@@ -69,11 +94,11 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
6994
/// <param name="path">The path to outline</param>
7095
/// <param name="width">The outline width.</param>
7196
/// <param name="pattern">The pattern made of multiples of the width.</param>
72-
/// <param name="invert">Whether the first item in the pattern is off.</param>
97+
/// <param name="startOff">Whether the first item in the pattern is on or off.</param>
7398
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
7499
/// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
75-
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool invert)
76-
=> GenerateOutline(path, width, pattern, invert, DefaultJointStyle, DefaultEndCapStyle);
100+
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool startOff)
101+
=> GenerateOutline(path, width, pattern, startOff, DefaultJointStyle, DefaultEndCapStyle);
77102

78103
/// <summary>
79104
/// Generates an outline of the path with alternating on and off segments based on the pattern.
@@ -94,12 +119,12 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
94119
/// <param name="path">The path to outline</param>
95120
/// <param name="width">The outline width.</param>
96121
/// <param name="pattern">The pattern made of multiples of the width.</param>
97-
/// <param name="invert">Whether the first item in the pattern is off.</param>
122+
/// <param name="startOff">Whether the first item in the pattern is on or off.</param>
98123
/// <param name="jointStyle">The style to apply to the joints.</param>
99124
/// <param name="endCapStyle">The style to apply to the end caps.</param>
100125
/// <returns>A new <see cref="IPath"/> representing the outline.</returns>
101126
/// <exception cref="ClipperException">Thrown when an offset cannot be calculated.</exception>
102-
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool invert, JointStyle jointStyle, EndCapStyle endCapStyle)
127+
public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<float> pattern, bool startOff, JointStyle jointStyle, EndCapStyle endCapStyle)
103128
{
104129
if (width <= 0)
105130
{
@@ -111,20 +136,22 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
111136
return path.GenerateOutline(width, jointStyle, endCapStyle);
112137
}
113138

114-
PolygonStroker stroker = new() { Width = width, LineJoin = GetLineJoin(jointStyle), LineCap = GetLineCap(endCapStyle) };
115-
PathsF stroked = [];
116-
List<PointF> buffer = [];
139+
width = CalculateScalingMatrix(width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix);
140+
141+
IEnumerable<ISimplePath> paths = path.Transform(scaleUpMartrix).Flatten();
117142

118-
foreach (ISimplePath simplePath in path.Flatten())
143+
ClipperOffset offset = new(MiterOffsetDelta);
144+
List<PointF> buffer = new();
145+
foreach (ISimplePath p in paths)
119146
{
120-
bool online = !invert;
147+
bool online = !startOff;
121148
float targetLength = pattern[0] * width;
122149
int patternPos = 0;
123-
ReadOnlySpan<PointF> points = simplePath.Points.Span;
150+
ReadOnlySpan<PointF> points = p.Points.Span;
124151

125152
// Create a new list of points representing the new outline
126153
int pCount = points.Length;
127-
if (!simplePath.IsClosed)
154+
if (!p.IsClosed)
128155
{
129156
pCount--;
130157
}
@@ -136,20 +163,20 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
136163
{
137164
int next = (i + 1) % points.Length;
138165
Vector2 targetPoint = points[next];
139-
float distanceToNext = Vector2.Distance(currentPoint, targetPoint);
140-
if (distanceToNext > targetLength)
166+
float distToNext = Vector2.Distance(currentPoint, targetPoint);
167+
if (distToNext > targetLength)
141168
{
142-
// Find a point between the 2
143-
float t = targetLength / distanceToNext;
169+
// find a point between the 2
170+
float t = targetLength / distToNext;
144171

145172
Vector2 point = (currentPoint * (1 - t)) + (targetPoint * t);
146173
buffer.Add(currentPoint);
147174
buffer.Add(point);
148175

149-
// We now insert a line
176+
// we now inset a line joining
150177
if (online)
151178
{
152-
stroked.Add(stroker.ProcessPath(CollectionsMarshal.AsSpan(buffer), false));
179+
offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle);
153180
}
154181

155182
online = !online;
@@ -158,22 +185,22 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
158185

159186
currentPoint = point;
160187

161-
// Next length
188+
// next length
162189
patternPos = (patternPos + 1) % pattern.Length;
163190
targetLength = pattern[patternPos] * width;
164191
}
165-
else if (distanceToNext <= targetLength)
192+
else if (distToNext <= targetLength)
166193
{
167194
buffer.Add(currentPoint);
168195
currentPoint = targetPoint;
169196
i++;
170-
targetLength -= distanceToNext;
197+
targetLength -= distToNext;
171198
}
172199
}
173200

174201
if (buffer.Count > 0)
175202
{
176-
if (simplePath.IsClosed)
203+
if (p.IsClosed)
177204
{
178205
buffer.Add(points[0]);
179206
}
@@ -184,54 +211,13 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
184211

185212
if (online)
186213
{
187-
stroked.Add(stroker.ProcessPath(CollectionsMarshal.AsSpan(buffer), false));
214+
offset.AddPath(CollectionsMarshal.AsSpan(buffer), jointStyle, endCapStyle);
188215
}
189216

190217
buffer.Clear();
191218
}
192219
}
193220

194-
// Clean up self intersections.
195-
PolygonClipper clipper = new() { PreserveCollinear = true };
196-
clipper.AddSubject(stroked);
197-
PathsF clipped = [];
198-
clipper.Execute(ClippingOperation.Union, FillRule.Positive, clipped);
199-
200-
if (clipped.Count == 0)
201-
{
202-
// Cannot clip. Return the stroked path.
203-
Polygon[] polygons = new Polygon[stroked.Count];
204-
for (int i = 0; i < stroked.Count; i++)
205-
{
206-
polygons[i] = new Polygon(stroked[i].ToArray());
207-
}
208-
209-
return new ComplexPolygon(polygons);
210-
}
211-
212-
// Convert the clipped paths back to polygons.
213-
Polygon[] result = new Polygon[clipped.Count];
214-
for (int i = 0; i < clipped.Count; i++)
215-
{
216-
result[i] = new Polygon(clipped[i].ToArray());
217-
}
218-
219-
return new ComplexPolygon(result);
221+
return offset.Execute(width).Transform(scaleDownMartrix);
220222
}
221-
222-
private static LineJoin GetLineJoin(JointStyle value)
223-
=> value switch
224-
{
225-
JointStyle.Square => LineJoin.BevelJoin,
226-
JointStyle.Round => LineJoin.RoundJoin,
227-
_ => LineJoin.MiterJoin,
228-
};
229-
230-
private static LineCap GetLineCap(EndCapStyle value)
231-
=> value switch
232-
{
233-
EndCapStyle.Round => LineCap.Round,
234-
EndCapStyle.Square => LineCap.Square,
235-
_ => LineCap.Butt,
236-
};
237223
}

tests/ImageSharp.Drawing.Tests/Drawing/Text/DrawTextOnImageTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,8 @@ public void CanRotateFilledFont_Issue175<TPixel>(
451451
AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle);
452452

453453
RichTextOptions textOptions = new(font);
454-
FontRectangle bounds = TextMeasurer.MeasureSize(text, textOptions);
455-
Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(bounds.X, bounds.Y, bounds.Width, bounds.Height)));
454+
FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions);
455+
Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height)));
456456

457457
provider.RunValidatingProcessorTest(
458458
x => x.SetDrawingTransform(transform).DrawText(textOptions, text, Color.Black),
@@ -478,8 +478,8 @@ public void CanRotateOutlineFont_Issue175<TPixel>(
478478
AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(angle);
479479

480480
RichTextOptions textOptions = new(font);
481-
FontRectangle bounds = TextMeasurer.MeasureSize(text, textOptions);
482-
Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(bounds.X, bounds.Y, bounds.Width, bounds.Height)));
481+
FontRectangle advance = TextMeasurer.MeasureAdvance(text, textOptions);
482+
Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(advance.X, advance.Y, advance.Width, advance.Height)));
483483

484484
provider.RunValidatingProcessorTest(
485485
x => x.SetDrawingTransform(transform)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Drawing.Processing;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
7+
namespace SixLabors.ImageSharp.Drawing.Tests.Issues;
8+
9+
public class Issue_323
10+
{
11+
[Theory]
12+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 3f)]
13+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 1f)]
14+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.3f)]
15+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.7f)]
16+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.003f)]
17+
public void DrawPolygonMustDrawoutlineOnly<TPixel>(TestImageProvider<TPixel> provider, float scale)
18+
where TPixel : unmanaged, IPixel<TPixel>
19+
{
20+
Color color = Color.RebeccaPurple;
21+
provider.RunValidatingProcessorTest(
22+
x => x.DrawPolygon(
23+
color,
24+
scale,
25+
new PointF[] {
26+
new(5, 5),
27+
new(5, 150),
28+
new(190, 150),
29+
}),
30+
new { scale });
31+
}
32+
33+
[Theory]
34+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 3f)]
35+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 1f)]
36+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.3f)]
37+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.7f)]
38+
[WithSolidFilledImages(300, 300, "White", PixelTypes.Rgba32, 0.003f)]
39+
public void DrawPolygonMustDrawoutlineOnly_Pattern<TPixel>(TestImageProvider<TPixel> provider, float scale)
40+
where TPixel : unmanaged, IPixel<TPixel>
41+
{
42+
Color color = Color.RebeccaPurple;
43+
var pen = Pens.DashDot(color, scale);
44+
provider.RunValidatingProcessorTest(
45+
x => x.DrawPolygon(
46+
pen,
47+
new PointF[] {
48+
new(5, 5),
49+
new(5, 150),
50+
new(190, 150),
51+
}),
52+
new { scale });
53+
}
54+
}

0 commit comments

Comments
 (0)