first commit
This commit is contained in:
@@ -0,0 +1,584 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Rendering;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
/// <summary>
|
||||
/// Defines a base rendering surface that Fonts can use to generate shapes.
|
||||
/// </summary>
|
||||
internal class BaseGlyphBuilder : IGlyphRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// The last point emitted by <c>MoveTo</c> / <c>LineTo</c> / curve commands.
|
||||
/// Used as the implicit start of the next segment.
|
||||
/// </summary>
|
||||
private Vector2 currentPoint;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of the <see cref="GlyphRendererParameters"/> for the glyph currently
|
||||
/// being processed. Set at the start of each <c>BeginGlyph</c> call and read by
|
||||
/// <c>SetDecoration</c> to determine layout orientation.
|
||||
/// </summary>
|
||||
private GlyphRendererParameters parameters;
|
||||
|
||||
// Tracks whether geometry was emitted inside BeginLayer/EndLayer pairs for this glyph.
|
||||
// When true, EndGlyph skips its default single-layer path capture because layers
|
||||
// already contributed their paths individually.
|
||||
private bool usedLayers;
|
||||
|
||||
// Tracks whether we are currently inside a layer block.
|
||||
// Guards against unbalanced EndLayer calls.
|
||||
private bool inLayer;
|
||||
|
||||
// --- Per-GRAPHEME layered capture ---
|
||||
// A grapheme cluster (e.g. a base glyph + COLR v0 color layers) may span
|
||||
// multiple BeginGlyph/EndGlyph calls. These fields aggregate all layers
|
||||
// belonging to the same grapheme into a single GlyphPathCollection.
|
||||
private GlyphPathCollection.Builder? graphemeBuilder;
|
||||
private int graphemePathCount;
|
||||
private int currentGraphemeIndex = -1;
|
||||
private readonly List<GlyphPathCollection> currentGlyphs = [];
|
||||
|
||||
// Previous decoration details per decoration type, used to stitch adjacent
|
||||
// decorations together and eliminate sub-pixel gaps between glyphs.
|
||||
private TextDecorationDetails? previousUnderlineTextDecoration;
|
||||
private TextDecorationDetails? previousOverlineTextDecoration;
|
||||
private TextDecorationDetails? previousStrikeoutTextDecoration;
|
||||
|
||||
// Per-layer (within current grapheme) bookkeeping:
|
||||
private int layerStartIndex;
|
||||
private Paint? currentLayerPaint;
|
||||
private FillRule currentLayerFillRule;
|
||||
private ClipQuad? currentClipBounds;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseGlyphBuilder"/> class
|
||||
/// with an identity transform.
|
||||
/// </summary>
|
||||
public BaseGlyphBuilder() => this.Builder = new PathBuilder();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseGlyphBuilder"/> class
|
||||
/// with the specified transform applied to all incoming glyph geometry.
|
||||
/// </summary>
|
||||
/// <param name="transform">A matrix transform applied to every point received from the font engine.</param>
|
||||
public BaseGlyphBuilder(Matrix4x4 transform) => this.Builder = new PathBuilder(transform);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flattened paths captured for all glyphs/graphemes.
|
||||
/// </summary>
|
||||
public IPathCollection Paths => new PathCollection(this.CurrentPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the layer-preserving collections captured per grapheme in rendering order.
|
||||
/// Each entry aggregates all glyph layers that belong to a single grapheme cluster.
|
||||
/// </summary>
|
||||
public IReadOnlyList<GlyphPathCollection> Glyphs => this.currentGlyphs;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="PathBuilder"/> used to accumulate outline segments
|
||||
/// (<c>MoveTo</c>, <c>LineTo</c>, curves) for the current glyph or layer.
|
||||
/// The builder is cleared between glyphs / layers.
|
||||
/// </summary>
|
||||
protected PathBuilder Builder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the running list of all <see cref="IPath"/> instances produced so far
|
||||
/// (glyph outlines, layer outlines, and decoration rectangles). Subclasses
|
||||
/// read from the end of this list (e.g. <c>CurrentPaths[^1]</c>) to obtain
|
||||
/// the most recently built path.
|
||||
/// </summary>
|
||||
protected List<IPath> CurrentPaths { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Called by the font engine after all glyphs in the text block have been rendered.
|
||||
/// Flushes any in-progress grapheme aggregate and resets per-text-block state.
|
||||
/// </summary>
|
||||
void IGlyphRenderer.EndText()
|
||||
{
|
||||
// Finalize the last grapheme, if any:
|
||||
if (this.graphemeBuilder is not null && this.graphemePathCount > 0)
|
||||
{
|
||||
this.currentGlyphs.Add(this.graphemeBuilder.Build());
|
||||
}
|
||||
|
||||
this.graphemeBuilder = null;
|
||||
this.graphemePathCount = 0;
|
||||
this.currentGraphemeIndex = -1;
|
||||
this.previousUnderlineTextDecoration = null;
|
||||
this.previousOverlineTextDecoration = null;
|
||||
this.previousStrikeoutTextDecoration = null;
|
||||
|
||||
this.EndText();
|
||||
}
|
||||
|
||||
void IGlyphRenderer.BeginText(in FontRectangle bounds) => this.BeginText(bounds);
|
||||
|
||||
/// <summary>
|
||||
/// Called by the font engine before emitting outline data for a single glyph.
|
||||
/// Manages grapheme-cluster transitions and resets per-glyph state.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> to have the font engine emit the full outline
|
||||
/// (MoveTo/LineTo/curves/EndGlyph); <see langword="false"/> to skip it entirely,
|
||||
/// which is used by caching subclasses when the glyph path is already available.
|
||||
/// </returns>
|
||||
bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters)
|
||||
{
|
||||
// If grapheme changed, flush previous aggregate and start a new one:
|
||||
if (this.graphemeBuilder is not null && this.currentGraphemeIndex != parameters.GraphemeIndex)
|
||||
{
|
||||
if (this.graphemePathCount > 0)
|
||||
{
|
||||
this.currentGlyphs.Add(this.graphemeBuilder.Build());
|
||||
}
|
||||
|
||||
this.graphemeBuilder = null;
|
||||
this.graphemePathCount = 0;
|
||||
}
|
||||
|
||||
if (this.graphemeBuilder is null)
|
||||
{
|
||||
this.graphemeBuilder = new GlyphPathCollection.Builder();
|
||||
this.currentGraphemeIndex = parameters.GraphemeIndex;
|
||||
this.graphemePathCount = 0;
|
||||
}
|
||||
|
||||
this.parameters = parameters;
|
||||
this.Builder.Clear();
|
||||
this.usedLayers = false;
|
||||
this.inLayer = false;
|
||||
|
||||
this.layerStartIndex = this.graphemePathCount;
|
||||
this.currentLayerPaint = null;
|
||||
this.currentLayerFillRule = FillRule.NonZero;
|
||||
this.currentClipBounds = null;
|
||||
return this.BeginGlyph(in bounds, in parameters);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IGlyphRenderer.BeginFigure() => this.Builder.StartFigure();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point)
|
||||
{
|
||||
this.Builder.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point);
|
||||
this.currentPoint = point;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the font engine after the outline for a single glyph has been fully emitted.
|
||||
/// Builds the accumulated path and registers it as a grapheme layer unless explicit
|
||||
/// <c>BeginLayer</c>/<c>EndLayer</c> pairs already handled layer registration.
|
||||
/// </summary>
|
||||
void IGlyphRenderer.EndGlyph()
|
||||
{
|
||||
// If the glyph did not open any explicit layer, treat its geometry as a single
|
||||
// implicit layer so that non-color glyphs still produce a GlyphPathCollection entry.
|
||||
if (!this.usedLayers)
|
||||
{
|
||||
IPath path = this.Builder.Build();
|
||||
|
||||
this.CurrentPaths.Add(path);
|
||||
|
||||
if (this.graphemeBuilder is not null)
|
||||
{
|
||||
this.graphemeBuilder.AddPath(path);
|
||||
this.graphemeBuilder.AddLayer(
|
||||
startIndex: this.graphemePathCount,
|
||||
count: 1,
|
||||
paint: null,
|
||||
fillRule: FillRule.NonZero,
|
||||
bounds: path.Bounds,
|
||||
kind: GlyphLayerKind.Glyph);
|
||||
|
||||
this.graphemePathCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.EndGlyph();
|
||||
this.Builder.Clear();
|
||||
this.inLayer = false;
|
||||
this.usedLayers = false;
|
||||
this.layerStartIndex = this.graphemePathCount;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IGlyphRenderer.EndFigure() => this.Builder.CloseFigure();
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IGlyphRenderer.LineTo(Vector2 point)
|
||||
{
|
||||
this.Builder.AddLine(this.currentPoint, point);
|
||||
this.currentPoint = point;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IGlyphRenderer.MoveTo(Vector2 point)
|
||||
{
|
||||
this.Builder.StartFigure();
|
||||
this.currentPoint = point;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IGlyphRenderer.ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point)
|
||||
{
|
||||
this.Builder.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point);
|
||||
this.currentPoint = point;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point)
|
||||
{
|
||||
this.Builder.AddQuadraticBezier(this.currentPoint, secondControlPoint, point);
|
||||
this.currentPoint = point;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the font engine to begin a color layer within a COLR v0/v1 glyph.
|
||||
/// Each layer receives its own paint, fill rule, and optional clip bounds.
|
||||
/// </summary>
|
||||
void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds)
|
||||
{
|
||||
this.usedLayers = true;
|
||||
this.inLayer = true;
|
||||
this.layerStartIndex = this.graphemePathCount;
|
||||
this.currentLayerPaint = paint;
|
||||
this.currentLayerFillRule = fillRule;
|
||||
this.currentClipBounds = clipBounds;
|
||||
|
||||
this.Builder.Clear();
|
||||
this.BeginLayer(paint, fillRule, clipBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the font engine to close a color layer opened by <c>BeginLayer</c>.
|
||||
/// Builds the layer path, applies any clip quad, and registers the result
|
||||
/// as a painted layer in the current grapheme aggregate.
|
||||
/// </summary>
|
||||
void IGlyphRenderer.EndLayer()
|
||||
{
|
||||
if (!this.inLayer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IPath path = this.Builder.Build();
|
||||
|
||||
// If the layer defines a clip quad (e.g. from COLR v1), intersect the
|
||||
// built path with the quad polygon to constrain rendering.
|
||||
if (this.currentClipBounds is not null)
|
||||
{
|
||||
ClipQuad clip = this.currentClipBounds.Value;
|
||||
PointF[] points = [clip.TopLeft, clip.TopRight, clip.BottomRight, clip.BottomLeft];
|
||||
LinearLineSegment segment = new(points);
|
||||
Polygon polygon = new(segment);
|
||||
|
||||
ShapeOptions options = new()
|
||||
{
|
||||
BooleanOperation = BooleanOperation.Intersection,
|
||||
IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule)
|
||||
};
|
||||
|
||||
path = path.Clip(options, polygon);
|
||||
}
|
||||
|
||||
this.CurrentPaths.Add(path);
|
||||
|
||||
if (this.graphemeBuilder is not null)
|
||||
{
|
||||
this.graphemeBuilder.AddPath(path);
|
||||
this.graphemeBuilder.AddLayer(
|
||||
startIndex: this.layerStartIndex,
|
||||
count: 1,
|
||||
paint: this.currentLayerPaint,
|
||||
fillRule: this.currentLayerFillRule,
|
||||
bounds: path.Bounds,
|
||||
kind: GlyphLayerKind.Painted);
|
||||
|
||||
this.graphemePathCount++;
|
||||
}
|
||||
|
||||
this.Builder.Clear();
|
||||
this.inLayer = false;
|
||||
this.currentLayerPaint = null;
|
||||
this.currentLayerFillRule = FillRule.NonZero;
|
||||
this.currentClipBounds = null;
|
||||
this.EndLayer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the font engine to emit a text decoration (underline, strikeout, or overline)
|
||||
/// for the current glyph. Builds a filled rectangle path from the start/end positions and
|
||||
/// thickness, then registers it as a <see cref="GlyphLayerKind.Decoration"/> layer.
|
||||
/// Adjacent decorations are stitched together using the previous decoration details to
|
||||
/// eliminate sub-pixel gaps caused by font metric rounding.
|
||||
/// </summary>
|
||||
void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness)
|
||||
{
|
||||
if (thickness == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp the thickness to whole pixels.
|
||||
thickness = MathF.Max(1F, (float)Math.Round(thickness));
|
||||
IGlyphRenderer renderer = this;
|
||||
|
||||
bool rotated = this.parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated;
|
||||
Vector2 pad = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, thickness * .5F);
|
||||
|
||||
start = ClampToPixel(start, (int)thickness, rotated);
|
||||
end = ClampToPixel(end, (int)thickness, rotated);
|
||||
|
||||
// Sometimes the start and end points do not align properly leaving pixel sized gaps
|
||||
// so we need to adjust them. Use any previous decoration to try and continue the line.
|
||||
TextDecorationDetails? previous = textDecorations switch
|
||||
{
|
||||
TextDecorations.Underline => this.previousUnderlineTextDecoration,
|
||||
TextDecorations.Overline => this.previousOverlineTextDecoration,
|
||||
TextDecorations.Strikeout => this.previousStrikeoutTextDecoration,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (previous != null)
|
||||
{
|
||||
float prevThickness = previous.Value.Thickness;
|
||||
Vector2 prevStart = previous.Value.Start;
|
||||
Vector2 prevEnd = previous.Value.End;
|
||||
|
||||
// If the previous line is identical to the new one ignore it.
|
||||
// This can happen when multiple glyph layers are used.
|
||||
if (prevStart == start && prevEnd == end)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Align the new line with the previous one if they are close enough.
|
||||
// Use a 2 pixel threshold to account for anti-aliasing gaps.
|
||||
if (rotated)
|
||||
{
|
||||
if (thickness == prevThickness
|
||||
&& prevEnd.Y + 2 >= start.Y
|
||||
&& prevEnd.X == start.X)
|
||||
{
|
||||
start = prevEnd;
|
||||
}
|
||||
}
|
||||
else if (thickness == prevThickness
|
||||
&& prevEnd.Y == start.Y
|
||||
&& prevEnd.X + 2 >= start.X)
|
||||
{
|
||||
start = prevEnd;
|
||||
}
|
||||
}
|
||||
|
||||
TextDecorationDetails current = new()
|
||||
{
|
||||
Start = start,
|
||||
End = end,
|
||||
Thickness = thickness
|
||||
};
|
||||
|
||||
switch (textDecorations)
|
||||
{
|
||||
case TextDecorations.Underline:
|
||||
this.previousUnderlineTextDecoration = current;
|
||||
break;
|
||||
case TextDecorations.Strikeout:
|
||||
this.previousStrikeoutTextDecoration = current;
|
||||
break;
|
||||
case TextDecorations.Overline:
|
||||
this.previousOverlineTextDecoration = current;
|
||||
break;
|
||||
}
|
||||
|
||||
Vector2 a = start - pad;
|
||||
Vector2 b = start + pad;
|
||||
Vector2 c = end + pad;
|
||||
Vector2 d = end - pad;
|
||||
|
||||
// Drawing is always centered around the point so we need to offset by half.
|
||||
Vector2 offset = Vector2.Zero;
|
||||
if (textDecorations == TextDecorations.Overline)
|
||||
{
|
||||
// CSS overline is drawn above the position, so we need to move it up.
|
||||
offset = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, -(thickness * .5F));
|
||||
}
|
||||
else if (textDecorations == TextDecorations.Underline)
|
||||
{
|
||||
// CSS underline is drawn below the position, so we need to move it down.
|
||||
offset = rotated ? new Vector2(-(thickness * .5F), 0) : new Vector2(0, thickness * .5F);
|
||||
}
|
||||
|
||||
// We clamp the start and end points to the pixel grid to avoid anti-aliasing
|
||||
// when there is no transform.
|
||||
renderer.BeginFigure();
|
||||
renderer.MoveTo(ClampToPixel(a + offset));
|
||||
renderer.LineTo(ClampToPixel(b + offset));
|
||||
renderer.LineTo(ClampToPixel(c + offset));
|
||||
renderer.LineTo(ClampToPixel(d + offset));
|
||||
renderer.EndFigure();
|
||||
|
||||
IPath path = this.Builder.Build();
|
||||
|
||||
// If the path is degenerate (e.g. zero width line) we just skip it
|
||||
// and return. This might happen when clamping moves the points.
|
||||
if (path.Bounds.IsEmpty)
|
||||
{
|
||||
this.Builder.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.CurrentPaths.Add(path);
|
||||
if (this.graphemeBuilder is not null)
|
||||
{
|
||||
// Decorations are emitted as independent paths; each layer must point
|
||||
// at the path index appended for this specific decoration.
|
||||
this.graphemeBuilder.AddPath(path);
|
||||
this.graphemeBuilder.AddLayer(
|
||||
startIndex: this.graphemePathCount,
|
||||
count: 1,
|
||||
paint: this.currentLayerPaint,
|
||||
fillRule: FillRule.NonZero,
|
||||
bounds: path.Bounds,
|
||||
kind: GlyphLayerKind.Decoration);
|
||||
|
||||
this.graphemePathCount++;
|
||||
}
|
||||
|
||||
this.Builder.Clear();
|
||||
this.SetDecoration(textDecorations, start, end, thickness);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IGlyphRenderer.BeginText(in FontRectangle)"/>
|
||||
protected virtual void BeginText(in FontRectangle bounds)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after base-class bookkeeping in <c>IGlyphRenderer.BeginGlyph</c>.
|
||||
/// Subclasses override this to apply transforms, consult caches, or opt out of
|
||||
/// outline emission by returning <see langword="false"/>.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The font-metric bounding rectangle of the glyph.</param>
|
||||
/// <param name="parameters">Identifies the glyph (id, font, layout mode, text run, etc.).</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> to receive outline data and an <c>EndGlyph</c> call;
|
||||
/// <see langword="false"/> to skip outline emission for this glyph entirely.
|
||||
/// </returns>
|
||||
protected virtual bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters)
|
||||
=> true;
|
||||
|
||||
/// <summary>
|
||||
/// Called after the base class has built and registered the glyph path.
|
||||
/// Subclasses override this to emit drawing operations from the captured path.
|
||||
/// </summary>
|
||||
protected virtual void EndGlyph()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after the base class has flushed all grapheme aggregates.
|
||||
/// Subclasses override this for any per-text-block finalization.
|
||||
/// </summary>
|
||||
protected virtual void EndText()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a COLR color layer begins. Subclasses override this to
|
||||
/// capture the layer's paint and composite mode.
|
||||
/// </summary>
|
||||
/// <param name="paint">The paint for this color layer, or <see langword="null"/> for the default foreground.</param>
|
||||
/// <param name="fillRule">The fill rule to use when rasterizing this layer.</param>
|
||||
/// <param name="clipBounds">Optional clip quad constraining the layer region.</param>
|
||||
protected virtual void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a COLR color layer ends. Subclasses override this to
|
||||
/// emit the layer as a drawing operation.
|
||||
/// </summary>
|
||||
protected virtual void EndLayer()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the set of text decorations enabled for the current glyph.
|
||||
/// The font engine calls this to decide which <c>SetDecoration</c> callbacks to emit.
|
||||
/// Subclasses override this to include decorations implied by rich-text pens
|
||||
/// (e.g. <see cref="RichTextRun.UnderlinePen"/>).
|
||||
/// </summary>
|
||||
/// <returns>A flags enum of the active text decorations.</returns>
|
||||
public virtual TextDecorations EnabledDecorations()
|
||||
=> this.parameters.TextRun.TextDecorations;
|
||||
|
||||
/// <summary>
|
||||
/// Override point for subclasses to emit decoration drawing operations.
|
||||
/// Called after the base class has built and registered the decoration path
|
||||
/// in <see cref="CurrentPaths"/>.
|
||||
/// </summary>
|
||||
/// <param name="textDecorations">The type of decoration (underline, strikeout, or overline).</param>
|
||||
/// <param name="start">The start position of the decoration line.</param>
|
||||
/// <param name="end">The end position of the decoration line.</param>
|
||||
/// <param name="thickness">The thickness of the decoration line in pixels.</param>
|
||||
public virtual void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates a floating-point position to the nearest whole pixel toward negative infinity.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Point ClampToPixel(PointF point) => Point.Truncate(point);
|
||||
|
||||
/// <summary>
|
||||
/// Snaps a decoration endpoint to the pixel grid, taking stroke thickness and
|
||||
/// orientation into account. Even-thickness lines snap to whole pixels; odd-thickness
|
||||
/// lines snap to half pixels so the stroke center lands on a pixel boundary.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static PointF ClampToPixel(PointF point, int thickness, bool rotated)
|
||||
{
|
||||
// Even thickness: snap to whole pixels.
|
||||
if ((thickness & 1) == 0)
|
||||
{
|
||||
return Point.Truncate(point);
|
||||
}
|
||||
|
||||
// Odd thickness: snap to half pixels along the perpendicular axis
|
||||
// so the 1px-wide center row/column aligns with physical pixels.
|
||||
if (rotated)
|
||||
{
|
||||
return Point.Truncate(point) + new Vector2(.5F, 0);
|
||||
}
|
||||
|
||||
return Point.Truncate(point) + new Vector2(0, .5F);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records the start, end, and thickness of a previously emitted decoration line
|
||||
/// so that the next adjacent decoration can be stitched seamlessly.
|
||||
/// </summary>
|
||||
private struct TextDecorationDetails
|
||||
{
|
||||
/// <summary>Gets or sets the start position of the decoration.</summary>
|
||||
public Vector2 Start { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the end position of the decoration.</summary>
|
||||
public Vector2 End { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the decoration thickness in pixels.</summary>
|
||||
public float Thickness { get; internal set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
/// <summary>
|
||||
/// A rendering surface that Fonts can use to generate shapes.
|
||||
/// Extends <see cref="BaseGlyphBuilder"/> by adding a configurable origin offset
|
||||
/// so that all captured geometry is translated by the specified amount.
|
||||
/// </summary>
|
||||
internal class GlyphBuilder : BaseGlyphBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GlyphBuilder"/> class.
|
||||
/// </summary>
|
||||
public GlyphBuilder()
|
||||
: this(Vector2.Zero)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GlyphBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="origin">The origin.</param>
|
||||
public GlyphBuilder(Vector2 origin) => this.Builder.SetOrigin(origin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Numerics;
|
||||
using SixLabors.Fonts.Rendering;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
/// <summary>
|
||||
/// Describes a single painted layer as a span within the glyph's path list.
|
||||
/// </summary>
|
||||
public readonly struct GlyphLayerInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GlyphLayerInfo"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">Start index (inclusive) of the layer's paths within the glyph's path list.</param>
|
||||
/// <param name="count">Number of paths in this layer.</param>
|
||||
/// <param name="paint">The layer paint (null means use renderer default).</param>
|
||||
/// <param name="fillRule">The fill rule to use for this layer.</param>
|
||||
/// <param name="bounds">Axis-aligned bounds of the layer geometry.</param>
|
||||
/// <param name="kind">An optional semantic hint for the layer type.</param>
|
||||
internal GlyphLayerInfo(
|
||||
int startIndex,
|
||||
int count,
|
||||
Paint? paint,
|
||||
FillRule fillRule,
|
||||
RectangleF bounds,
|
||||
GlyphLayerKind kind)
|
||||
{
|
||||
this.StartIndex = startIndex;
|
||||
this.Count = count;
|
||||
this.Paint = paint;
|
||||
this.IntersectionRule = TextUtilities.MapFillRule(fillRule);
|
||||
|
||||
CompositeMode compositeMode = paint?.CompositeMode ?? CompositeMode.SrcOver;
|
||||
this.PixelAlphaCompositionMode = TextUtilities.MapCompositionMode(compositeMode);
|
||||
this.PixelColorBlendingMode = TextUtilities.MapBlendingMode(compositeMode);
|
||||
this.Bounds = bounds;
|
||||
this.Kind = kind;
|
||||
}
|
||||
|
||||
private GlyphLayerInfo(
|
||||
int startIndex,
|
||||
int count,
|
||||
Paint? paint,
|
||||
IntersectionRule intersectionRule,
|
||||
PixelAlphaCompositionMode compositionMode,
|
||||
PixelColorBlendingMode colorBlendingMode,
|
||||
RectangleF bounds,
|
||||
GlyphLayerKind kind)
|
||||
{
|
||||
this.StartIndex = startIndex;
|
||||
this.Count = count;
|
||||
this.Paint = paint;
|
||||
this.IntersectionRule = intersectionRule;
|
||||
this.PixelAlphaCompositionMode = compositionMode;
|
||||
this.PixelColorBlendingMode = colorBlendingMode;
|
||||
this.Bounds = bounds;
|
||||
this.Kind = kind;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start index (inclusive) of the layer span within the glyph's path list.
|
||||
/// </summary>
|
||||
public int StartIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of paths in this layer.
|
||||
/// </summary>
|
||||
public int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the paint definition to use for this layer; may be <see langword="null"/>.
|
||||
/// </summary>
|
||||
public Paint? Paint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fill rule for rasterization of this layer.
|
||||
/// </summary>
|
||||
public IntersectionRule IntersectionRule { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel alpha composition mode to use for this layer.
|
||||
/// </summary>
|
||||
public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pixel color blending mode to use for this layer.
|
||||
/// </summary>
|
||||
public PixelColorBlendingMode PixelColorBlendingMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bounds of the layer geometry (device space).
|
||||
/// </summary>
|
||||
public RectangleF Bounds { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the semantic kind of the layer (for policy decisions).
|
||||
/// </summary>
|
||||
public GlyphLayerKind Kind { get; }
|
||||
|
||||
internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix4x4 matrix)
|
||||
=> new(
|
||||
info.StartIndex,
|
||||
info.Count,
|
||||
info.Paint,
|
||||
info.IntersectionRule,
|
||||
info.PixelAlphaCompositionMode,
|
||||
info.PixelColorBlendingMode,
|
||||
RectangleF.Transform(info.Bounds, matrix),
|
||||
info.Kind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
/// <summary>
|
||||
/// Optional semantic classification for layers to aid monochrome projection or decoration handling.
|
||||
/// </summary>
|
||||
public enum GlyphLayerKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Regular glyph geometry layer.
|
||||
/// </summary>
|
||||
Glyph = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Text decoration geometry (underline/overline/strikethrough).
|
||||
/// </summary>
|
||||
Decoration = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Painted layer (e.g. color emoji glyph).
|
||||
/// </summary>
|
||||
Painted = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Numerics;
|
||||
using SixLabors.Fonts.Rendering;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
/// <summary>
|
||||
/// A geometry + paint container for a single glyph, preserving painted layer boundaries.
|
||||
/// </summary>
|
||||
public sealed class GlyphPathCollection
|
||||
{
|
||||
private readonly List<IPath> paths;
|
||||
private readonly ReadOnlyCollection<IPath> readOnlyPaths;
|
||||
private readonly List<GlyphLayerInfo> layers;
|
||||
private readonly ReadOnlyCollection<GlyphLayerInfo> readOnlyLayers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GlyphPathCollection"/> class.
|
||||
/// </summary>
|
||||
/// <param name="paths">All paths emitted for the glyph in z-order.</param>
|
||||
/// <param name="layers">Layer descriptors referring to spans within <paramref name="paths"/>.</param>
|
||||
internal GlyphPathCollection(List<IPath> paths, List<GlyphLayerInfo> layers)
|
||||
{
|
||||
Guard.NotNull(paths, nameof(paths));
|
||||
Guard.NotNull(layers, nameof(layers));
|
||||
|
||||
this.paths = paths;
|
||||
this.layers = layers;
|
||||
|
||||
this.readOnlyPaths = new ReadOnlyCollection<IPath>(this.paths);
|
||||
this.readOnlyLayers = new ReadOnlyCollection<GlyphLayerInfo>(this.layers);
|
||||
this.Paths = new PathCollection(this.paths);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flattened geometry for the glyph (all paths in z-order).
|
||||
/// This is equivalent to concatenating all layer spans.
|
||||
/// </summary>
|
||||
public IPathCollection Paths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only view of all individual paths in z-order.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IPath> PathList => this.readOnlyPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only list of layer descriptors preserving paint, fill rule and path spans.
|
||||
/// </summary>
|
||||
public IReadOnlyList<GlyphLayerInfo> Layers => this.readOnlyLayers;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of layers.
|
||||
/// </summary>
|
||||
public int LayerCount => this.layers.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an axis-aligned bounding box of the entire glyph in device space.
|
||||
/// </summary>
|
||||
public RectangleF Bounds => this.Paths.Bounds;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the glyph using the specified matrix.
|
||||
/// </summary>
|
||||
/// <param name="matrix">The transform matrix.</param>
|
||||
/// <returns>
|
||||
/// A new <see cref="GlyphPathCollection"/> with the matrix applied to it.
|
||||
/// </returns>
|
||||
public GlyphPathCollection Transform(Matrix4x4 matrix)
|
||||
{
|
||||
List<IPath> transformed = new(this.paths.Count);
|
||||
|
||||
for (int i = 0; i < this.paths.Count; i++)
|
||||
{
|
||||
transformed.Add(this.paths[i].Transform(matrix));
|
||||
}
|
||||
|
||||
List<GlyphLayerInfo> transformedLayers = new(this.layers.Count);
|
||||
for (int i = 0; i < this.layers.Count; i++)
|
||||
{
|
||||
transformedLayers.Add(GlyphLayerInfo.Transform(this.layers[i], matrix));
|
||||
}
|
||||
|
||||
return new GlyphPathCollection(transformed, transformedLayers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="PathCollection"/> containing only the paths from layers that
|
||||
/// satisfy <paramref name="predicate"/>. Useful to project to monochrome.
|
||||
/// </summary>
|
||||
/// <param name="predicate">A filter deciding whether to keep a layer.</param>
|
||||
/// <returns>A new <see cref="PathCollection"/> with the selected paths.</returns>
|
||||
public PathCollection ToPathCollection(Func<GlyphLayerInfo, bool>? predicate = null)
|
||||
{
|
||||
List<IPath> kept = [];
|
||||
for (int i = 0; i < this.layers.Count; i++)
|
||||
{
|
||||
GlyphLayerInfo li = this.layers[i];
|
||||
if (predicate?.Invoke(li) == false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int end = li.StartIndex + li.Count;
|
||||
for (int p = li.StartIndex; p < end; p++)
|
||||
{
|
||||
kept.Add(this.paths[p]);
|
||||
}
|
||||
}
|
||||
|
||||
return new PathCollection(kept);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="PathCollection"/> view of a single layer's geometry.
|
||||
/// </summary>
|
||||
/// <param name="layerIndex">The zero-based layer index.</param>
|
||||
/// <returns>A path collection comprising only that layer's span.</returns>
|
||||
public PathCollection GetLayerPaths(int layerIndex)
|
||||
{
|
||||
Guard.MustBeLessThan(layerIndex, this.layers.Count, nameof(layerIndex));
|
||||
|
||||
GlyphLayerInfo li = this.layers[layerIndex];
|
||||
List<IPath> chunk = new(li.Count);
|
||||
int end = li.StartIndex + li.Count;
|
||||
for (int p = li.StartIndex; p < end; p++)
|
||||
{
|
||||
chunk.Add(this.paths[p]);
|
||||
}
|
||||
|
||||
return new PathCollection(chunk);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder used by glyph renderers to populate a <see cref="GlyphPathCollection"/>.
|
||||
/// </summary>
|
||||
internal sealed class Builder
|
||||
{
|
||||
private readonly List<IPath> paths = [];
|
||||
private readonly List<GlyphLayerInfo> layers = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a completed path to the collection (current z-order position).
|
||||
/// </summary>
|
||||
/// <param name="path">The path to add.</param>
|
||||
public void AddPath(IPath path) => this.paths.Add(path);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a layer descriptor pointing at the most recently added paths.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">Start index within the path list (inclusive).</param>
|
||||
/// <param name="count">Number of paths belonging to this layer.</param>
|
||||
/// <param name="paint">The paint for this layer (may be null for default).</param>
|
||||
/// <param name="fillRule">The fill rule for this layer.</param>
|
||||
/// <param name="bounds">Optional cached bounds for this layer.</param>
|
||||
/// <param name="kind">Optional semantic kind (eg. Decoration).</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown if the specified span is out of range of the current path list.
|
||||
/// </exception>
|
||||
public void AddLayer(
|
||||
int startIndex,
|
||||
int count,
|
||||
Paint? paint,
|
||||
FillRule fillRule,
|
||||
RectangleF bounds,
|
||||
GlyphLayerKind kind = GlyphLayerKind.Glyph)
|
||||
{
|
||||
if (startIndex < 0 || count < 0 || startIndex + count > this.paths.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), "Layer span is out of range of the current path list.");
|
||||
}
|
||||
|
||||
this.layers.Add(new GlyphLayerInfo(startIndex, count, paint, fillRule, bounds, kind));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the immutable <see cref="GlyphPathCollection"/>.
|
||||
/// </summary>
|
||||
/// <returns>The collection.</returns>
|
||||
public GlyphPathCollection Build() => new(this.paths, this.layers);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Rendering;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
/// <summary>
|
||||
/// A rendering surface that Fonts can use to generate shapes by following a path.
|
||||
/// Each glyph is positioned along the path and rotated to match the path tangent
|
||||
/// at the glyph's horizontal center.
|
||||
/// </summary>
|
||||
internal sealed class PathGlyphBuilder : GlyphBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// The path that glyphs are laid out along. Exposed as <see cref="IPathInternals"/>
|
||||
/// to access the <see cref="IPathInternals.PointAlongPath"/> method for efficient
|
||||
/// position + tangent queries.
|
||||
/// </summary>
|
||||
private readonly IPathInternals path;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PathGlyphBuilder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to render the glyphs along.</param>
|
||||
public PathGlyphBuilder(IPath path)
|
||||
{
|
||||
if (path is IPathInternals internals)
|
||||
{
|
||||
this.path = internals;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wrap in ComplexPolygon to gain IPathInternals.
|
||||
this.path = new ComplexPolygon(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters)
|
||||
{
|
||||
// Translate + rotate the glyph to follow the path. Always returns true because
|
||||
// path-based glyphs are never cached (each has a unique per-position transform).
|
||||
this.TransformGlyph(in bounds);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the translation + rotation matrix that places a glyph along the path.
|
||||
/// The glyph's horizontal center is mapped to the path distance, and the glyph
|
||||
/// is rotated to match the path tangent at that point.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The font-metric bounding rectangle of the glyph.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void TransformGlyph(in FontRectangle bounds)
|
||||
{
|
||||
// Query the path at the glyph's horizontal center.
|
||||
Vector2 half = new(bounds.Width * .5F, 0);
|
||||
SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X);
|
||||
|
||||
// Translate so the glyph's top-left aligns with the path point,
|
||||
// then rotate around the path point to follow the tangent.
|
||||
Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top);
|
||||
Matrix4x4 matrix = Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point));
|
||||
|
||||
this.Builder.SetTransform(matrix);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using SixLabors.Fonts;
|
||||
using SixLabors.Fonts.Rendering;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
/// <summary>
|
||||
/// Builds vector shapes from text using the provided layout and rendering options.
|
||||
/// </summary>
|
||||
public static class TextBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the combined outline paths for all rendered glyphs in <paramref name="text"/>.
|
||||
/// The result merges per-glyph outlines into a single <see cref="IPathCollection"/> suitable for filling or stroking as one unit.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to shape and render.</param>
|
||||
/// <param name="textOptions">The text rendering and layout options.</param>
|
||||
/// <returns>The combined <see cref="IPathCollection"/> for the rendered glyphs.</returns>
|
||||
public static IPathCollection GeneratePaths(string text, TextOptions textOptions)
|
||||
{
|
||||
GlyphBuilder glyphBuilder = new();
|
||||
TextRenderer renderer = new(glyphBuilder);
|
||||
|
||||
renderer.RenderText(text, textOptions);
|
||||
|
||||
return glyphBuilder.Paths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates per-glyph path data and metadata for the rendered <paramref name="text"/>.
|
||||
/// Each entry contains the combined outline paths for a glyph and associated metadata that enables intelligent fill or stroke decisions at the glyph level.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to shape and render.</param>
|
||||
/// <param name="textOptions">The text rendering and layout options.</param>
|
||||
/// <returns>A read-only list of <see cref="GlyphPathCollection"/> entries, one for each rendered glyph.</returns>
|
||||
public static IReadOnlyList<GlyphPathCollection> GenerateGlyphs(string text, TextOptions textOptions)
|
||||
{
|
||||
GlyphBuilder glyphBuilder = new();
|
||||
TextRenderer renderer = new(glyphBuilder);
|
||||
|
||||
renderer.RenderText(text, textOptions);
|
||||
|
||||
return glyphBuilder.Glyphs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the combined outline paths for all rendered glyphs in <paramref name="text"/>,
|
||||
/// laid out along the supplied <paramref name="path"/> baseline.
|
||||
/// The result merges per-glyph outlines into a single <see cref="IPathCollection"/>.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to shape and render.</param>
|
||||
/// <param name="path">The path that defines the text baseline.</param>
|
||||
/// <param name="textOptions">The text rendering and layout options.</param>
|
||||
/// <returns>The combined <see cref="IPathCollection"/> for the rendered glyphs.</returns>
|
||||
public static IPathCollection GeneratePaths(string text, IPath path, TextOptions textOptions)
|
||||
{
|
||||
(IPath Path, TextOptions TextOptions) transformed = ConfigureOptions(textOptions, path);
|
||||
PathGlyphBuilder glyphBuilder = new(transformed.Path);
|
||||
TextRenderer renderer = new(glyphBuilder);
|
||||
|
||||
renderer.RenderText(text, transformed.TextOptions);
|
||||
|
||||
return glyphBuilder.Paths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates per-glyph path data and metadata for the rendered <paramref name="text"/>,
|
||||
/// laid out along the supplied <paramref name="path"/> baseline.
|
||||
/// Each entry contains the combined outline paths for a glyph and associated metadata.
|
||||
/// </summary>
|
||||
/// <param name="text">The text to shape and render.</param>
|
||||
/// <param name="path">The path that defines the text baseline.</param>
|
||||
/// <param name="textOptions">The text rendering and layout options.</param>
|
||||
/// <returns>A read-only list of <see cref="GlyphPathCollection"/> entries, one for each rendered glyph.</returns>
|
||||
public static IReadOnlyList<GlyphPathCollection> GenerateGlyphs(string text, IPath path, TextOptions textOptions)
|
||||
{
|
||||
(IPath Path, TextOptions TextOptions) transformed = ConfigureOptions(textOptions, path);
|
||||
PathGlyphBuilder glyphBuilder = new(transformed.Path);
|
||||
TextRenderer renderer = new(glyphBuilder);
|
||||
|
||||
renderer.RenderText(text, transformed.TextOptions);
|
||||
|
||||
return glyphBuilder.Glyphs;
|
||||
}
|
||||
|
||||
private static (IPath Path, TextOptions TextOptions) ConfigureOptions(TextOptions options, IPath path)
|
||||
{
|
||||
// When a path is specified we should explicitly follow that path
|
||||
// and not adjust the origin. Any translation should be applied to the path.
|
||||
if (options.Origin != Vector2.Zero)
|
||||
{
|
||||
TextOptions clone = new(options)
|
||||
{
|
||||
Origin = Vector2.Zero
|
||||
};
|
||||
|
||||
return (path.Translate(options.Origin), clone);
|
||||
}
|
||||
|
||||
return (path, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.Fonts.Rendering;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.Text {
|
||||
internal static class TextUtilities
|
||||
{
|
||||
public static IntersectionRule MapFillRule(FillRule fillRule)
|
||||
=> fillRule switch
|
||||
{
|
||||
FillRule.EvenOdd => IntersectionRule.EvenOdd,
|
||||
FillRule.NonZero => IntersectionRule.NonZero,
|
||||
_ => IntersectionRule.NonZero,
|
||||
};
|
||||
|
||||
public static PixelAlphaCompositionMode MapCompositionMode(CompositeMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
CompositeMode.Clear => PixelAlphaCompositionMode.Clear,
|
||||
CompositeMode.Src => PixelAlphaCompositionMode.Src,
|
||||
CompositeMode.Dest => PixelAlphaCompositionMode.Dest,
|
||||
CompositeMode.SrcOver => PixelAlphaCompositionMode.SrcOver,
|
||||
CompositeMode.DestOver => PixelAlphaCompositionMode.DestOver,
|
||||
CompositeMode.SrcIn => PixelAlphaCompositionMode.SrcIn,
|
||||
CompositeMode.DestIn => PixelAlphaCompositionMode.DestIn,
|
||||
CompositeMode.SrcOut => PixelAlphaCompositionMode.SrcOut,
|
||||
CompositeMode.DestOut => PixelAlphaCompositionMode.DestOut,
|
||||
CompositeMode.SrcAtop => PixelAlphaCompositionMode.SrcAtop,
|
||||
CompositeMode.DestAtop => PixelAlphaCompositionMode.DestAtop,
|
||||
CompositeMode.Xor => PixelAlphaCompositionMode.Xor,
|
||||
_ => PixelAlphaCompositionMode.SrcOver,
|
||||
};
|
||||
|
||||
public static PixelColorBlendingMode MapBlendingMode(CompositeMode mode)
|
||||
=> mode switch
|
||||
{
|
||||
CompositeMode.Plus => PixelColorBlendingMode.Add,
|
||||
CompositeMode.Screen => PixelColorBlendingMode.Screen,
|
||||
CompositeMode.Overlay => PixelColorBlendingMode.Overlay,
|
||||
CompositeMode.Darken => PixelColorBlendingMode.Darken,
|
||||
CompositeMode.Lighten => PixelColorBlendingMode.Lighten,
|
||||
CompositeMode.HardLight => PixelColorBlendingMode.HardLight,
|
||||
CompositeMode.Multiply => PixelColorBlendingMode.Multiply,
|
||||
|
||||
// TODO: We do not support the following separate alpha blending modes:
|
||||
// - ColorDodge, ColorBurn, SoftLight, Difference, Exclusion
|
||||
// TODO: We do not support the non-alpha blending modes.
|
||||
// - Hue, Saturation, Color, Luminosity
|
||||
_ => PixelColorBlendingMode.Normal
|
||||
};
|
||||
|
||||
public static DrawingOptions CloneOrReturnForRules(
|
||||
this DrawingOptions drawingOptions,
|
||||
IntersectionRule intersectionRule,
|
||||
PixelAlphaCompositionMode compositionMode,
|
||||
PixelColorBlendingMode colorBlendingMode)
|
||||
{
|
||||
if (drawingOptions.ShapeOptions.IntersectionRule == intersectionRule &&
|
||||
drawingOptions.GraphicsOptions.AlphaCompositionMode == compositionMode &&
|
||||
drawingOptions.GraphicsOptions.ColorBlendingMode == colorBlendingMode)
|
||||
{
|
||||
return drawingOptions;
|
||||
}
|
||||
|
||||
ShapeOptions shapeOptions = drawingOptions.ShapeOptions.DeepClone();
|
||||
shapeOptions.IntersectionRule = intersectionRule;
|
||||
|
||||
GraphicsOptions graphicsOptions = drawingOptions.GraphicsOptions.DeepClone();
|
||||
graphicsOptions.AlphaCompositionMode = compositionMode;
|
||||
graphicsOptions.ColorBlendingMode = colorBlendingMode;
|
||||
|
||||
return new DrawingOptions(graphicsOptions, shapeOptions, drawingOptions.Transform);
|
||||
}
|
||||
|
||||
public static GraphicsOptions CloneOrReturnForRules(
|
||||
this GraphicsOptions graphicsOptions,
|
||||
PixelAlphaCompositionMode compositionMode,
|
||||
PixelColorBlendingMode colorBlendingMode)
|
||||
{
|
||||
if (graphicsOptions.AlphaCompositionMode == compositionMode &&
|
||||
graphicsOptions.ColorBlendingMode == colorBlendingMode)
|
||||
{
|
||||
return graphicsOptions;
|
||||
}
|
||||
|
||||
GraphicsOptions clone = graphicsOptions.DeepClone();
|
||||
clone.AlphaCompositionMode = compositionMode;
|
||||
clone.ColorBlendingMode = colorBlendingMode;
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user