Skip to content

Commit b23e2ae

Browse files
Merge pull request #399 from SixLabors/js/fix-text-clip
Translate clip paths for text with RenderLocation.
2 parents 0019fdb + f006d07 commit b23e2ae

10 files changed

Lines changed: 146 additions & 2 deletions

src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,21 @@ private CompositionSceneCommand CreateTextCompositionCommand(
13411341
? drawingOptions
13421342
: new DrawingOptions(graphicsOptions, shapeOptions, Matrix4x4.Identity);
13431343

1344+
IReadOnlyList<IPath>? operationClipPaths = clipPaths;
1345+
if (clipPaths != null && clipPaths.Count > 0 && (operation.RenderLocation.X != 0 || operation.RenderLocation.Y != 0))
1346+
{
1347+
IPath[] translatedClipPaths = new IPath[clipPaths.Count];
1348+
1349+
// Text glyph paths are queued in glyph-local coordinates and placed with RenderLocation,
1350+
// so canvas-space clip paths must be moved into that same local space before clipping.
1351+
for (int i = 0; i < clipPaths.Count; i++)
1352+
{
1353+
translatedClipPaths[i] = clipPaths[i].Translate(-operation.RenderLocation);
1354+
}
1355+
1356+
operationClipPaths = translatedClipPaths;
1357+
}
1358+
13441359
if (pen is null)
13451360
{
13461361
return new PathCompositionSceneCommand(
@@ -1351,7 +1366,7 @@ private CompositionSceneCommand CreateTextCompositionCommand(
13511366
in rasterizerOptions,
13521367
state.TargetBounds,
13531368
destinationOffset,
1354-
clipPaths,
1369+
operationClipPaths,
13551370
state.IsLayer));
13561371
}
13571372

@@ -1364,7 +1379,7 @@ private CompositionSceneCommand CreateTextCompositionCommand(
13641379
state.TargetBounds,
13651380
destinationOffset,
13661381
pen,
1367-
clipPaths,
1382+
operationClipPaths,
13681383
state.IsLayer));
13691384
}
13701385

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.Fonts;
5+
using SixLabors.ImageSharp.Drawing.Processing;
6+
using SixLabors.ImageSharp.PixelFormats;
7+
8+
namespace SixLabors.ImageSharp.Drawing.Tests.Issues;
9+
10+
public class Issue_397
11+
{
12+
[Theory]
13+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Intersection)]
14+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Union)]
15+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Difference)]
16+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Xor)]
17+
public void DrawTextWithIntersectingClip<TPixel>(
18+
TestImageProvider<TPixel> provider,
19+
BooleanOperation operation)
20+
where TPixel : unmanaged, IPixel<TPixel>
21+
{
22+
PointF textOrigin = new(54, 78);
23+
PointF clipCenter = new(104, 70);
24+
DrawingOptions clipOptions = CreateClipOptions(operation);
25+
Font font = TestFontUtilities.GetFont("OpenSans-Regular.ttf", 18);
26+
27+
// Expected output:
28+
// - Intersection shows only red text inside the moved star.
29+
// - Difference shows only red text outside the moved star.
30+
// - Union and Xor can show a red star because the boolean-combined path includes the clip path,
31+
// and DrawText fills that combined result with the text brush.
32+
provider.RunValidatingProcessorTest(
33+
x => x.Paint(canvas => DrawIssue397Sample(canvas, clipOptions, clipCenter, textOrigin, font)),
34+
testOutputDetails: $"{operation}_IntersectingClip",
35+
appendPixelTypeToFileName: false,
36+
appendSourceFileOrDescription: false);
37+
}
38+
39+
[Theory]
40+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Intersection)]
41+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Union)]
42+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Difference)]
43+
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Xor)]
44+
public void DrawTextWithNonIntersectingClip<TPixel>(
45+
TestImageProvider<TPixel> provider,
46+
BooleanOperation operation)
47+
where TPixel : unmanaged, IPixel<TPixel>
48+
{
49+
PointF textOrigin = new(54, 78);
50+
PointF clipCenter = new(192, 116);
51+
DrawingOptions clipOptions = CreateClipOptions(operation);
52+
Font font = TestFontUtilities.GetFont("OpenSans-Regular.ttf", 18);
53+
54+
// Expected output:
55+
// - Intersection shows no red text because the moved star and text do not overlap.
56+
// - Difference shows the full red text because the moved star removes nothing from it.
57+
// - Union and Xor show both the full red text and a red star because disjoint Xor matches Union,
58+
// and DrawText fills the boolean-combined result with the text brush.
59+
provider.RunValidatingProcessorTest(
60+
x => x.Paint(canvas => DrawIssue397Sample(canvas, clipOptions, clipCenter, textOrigin, font)),
61+
testOutputDetails: $"{operation}_NonIntersectingClip",
62+
appendPixelTypeToFileName: false,
63+
appendSourceFileOrDescription: false);
64+
}
65+
66+
private static void DrawIssue397Sample(
67+
DrawingCanvas canvas,
68+
DrawingOptions clipOptions,
69+
PointF clipCenter,
70+
PointF textOrigin,
71+
Font font)
72+
{
73+
canvas.Clear(Brushes.Solid(Color.White));
74+
StarPolygon clipPath = new(clipCenter, 7, 16, 38, 18);
75+
RichTextOptions textOptions = new(font)
76+
{
77+
Origin = textOrigin
78+
};
79+
80+
// The gray outline is the unclipped text guide; the red draw below shows the boolean clip result.
81+
canvas.DrawText(textOptions, "This is a test", brush: null, Pens.Solid(Color.LightGray, 1F));
82+
83+
// The blue outline marks the moved clipping path without adding a filled shape behind the text.
84+
canvas.Draw(Pens.Solid(Color.DarkBlue, 1F), clipPath);
85+
canvas.Save(clipOptions, clipPath);
86+
87+
canvas.DrawText(
88+
textOptions,
89+
"This is a test",
90+
Brushes.Solid(Color.Crimson),
91+
pen: null);
92+
93+
canvas.Restore();
94+
canvas.Draw(Pens.Solid(Color.DarkBlue, 1F), clipPath);
95+
}
96+
97+
private static DrawingOptions CreateClipOptions(BooleanOperation operation)
98+
=> new()
99+
{
100+
ShapeOptions = new()
101+
{
102+
BooleanOperation = operation
103+
}
104+
};
105+
}
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)