// 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 {
///
/// Default shaper, which will be applied to all glyphs.
/// Based on fontkit:
///
internal class DefaultShaper : BaseShaper
{
/// The 'rvrn' (required variation alternates) feature tag.
protected static readonly Tag RvnrTag = Tag.Parse("rvrn");
/// The 'ltra' (left-to-right alternates) feature tag.
protected static readonly Tag LtraTag = Tag.Parse("ltra");
/// The 'ltrm' (left-to-right mirrored forms) feature tag.
protected static readonly Tag LtrmTag = Tag.Parse("ltrm");
/// The 'rtla' (right-to-left alternates) feature tag.
protected static readonly Tag RtlaTag = Tag.Parse("rtla");
/// The 'rtlm' (right-to-left mirrored forms) feature tag.
protected static readonly Tag RtlmTag = Tag.Parse("rtlm");
/// The 'frac' (fractions) feature tag.
protected static readonly Tag FracTag = Tag.Parse("frac");
/// The 'numr' (numerators) feature tag.
protected static readonly Tag NumrTag = Tag.Parse("numr");
/// The 'dnom' (denominators) feature tag.
protected static readonly Tag DnomTag = Tag.Parse("dnom");
/// The 'ccmp' (glyph composition/decomposition) feature tag.
protected static readonly Tag CcmpTag = Tag.Parse("ccmp");
/// The 'locl' (localized forms) feature tag.
protected static readonly Tag LoclTag = Tag.Parse("locl");
/// The 'rlig' (required ligatures) feature tag.
protected static readonly Tag RligTag = Tag.Parse("rlig");
/// The 'mark' (mark positioning) feature tag.
protected static readonly Tag MarkTag = Tag.Parse("mark");
/// The 'mkmk' (mark-to-mark positioning) feature tag.
protected static readonly Tag MkmkTag = Tag.Parse("mkmk");
/// The 'calt' (contextual alternates) feature tag.
protected static readonly Tag CaltTag = Tag.Parse("calt");
/// The 'clig' (contextual ligatures) feature tag.
protected static readonly Tag CligTag = Tag.Parse("clig");
/// The 'liga' (standard ligatures) feature tag.
protected static readonly Tag LigaTag = Tag.Parse("liga");
/// The 'rclt' (required contextual alternates) feature tag.
protected static readonly Tag RcltTag = Tag.Parse("rclt");
/// The 'curs' (cursive positioning) feature tag.
protected static readonly Tag CursTag = Tag.Parse("curs");
/// The 'kern' (kerning) feature tag.
protected static readonly Tag KernTag = Tag.Parse("kern");
/// The 'vert' (vertical alternates) feature tag.
protected static readonly Tag VertTag = Tag.Parse("vert");
/// The 'vkrn' (vertical kerning) feature tag.
protected static readonly Tag VKernTag = Tag.Parse("vkrn");
/// The fraction slash code point (U+2044).
private static readonly CodePoint FractionSlash = new(0x2044);
/// The solidus (slash) code point (U+002F).
private static readonly CodePoint Slash = new(0x002F);
/// The set of shaping stages accumulated during feature planning.
private readonly HashSet shapingStages = [];
/// The kerning mode from the text options.
private readonly KerningMode kerningMode;
/// The user-specified feature tags from the text options.
private readonly IReadOnlyList featureTags;
///
/// Initializes a new instance of the class with PostGpos mark zeroing.
///
/// The script classification.
/// The text options.
internal DefaultShaper(ScriptClass script, TextOptions textOptions)
: this(script, MarkZeroingMode.PostGpos, textOptions)
{
}
///
/// Initializes a new instance of the class.
///
/// The script classification.
/// The mark zeroing mode.
/// The text options.
protected DefaultShaper(ScriptClass script, MarkZeroingMode markZeroingMode, TextOptions textOptions)
{
this.ScriptClass = script;
this.MarkZeroingMode = markZeroingMode;
this.kerningMode = textOptions.KerningMode;
this.featureTags = textOptions.FeatureTags;
}
///
protected override void PlanFeatures(IGlyphShapingCollection collection, int index, int count)
{
}
///
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.
}
///
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);
}
}
}
///
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);
}
}
///
/// Adds a shaping feature to the specified range of glyphs in the collection and registers the corresponding shaping stage.
///
/// The glyph shaping collection.
/// The zero-based index of the first element.
/// The number of elements.
/// The feature tag to add.
/// Whether the feature is initially enabled.
/// An optional action to invoke before the feature is applied.
/// An optional action to invoke after the feature is applied.
protected void AddFeature(
IGlyphShapingCollection collection,
int index,
int count,
Tag feature,
bool enabled = true,
Action? preAction = null,
Action? 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));
}
///
public override IEnumerable GetShapingStages() => this.shapingStages;
///
/// Assigns fractional feature tags (numerator, denominator, fraction) to glyphs forming fraction sequences.
///
/// The glyph shaping collection.
/// The zero-based index of the first element.
/// The number of elements.
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;
}
}
}
///
/// Determines whether the user-specified feature tags include fractional features.
///
/// if fractional features are present; otherwise, .
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;
}
}
}