Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/SixLabors.Fonts/FileFontMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ private FileFontMetrics(FontDescription description, string path, long offset)
/// <inheritdoc/>
public override short StrikeoutSize => this.fontMetrics.Value.StrikeoutSize;

/// <inheritdoc/>
public override short XHeight => this.fontMetrics.Value.XHeight;

/// <inheritdoc/>
public override short CapHeight => this.fontMetrics.Value.CapHeight;

/// <inheritdoc/>
public override short StrikeoutPosition => this.fontMetrics.Value.StrikeoutPosition;

Expand Down Expand Up @@ -196,6 +202,10 @@ public override ReadOnlyMemory<CodePoint> GetAvailableCodePoints()
internal override bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTable)
=> this.fontMetrics.Value.TryGetGSubTable(out gSubTable);

/// <inheritdoc/>
internal override bool TryGetBaselineCoordinate(Tag baselineTag, bool isVerticalLayout, out short coordinate)
=> this.fontMetrics.Value.TryGetBaselineCoordinate(baselineTag, isVerticalLayout, out coordinate);

/// <inheritdoc/>
internal override void ApplySubstitution(GlyphSubstitutionCollection collection)
=> this.fontMetrics.Value.ApplySubstitution(collection);
Expand Down
26 changes: 26 additions & 0 deletions src/SixLabors.Fonts/FontMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ internal FontMetrics()
/// </summary>
public abstract short StrikeoutSize { get; }

/// <summary>
/// Gets the x-height in font design units, or 0 when the font does not provide it.
/// </summary>
public abstract short XHeight { get; }

/// <summary>
/// Gets the cap height in font design units, or 0 when the font does not provide it.
/// </summary>
public abstract short CapHeight { get; }

/// <summary>
/// Gets the position of the top of the strikeout stroke relative to the baseline in font design units.
/// </summary>
Expand Down Expand Up @@ -289,6 +299,22 @@ internal abstract FontGlyphMetrics GetGlyphMetrics(
/// <returns>true, if the glyph class could be retrieved.</returns>
internal abstract bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTable);

/// <summary>
/// Tries to get the coordinate of the named baseline from the font's baseline table for
/// the given layout direction, read from the default script record of the matching axis.
/// </summary>
/// <param name="baselineTag">The baseline identification tag, for example 'hang' or 'ideo'.</param>
/// <param name="isVerticalLayout">
/// Whether to read the vertical axis, whose coordinates are X values, rather than the
/// horizontal axis, whose coordinates are Y values.
/// </param>
/// <param name="coordinate">
/// The baseline coordinate in design units, measured from the zero position on the
/// relevant axis.
/// </param>
/// <returns><see langword="true"/> when the font defines the named baseline; otherwise <see langword="false"/>.</returns>
internal abstract bool TryGetBaselineCoordinate(Tag baselineTag, bool isVerticalLayout, out short coordinate);

/// <summary>
/// Applies any available substitutions to the collection of glyphs.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/SixLabors.Fonts/GlyphOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@ public float Dpi
/// <inheritdoc cref="TextOptions.Origin"/>
public Vector2 Origin { get; set; } = Vector2.Zero;

/// <summary>
/// Gets or sets the visible region in pixel units (px) used when rendering the glyph.
/// A glyph that falls outside the region is skipped.
/// </summary>
/// <remarks>
/// If value is <see langword="null"/> then culling is disabled.
/// </remarks>
public FontRectangle? VisibleBounds { get; set; }

/// <summary>
/// Gets or sets which reference line of the glyph's em box is placed at
/// <see cref="Origin"/> along the block flow axis.
/// </summary>
/// <remarks>
/// Baseline positions derive from the metrics of <see cref="Font"/>. For a single glyph
/// <see cref="TextBaseline.LineBox"/> anchors the top of the em box, matching text layout.
/// </remarks>
public TextBaseline TextBaseline { get; set; }

/// <inheritdoc cref="TextOptions.BaselineOffset"/>
public float BaselineOffset { get; set; }

/// <summary>
/// Gets or sets the zero-based grapheme cluster index represented by the glyph.
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions src/SixLabors.Fonts/MemoryFontMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ private MemoryFontMetrics(FontDescription description, byte[] data, long offset)
/// <inheritdoc/>
public override short StrikeoutSize => this.fontMetrics.Value.StrikeoutSize;

/// <inheritdoc/>
public override short XHeight => this.fontMetrics.Value.XHeight;

/// <inheritdoc/>
public override short CapHeight => this.fontMetrics.Value.CapHeight;

/// <inheritdoc/>
public override short StrikeoutPosition => this.fontMetrics.Value.StrikeoutPosition;

Expand Down Expand Up @@ -190,6 +196,10 @@ public override ReadOnlyMemory<CodePoint> GetAvailableCodePoints()
internal override bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTable)
=> this.fontMetrics.Value.TryGetGSubTable(out gSubTable);

/// <inheritdoc/>
internal override bool TryGetBaselineCoordinate(Tag baselineTag, bool isVerticalLayout, out short coordinate)
=> this.fontMetrics.Value.TryGetBaselineCoordinate(baselineTag, isVerticalLayout, out coordinate);

/// <inheritdoc/>
internal override void ApplySubstitution(GlyphSubstitutionCollection collection)
=> this.fontMetrics.Value.ApplySubstitution(collection);
Expand Down
169 changes: 165 additions & 4 deletions src/SixLabors.Fonts/Rendering/TextRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,16 @@ public void Render(string text, TextOptions options)
/// <param name="options">The text options. <see cref="TextOptions.WrappingLength"/> controls wrapping; use <c>-1</c> to disable wrapping.</param>
public void Render(ReadOnlySpan<char> text, TextOptions options)
{
TextBlock block = new(text, options);
block.RenderTo(this.renderer, options.WrappingLength);
if (text.IsEmpty)
{
this.renderer.BeginText(FontRectangle.Empty);
this.renderer.EndText();
return;
}

ShapedText shaped = TextLayout.ShapeText(text, options);
LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(shaped, text, options);
this.RenderText(logicalLine, options);
}

/// <summary>
Expand All @@ -99,11 +107,49 @@ public void Render(ushort glyphId, GlyphOptions options)
return;
}

TextRun textRun = options.CreateTextRun();
FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun);
Vector2 origin = options.Origin / options.Dpi;
GlyphLayoutMode glyphLayoutMode = options.GetGlyphLayoutMode(metrics.CodePoint);

if (glyphLayoutMode == GlyphLayoutMode.Horizontal)
{
// The renderer positions glyphs by their alphabetic baseline; shifting the origin
// by the combined anchor and baseline-shift offset puts the selected reference
// line, shifted as requested, on the caller's origin.
origin.Y -= TextLayout.GetBaselineOffset(options, false);
}
else
{
// Vertical rendering positions glyphs about the column's central axis; the same
// combined offset applies along X from that axis.
origin.X -= TextLayout.GetBaselineOffset(options, true);
}

if (options.VisibleBounds is FontRectangle visibleBounds)
{
// The origin is the baseline-anchored render position in layout units (pixels
// divided by DPI), so the ink box computed against it compares directly with the
// scaled region. Inflating by the scaled line height for the glyph's orientation
// gives em-box anchored decorations the same tolerance culled text receives, and
// rejecting here also skips the per-glyph metrics clone below.
FontRectangle box = metrics.GetBoundingBox(glyphLayoutMode, origin, options.Font.Size);
IMetricsHeader metricsHeader = glyphLayoutMode == GlyphLayoutMode.Vertical
? fontMetrics.VerticalMetrics
: fontMetrics.HorizontalMetrics;

float tolerance = metricsHeader.LineHeight * (options.Font.Size / metrics.ScaleFactor.Y);
float dpi = options.Dpi;
if (box.Right + tolerance < visibleBounds.Left / dpi ||
box.Left - tolerance > visibleBounds.Right / dpi ||
box.Bottom + tolerance < visibleBounds.Top / dpi ||
box.Top - tolerance > visibleBounds.Bottom / dpi)
{
return;
}
}

TextRun textRun = options.CreateTextRun();
FontGlyphMetrics renderMetrics = metrics.CloneForRendering(textRun);

renderMetrics.RenderTo(
this.renderer,
options.GraphemeIndex,
Expand Down Expand Up @@ -151,4 +197,119 @@ public void Render(GlyphRun glyphRun, GlyphOptions options)
options.GraphemeIndex = originalGraphemeIndex;
}
}

/// <summary>
/// Line-breaks and renders prepared text without retaining any layout state. When
/// <see cref="TextOptions.VisibleBounds"/> is set, whole lines outside the region are
/// culled and breaking stops at the region when line order and alignment permit.
/// </summary>
/// <remarks>
/// A full render reports the rendered ink bounds to <see cref="IGlyphRenderer.BeginText"/>,
/// matching <see cref="TextBlock.RenderTo(IGlyphRenderer, float)"/>. A culled render
/// reports the logical advance bounds of the broken lines instead: ink bounds would cost
/// an extra pass over the visible glyphs and are unknowable once breaking stops early.
/// </remarks>
/// <param name="logicalLine">The prepared logical line and line break opportunities.</param>
/// <param name="options">The text options used for layout and rendering.</param>
private void RenderText(in LogicalTextLine logicalLine, TextOptions options)
{
float wrappingLength = options.WrappingLength;
float dpi = options.Dpi;
bool isHorizontal = options.LayoutMode.IsHorizontal();

float visibleFlowMin = float.NegativeInfinity;
float visibleFlowMax = float.PositiveInfinity;
if (options.VisibleBounds is FontRectangle visibleBounds)
{
visibleFlowMin = (isHorizontal ? visibleBounds.Top : visibleBounds.Left) / dpi;
visibleFlowMax = (isHorizontal ? visibleBounds.Bottom : visibleBounds.Right) / dpi;
}

TextBox textBox = BreakVisibleLines(logicalLine, options, wrappingLength, isHorizontal, visibleFlowMax);
if (textBox.TextLines.Count == 0)
{
this.renderer.BeginText(FontRectangle.Empty);
this.renderer.EndText();
return;
}

FontRectangle bounds;
if (options.VisibleBounds is null)
{
// The full render keeps the shipped BeginText contract: the rendered ink bounds,
// accumulated in the same measuring pass TextBlock uses.
TextBlock.RenderedRectangleAccumulator accumulator = new(dpi);
TextLayout.LayoutText(textBox, options, wrappingLength, ref accumulator);
bounds = accumulator.Result();
}
else
{
// The logical advance box comes straight from the line aggregates in one scan
// over the broken lines.
FontRectangle advance = TextBlock.GetAdvance(textBox, dpi, isHorizontal);
bounds = new(options.Origin.X, options.Origin.Y, advance.Width, advance.Height);
}

this.renderer.BeginText(in bounds);

TextBlock.GlyphRendererVisitor visitor = new(this.renderer, options, -1);
TextLayout.LayoutText(textBox, options, wrappingLength, visibleFlowMin, visibleFlowMax, ref visitor);

this.renderer.EndText();
}

/// <summary>
/// Line-breaks prepared text, stopping once the flow position passes the visible band
/// when line order and alignment permit. An infinite band breaks every line.
/// </summary>
/// <param name="logicalLine">The prepared logical line and line break opportunities.</param>
/// <param name="options">The text options used for layout.</param>
/// <param name="wrappingLength">The wrapping length in pixels. Use <c>-1</c> to disable wrapping.</param>
/// <param name="isHorizontal">Whether the layout direction is horizontal.</param>
/// <param name="visibleFlowMax">The upper visible-band edge along the block flow axis in layout units.</param>
/// <returns>The line-broken text box, possibly truncated after the visible band.</returns>
private static TextBox BreakVisibleLines(
in LogicalTextLine logicalLine,
TextOptions options,
float wrappingLength,
bool isHorizontal,
float visibleFlowMax)
{
TextDirection textDirection = TextLayout.GetTextDirection(logicalLine, options);
if (TextLayout.LayoutRequiresFullLineSet(options, textDirection))
{
return TextLayout.BreakLines(logicalLine, options, wrappingLength);
}

// A line can never be taller than the tallest entry in the prepared text, and
// TextLine.Add keeps that maximum current while the logical line is composed. Once the
// running flow position is more than two of those heights past the end of the visible
// band, no later line can still be visible: one height covers the line itself, the
// other covers the walk's one-line-height visibility tolerance. Line spacing below one
// shifts each line upward when centering it, so that shift widens the margin too.
float maxLineExtent = logicalLine.TextLine.ScaledMaxLineHeight;
float stopSlack = maxLineExtent * 2F;
if (options.LineSpacing < 1F)
{
stopSlack += maxLineExtent * (1F - options.LineSpacing) / (2F * options.LineSpacing);
}

List<TextLine> textLines = [];
TextLineBreakEnumerator lineEnumerator = new(logicalLine, options);
float flowPosition = (isHorizontal ? options.Origin.Y : options.Origin.X) / options.Dpi;

while (lineEnumerator.MoveNext(wrappingLength))
{
TextLine line = lineEnumerator.Current;
textLines.Add(line);
flowPosition += line.ScaledMaxLineHeight;

if (flowPosition - visibleFlowMax > stopSlack)
{
break;
}
}

return new TextBox(textLines, textDirection);
}
}
2 changes: 2 additions & 0 deletions src/SixLabors.Fonts/StreamFontMetrics.Cff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader, FontSource s

KerningTable? kern = reader.TryGetTable<KerningTable>();

BaseTable? baseTable = reader.TryGetTable<BaseTable>();
GlyphDefinitionTable? gdef = reader.TryGetTable<GlyphDefinitionTable>();
GSubTable? gSub = reader.TryGetTable<GSubTable>();
GPosTable? gPos = reader.TryGetTable<GPosTable>();
Expand Down Expand Up @@ -80,6 +81,7 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader, FontSource s
Kern = kern,
Vhea = vhea,
Vmtx = vmtx,
Base = baseTable,
Gdef = gdef,
GSub = gSub,
GPos = gPos,
Expand Down
2 changes: 2 additions & 0 deletions src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ private static StreamFontMetrics LoadTrueTypeFont(FontReader reader, FontSource
vmtx = reader.TryGetTable<VerticalMetricsTable>();
}

BaseTable? baseTable = reader.TryGetTable<BaseTable>();
GlyphDefinitionTable? gdef = reader.TryGetTable<GlyphDefinitionTable>();
GSubTable? gSub = reader.TryGetTable<GSubTable>();
GPosTable? gPos = reader.TryGetTable<GPosTable>();
Expand Down Expand Up @@ -154,6 +155,7 @@ private static StreamFontMetrics LoadTrueTypeFont(FontReader reader, FontSource
Kern = kern,
Vhea = vhea,
Vmtx = vmtx,
Base = baseTable,
Gdef = gdef,
GSub = gSub,
GPos = gPos,
Expand Down
26 changes: 26 additions & 0 deletions src/SixLabors.Fonts/StreamFontMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ internal partial class StreamFontMetrics : FontMetrics
private short superscriptYOffset;
private short strikeoutSize;
private short strikeoutPosition;
private short xHeight;
private short capHeight;
private short underlinePosition;
private short underlineThickness;
private float italicAngle;
Expand Down Expand Up @@ -206,6 +208,12 @@ private StreamFontMetrics(
/// <inheritdoc/>
public override short StrikeoutSize => this.strikeoutSize;

/// <inheritdoc/>
public override short XHeight => this.xHeight;

/// <inheritdoc/>
public override short CapHeight => this.capHeight;

/// <inheritdoc/>
public override short StrikeoutPosition => this.strikeoutPosition;

Expand Down Expand Up @@ -426,6 +434,22 @@ internal override bool TryGetGSubTable([NotNullWhen(true)] out GSubTable? gSubTa
return gSubTable is not null;
}

/// <inheritdoc/>
internal override bool TryGetBaselineCoordinate(Tag baselineTag, bool isVerticalLayout, out short coordinate)
{
BaseTable? baseTable = this.outlineType == OutlineType.TrueType
? this.trueTypeFontTables!.Base
: this.compactFontTables!.Base;

if (baseTable is null)
{
coordinate = 0;
return false;
}

return baseTable.TryGetBaselineCoordinate(baselineTag, isVerticalLayout, out coordinate);
}

/// <inheritdoc/>
internal override void ApplySubstitution(GlyphSubstitutionCollection collection)
{
Expand Down Expand Up @@ -640,6 +664,8 @@ private static StreamFontMetrics LoadFont(FontReader reader, FontSource source)
this.superscriptYOffset = os2.SuperscriptYOffset;
this.strikeoutSize = os2.StrikeoutSize;
this.strikeoutPosition = os2.StrikeoutPosition;
this.xHeight = os2.XHeight;
this.capHeight = os2.CapHeight;
this.underlinePosition = post.UnderlinePosition;
this.underlineThickness = post.UnderlineThickness;
this.italicAngle = post.ItalicAngle;
Expand Down
Loading
Loading