Skip to content

Commit 6b62e73

Browse files
Merge pull request #324 from SixLabors/issue323
Mitigate issue where outline thicknesses less than 0.5 fail out generate solid shapes not outlines
2 parents 2a9ee51 + ab658c0 commit 6b62e73

12 files changed

+119
-4
lines changed

src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs

+35-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,31 @@ public static class OutlinePathExtensions
1515
private const JointStyle DefaultJointStyle = JointStyle.Square;
1616
private const EndCapStyle DefaultEndCapStyle = EndCapStyle.Butt;
1717

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

69+
width = CalculateScalingMatrix(width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix);
70+
4471
ClipperOffset offset = new(MiterOffsetDelta);
45-
offset.AddPath(path, jointStyle, endCapStyle);
4672

47-
return offset.Execute(width);
73+
// transform is noop for Matrix3x2.Identity
74+
offset.AddPath(path.Transform(scaleUpMartrix), jointStyle, endCapStyle);
75+
76+
return offset.Execute(width).Transform(scaleDownMartrix);
4877
}
4978

5079
/// <summary>
@@ -106,7 +135,9 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
106135
return path.GenerateOutline(width, jointStyle, endCapStyle);
107136
}
108137

109-
IEnumerable<ISimplePath> paths = path.Flatten();
138+
width = CalculateScalingMatrix(width, out Matrix3x2 scaleUpMartrix, out Matrix3x2 scaleDownMartrix);
139+
140+
IEnumerable<ISimplePath> paths = path.Transform(scaleUpMartrix).Flatten();
110141

111142
ClipperOffset offset = new(MiterOffsetDelta);
112143
List<PointF> buffer = new();
@@ -186,6 +217,6 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan<f
186217
}
187218
}
188219

189-
return offset.Execute(width);
220+
return offset.Execute(width).Transform(scaleDownMartrix);
190221
}
191222
}
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)