// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using SixLabors.Fonts.Tables.AdvancedTypographic.GPos; using SixLabors.Fonts.Tables.AdvancedTypographic.Shapers; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tables.AdvancedTypographic { /// /// The Glyph Positioning table (GPOS) provides precise control over glyph placement for /// sophisticated text layout and rendering in each script and language system that a font supports. /// /// internal class GPosTable : Table { /// /// The tag for the horizontal kerning feature ('kern'). /// private static readonly Tag KernTag = Tag.Parse("kern"); /// /// The tag for the vertical kerning feature ('vkrn'). /// private static readonly Tag VKernTag = Tag.Parse("vkrn"); /// /// The OpenType table tag for the GPOS table. /// internal const string TableName = "GPOS"; /// /// 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 GPosTable(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 positioning 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 GPosTable? 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 GPosTable Load(BigEndianBinaryReader reader) { // GPOS Header, Version 1.0 // +----------+-------------------+-----------------------------------------------------------+ // | Type | Name | Description | // +==========+===================+===========================================================+ // | uint16 | majorVersion | Major version of the GPOS table, = 1 | // +----------+-------------------+-----------------------------------------------------------+ // | uint16 | minorVersion | Minor version of the GPOS table, = 0 | // +----------+-------------------+-----------------------------------------------------------+ // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GPOS table | // +----------+-------------------+-----------------------------------------------------------+ // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table | // +----------+-------------------+-----------------------------------------------------------+ // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GPOS table | // +----------+-------------------+-----------------------------------------------------------+ // GPOS Header, Version 1.1 // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Type | Name | Description | // +==========+=========================+===============================================================================+ // | uint16 | majorVersion | Major version of the GPOS table, = 1 | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | uint16 | minorVersion | Minor version of the GPOS table, = 1 | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset16 | scriptListOffset | Offset to ScriptList table, from beginning of GPOS table | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset16 | lookupListOffset | Offset to LookupList table, from beginning of GPOS table | // +----------+-------------------------+-------------------------------------------------------------------------------+ // | Offset32 | featureVariationsOffset | Offset to FeatureVariations table, from beginning of GPOS 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 GPosTable(scriptList, featureList, lookupList, featureVariations); } /// /// Tries to update the positions of glyphs in the collection using GPOS lookup rules. /// /// The font metrics. /// The glyph positioning collection. /// When this method returns, indicates whether kerning was applied. /// if any positioning was updated; otherwise, . public bool TryUpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, out bool kerned) { // 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; bool maxOperationsReached = false; kerned = false; bool updated = false; for (int i = 0; i < collection.Count; i++) { if (!collection.ShouldProcess(fontMetrics, i)) { continue; } 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. int ni = i + 1; GlyphShapingData nextData = collection[ni]; if (!collection.ShouldProcess(fontMetrics, ni)) { break; } ScriptClass next = this.GetScriptClass(CodePoint.GetScriptClass(nextData.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); if (shaper.MarkZeroingMode == MarkZeroingMode.PreGPos) { ZeroMarkAdvances(fontMetrics, collection, index, count); } // Plan positioning features for each glyph. shaper.Plan(collection, index, count); IEnumerable shapingStages = shaper.GetShapingStages(); SkippingGlyphIterator iterator = new(fontMetrics, collection, index, default, 0); foreach (ShapingStage stage in shapingStages) { stage.PreProcessFeature(collection, index, count); Tag featureTag = stage.FeatureTag; 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 (currentOperations++ >= maxOperationsCount) { maxOperationsReached = true; goto EndLookups; } if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature)) { iterator.Next(); continue; } bool success = featureLookup.LookupTable.TryUpdatePosition(fontMetrics, this, collection, featureLookup.Feature, iterator.Index, count - (iterator.Index - index)); kerned |= success && (feature == KernTag || feature == VKernTag); updated |= success; iterator.Next(); } } } stage.PostProcessFeature(collection, index, count); } EndLookups: if (shaper.MarkZeroingMode == MarkZeroingMode.PostGpos) { ZeroMarkAdvances(fontMetrics, collection, index, count); } FixCursiveAttachment(collection, index, count); FixMarkAttachment(collection, index, count); UpdatePositions(fontMetrics, collection, index, count); if (i >= maxCount || maxOperationsReached) { return updated; } } return updated; } /// /// 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, . private 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; } /// /// Fixes cursive attachment positioning by propagating Y (or X for vertical) offsets. /// /// The glyph positioning collection. /// The starting index. /// The number of glyphs to process. private static void FixCursiveAttachment(GlyphPositioningCollection collection, int index, int count) { LayoutMode layoutMode = collection.TextOptions.LayoutMode; for (int i = 0; i < count; i++) { int currentIndex = i + index; GlyphShapingData data = collection[currentIndex]; if (data.CursiveAttachment != -1) { int j = data.CursiveAttachment + currentIndex; if (j < index || j >= index + count) { return; } GlyphShapingData cursiveData = collection[j]; if (!AdvancedTypographicUtils.IsVerticalGlyph(data.CodePoint, layoutMode)) { data.Bounds.Y += cursiveData.Bounds.Y; } else { data.Bounds.X += cursiveData.Bounds.X; } } } } /// /// Fixes mark attachment positioning by propagating offsets from base glyphs. /// /// The glyph positioning collection. /// The starting index. /// The number of glyphs to process. private static void FixMarkAttachment(GlyphPositioningCollection collection, int index, int count) { for (int i = 0; i < count; i++) { int currentIndex = i + index; GlyphShapingData data = collection[currentIndex]; if (data.MarkAttachment != -1) { int j = data.MarkAttachment; GlyphShapingData markData = collection[j]; data.Bounds.X += markData.Bounds.X; data.Bounds.Y += markData.Bounds.Y; if (data.Direction == TextDirection.LeftToRight) { for (int k = j; k < currentIndex; k++) { markData = collection[k]; data.Bounds.X -= markData.Bounds.Width; data.Bounds.Y -= markData.Bounds.Height; } } else { for (int k = j + 1; k < currentIndex + 1; k++) { markData = collection[k]; data.Bounds.X += markData.Bounds.Width; data.Bounds.Y += markData.Bounds.Height; } } } } } /// /// Zeros the advance widths and heights for mark glyphs within the specified range. /// /// The font metrics. /// The glyph positioning collection. /// The starting index. /// The number of glyphs to process. private static void ZeroMarkAdvances(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count) { for (int i = 0; i < count; i++) { int currentIndex = i + index; GlyphShapingData data = collection[currentIndex]; if (AdvancedTypographicUtils.IsMarkGlyph(fontMetrics, data.GlyphId, data)) { data.Bounds.Width = 0; data.Bounds.Height = 0; } } } /// /// Updates glyph positions in the collection for the specified range. /// /// The font metrics. /// The glyph positioning collection. /// The starting index. /// The number of glyphs to process. private static void UpdatePositions(FontMetrics fontMetrics, GlyphPositioningCollection collection, int index, int count) { for (int i = 0; i < count; i++) { int currentIndex = i + index; collection.UpdatePosition(fontMetrics, currentIndex); } } } }