Skip to content

Commit 9b0afcb

Browse files
Fix layerd rendering
1 parent 503b167 commit 9b0afcb

File tree

13 files changed

+778
-208
lines changed

13 files changed

+778
-208
lines changed

src/ImageSharp.Drawing/Processing/Extensions/ClearExtensions.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,22 @@ public static IImageProcessingContext Clear(this IImageProcessingContext source,
5454
internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions)
5555
{
5656
GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone();
57-
options.ColorBlendingMode = PixelFormats.PixelColorBlendingMode.Normal;
58-
options.AlphaCompositionMode = PixelFormats.PixelAlphaCompositionMode.Src;
57+
options.ColorBlendingMode = PixelColorBlendingMode.Normal;
58+
options.AlphaCompositionMode = PixelAlphaCompositionMode.Src;
5959
options.BlendPercentage = 1F;
6060

6161
return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform);
6262
}
63+
64+
internal static DrawingOptions CloneOrReturnForIntersectionRule(this DrawingOptions drawingOptions, IntersectionRule intersectionRule)
65+
{
66+
if (drawingOptions.ShapeOptions.IntersectionRule == intersectionRule)
67+
{
68+
return drawingOptions;
69+
}
70+
71+
ShapeOptions shapeOptions = drawingOptions.ShapeOptions.DeepClone();
72+
shapeOptions.IntersectionRule = intersectionRule;
73+
return new DrawingOptions(drawingOptions.GraphicsOptions.DeepClone(), shapeOptions, drawingOptions.Transform);
74+
}
6375
}

src/ImageSharp.Drawing/Processing/Extensions/DrawPathCollectionExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ public static IImageProcessingContext Draw(
3838
/// <param name="paths">The paths.</param>
3939
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
4040
public static IImageProcessingContext
41-
Draw(this IImageProcessingContext source, Pen pen, IPathCollection paths) =>
42-
source.Draw(source.GetDrawingOptions(), pen, paths);
41+
Draw(this IImageProcessingContext source, Pen pen, IPathCollection paths)
42+
=> source.Draw(source.GetDrawingOptions(), pen, paths);
4343

4444
/// <summary>
4545
/// Draws the outline of the polygon with the provided brush at the provided thickness.

src/ImageSharp.Drawing/Processing/Extensions/FillPathCollectionExtensions.cs

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using SixLabors.Fonts.Rendering;
5+
using SixLabors.ImageSharp.Drawing.Shapes.Text;
6+
47
namespace SixLabors.ImageSharp.Drawing.Processing;
58

69
/// <summary>
@@ -14,7 +17,7 @@ public static class FillPathCollectionExtensions
1417
/// <param name="source">The source image processing context.</param>
1518
/// <param name="options">The graphics options.</param>
1619
/// <param name="brush">The brush.</param>
17-
/// <param name="paths">The shapes.</param>
20+
/// <param name="paths">The collection of paths.</param>
1821
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
1922
public static IImageProcessingContext Fill(
2023
this IImageProcessingContext source,
@@ -30,12 +33,120 @@ public static IImageProcessingContext Fill(
3033
return source;
3134
}
3235

36+
/// <summary>
37+
/// Flood fills the image in the shape of the provided glyphs with the specified brush and pen.
38+
/// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer.
39+
/// </summary>
40+
/// <param name="source">The source image processing context.</param>
41+
/// <param name="options">The graphics options.</param>
42+
/// <param name="brush">The brush.</param>
43+
/// <param name="pen">The pen.</param>
44+
/// <param name="paths">The collection of glyph paths.</param>
45+
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
46+
public static IImageProcessingContext Fill(
47+
this IImageProcessingContext source,
48+
DrawingOptions options,
49+
Brush brush,
50+
Pen pen,
51+
IReadOnlyList<GlyphPathCollection> paths)
52+
=> source.Fill(options, brush, pen, paths, static (gp, layer) =>
53+
{
54+
if (layer.Kind == GlyphLayerKind.Decoration)
55+
{
56+
// Decorations (underlines, strikethroughs, etc) are always filled.
57+
return true;
58+
}
59+
60+
if (layer.Kind == GlyphLayerKind.Glyph)
61+
{
62+
// Standard glyph layers are filled by default.
63+
return true;
64+
}
65+
66+
// Default heuristic: stroke "background-like" layers (large coverage), fill others.
67+
// TODO: We should be using the area, not the bounds. Thin layers with large width/height
68+
// will be misclassified. e.g. shadows.
69+
RectangleF glyphBounds = gp.Bounds;
70+
RectangleF layerBounds = layer.Bounds;
71+
72+
if (glyphBounds.Width <= 0 || glyphBounds.Height <= 0)
73+
{
74+
return true; // degenerate glyph, just fill
75+
}
76+
77+
// Use each dimension independently to avoid misclassifying thin layers.
78+
float rx = layerBounds.Width / glyphBounds.Width;
79+
float ry = layerBounds.Height / glyphBounds.Height;
80+
81+
// ≥50% coverage, stroke (don't fill). Otherwise, fill.
82+
return rx < 0.5F || ry < 0.5F;
83+
});
84+
85+
/// <summary>
86+
/// Flood fills the image in the shape of the provided glyphs with the specified brush and pen.
87+
/// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer.
88+
/// </summary>
89+
/// <param name="source">The source image processing context.</param>
90+
/// <param name="options">The graphics options.</param>
91+
/// <param name="brush">The brush.</param>
92+
/// <param name="pen">The pen.</param>
93+
/// <param name="paths">The collection of glyph paths.</param>
94+
/// <param name="shouldFillLayer">A function that decides whether to fill or stroke a given layer.</param>
95+
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
96+
public static IImageProcessingContext Fill(
97+
this IImageProcessingContext source,
98+
DrawingOptions options,
99+
Brush brush,
100+
Pen pen,
101+
IReadOnlyList<GlyphPathCollection> paths,
102+
Func<GlyphPathCollection, GlyphLayerInfo, bool> shouldFillLayer)
103+
{
104+
foreach (GlyphPathCollection gp in paths)
105+
{
106+
if (gp.LayerCount == 0)
107+
{
108+
continue;
109+
}
110+
111+
if (gp.LayerCount == 1)
112+
{
113+
// Single-layer glyph: just fill with the supplied brush.
114+
source.Fill(options, brush, gp.Paths);
115+
continue;
116+
}
117+
118+
// Multi-layer: decide per layer whether to fill or stroke.
119+
for (int i = 0; i < gp.Layers.Count; i++)
120+
{
121+
GlyphLayerInfo layer = gp.Layers[i];
122+
IPath path = gp.PathList[i];
123+
124+
if (shouldFillLayer(gp, layer))
125+
{
126+
IntersectionRule fillRule = layer.FillRule == FillRule.EvenOdd
127+
? IntersectionRule.EvenOdd
128+
: IntersectionRule.NonZero;
129+
130+
// Respect the layer's fill rule if different to the drawing options.
131+
source.Fill(options.CloneOrReturnForIntersectionRule(fillRule), brush, path);
132+
}
133+
else
134+
{
135+
// Outline only to preserve interior detail.
136+
source.Draw(options, pen, path);
137+
}
138+
}
139+
}
140+
141+
return source;
142+
}
143+
33144
/// <summary>
34145
/// Flood fills the image in the shape of the provided polygon with the specified brush.
35146
/// </summary>
36147
/// <param name="source">The source image processing context.</param>
37148
/// <param name="brush">The brush.</param>
38-
/// <param name="paths">The paths.</param>
149+
/// <param name="paths">The collection of paths.</param>
39150
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
40151
public static IImageProcessingContext Fill(
41152
this IImageProcessingContext source,
@@ -44,7 +155,23 @@ public static IImageProcessingContext Fill(
44155
source.Fill(source.GetDrawingOptions(), brush, paths);
45156

46157
/// <summary>
47-
/// Flood fills the image in the shape of the provided polygon with the specified brush.
158+
/// Flood fills the image in the shape of the provided glyphs with the specified brush and pen.
159+
/// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer.
160+
/// </summary>
161+
/// <param name="source">The source image processing context.</param>
162+
/// <param name="brush">The brush.</param>
163+
/// <param name="pen">The pen.</param>
164+
/// <param name="paths">The collection of glyph paths.</param>
165+
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
166+
public static IImageProcessingContext Fill(
167+
this IImageProcessingContext source,
168+
Brush brush,
169+
Pen pen,
170+
IReadOnlyList<GlyphPathCollection> paths) =>
171+
source.Fill(source.GetDrawingOptions(), brush, pen, paths);
172+
173+
/// <summary>
174+
/// Flood fills the image in the shape of the provided polygon with the specified color.
48175
/// </summary>
49176
/// <param name="source">The source image processing context.</param>
50177
/// <param name="options">The options.</param>
@@ -59,15 +186,29 @@ public static IImageProcessingContext Fill(
59186
source.Fill(options, new SolidBrush(color), paths);
60187

61188
/// <summary>
62-
/// Flood fills the image in the shape of the provided polygon with the specified brush.
189+
/// Flood fills the image in the shape of the provided polygon with the specified color.
63190
/// </summary>
64191
/// <param name="source">The source image processing context.</param>
65192
/// <param name="color">The color.</param>
66-
/// <param name="paths">The paths.</param>
193+
/// <param name="paths">The collection of paths.</param>
67194
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
68195
public static IImageProcessingContext Fill(
69196
this IImageProcessingContext source,
70197
Color color,
71198
IPathCollection paths) =>
72199
source.Fill(new SolidBrush(color), paths);
200+
201+
/// <summary>
202+
/// Flood fills the image in the shape of the provided glyphs with the specified color.
203+
/// For multi-layer glyphs, a heuristic is used to decide whether to fill or stroke each layer.
204+
/// </summary>
205+
/// <param name="source">The source image processing context.</param>
206+
/// <param name="color">The color.</param>
207+
/// <param name="paths">The collection of glyph paths.</param>
208+
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
209+
public static IImageProcessingContext Fill(
210+
this IImageProcessingContext source,
211+
Color color,
212+
IReadOnlyList<GlyphPathCollection> paths) =>
213+
source.Fill(new SolidBrush(color), new SolidPen(color), paths);
73214
}

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

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using System.Numerics;
56
using SixLabors.Fonts;
67
using SixLabors.Fonts.Rendering;
78

@@ -16,9 +17,10 @@ internal sealed partial class RichTextGlyphRenderer
1617
/// Attempts to create an ImageSharp.Drawing <see cref="Brush"/> from a <see cref="Paint"/>.
1718
/// </summary>
1819
/// <param name="paint">The paint definition coming from the interpreter.</param>
20+
/// <param name="transform">A transform to apply to the brush coordinates.</param>
1921
/// <param name="brush">The resulting brush, or <see langword="null"/> if the paint is unsupported.</param>
2022
/// <returns><see langword="true"/> if a brush could be created; otherwise, <see langword="false"/>.</returns>
21-
public static bool TryCreateBrush(Paint? paint, [NotNullWhen(true)] out Brush? brush)
23+
public static bool TryCreateBrush(Paint? paint, Matrix3x2 transform, [NotNullWhen(true)] out Brush? brush)
2224
{
2325
brush = null;
2426

@@ -27,19 +29,18 @@ public static bool TryCreateBrush(Paint? paint, [NotNullWhen(true)] out Brush? b
2729
return false;
2830
}
2931

30-
// TODO: Do we need to apply the transform assigned to th underlying builder here?
3132
switch (paint)
3233
{
3334
case SolidPaint sp:
3435
brush = new SolidBrush(ToColor(sp.Color, sp.Opacity));
3536
return true;
3637

3738
case LinearGradientPaint lg:
38-
return TryCreateLinearGradientBrush(lg, out brush);
39+
return TryCreateLinearGradientBrush(lg, transform, out brush);
3940
case RadialGradientPaint rg:
40-
return TryCreateRadialGradientBrush(rg, out brush);
41+
return TryCreateRadialGradientBrush(rg, transform, out brush);
4142
case SweepGradientPaint sg:
42-
return TryCreateSweepGradientBrush(sg, out brush);
43+
return TryCreateSweepGradientBrush(sg, transform, out brush);
4344
default:
4445
return false;
4546
}
@@ -48,27 +49,26 @@ public static bool TryCreateBrush(Paint? paint, [NotNullWhen(true)] out Brush? b
4849
/// <summary>
4950
/// Creates a <see cref="LinearGradientBrush"/> from a <see cref="LinearGradientPaint"/>.
5051
/// </summary>
51-
/// <param name="lg">The linear gradient paint.</param>
52+
/// <param name="paint">The linear gradient paint.</param>
53+
/// <param name="transform">The transform to apply to the gradient points.</param>
5254
/// <param name="brush">The resulting brush.</param>
5355
/// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns>
54-
private static bool TryCreateLinearGradientBrush(LinearGradientPaint lg, out Brush? brush)
56+
private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix3x2 transform, out Brush? brush)
5557
{
56-
// Map gradient stops (apply paint opacity multiplier to each stops alpha).
57-
ColorStop[] stops = ToColorStops(lg.Stops, lg.Opacity);
58+
// Map gradient stops (apply paint opacity multiplier to each stop's alpha).
59+
ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity);
5860

5961
// Map spread method.
60-
GradientRepetitionMode mode = MapSpread(lg.Spread);
62+
GradientRepetitionMode mode = MapSpread(paint.Spread);
6163

62-
PointF p0 = lg.P0;
63-
PointF p1 = lg.P1;
64+
PointF p0 = paint.P0;
65+
PointF p1 = paint.P1;
6466

65-
// Degenerate gradient, fall back to solid using last stop.
66-
if (ApproximatelyEqual(p0, p1))
67+
// Apply any transform defined on the paint.
68+
if (!transform.IsIdentity)
6769
{
68-
// TODO: Consider using this.currentColor instead?
69-
Color fallback = stops.Length > 0 ? stops[^1].Color : Color.Black;
70-
brush = new SolidBrush(fallback);
71-
return true;
70+
p0 = Vector2.Transform(p0, transform);
71+
p1 = Vector2.Transform(p1, transform);
7272
}
7373

7474
brush = new LinearGradientBrush(p0, p1, mode, stops);
@@ -78,36 +78,51 @@ private static bool TryCreateLinearGradientBrush(LinearGradientPaint lg, out Bru
7878
/// <summary>
7979
/// Creates a <see cref="RadialGradientBrush"/> from a <see cref="RadialGradientPaint"/>.
8080
/// </summary>
81-
/// <param name="rg">The radial gradient paint.</param>
81+
/// <param name="paint">The radial gradient paint.</param>
82+
/// <param name="transform">The transform to apply to the gradient center point.</param>
8283
/// <param name="brush">The resulting brush.</param>
8384
/// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns>
84-
private static bool TryCreateRadialGradientBrush(RadialGradientPaint rg, out Brush? brush)
85+
private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix3x2 transform, out Brush? brush)
8586
{
86-
// Map gradient stops (apply paint opacity multiplier to each stops alpha).
87-
ColorStop[] stops = ToColorStops(rg.Stops, rg.Opacity);
87+
// Map gradient stops (apply paint opacity multiplier to each stop's alpha).
88+
ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity);
8889

8990
// Map spread method.
90-
GradientRepetitionMode mode = MapSpread(rg.Spread);
91+
GradientRepetitionMode mode = MapSpread(paint.Spread);
9192

92-
brush = new RadialGradientBrush(rg.Center, rg.Radius, mode, stops);
93+
// Apply any transform defined on the paint.
94+
PointF center = paint.Center;
95+
if (!transform.IsIdentity)
96+
{
97+
center = Vector2.Transform(center, transform);
98+
}
99+
100+
brush = new RadialGradientBrush(center, paint.Radius, mode, stops);
93101
return true;
94102
}
95103

96104
/// <summary>
97105
/// Creates a <see cref="SweepGradientBrush"/> from a <see cref="SweepGradientPaint"/>.
98106
/// </summary>
99-
/// <param name="sg">The sweep gradient paint.</param>
107+
/// <param name="paint">The sweep gradient paint.</param>
100108
/// <param name="brush">The resulting brush.</param>
101109
/// <returns><see langword="true"/> if created; otherwise, <see langword="false"/>.</returns>
102-
private static bool TryCreateSweepGradientBrush(SweepGradientPaint sg, out Brush? brush)
110+
private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix3x2 transform, out Brush? brush)
103111
{
104-
// Map gradient stops (apply paint opacity multiplier to each stops alpha).
105-
ColorStop[] stops = ToColorStops(sg.Stops, sg.Opacity);
112+
// Map gradient stops (apply paint opacity multiplier to each stop's alpha).
113+
ColorStop[] stops = ToColorStops(paint.Stops, paint.Opacity);
106114

107115
// Map spread method.
108-
GradientRepetitionMode mode = MapSpread(sg.Spread);
116+
GradientRepetitionMode mode = MapSpread(paint.Spread);
109117

110-
brush = new SweepGradientBrush(sg.Center, sg.StartAngle, sg.EndAngle, mode, stops);
118+
// Apply any transform defined on the paint.
119+
PointF center = paint.Center;
120+
if (!transform.IsIdentity)
121+
{
122+
center = Vector2.Transform(center, transform);
123+
}
124+
125+
brush = new SweepGradientBrush(center, paint.StartAngle, paint.EndAngle, mode, stops);
111126
return true;
112127
}
113128

@@ -163,14 +178,4 @@ private static Color ToColor(in GlyphColor c, float opacity)
163178
byte aa = (byte)MathF.Round(a * 255f);
164179
return Color.FromPixel(new Rgba32(c.Red, c.Green, c.Blue, aa));
165180
}
166-
167-
/// <summary>
168-
/// Compares two points for near-equality.
169-
/// </summary>
170-
/// <param name="a">The first point.</param>
171-
/// <param name="b">The second point.</param>
172-
/// <param name="eps">Tolerance.</param>
173-
/// <returns><see langword="true"/> if near-equal; otherwise <see langword="false"/>.</returns>
174-
private static bool ApproximatelyEqual(in PointF a, in PointF b, float eps = 1e-4f)
175-
=> MathF.Abs(a.X - b.X) <= eps && MathF.Abs(a.Y - b.Y) <= eps;
176181
}

0 commit comments

Comments
 (0)