// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts { /// /// Represents a collection of glyph indices that are mapped to input codepoints. /// internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection { /// /// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids. /// private readonly List glyphs = []; /// /// Initializes a new instance of the class. /// /// The text options. public GlyphSubstitutionCollection(TextOptions textOptions) => this.TextOptions = textOptions; /// /// Gets the number of glyphs ids contained in the collection. /// This may be more or less than original input codepoint count (due to substitution process). /// public int Count => this.glyphs.Count; /// public TextOptions TextOptions { get; } /// /// Gets or sets the running id of any ligature glyphs contained withing this collection are a member of. /// public int LigatureId { get; set; } = 1; /// public GlyphShapingData this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.glyphs[index].Data; } /// /// Gets the shaping data at the specified position. /// /// The zero-based index of the elements to get. /// The zero-based index within the input codepoint collection. /// The . internal GlyphShapingData GetGlyphShapingData(int index, out int offset) { OffsetGlyphDataPair pair = this.glyphs[index]; offset = pair.Offset; return pair.Data; } /// public void AddShapingFeature(int index, TagEntry feature) { GlyphShapingData data = this.glyphs[index].Data; data.Features.Add(feature); if (feature.Enabled) { data.EnabledFeatureTags.Add(feature.Tag); } } /// public void EnableShapingFeature(int index, Tag feature) { GlyphShapingData data = this.glyphs[index].Data; List features = data.Features; for (int i = 0; i < features.Count; i++) { TagEntry tagEntry = features[i]; if (tagEntry.Tag == feature) { tagEntry.Enabled = true; features[i] = tagEntry; data.EnabledFeatureTags.Add(feature); break; } } } /// public void DisableShapingFeature(int index, Tag feature) { GlyphShapingData data = this.glyphs[index].Data; List features = data.Features; for (int i = 0; i < features.Count; i++) { TagEntry tagEntry = features[i]; if (tagEntry.Tag == feature) { tagEntry.Enabled = false; features[i] = tagEntry; data.EnabledFeatureTags.Remove(feature); break; } } } /// /// Adds a clone of the glyph shaping data to the collection at the specified offset. /// /// The data. /// The zero-based index within the input codepoint collection. public void AddGlyph(GlyphShapingData data, int offset) => this.glyphs.Add(new(offset, new(data, false))); /// /// Adds the glyph id and the codepoint it represents to the collection. /// /// The id of the glyph to add. /// The codepoint the glyph represents. /// The resolved text direction for the codepoint. /// The text run this glyph belongs to. /// The zero-based index within the input codepoint collection. public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection direction, TextRun textRun, int offset) => this.glyphs.Add(new(offset, new(textRun) { CodePoint = codePoint, Direction = direction, GlyphId = glyphId, })); /// /// Adds an atomic inline placeholder to the collection. /// /// The object replacement codepoint used for Unicode processing. /// The resolved bidi run for the placeholder. /// The text run this placeholder belongs to. /// The zero-based index within the input codepoint collection. public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun, int offset) => this.glyphs.Add(new(offset, new(textRun) { CodePoint = codePoint, Direction = (TextDirection)bidiRun.Direction, GlyphId = 0, IsPlaceholder = true, BidiRun = bidiRun, })); /// /// Moves the specified glyph to the specified position. /// /// The index to move from. /// The index to move to. public void MoveGlyph(int fromIndex, int toIndex) { if (fromIndex == toIndex) { return; } GlyphShapingData data = this[fromIndex]; if (fromIndex > toIndex) { // Move item to the right for (int i = fromIndex; i > toIndex; i--) { this.glyphs[i].Data = this.glyphs[i - 1].Data; } } else { // Move item to the left for (int i = fromIndex; i < toIndex; i++) { this.glyphs[i].Data = this.glyphs[i + 1].Data; } } this.glyphs[toIndex].Data = data; } /// /// Reverses the order of elements in the specified range of the collection. /// /// /// The range is interpreted as half-open, from (inclusive) /// to (exclusive). /// /// Both indices are clamped to the valid range [0, ]. /// If the resulting range contains fewer than two elements, the method performs no action. /// The method does not throw if either index is equal to ; in such /// cases the range is considered valid but may be empty. /// /// /// The zero-based index at which to start reversing (inclusive). This value should be /// greater than or equal to 0. Values greater than are treated as /// . /// /// /// The zero-based index at which to stop reversing (exclusive). This value should be /// greater than or equal to . Values greater than /// are treated as . /// public void ReverseRange(int startIndex, int endIndex) { int s = Math.Min(startIndex, this.Count); int e = Math.Min(endIndex, this.Count); if (e < s + 2) { return; } this.glyphs.Reverse(s, e - s); } /// /// Performs a stable sort of the glyphs by the comparison delegate starting at the specified index. /// Only the references are reordered; offsets remain in place. /// /// The start index. /// The end index. /// The comparison delegate. public void Sort(int startIndex, int endIndex, Comparison comparer) { // Stable insertion sort using adjacent swaps of Data references. // The sorted ranges are typically small (syllable clusters of 2-10 glyphs), // so insertion sort is optimal and avoids allocations. Adjacent swaps // replace the previous MoveGlyph approach which shifted all intermediate elements. List glyphs = this.glyphs; for (int i = startIndex + 1; i < endIndex; i++) { int j = i; while (j > startIndex && comparer(glyphs[j - 1].Data, glyphs[j].Data) > 0) { // Swap Data references between adjacent slots. (glyphs[j].Data, glyphs[j - 1].Data) = (glyphs[j - 1].Data, glyphs[j].Data); j--; } } } /// /// Removes all elements from the collection. /// public void Clear() { this.glyphs.Clear(); this.LigatureId = 1; } /// /// Gets the specified glyph ids matching the given codepoint offset. /// /// The zero-based index within the input codepoint collection. /// /// When this method returns, contains the shaping data associated with the specified offset, /// if the value is found; otherwise, the default value for the type of the data parameter. /// This parameter is passed uninitialized. /// /// /// if the contains glyph ids /// for the specified offset; otherwise, . /// public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList? data) { List match = []; for (int i = 0; i < this.glyphs.Count; i++) { if (this.glyphs[i].Offset == offset) { match.Add(this.glyphs[i].Data); } else if (match.Count > 0) { // Offsets, though non-sequential, are sorted, so we can stop searching. break; } } data = match; return match.Count > 0; } /// /// Performs a 1:1 replacement of a glyph id at the given position. /// /// The zero-based index of the element to replace. /// The replacement glyph id. /// The feature to apply to the glyph at the specified index. public void Replace(int index, ushort glyphId, Tag feature) { GlyphShapingData current = this.glyphs[index].Data; current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; current.AppliedFeatures.Add(feature); } /// /// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids at the given positions within the sequence. /// /// The zero-based index of the element to replace. /// The indices at which to remove elements. /// The replacement glyph id. /// The ligature id. /// The feature to apply to the glyph at the specified index. public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, int ligatureId, Tag feature) { // Remove the glyphs at each index. int codePointCount = 0; CodePoint codePoint = default; for (int i = removalIndices.Length - 1; i >= 0; i--) { int match = removalIndices[i]; codePointCount += this.glyphs[match].Data.CodePointCount; CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) { if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) { codePoint = currentCodePoint; } } this.glyphs.RemoveAt(match); } // Assign our new id at the index. GlyphShapingData current = this.glyphs[index].Data; if (codePoint != default) { current.CodePoint = codePoint; } current.CodePointCount += codePointCount; current.GlyphId = glyphId; current.LigatureId = ligatureId; current.IsLigated = true; current.LigatureComponent = -1; current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; current.AppliedFeatures.Add(feature); } /// /// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids. /// /// The zero-based index of the element to replace. /// The number of glyphs to remove. /// The replacement glyph id. /// The feature to apply to the glyph at the specified index. public void Replace(int index, int count, ushort glyphId, Tag feature) { // Remove the glyphs at each index. int codePointCount = 0; CodePoint codePoint = default; for (int i = count; i > 0; i--) { int match = index + i; codePointCount += this.glyphs[match].Data.CodePointCount; CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) { if (!CodePoint.IsZeroWidthJoiner(currentCodePoint) && !CodePoint.IsZeroWidthNonJoiner(currentCodePoint)) { codePoint = currentCodePoint; } } this.glyphs.RemoveAt(match); } // Assign our new id at the index. GlyphShapingData current = this.glyphs[index].Data; if (codePoint != default) { current.CodePoint = codePoint; } current.CodePointCount += codePointCount; current.GlyphId = glyphId; current.LigatureId = 0; current.LigatureComponent = -1; current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; current.AppliedFeatures.Add(feature); } /// /// Replaces a single glyph id with a collection of glyph ids. /// /// The zero-based index of the element to replace. /// The collection of replacement glyph ids. /// The feature to apply to the glyph at the specified index. public void Replace(int index, ReadOnlySpan glyphIds, Tag feature) { if (glyphIds.Length > 0) { OffsetGlyphDataPair pair = this.glyphs[index]; GlyphShapingData current = pair.Data; current.GlyphId = glyphIds[0]; current.LigatureComponent = 0; current.MarkAttachment = -1; current.CursiveAttachment = -1; current.IsSubstituted = true; current.IsDecomposed = true; // Add additional glyphs from the rest of the sequence. if (glyphIds.Length > 1) { glyphIds = glyphIds[1..]; for (int i = 0; i < glyphIds.Length; i++) { GlyphShapingData data = new(current, false) { GlyphId = glyphIds[i], LigatureComponent = i + 1 }; data.AppliedFeatures.Add(feature); this.glyphs.Insert(++index, new(pair.Offset, data)); } } } else { // Spec disallows removal of glyphs in this manner but it's common enough practice to allow it. // https://github.com/MicrosoftDocs/typography-issues/issues/673 this.glyphs.RemoveAt(index); } } public void Insert(int index, GlyphShapingData data) { OffsetGlyphDataPair pair = this.glyphs[index]; this.glyphs.Insert(index, new(pair.Offset, data)); } [DebuggerDisplay("{DebuggerDisplay,nq}")] private class OffsetGlyphDataPair { public OffsetGlyphDataPair(int offset, GlyphShapingData data) { this.Offset = offset; this.Data = data; } public int Offset { get; set; } public GlyphShapingData Data { get; set; } private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); } } }