Skip to content

Commit fad1078

Browse files
Pass composition options to Fill
1 parent 4067cec commit fad1078

File tree

8 files changed

+235
-59
lines changed

8 files changed

+235
-59
lines changed

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,4 @@ internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawin
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-
}
7563
}

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

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

4-
using SixLabors.Fonts.Rendering;
54
using SixLabors.ImageSharp.Drawing.Shapes.Text;
65

76
namespace SixLabors.ImageSharp.Drawing.Processing;
@@ -49,7 +48,7 @@ public static IImageProcessingContext Fill(
4948
Brush brush,
5049
Pen pen,
5150
IReadOnlyList<GlyphPathCollection> paths)
52-
=> source.Fill(options, brush, pen, paths, static (gp, layer) =>
51+
=> source.Fill(options, brush, pen, paths, static (gp, layer, path) =>
5352
{
5453
if (layer.Kind == GlyphLayerKind.Decoration)
5554
{
@@ -64,22 +63,19 @@ public static IImageProcessingContext Fill(
6463
}
6564

6665
// 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;
66+
// Use the bounding box area as an approximation of the glyph area as it is cheaper to compute.
67+
float glyphArea = gp.Bounds.Width * gp.Bounds.Height;
68+
float layerArea = path.ComputeArea();
7169

72-
if (glyphBounds.Width <= 0 || glyphBounds.Height <= 0)
70+
if (layerArea <= 0 || glyphArea <= 0)
7371
{
74-
return true; // degenerate glyph, just fill
72+
return false; // degenerate glyph, don't fill
7573
}
7674

77-
// Use each dimension independently to avoid misclassifying thin layers.
78-
float rx = layerBounds.Width / glyphBounds.Width;
79-
float ry = layerBounds.Height / glyphBounds.Height;
75+
float coverage = layerArea / glyphArea;
8076

81-
// 50% coverage, stroke (don't fill). Otherwise, fill.
82-
return rx < 0.5F || ry < 0.5F;
77+
// <50% coverage, fill. Otherwise, stroke.
78+
return coverage < 0.50F;
8379
});
8480

8581
/// <summary>
@@ -91,15 +87,17 @@ public static IImageProcessingContext Fill(
9187
/// <param name="brush">The brush.</param>
9288
/// <param name="pen">The pen.</param>
9389
/// <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>
90+
/// <param name="shouldFillLayer">
91+
/// A function that decides whether to fill or stroke a given layer within a multi-layer (painted) glyph.
92+
/// </param>
9593
/// <returns>The <see cref="IImageProcessingContext"/> to allow chaining of operations.</returns>
9694
public static IImageProcessingContext Fill(
9795
this IImageProcessingContext source,
9896
DrawingOptions options,
9997
Brush brush,
10098
Pen pen,
10199
IReadOnlyList<GlyphPathCollection> paths,
102-
Func<GlyphPathCollection, GlyphLayerInfo, bool> shouldFillLayer)
100+
Func<GlyphPathCollection, GlyphLayerInfo, IPath, bool> shouldFillLayer)
103101
{
104102
foreach (GlyphPathCollection gp in paths)
105103
{
@@ -121,14 +119,15 @@ public static IImageProcessingContext Fill(
121119
GlyphLayerInfo layer = gp.Layers[i];
122120
IPath path = gp.PathList[i];
123121

124-
if (shouldFillLayer(gp, layer))
122+
if (shouldFillLayer(gp, layer, path))
125123
{
126-
IntersectionRule fillRule = layer.FillRule == FillRule.EvenOdd
127-
? IntersectionRule.EvenOdd
128-
: IntersectionRule.NonZero;
129-
130124
// Respect the layer's fill rule if different to the drawing options.
131-
source.Fill(options.CloneOrReturnForIntersectionRule(fillRule), brush, path);
125+
DrawingOptions o = options.CloneOrReturnForRules(
126+
layer.IntersectionRule,
127+
layer.PixelAlphaCompositionMode,
128+
layer.PixelColorBlendingMode);
129+
130+
source.Fill(o, brush, path);
132131
}
133132
else
134133
{

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,8 @@ protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
144144
this.rasterizationRequired = true;
145145
}
146146

147-
protected override void BeginLayer(Paint? paint, FillRule fillRule, in FontRectangle? clipBounds)
147+
protected override void BeginLayer(Paint? paint, FillRule fillRule, in ClipQuad? clipBounds)
148148
{
149-
// TODO: We may have do some sort of translation based on the delta
150-
// between the bounds and clipbounds.
151149
this.hasLayer = true;
152150
if (TryCreateBrush(paint, this.Builder.Transform, out Brush? brush))
153151
{
@@ -657,7 +655,6 @@ public static CacheKey FromParameters(in GlyphRendererParameters parameters, Rec
657655
{
658656
// Our caching does not need the grapheme index as that is only relevant to the text layout.
659657
Font = parameters.Font,
660-
GlyphColor = parameters.GlyphColor,
661658
GlyphType = parameters.GlyphType,
662659
FontStyle = parameters.FontStyle,
663660
GlyphId = parameters.GlyphId,

src/ImageSharp.Drawing/Shapes/PathExtensions.cs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,70 @@ public static float ComputeLength(this IPath path)
149149

150150
if (s.IsClosed)
151151
{
152-
dist += Vector2.Distance(points[0], points[points.Length - 1]);
152+
dist += Vector2.Distance(points[0], points[^1]);
153153
}
154154
}
155155

156156
return dist;
157157
}
158+
159+
/// <summary>
160+
/// Calculates the total area of all paths in the specified collection.
161+
/// </summary>
162+
/// <param name="paths">A collection of paths for which to compute the combined area. Cannot be null.</param>
163+
/// <returns>
164+
/// The total area, in square units, enclosed by all paths in the collection.
165+
/// </returns>
166+
public static float ComputeArea(this IPathCollection paths)
167+
{
168+
float area = 0;
169+
foreach (IPath path in paths)
170+
{
171+
area += path.ComputeArea();
172+
}
173+
174+
return area;
175+
}
176+
177+
/// <summary>
178+
/// Calculates the total area enclosed by the specified path.
179+
/// </summary>
180+
/// <remarks>
181+
/// This method sums the areas of all subpaths within the path. Subpaths with fewer than three
182+
/// points are ignored, as they do not form a closed region. The result is always non-negative, regardless of the
183+
/// winding direction of the subpaths.
184+
/// </remarks>
185+
/// <param name="path">
186+
/// The path for which to compute the enclosed area. Must contain at least one subpath with three or more points to
187+
/// contribute to the area calculation.
188+
/// </param>
189+
/// <returns>
190+
/// The total area, in square units, enclosed by all subpaths of the path. Returns 0 if the path does not contain
191+
/// any subpaths with at least three points.
192+
/// </returns>
193+
public static float ComputeArea(this IPath path)
194+
{
195+
float area = 0;
196+
foreach (ISimplePath s in path.Flatten())
197+
{
198+
ReadOnlySpan<PointF> points = s.Points.Span;
199+
if (points.Length < 3)
200+
{
201+
// Not enough points to form an area
202+
continue;
203+
}
204+
205+
float subArea = 0;
206+
for (int i = 0; i < points.Length; i++)
207+
{
208+
PointF p1 = points[i];
209+
PointF p2 = points[(i + 1) % points.Length];
210+
subArea += (p1.X * p2.Y) - (p2.X * p1.Y);
211+
}
212+
213+
area += MathF.Abs(subArea) * .5F;
214+
}
215+
216+
return area;
217+
}
158218
}

src/ImageSharp.Drawing/Shapes/Text/BaseGlyphBuilder.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ internal class BaseGlyphBuilder : IGlyphRenderer
3636
private int layerStartIndex;
3737
private Paint? activeLayerPaint;
3838
private FillRule activeLayerFillRule;
39-
private FontRectangle? activeClipBounds;
39+
private ClipQuad? activeClipBounds;
4040

4141
public BaseGlyphBuilder() => this.Builder = new PathBuilder();
4242

@@ -188,7 +188,7 @@ void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point)
188188
}
189189

190190
/// <inheritdoc/>
191-
void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, in FontRectangle? clipBounds)
191+
void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, in ClipQuad? clipBounds)
192192
{
193193
this.usedLayers = true;
194194
this.inLayer = true;
@@ -211,7 +211,11 @@ void IGlyphRenderer.EndLayer()
211211

212212
IPath path = this.Builder.Build();
213213

214-
// TODO: We need to clip the path by activeClipBounds if set.
214+
if (this.activeClipBounds is not null)
215+
{
216+
// TODO:Clipping not yet implemented.
217+
}
218+
215219
this.CurrentPaths.Add(path);
216220

217221
if (this.graphemeBuilder is not null)
@@ -392,8 +396,8 @@ protected virtual void EndText()
392396
{
393397
}
394398

395-
/// <inheritdoc cref="IGlyphRenderer.BeginLayer(Paint?, FillRule, in FontRectangle?)"/>
396-
protected virtual void BeginLayer(Paint? paint, FillRule fillRule, in FontRectangle? clipBounds)
399+
/// <inheritdoc cref="IGlyphRenderer.BeginLayer(Paint?, FillRule, in ClipQuad?)"/>
400+
protected virtual void BeginLayer(Paint? paint, FillRule fillRule, in ClipQuad? clipBounds)
397401
{
398402
}
399403

src/ImageSharp.Drawing/Shapes/Text/GlyphLayerInfo.cs

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

4+
using System.Numerics;
45
using SixLabors.Fonts.Rendering;
56

67
namespace SixLabors.ImageSharp.Drawing.Shapes.Text;
@@ -19,12 +20,42 @@ public readonly struct GlyphLayerInfo
1920
/// <param name="fillRule">The fill rule to use for this layer.</param>
2021
/// <param name="bounds">Axis-aligned bounds of the layer geometry.</param>
2122
/// <param name="kind">An optional semantic hint for the layer type.</param>
22-
public GlyphLayerInfo(int startIndex, int count, Paint? paint, FillRule fillRule, RectangleF bounds, GlyphLayerKind kind = GlyphLayerKind.Glyph)
23+
internal GlyphLayerInfo(
24+
int startIndex,
25+
int count,
26+
Paint? paint,
27+
FillRule fillRule,
28+
RectangleF bounds,
29+
GlyphLayerKind kind)
2330
{
2431
this.StartIndex = startIndex;
2532
this.Count = count;
2633
this.Paint = paint;
27-
this.FillRule = fillRule;
34+
this.IntersectionRule = TextUtilities.MapFillRule(fillRule);
35+
36+
CompositeMode compositeMode = paint?.CompositeMode ?? CompositeMode.SrcOver;
37+
this.PixelAlphaCompositionMode = TextUtilities.MapCompositionMode(compositeMode);
38+
this.PixelColorBlendingMode = TextUtilities.MapBlendingMode(compositeMode);
39+
this.Bounds = bounds;
40+
this.Kind = kind;
41+
}
42+
43+
private GlyphLayerInfo(
44+
int startIndex,
45+
int count,
46+
Paint? paint,
47+
IntersectionRule intersectionRule,
48+
PixelAlphaCompositionMode compositionMode,
49+
PixelColorBlendingMode colorBlendingMode,
50+
RectangleF bounds,
51+
GlyphLayerKind kind)
52+
{
53+
this.StartIndex = startIndex;
54+
this.Count = count;
55+
this.Paint = paint;
56+
this.IntersectionRule = intersectionRule;
57+
this.PixelAlphaCompositionMode = compositionMode;
58+
this.PixelColorBlendingMode = colorBlendingMode;
2859
this.Bounds = bounds;
2960
this.Kind = kind;
3061
}
@@ -47,7 +78,17 @@ public GlyphLayerInfo(int startIndex, int count, Paint? paint, FillRule fillRule
4778
/// <summary>
4879
/// Gets the fill rule for rasterization of this layer.
4980
/// </summary>
50-
public FillRule FillRule { get; }
81+
public IntersectionRule IntersectionRule { get; }
82+
83+
/// <summary>
84+
/// Gets the pixel alpha composition mode to use for this layer.
85+
/// </summary>
86+
public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; }
87+
88+
/// <summary>
89+
/// Gets the pixel color blending mode to use for this layer.
90+
/// </summary>
91+
public PixelColorBlendingMode PixelColorBlendingMode { get; }
5192

5293
/// <summary>
5394
/// Gets the bounds of the layer geometry (device space).
@@ -58,4 +99,15 @@ public GlyphLayerInfo(int startIndex, int count, Paint? paint, FillRule fillRule
5899
/// Gets the semantic kind of the layer (for policy decisions).
59100
/// </summary>
60101
public GlyphLayerKind Kind { get; }
102+
103+
internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix3x2 matrix)
104+
=> new(
105+
info.StartIndex,
106+
info.Count,
107+
info.Paint,
108+
info.IntersectionRule,
109+
info.PixelAlphaCompositionMode,
110+
info.PixelColorBlendingMode,
111+
RectangleF.Transform(info.Bounds, matrix),
112+
info.Kind);
61113
}

src/ImageSharp.Drawing/Shapes/Text/GlyphPathCollection.cs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,30 +66,23 @@ internal GlyphPathCollection(List<IPath> paths, List<GlyphLayerInfo> layers)
6666
/// <summary>
6767
/// Transforms the glyph using the specified matrix.
6868
/// </summary>
69-
/// <param name="transform">The matrix.</param>
69+
/// <param name="matrix">The transform matrix.</param>
7070
/// <returns>
7171
/// A new <see cref="GlyphPathCollection"/> with the matrix applied to it.
7272
/// </returns>
73-
public GlyphPathCollection Translate(Matrix3x2 transform)
73+
public GlyphPathCollection Transform(Matrix3x2 matrix)
7474
{
7575
List<IPath> transformed = new(this.paths.Count);
7676

7777
for (int i = 0; i < this.paths.Count; i++)
7878
{
79-
transformed.Add(this.paths[i].Transform(transform));
79+
transformed.Add(this.paths[i].Transform(matrix));
8080
}
8181

8282
List<GlyphLayerInfo> transformedLayers = new(this.layers.Count);
8383
for (int i = 0; i < this.layers.Count; i++)
8484
{
85-
GlyphLayerInfo li = this.layers[i];
86-
RectangleF bounds = li.Bounds;
87-
if (bounds != RectangleF.Empty)
88-
{
89-
bounds = RectangleF.Transform(bounds, transform);
90-
}
91-
92-
transformedLayers.Add(new GlyphLayerInfo(li.StartIndex, li.Count, li.Paint, li.FillRule, bounds, li.Kind));
85+
transformedLayers.Add(GlyphLayerInfo.Transform(this.layers[i], matrix));
9386
}
9487

9588
return new GlyphPathCollection(transformed, transformedLayers);
@@ -101,7 +94,7 @@ public GlyphPathCollection Translate(Matrix3x2 transform)
10194
/// </summary>
10295
/// <param name="predicate">A filter deciding whether to keep a layer.</param>
10396
/// <returns>A new <see cref="PathCollection"/> with the selected paths.</returns>
104-
public PathCollection ToPathCollection(Func<GlyphLayerInfo, bool>? predicate)
97+
public PathCollection ToPathCollection(Func<GlyphLayerInfo, bool>? predicate = null)
10598
{
10699
List<IPath> kept = [];
107100
for (int i = 0; i < this.layers.Count; i++)
@@ -165,7 +158,13 @@ internal sealed class Builder
165158
/// <param name="fillRule">The fill rule for this layer.</param>
166159
/// <param name="bounds">Optional cached bounds for this layer.</param>
167160
/// <param name="kind">Optional semantic kind (eg. Decoration).</param>
168-
public void AddLayer(int startIndex, int count, Paint? paint, FillRule fillRule, RectangleF bounds, GlyphLayerKind kind = GlyphLayerKind.Glyph)
161+
public void AddLayer(
162+
int startIndex,
163+
int count,
164+
Paint? paint,
165+
FillRule fillRule,
166+
RectangleF bounds,
167+
GlyphLayerKind kind = GlyphLayerKind.Glyph)
169168
{
170169
if (startIndex < 0 || count < 0 || startIndex + count > this.paths.Count)
171170
{

0 commit comments

Comments
 (0)