ImageSharp/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs
2026-08-03 22:31:27 +02:00

339 lines
14 KiB
C#

// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.Fonts.Unicode;
using System;
using System.Collections.Generic;
namespace SixLabors.Fonts.Tables.AdvancedTypographic.Shapers {
/// <summary>
/// Default shaper, which will be applied to all glyphs.
/// Based on fontkit: <see href="https://github.com/foliojs/fontkit/blob/master/src/opentype/shapers/DefaultShaper.js"/>
/// </summary>
internal class DefaultShaper : BaseShaper
{
/// <summary>The 'rvrn' (required variation alternates) feature tag.</summary>
protected static readonly Tag RvnrTag = Tag.Parse("rvrn");
/// <summary>The 'ltra' (left-to-right alternates) feature tag.</summary>
protected static readonly Tag LtraTag = Tag.Parse("ltra");
/// <summary>The 'ltrm' (left-to-right mirrored forms) feature tag.</summary>
protected static readonly Tag LtrmTag = Tag.Parse("ltrm");
/// <summary>The 'rtla' (right-to-left alternates) feature tag.</summary>
protected static readonly Tag RtlaTag = Tag.Parse("rtla");
/// <summary>The 'rtlm' (right-to-left mirrored forms) feature tag.</summary>
protected static readonly Tag RtlmTag = Tag.Parse("rtlm");
/// <summary>The 'frac' (fractions) feature tag.</summary>
protected static readonly Tag FracTag = Tag.Parse("frac");
/// <summary>The 'numr' (numerators) feature tag.</summary>
protected static readonly Tag NumrTag = Tag.Parse("numr");
/// <summary>The 'dnom' (denominators) feature tag.</summary>
protected static readonly Tag DnomTag = Tag.Parse("dnom");
/// <summary>The 'ccmp' (glyph composition/decomposition) feature tag.</summary>
protected static readonly Tag CcmpTag = Tag.Parse("ccmp");
/// <summary>The 'locl' (localized forms) feature tag.</summary>
protected static readonly Tag LoclTag = Tag.Parse("locl");
/// <summary>The 'rlig' (required ligatures) feature tag.</summary>
protected static readonly Tag RligTag = Tag.Parse("rlig");
/// <summary>The 'mark' (mark positioning) feature tag.</summary>
protected static readonly Tag MarkTag = Tag.Parse("mark");
/// <summary>The 'mkmk' (mark-to-mark positioning) feature tag.</summary>
protected static readonly Tag MkmkTag = Tag.Parse("mkmk");
/// <summary>The 'calt' (contextual alternates) feature tag.</summary>
protected static readonly Tag CaltTag = Tag.Parse("calt");
/// <summary>The 'clig' (contextual ligatures) feature tag.</summary>
protected static readonly Tag CligTag = Tag.Parse("clig");
/// <summary>The 'liga' (standard ligatures) feature tag.</summary>
protected static readonly Tag LigaTag = Tag.Parse("liga");
/// <summary>The 'rclt' (required contextual alternates) feature tag.</summary>
protected static readonly Tag RcltTag = Tag.Parse("rclt");
/// <summary>The 'curs' (cursive positioning) feature tag.</summary>
protected static readonly Tag CursTag = Tag.Parse("curs");
/// <summary>The 'kern' (kerning) feature tag.</summary>
protected static readonly Tag KernTag = Tag.Parse("kern");
/// <summary>The 'vert' (vertical alternates) feature tag.</summary>
protected static readonly Tag VertTag = Tag.Parse("vert");
/// <summary>The 'vkrn' (vertical kerning) feature tag.</summary>
protected static readonly Tag VKernTag = Tag.Parse("vkrn");
/// <summary>The fraction slash code point (U+2044).</summary>
private static readonly CodePoint FractionSlash = new(0x2044);
/// <summary>The solidus (slash) code point (U+002F).</summary>
private static readonly CodePoint Slash = new(0x002F);
/// <summary>The set of shaping stages accumulated during feature planning.</summary>
private readonly HashSet<ShapingStage> shapingStages = [];
/// <summary>The kerning mode from the text options.</summary>
private readonly KerningMode kerningMode;
/// <summary>The user-specified feature tags from the text options.</summary>
private readonly IReadOnlyList<Tag> featureTags;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultShaper"/> class with PostGpos mark zeroing.
/// </summary>
/// <param name="script">The script classification.</param>
/// <param name="textOptions">The text options.</param>
internal DefaultShaper(ScriptClass script, TextOptions textOptions)
: this(script, MarkZeroingMode.PostGpos, textOptions)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DefaultShaper"/> class.
/// </summary>
/// <param name="script">The script classification.</param>
/// <param name="markZeroingMode">The mark zeroing mode.</param>
/// <param name="textOptions">The text options.</param>
protected DefaultShaper(ScriptClass script, MarkZeroingMode markZeroingMode, TextOptions textOptions)
{
this.ScriptClass = script;
this.MarkZeroingMode = markZeroingMode;
this.kerningMode = textOptions.KerningMode;
this.featureTags = textOptions.FeatureTags;
}
/// <inheritdoc />
protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count)
{
}
/// <inheritdoc />
protected override void PlanPreprocessingFeatures(IGlyphShapingCollection collection, int index, int count)
{
// Add variation Features.
this.AddFeature(collection, index, count, RvnrTag);
// Add directional features.
for (int i = index; i < index + count; i++)
{
GlyphShapingData shapingData = collection[i];
if (shapingData.Direction == TextDirection.LeftToRight)
{
this.AddFeature(collection, i, 1, LtraTag);
this.AddFeature(collection, i, 1, LtrmTag);
}
else
{
this.AddFeature(collection, i, 1, RtlaTag);
this.AddFeature(collection, i, 1, RtlmTag);
}
}
// TODO: Fractional feature should be assigned here but disabled.
// They should then be enabled in AssignFeatures.
}
/// <inheritdoc />
protected override void PlanPostprocessingFeatures(IGlyphShapingCollection collection, int index, int count)
{
// Add common features.
this.AddFeature(collection, index, count, CcmpTag);
this.AddFeature(collection, index, count, LoclTag);
this.AddFeature(collection, index, count, RligTag);
this.AddFeature(collection, index, count, MarkTag);
this.AddFeature(collection, index, count, MkmkTag);
LayoutMode layoutMode = collection.TextOptions.LayoutMode;
bool isVerticalLayout = false;
for (int i = index; i < index + count; i++)
{
GlyphShapingData shapingData = collection[i];
isVerticalLayout |= AdvancedTypographicUtils.IsVerticalGlyph(shapingData.CodePoint, layoutMode);
}
// Add horizontal or vertical features.
if (!isVerticalLayout)
{
// Add horizontal features.
this.AddFeature(collection, index, count, CaltTag);
this.AddFeature(collection, index, count, CligTag);
this.AddFeature(collection, index, count, LigaTag);
this.AddFeature(collection, index, count, RcltTag);
this.AddFeature(collection, index, count, CursTag);
this.AddFeature(collection, index, count, KernTag);
}
else
{
// We only apply `vert` feature.See:
// https://github.com/harfbuzz/harfbuzz/commit/d71c0df2d17f4590d5611239577a6cb532c26528
// https://lists.freedesktop.org/archives/harfbuzz/2013-August/003490.html
// We really want to find a 'vert' feature if there's any in the font, no
// matter which script/langsys it is listed (or not) under.
// See various bugs referenced from:
// https://github.com/harfbuzz/harfbuzz/issues/63
this.AddFeature(collection, index, count, VertTag);
}
// Add user defined features.
foreach (Tag feature in this.featureTags)
{
// We've already dealt with fractional features.
if (feature != FracTag && feature != NumrTag && feature != DnomTag)
{
this.AddFeature(collection, index, count, feature);
}
}
}
/// <inheritdoc />
protected override void AssignFeatures(IGlyphShapingCollection collection, int index, int count)
{
// TODO: We shouldn't be relying on the feature list
// User defined fractional features require special treatment.
// https://docs.microsoft.com/en-us/typography/opentype/spec/features_fj#tag-frac
if (this.HasFractions())
{
this.AssignFractionalFeatures(collection, index, count);
}
}
/// <summary>
/// Adds a shaping feature to the specified range of glyphs in the collection and registers the corresponding shaping stage.
/// </summary>
/// <param name="collection">The glyph shaping collection.</param>
/// <param name="index">The zero-based index of the first element.</param>
/// <param name="count">The number of elements.</param>
/// <param name="feature">The feature tag to add.</param>
/// <param name="enabled">Whether the feature is initially enabled.</param>
/// <param name="preAction">An optional action to invoke before the feature is applied.</param>
/// <param name="postAction">An optional action to invoke after the feature is applied.</param>
protected void AddFeature(
IGlyphShapingCollection collection,
int index,
int count,
Tag feature,
bool enabled = true,
Action<IGlyphShapingCollection, int, int>? preAction = null,
Action<IGlyphShapingCollection, int, int>? postAction = null)
{
if (this.kerningMode == KerningMode.None)
{
if (feature == KernTag || feature == VKernTag)
{
return;
}
}
int end = index + count;
for (int i = index; i < end; i++)
{
collection.AddShapingFeature(i, new TagEntry(feature, enabled));
}
this.shapingStages.Add(new ShapingStage(feature, preAction, postAction));
}
/// <inheritdoc />
public override IEnumerable<ShapingStage> GetShapingStages() => this.shapingStages;
/// <summary>
/// Assigns fractional feature tags (numerator, denominator, fraction) to glyphs forming fraction sequences.
/// </summary>
/// <param name="collection">The glyph shaping collection.</param>
/// <param name="index">The zero-based index of the first element.</param>
/// <param name="count">The number of elements.</param>
private void AssignFractionalFeatures(IGlyphShapingCollection collection, int index, int count)
{
// Enable contextual fractions.
for (int i = index; i < index + count; i++)
{
GlyphShapingData shapingData = collection[i];
if (shapingData.CodePoint == FractionSlash || shapingData.CodePoint == Slash)
{
int start = i;
int end = i + 1;
// Apply numerator.
if (start > 0)
{
shapingData = collection[start - 1];
while (start > 0 && CodePoint.IsDigit(shapingData.CodePoint))
{
this.AddFeature(collection, start - 1, 1, NumrTag);
this.AddFeature(collection, start - 1, 1, FracTag);
start--;
}
}
// Apply denominator.
if (end < collection.Count)
{
shapingData = collection[end];
while (end < collection.Count && CodePoint.IsDigit(shapingData.CodePoint))
{
this.AddFeature(collection, end, 1, DnomTag);
this.AddFeature(collection, end, 1, FracTag);
end++;
}
}
// Apply fraction slash.
this.AddFeature(collection, i, 1, FracTag);
i = end - 1;
}
}
}
/// <summary>
/// Determines whether the user-specified feature tags include fractional features.
/// </summary>
/// <returns><see langword="true"/> if fractional features are present; otherwise, <see langword="false"/>.</returns>
private bool HasFractions()
{
bool hasNmr = false;
bool hasDnom = false;
// My kingdom for a binary search on IReadOnlyList
for (int i = 0; i < this.featureTags.Count; i++)
{
Tag feature = this.featureTags[i];
if (feature == FracTag)
{
return true;
}
if (feature == DnomTag)
{
hasDnom = true;
}
if (feature == NumrTag)
{
hasNmr = true;
}
if (hasDnom && hasNmr)
{
return true;
}
}
return false;
}
}
}