Skip to content

Commit e8f39c7

Browse files
Use the base path for rendering decorations.
1 parent 9b0afcb commit e8f39c7

File tree

2 files changed

+92
-250
lines changed

2 files changed

+92
-250
lines changed

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

Lines changed: 51 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa
3131
private TextRun? currentTextRun;
3232
private Brush? currentBrush;
3333
private Pen? currentPen;
34-
private TextDecorationDetails? currentUnderline;
35-
private TextDecorationDetails? currentStrikeout;
36-
private TextDecorationDetails? currentOverline;
3734
private bool currentDecorationIsVertical;
3835
private bool hasLayer;
3936

@@ -166,20 +163,9 @@ protected override void EndLayer()
166163
this.currentPen = this.defaultPen;
167164
}
168165

169-
bool renderFill = false;
170-
bool renderOutline = false;
171-
172-
// If we are using the fonts color layers we ignore the request to draw an outline only
173-
// because that won't really work. Instead we force drawing using fill with the requested color.
174-
if (this.currentBrush != null)
175-
{
176-
renderFill = true;
177-
}
178-
179-
if (this.currentPen != null)
180-
{
181-
// renderOutline = true;
182-
}
166+
// When rendering layers we only fill them.
167+
// Any drawing of outlines is ignored as that doesn't really make sense.
168+
bool renderFill = this.currentBrush != null;
183169

184170
// Path has already been added to the collection via the base class.
185171
IPath path = this.CurrentPaths.Last();
@@ -200,12 +186,6 @@ protected override void EndLayer()
200186
// We can use this to offset the render location on the next instance of this glyph.
201187
renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation);
202188

203-
if (renderOutline)
204-
{
205-
path = this.currentPen!.GeneratePath(path);
206-
renderData.OutlineMap = this.Render(path);
207-
}
208-
209189
if (!this.noCache)
210190
{
211191
this.UpdateCache(renderData);
@@ -254,18 +234,6 @@ protected override void EndLayer()
254234
RenderPass = RenderOrderFill
255235
});
256236
}
257-
258-
if (renderData.OutlineMap != null)
259-
{
260-
int offset = (int)((this.currentPen?.StrokeWidth ?? 0) / 2);
261-
this.DrawingOperations.Add(new DrawingOperation
262-
{
263-
RenderLocation = renderLocation - new Size(offset, offset),
264-
Map = renderData.OutlineMap,
265-
Brush = this.currentPen?.StrokeFill ?? this.currentBrush!,
266-
RenderPass = RenderOrderOutline
267-
});
268-
}
269237
}
270238

271239
public override TextDecorations EnabledDecorations()
@@ -301,24 +269,6 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star
301269
return;
302270
}
303271

304-
ref TextDecorationDetails? targetDecoration = ref this.currentStrikeout;
305-
if (textDecorations == TextDecorations.Strikeout)
306-
{
307-
targetDecoration = ref this.currentStrikeout;
308-
}
309-
else if (textDecorations == TextDecorations.Underline)
310-
{
311-
targetDecoration = ref this.currentUnderline;
312-
}
313-
else if (textDecorations == TextDecorations.Overline)
314-
{
315-
targetDecoration = ref this.currentOverline;
316-
}
317-
else
318-
{
319-
return;
320-
}
321-
322272
Pen? pen = null;
323273
if (this.currentTextRun is RichTextRun drawingRun)
324274
{
@@ -339,40 +289,66 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star
339289
}
340290

341291
// Always respect the pen stroke width if explicitly set.
292+
float originalThickness = thickness;
342293
if (pen is not null)
343294
{
344-
thickness = pen.StrokeWidth;
295+
// Clamp the thickness to whole pixels.
296+
thickness = MathF.Max(1F, (float)Math.Round(pen.StrokeWidth));
345297
}
346298
else
347299
{
348-
// Clamp the thickness to whole pixels.
349300
// Brush cannot be null if pen is null.
350-
thickness = MathF.Max(1F, MathF.Round(thickness));
301+
// The thickness of the line has already been clamped in the base class.
351302
pen = new SolidPen((this.currentBrush ?? this.defaultBrush)!, thickness);
352303
}
353304

354-
// Drawing is always centered around the point so we need to offset by half.
355-
Vector2 offset = Vector2.Zero;
356-
bool rotated = this.currentDecorationIsVertical;
357-
if (textDecorations == TextDecorations.Overline)
358-
{
359-
// CSS overline is drawn above the position, so we need to move it up.
360-
offset = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, -(thickness * .5F));
361-
}
362-
else if (textDecorations == TextDecorations.Underline)
305+
// Path has already been added to the collection via the base class.
306+
IPath path = this.CurrentPaths.Last();
307+
IPath outline = path;
308+
309+
if (originalThickness != thickness)
363310
{
364-
// CSS underline is drawn below the position, so we need to move it down.
365-
offset = rotated ? new Vector2(-(thickness * .5F), 0) : new Vector2(0, thickness * .5F);
311+
// Respect edge anchoring per decoration type:
312+
// - Overline: keep the base edge fixed (bottom in horizontal; left in vertical)
313+
// - Underline: keep the top edge fixed (top in horizontal; right in vertical)
314+
// - Strikeout: keep the center fixed (default behavior)
315+
float ratio = thickness / originalThickness;
316+
if (ratio != 1f)
317+
{
318+
Vector2 scale = this.currentDecorationIsVertical
319+
? new Vector2(ratio, 1f)
320+
: new Vector2(1f, ratio);
321+
322+
RectangleF b = path.Bounds;
323+
Vector2 center = new(b.Left + (b.Width * 0.5f), b.Top + (b.Height * 0.5f));
324+
Vector2 anchor = center;
325+
326+
if (textDecorations == TextDecorations.Overline)
327+
{
328+
anchor = this.currentDecorationIsVertical
329+
? new Vector2(b.Left, center.Y) // vertical: anchor left edge
330+
: new Vector2(center.X, b.Bottom); // horizontal: anchor bottom edge
331+
}
332+
else if (textDecorations == TextDecorations.Underline)
333+
{
334+
anchor = this.currentDecorationIsVertical
335+
? new Vector2(b.Right, center.Y) // vertical: anchor right edge
336+
: new Vector2(center.X, b.Top); // horizontal: anchor top edge
337+
}
338+
339+
// Scale about the chosen anchor so the fixed edge stays in place.
340+
outline = path.Transform(Matrix3x2.CreateScale(scale, anchor));
341+
}
366342
}
367343

368-
// We clamp the start and end points to the pixel grid to avoid anti-aliasing.
369-
this.AppendDecoration(
370-
ref targetDecoration,
371-
ClampToPixel(start + offset, (int)thickness, rotated),
372-
ClampToPixel(end + offset, (int)thickness, rotated),
373-
pen,
374-
thickness,
375-
rotated);
344+
// Render the path here. Decorations are un-cached.
345+
this.DrawingOperations.Add(new DrawingOperation
346+
{
347+
Brush = pen.StrokeFill,
348+
RenderLocation = ClampToPixel(outline.Bounds.Location),
349+
Map = this.Render(outline),
350+
RenderPass = RenderOrderDecoration
351+
});
376352
}
377353

378354
protected override void EndGlyph()
@@ -506,122 +482,11 @@ private void UpdateCache(GlyphRenderData renderData)
506482
this.glyphCache[this.currentCacheKey].Add(renderData);
507483
}
508484

509-
protected override void EndText()
510-
{
511-
// Ensure we have captured the last overline/underline/strikeout path
512-
this.FinalizeDecoration(ref this.currentOverline);
513-
this.FinalizeDecoration(ref this.currentUnderline);
514-
this.FinalizeDecoration(ref this.currentStrikeout);
515-
}
516-
517485
public void Dispose() => this.Dispose(true);
518486

519487
[MethodImpl(MethodImplOptions.AggressiveInlining)]
520488
private static Point ClampToPixel(PointF point) => Point.Truncate(point);
521489

522-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
523-
private static PointF ClampToPixel(PointF point, int thickness, bool rotated)
524-
{
525-
// Even. Clamp to whole pixels.
526-
if ((thickness & 1) == 0)
527-
{
528-
return Point.Truncate(point);
529-
}
530-
531-
// Odd. Clamp to half pixels.
532-
if (rotated)
533-
{
534-
return Point.Truncate(point) + new Vector2(.5F, 0);
535-
}
536-
537-
return Point.Truncate(point) + new Vector2(0, .5F);
538-
}
539-
540-
private void FinalizeDecoration(ref TextDecorationDetails? decoration)
541-
{
542-
// TODO: Shouldn't we be using the path from the builder here?
543-
if (decoration != null)
544-
{
545-
// TODO: If the path is curved a line segment does not work well.
546-
// What would be great would be if we could take a slice of a path given start and end positions.
547-
IPath path = new Path(new LinearLineSegment(decoration.Value.Start, decoration.Value.End));
548-
IPath outline = decoration.Value.Pen.GeneratePath(path, decoration.Value.Thickness);
549-
550-
// Calculate the transform for this path.
551-
// We cannot use the path builder transform as this path is rendered independently.
552-
FontRectangle rectangle = new(outline.Bounds.Location, new Vector2(outline.Bounds.Width, outline.Bounds.Height));
553-
Matrix3x2 pathTransform = this.ComputeTransform(in rectangle);
554-
Matrix3x2 defaultTransform = this.drawingOptions.Transform;
555-
outline = outline.Transform(pathTransform * defaultTransform);
556-
557-
if (outline.Bounds.Width != 0 && outline.Bounds.Height != 0)
558-
{
559-
// Render the path here. Decorations are un-cached.
560-
this.DrawingOperations.Add(new DrawingOperation
561-
{
562-
Brush = decoration.Value.Pen.StrokeFill,
563-
RenderLocation = ClampToPixel(outline.Bounds.Location),
564-
Map = this.Render(outline),
565-
RenderPass = RenderOrderDecoration
566-
});
567-
}
568-
569-
decoration = null;
570-
}
571-
}
572-
573-
private void AppendDecoration(
574-
ref TextDecorationDetails? decoration,
575-
Vector2 start,
576-
Vector2 end,
577-
Pen pen,
578-
float thickness,
579-
bool rotated)
580-
{
581-
if (decoration != null)
582-
{
583-
// TODO: This only works well if we are not trying to follow a path.
584-
if (this.path is null)
585-
{
586-
// Let's try and expand it first.
587-
if (rotated)
588-
{
589-
if (thickness == decoration.Value.Thickness
590-
&& decoration.Value.End.Y + 1 >= start.Y
591-
&& decoration.Value.End.X == start.X
592-
&& decoration.Value.Pen.Equals(pen))
593-
{
594-
// Expand the line
595-
start = decoration.Value.Start;
596-
597-
// If this is null finalize does nothing.
598-
decoration = null;
599-
}
600-
}
601-
else if (thickness == decoration.Value.Thickness
602-
&& decoration.Value.End.Y == start.Y
603-
&& decoration.Value.End.X + 1 >= start.X
604-
&& decoration.Value.Pen.Equals(pen))
605-
{
606-
// Expand the line
607-
start = decoration.Value.Start;
608-
609-
// If this is null finalize does nothing.
610-
decoration = null;
611-
}
612-
}
613-
}
614-
615-
this.FinalizeDecoration(ref decoration);
616-
decoration = new TextDecorationDetails
617-
{
618-
Start = start,
619-
End = end,
620-
Pen = pen,
621-
Thickness = thickness
622-
};
623-
}
624-
625490
[MethodImpl(MethodImplOptions.AggressiveInlining)]
626491
private void TransformGlyph(in FontRectangle bounds)
627492
=> this.Builder.SetTransform(this.ComputeTransform(in bounds));

0 commit comments

Comments
 (0)