// 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()}");
}
}
}