Skip to content

Commit ab658c0

Browse files
committed
mitigate limits of outline generation
if the outline thickness is going to trigger the noop behaviour of the clipper then scale the inout and output to no trigger that behaviour.
1 parent 2a9ee51 commit ab658c0

12 files changed

+119
-4
lines changed

src/ImageSharp.Drawing/Shapes/OutlinePathExtensions.cs

Lines changed: 35 additions & 4 deletions
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
}
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+
}
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)