465 lines
19 KiB
C#
465 lines
19 KiB
C#
// 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 {
|
|
/// <summary>
|
|
/// Represents a collection of glyph indices that are mapped to input codepoints.
|
|
/// </summary>
|
|
internal sealed class GlyphSubstitutionCollection : IGlyphShapingCollection
|
|
{
|
|
/// <summary>
|
|
/// Contains a map the index of a map within the collection, non-sequential codepoint offsets, and their glyph ids.
|
|
/// </summary>
|
|
private readonly List<OffsetGlyphDataPair> glyphs = [];
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="GlyphSubstitutionCollection"/> class.
|
|
/// </summary>
|
|
/// <param name="textOptions">The text options.</param>
|
|
public GlyphSubstitutionCollection(TextOptions textOptions) => this.TextOptions = textOptions;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
public int Count => this.glyphs.Count;
|
|
|
|
/// <inheritdoc />
|
|
public TextOptions TextOptions { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the running id of any ligature glyphs contained withing this collection are a member of.
|
|
/// </summary>
|
|
public int LigatureId { get; set; } = 1;
|
|
|
|
/// <inheritdoc />
|
|
public GlyphShapingData this[int index]
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => this.glyphs[index].Data;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the shaping data at the specified position.
|
|
/// </summary>
|
|
/// <param name="index">The zero-based index of the elements to get.</param>
|
|
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
|
|
/// <returns>The <see cref="GlyphShapingData"/>.</returns>
|
|
internal GlyphShapingData GetGlyphShapingData(int index, out int offset)
|
|
{
|
|
OffsetGlyphDataPair pair = this.glyphs[index];
|
|
offset = pair.Offset;
|
|
return pair.Data;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void EnableShapingFeature(int index, Tag feature)
|
|
{
|
|
GlyphShapingData data = this.glyphs[index].Data;
|
|
List<TagEntry> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void DisableShapingFeature(int index, Tag feature)
|
|
{
|
|
GlyphShapingData data = this.glyphs[index].Data;
|
|
List<TagEntry> 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a clone of the glyph shaping data to the collection at the specified offset.
|
|
/// </summary>
|
|
/// <param name="data">The data.</param>
|
|
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
|
|
public void AddGlyph(GlyphShapingData data, int offset)
|
|
=> this.glyphs.Add(new(offset, new(data, false)));
|
|
|
|
/// <summary>
|
|
/// Adds the glyph id and the codepoint it represents to the collection.
|
|
/// </summary>
|
|
/// <param name="glyphId">The id of the glyph to add.</param>
|
|
/// <param name="codePoint">The codepoint the glyph represents.</param>
|
|
/// <param name="direction">The resolved text direction for the codepoint.</param>
|
|
/// <param name="textRun">The text run this glyph belongs to.</param>
|
|
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
|
|
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,
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Adds an atomic inline placeholder to the collection.
|
|
/// </summary>
|
|
/// <param name="codePoint">The object replacement codepoint used for Unicode processing.</param>
|
|
/// <param name="bidiRun">The resolved bidi run for the placeholder.</param>
|
|
/// <param name="textRun">The text run this placeholder belongs to.</param>
|
|
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
|
|
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,
|
|
}));
|
|
|
|
/// <summary>
|
|
/// Moves the specified glyph to the specified position.
|
|
/// </summary>
|
|
/// <param name="fromIndex">The index to move from.</param>
|
|
/// <param name="toIndex">The index to move to.</param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reverses the order of elements in the specified range of the collection.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The range is interpreted as half-open, from <paramref name="startIndex"/> (inclusive)
|
|
/// to <paramref name="endIndex"/> (exclusive).
|
|
///
|
|
/// Both indices are clamped to the valid range [0, <see cref="Count"/>].
|
|
/// 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 <see cref="Count"/>; in such
|
|
/// cases the range is considered valid but may be empty.
|
|
/// </remarks>
|
|
/// <param name="startIndex">
|
|
/// The zero-based index at which to start reversing (inclusive). This value should be
|
|
/// greater than or equal to 0. Values greater than <see cref="Count"/> are treated as
|
|
/// <see cref="Count"/>.
|
|
/// </param>
|
|
/// <param name="endIndex">
|
|
/// The zero-based index at which to stop reversing (exclusive). This value should be
|
|
/// greater than or equal to <paramref name="startIndex"/>. Values greater than
|
|
/// <see cref="Count"/> are treated as <see cref="Count"/>.
|
|
/// </param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs a stable sort of the glyphs by the comparison delegate starting at the specified index.
|
|
/// Only the <see cref="GlyphShapingData"/> references are reordered; offsets remain in place.
|
|
/// </summary>
|
|
/// <param name="startIndex">The start index.</param>
|
|
/// <param name="endIndex">The end index.</param>
|
|
/// <param name="comparer">The comparison delegate.</param>
|
|
public void Sort(int startIndex, int endIndex, Comparison<GlyphShapingData> 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<OffsetGlyphDataPair> 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--;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all elements from the collection.
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
this.glyphs.Clear();
|
|
this.LigatureId = 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the specified glyph ids matching the given codepoint offset.
|
|
/// </summary>
|
|
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
|
|
/// <param name="data">
|
|
/// 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.
|
|
/// </param>
|
|
/// <returns>
|
|
/// <see langword="true"/> if the <see cref="GlyphSubstitutionCollection"/> contains glyph ids
|
|
/// for the specified offset; otherwise, <see langword="false"/>.
|
|
/// </returns>
|
|
public bool TryGetGlyphShapingDataAtOffset(int offset, [NotNullWhen(true)] out IReadOnlyList<GlyphShapingData>? data)
|
|
{
|
|
List<GlyphShapingData> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs a 1:1 replacement of a glyph id at the given position.
|
|
/// </summary>
|
|
/// <param name="index">The zero-based index of the element to replace.</param>
|
|
/// <param name="glyphId">The replacement glyph id.</param>
|
|
/// <param name="feature">The feature to apply to the glyph at the specified index.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="index">The zero-based index of the element to replace.</param>
|
|
/// <param name="removalIndices">The indices at which to remove elements.</param>
|
|
/// <param name="glyphId">The replacement glyph id.</param>
|
|
/// <param name="ligatureId">The ligature id.</param>
|
|
/// <param name="feature">The feature to apply to the glyph at the specified index.</param>
|
|
public void Replace(int index, ReadOnlySpan<int> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs a 1:1 replacement of a glyph id at the given position while removing a series of glyph ids.
|
|
/// </summary>
|
|
/// <param name="index">The zero-based index of the element to replace.</param>
|
|
/// <param name="count">The number of glyphs to remove.</param>
|
|
/// <param name="glyphId">The replacement glyph id.</param>
|
|
/// <param name="feature">The feature to apply to the glyph at the specified index.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces a single glyph id with a collection of glyph ids.
|
|
/// </summary>
|
|
/// <param name="index">The zero-based index of the element to replace.</param>
|
|
/// <param name="glyphIds">The collection of replacement glyph ids.</param>
|
|
/// <param name="feature">The feature to apply to the glyph at the specified index.</param>
|
|
public void Replace(int index, ReadOnlySpan<ushort> 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()}");
|
|
}
|
|
}
|
|
}
|