// 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; } } }