// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using SixLabors.Fonts.Tables.AdvancedTypographic.GSub; using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tables.AdvancedTypographic { /// /// The Glyph Substitution (GSUB) table provides data for substitution of glyphs for appropriate rendering of scripts, /// such as cursively-connecting forms in Arabic script, or for advanced typographic effects, such as ligatures. /// /// internal class GSubTable : Table { /// /// The OpenType table tag for the GSUB table. /// internal const string TableName = "GSUB"; /// /// Initializes a new instance of the class. /// /// The script list table, or if not present. /// The feature list table. /// The lookup list table. /// The feature variations table for variable fonts, or . public GSubTable(ScriptList? scriptList, FeatureListTable featureList, LookupListTable lookupList, FeatureVariationsTable? featureVariations = null) { this.ScriptList = scriptList; this.FeatureList = featureList; this.LookupList = lookupList; this.FeatureVariations = featureVariations; } /// /// Gets the script list table, or if not present. /// public ScriptList? ScriptList { get; } /// /// Gets the feature list table. /// public FeatureListTable FeatureList { get; } /// /// Gets the lookup list table containing all substitution lookups. /// public LookupListTable LookupList { get; } /// /// Gets the feature variations table for variable fonts, or if not present. /// public FeatureVariationsTable? FeatureVariations { get; } /// /// Loads the from the font reader. /// /// The font reader. /// The , or if not present. public static GSubTable? Load(FontReader fontReader) { if (!fontReader.TryGetReaderAtTablePosition(TableName, out BigEndianBinaryReader? binaryReader)) { return null; } using (binaryReader) { return Load(binaryReader); } } /// /// Loads the from a big endian binary reader. /// /// The big endian binary reader. /// The . internal static GSubTable Load(BigEndianBinaryReader reader) { // GSUB Header, Version 1.0 // +----------+-------------------+-----------------------------------------------------------+ // | Type | Name | Description | // +==========+===================+===========================================================+ // | uint16 | majorVersion | Major version of the GSUB table, = 1 | // +----------+-------------------+-----------------------------------------------------------+ // | uint16 | minorVersion | Minor version of the GSUB table, = 0 | // +----------+-------------------+-----------------------------------------------------------+ // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GSUB table | // +----------+-------------------+-----------------------------------------------------------+ // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GSUB table | // +----------+-------------------+-----------------------------------------------------------+ // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GSUB table | // +----------+-------------------+-----------------------------------------------------------+ // GSUB Header, Version 1.1 // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Type | Name | Description | // +==========+=========================+===============================================================================+ // | uint16 | majorVersion | Major version of the GSUB table, = 1 | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | uint16 | minorVersion | Minor version of the GSUB table, = 1 | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GSUB table | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GSUB table | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GSUB table | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset32 | featureVariationsOffset | Offset to FeatureVariations table, from beginning of GSUB table (may be NULL) | // +----------+-------------------------+-------------------------------------------------------------------------------+ ushort majorVersion = reader.ReadUInt16(); ushort minorVersion = reader.ReadUInt16(); ushort scriptListOffset = reader.ReadOffset16(); ushort featureListOffset = reader.ReadOffset16(); ushort lookupListOffset = reader.ReadOffset16(); uint featureVariationsOffset = (minorVersion == 1) ? reader.ReadOffset32() : 0; // TODO: Optimization. Allow only reading the scriptList. ScriptList? scriptList = ScriptList.Load(reader, scriptListOffset); FeatureListTable featureList = FeatureListTable.Load(reader, featureListOffset); LookupListTable lookupList = LookupListTable.Load(reader, lookupListOffset); FeatureVariationsTable? featureVariations = featureVariationsOffset != 0 ? FeatureVariationsTable.Load(reader, featureVariationsOffset, featureList) : null; return new GSubTable(scriptList, featureList, lookupList, featureVariations); } /// /// Applies glyph substitution to the collection using GSUB lookup rules. /// /// The font metrics. /// The glyph substitution collection. public void ApplySubstitution(FontMetrics fontMetrics, GlyphSubstitutionCollection collection) { // Set max constraints to prevent OutOfMemoryException or infinite loops from attacks. int maxCount = AdvancedTypographicUtils.GetMaxAllowableShapingCollectionCount(collection.Count); int maxOperationsCount = AdvancedTypographicUtils.GetMaxAllowableShapingOperationsCount(collection.Count); int currentOperations = 0; for (int i = 0; i < collection.Count; i++) { // Choose a shaper based on the script. // This determines which features to apply to which glyphs. ScriptClass current = this.GetScriptClass(CodePoint.GetScriptClass(collection[i].CodePoint)); int index = i; int count = 1; while (i < collection.Count - 1) { // We want to assign the same feature lookups to individual sections of the text rather // than the text as a whole to ensure that different language shapers do not interfere // with each other when the text contains multiple languages. ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(collection[i + 1].CodePoint)); if (next != current && current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited && next is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClass.Inherited) { break; } if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) { current = next; } i++; count++; if (i >= maxCount) { break; } } Tag unicodeScriptTag = this.GetUnicodeScriptTag(current); BaseShaper shaper = ShaperFactory.Create(current, unicodeScriptTag, fontMetrics, collection.TextOptions); // Plan substitution features for each glyph. // Shapers can adjust the count during initialization and feature processing so we must capture // the current count to allow resetting indexes and processing counts. int collectionCount = collection.Count; shaper.Plan(collection, index, count); int delta = collection.Count - collectionCount; i += delta; count += delta; IEnumerable stages = shaper.GetShapingStages(); SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); foreach (ShapingStage stage in stages) { collectionCount = collection.Count; stage.PreProcessFeature(collection, index, count); // Account for substitutions changing the length of the collection. delta = collection.Count - collectionCount; count += delta; i += delta; Tag featureTag = stage.FeatureTag; this.ApplyFeature( fontMetrics, collection, ref iterator, in featureTag, current, index, ref count, ref i, ref collectionCount, maxCount, maxOperationsCount, ref currentOperations); collectionCount = collection.Count; stage.PostProcessFeature(collection, index, count); // Account for substitutions changing the length of the collection. delta = collection.Count - collectionCount; count += delta; i += delta; } } } /// /// Applies a specific feature's lookups to the glyph substitution collection. /// /// The font metrics. /// The glyph substitution collection. /// The skipping glyph iterator. /// The feature tag to apply. /// The current script class. /// The starting index in the collection. /// The number of glyphs to process (updated by substitutions). /// The outer loop index (updated by substitutions). /// The tracked collection count (updated by substitutions). /// The maximum allowable collection count. /// The maximum allowable operations count. /// The current operations counter. internal void ApplyFeature( FontMetrics fontMetrics, GlyphSubstitutionCollection collection, ref SkippingGlyphIterator iterator, in Tag featureTag, ScriptClass current, int index, ref int count, ref int i, ref int collectionCount, int maxCount, int maxOperationsCount, ref int currentOperations) { if (this.TryGetFeatureLookups(fontMetrics, in featureTag, current, out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? lookups)) { // Apply features in order. foreach ((Tag Feature, ushort Index, LookupTable LookupTable) featureLookup in lookups) { Tag feature = featureLookup.Feature; LookupTable featureLookupTable = featureLookup.LookupTable; iterator.Reset(index, featureLookupTable.LookupFlags, featureLookupTable.MarkFilteringSet); while (iterator.Index < index + count) { if (collection.Count >= maxCount || currentOperations++ >= maxOperationsCount) { return; } if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature)) { iterator.Next(); continue; } collectionCount = collection.Count; featureLookup.LookupTable.TrySubstitution(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); iterator.Next(); // Account for substitutions changing the length of the collection. int delta = collection.Count - collectionCount; count += delta; i += delta; } } } } /// /// Tries to get the feature lookups for the given stage feature and script. /// /// The font metrics. /// The feature tag for the current shaping stage. /// The script class. /// When this method returns, contains the list of feature lookups if found. /// if lookups were found; otherwise, . internal bool TryGetFeatureLookups( FontMetrics fontMetrics, in Tag stageFeature, ScriptClass script, [NotNullWhen(true)] out List<(Tag Feature, ushort Index, LookupTable LookupTable)>? value) { if (this.ScriptList is null) { value = null; return false; } // Resolve feature substitutions from FeatureVariations (variable fonts). FeatureTableSubstitutionRecord[]? substitutions = this.FeatureVariations ?.FindMatchingSubstitutions(fontMetrics.GetNormalizedCoordinates()); ScriptListTable scriptListTable = this.ScriptList.Default(); Tag[] tags = UnicodeScriptTagMap.Instance[script]; for (int i = 0; i < tags.Length; i++) { if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? table)) { scriptListTable = table; break; } } LangSysTable? defaultLangSysTable = scriptListTable.DefaultLangSysTable; if (defaultLangSysTable != null) { value = this.GetFeatureLookups(stageFeature, substitutions, defaultLangSysTable); return value.Count > 0; } value = this.GetFeatureLookups(stageFeature, substitutions, scriptListTable.LangSysTables); return value.Count > 0; } /// /// Gets the OpenType script tag for the given script class, checking against the font's ScriptList. /// /// The script class. /// The matching script tag, or default if not found. private Tag GetUnicodeScriptTag(ScriptClass script) { if (this.ScriptList is null) { return default; } Tag[] tags = UnicodeScriptTagMap.Instance[script]; for (int i = 0; i < tags.Length; i++) { if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) { return tags[i]; } } return default; } /// /// Gets the feature lookups for the given stage feature from the specified language system tables. /// /// The feature tag for the current shaping stage. /// Optional feature table substitutions from FeatureVariations. /// The language system tables to search. /// A sorted list of feature lookups. private List<(Tag Feature, ushort Index, LookupTable LookupTable)> GetFeatureLookups( in Tag stageFeature, FeatureTableSubstitutionRecord[]? substitutions, params LangSysTable[] langSysTables) { List<(Tag Feature, ushort Index, LookupTable LookupTable)> lookups = []; for (int i = 0; i < langSysTables.Length; i++) { ushort[] featureIndices = langSysTables[i].FeatureIndices; for (int j = 0; j < featureIndices.Length; j++) { ushort featureIndex = featureIndices[j]; FeatureTable featureTable = ResolveFeatureTable(this.FeatureList, featureIndex, substitutions); Tag feature = featureTable.FeatureTag; if (stageFeature != feature) { continue; } ushort[] lookupListIndices = featureTable.LookupListIndices; for (int k = 0; k < lookupListIndices.Length; k++) { ushort lookupIndex = lookupListIndices[k]; LookupTable lookupTable = this.LookupList.LookupTables[lookupIndex]; lookups.Add(new(feature, lookupIndex, lookupTable)); } } } lookups.Sort((x, y) => x.Index - y.Index); return lookups; } /// /// Resolves the feature table for the given index, checking for substitutions from FeatureVariations first. /// /// The feature list table. /// The feature index. /// Optional feature table substitutions from FeatureVariations. /// The resolved feature table. private static FeatureTable ResolveFeatureTable( FeatureListTable featureList, ushort featureIndex, FeatureTableSubstitutionRecord[]? substitutions) { if (substitutions is not null) { for (int i = 0; i < substitutions.Length; i++) { if (substitutions[i].FeatureIndex == featureIndex) { return substitutions[i].AlternateFeatureTable; } } } return featureList.FeatureTables[featureIndex]; } /// /// Maps a script class to an effective script class, checking whether the font supports it. /// Falls back to if the script is not present in the font. /// /// The script class to check. /// The effective script class. private ScriptClass GetScriptClass(ScriptClass current) { if (current is ScriptClass.Common or ScriptClass.Unknown or ScriptClass.Inherited) { return current; } if (this.ScriptList is null) { return ScriptClass.Default; } Tag[] tags = UnicodeScriptTagMap.Instance[current]; for (int i = 0; i < tags.Length; i++) { if (this.ScriptList.TryGetValue(tags[i].Value, out ScriptListTable? _)) { return current; } } // Script for `current` not present in the font: use default shaper. return ScriptClass.Default; } } }