Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 17 additions & 2 deletions src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,21 @@ private CompositionSceneCommand CreateTextCompositionCommand(
? drawingOptions
: new DrawingOptions(graphicsOptions, shapeOptions, Matrix4x4.Identity);

IReadOnlyList<IPath>? operationClipPaths = clipPaths;
if (clipPaths != null && clipPaths.Count > 0 && (operation.RenderLocation.X != 0 || operation.RenderLocation.Y != 0))
{
IPath[] translatedClipPaths = new IPath[clipPaths.Count];

// Text glyph paths are queued in glyph-local coordinates and placed with RenderLocation,
// so canvas-space clip paths must be moved into that same local space before clipping.
for (int i = 0; i < clipPaths.Count; i++)
{
translatedClipPaths[i] = clipPaths[i].Translate(-operation.RenderLocation);
}

operationClipPaths = translatedClipPaths;
}

if (pen is null)
{
return new PathCompositionSceneCommand(
Expand All @@ -1351,7 +1366,7 @@ private CompositionSceneCommand CreateTextCompositionCommand(
in rasterizerOptions,
state.TargetBounds,
destinationOffset,
clipPaths,
operationClipPaths,
state.IsLayer));
}

Expand All @@ -1364,7 +1379,7 @@ private CompositionSceneCommand CreateTextCompositionCommand(
state.TargetBounds,
destinationOffset,
pen,
clipPaths,
operationClipPaths,
state.IsLayer));
}

Expand Down
105 changes: 105 additions & 0 deletions tests/ImageSharp.Drawing.Tests/Issues/Issue_397.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using SixLabors.Fonts;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;

namespace SixLabors.ImageSharp.Drawing.Tests.Issues;

public class Issue_397
{
[Theory]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Intersection)]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Union)]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Difference)]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Xor)]
public void DrawTextWithIntersectingClip<TPixel>(
TestImageProvider<TPixel> provider,
BooleanOperation operation)
where TPixel : unmanaged, IPixel<TPixel>
{
PointF textOrigin = new(54, 78);
PointF clipCenter = new(104, 70);
DrawingOptions clipOptions = CreateClipOptions(operation);
Font font = TestFontUtilities.GetFont("OpenSans-Regular.ttf", 18);

// Expected output:
// - Intersection shows only red text inside the moved star.
// - Difference shows only red text outside the moved star.
// - Union and Xor can show a red star because the boolean-combined path includes the clip path,
// and DrawText fills that combined result with the text brush.
provider.RunValidatingProcessorTest(
x => x.Paint(canvas => DrawIssue397Sample(canvas, clipOptions, clipCenter, textOrigin, font)),
testOutputDetails: $"{operation}_IntersectingClip",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}

[Theory]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Intersection)]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Union)]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Difference)]
[WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Xor)]
public void DrawTextWithNonIntersectingClip<TPixel>(
TestImageProvider<TPixel> provider,
BooleanOperation operation)
where TPixel : unmanaged, IPixel<TPixel>
{
PointF textOrigin = new(54, 78);
PointF clipCenter = new(192, 116);
DrawingOptions clipOptions = CreateClipOptions(operation);
Font font = TestFontUtilities.GetFont("OpenSans-Regular.ttf", 18);

// Expected output:
// - Intersection shows no red text because the moved star and text do not overlap.
// - Difference shows the full red text because the moved star removes nothing from it.
// - Union and Xor show both the full red text and a red star because disjoint Xor matches Union,
// and DrawText fills the boolean-combined result with the text brush.
provider.RunValidatingProcessorTest(
x => x.Paint(canvas => DrawIssue397Sample(canvas, clipOptions, clipCenter, textOrigin, font)),
testOutputDetails: $"{operation}_NonIntersectingClip",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}

private static void DrawIssue397Sample(
DrawingCanvas canvas,
DrawingOptions clipOptions,
PointF clipCenter,
PointF textOrigin,
Font font)
{
canvas.Clear(Brushes.Solid(Color.White));
StarPolygon clipPath = new(clipCenter, 7, 16, 38, 18);
RichTextOptions textOptions = new(font)
{
Origin = textOrigin
};

// The gray outline is the unclipped text guide; the red draw below shows the boolean clip result.
canvas.DrawText(textOptions, "This is a test", brush: null, Pens.Solid(Color.LightGray, 1F));

// The blue outline marks the moved clipping path without adding a filled shape behind the text.
canvas.Draw(Pens.Solid(Color.DarkBlue, 1F), clipPath);
canvas.Save(clipOptions, clipPath);

canvas.DrawText(
textOptions,
"This is a test",
Brushes.Solid(Color.Crimson),
pen: null);

canvas.Restore();
canvas.Draw(Pens.Solid(Color.DarkBlue, 1F), clipPath);
}

private static DrawingOptions CreateClipOptions(BooleanOperation operation)
=> new()
{
ShapeOptions = new()
{
BooleanOperation = operation
}
};
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading