Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/SixLabors.Fonts/GlyphPositioningCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,19 +40,28 @@ public GlyphShapingData this[int index]

/// <inheritdoc />
public void AddShapingFeature(int index, TagEntry feature)
=> this.glyphs[index].Data.Features.Add(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)
{
List<TagEntry> features = this.glyphs[index].Data.Features;
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;
}
}
Expand All @@ -61,14 +70,16 @@ public void EnableShapingFeature(int index, Tag feature)
/// <inheritdoc />
public void DisableShapingFeature(int index, Tag feature)
{
List<TagEntry> features = this.glyphs[index].Data.Features;
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;
}
}
Expand All @@ -78,6 +89,10 @@ public void DisableShapingFeature(int index, Tag feature)
/// Gets the glyph metrics at the given codepoint offset.
/// </summary>
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
/// <param name="startIndex">
/// The index within the glyph list to start searching from. Updated to the position of the match
/// so that subsequent calls with increasing offsets avoid rescanning from the beginning.
/// </param>
/// <param name="pointSize">The font size in PT units of the font containing this glyph.</param>
/// <param name="isSubstituted">Whether the glyph is the result of a substitution.</param>
/// <param name="isVerticalSubstitution">Whether the glyph is the result of a vertical substitution.</param>
Expand All @@ -90,6 +105,7 @@ public void DisableShapingFeature(int index, Tag feature)
/// <returns>The metrics.</returns>
public bool TryGetGlyphMetricsAtOffset(
int offset,
ref int startIndex,
out float pointSize,
out bool isSubstituted,
out bool isVerticalSubstitution,
Expand All @@ -106,10 +122,15 @@ public bool TryGetGlyphMetricsAtOffset(
Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation;
Tag vrtr = FeatureTags.VerticalAlternatesForRotation;

for (int i = 0; i < this.glyphs.Count; i++)
for (int i = startIndex; i < this.glyphs.Count; i++)
{
if (this.glyphs[i].Offset == offset)
{
if (match.Count == 0)
{
startIndex = i;
}

GlyphPositioningData glyph = this.glyphs[i];
isSubstituted = glyph.Data.IsSubstituted;
isDecomposed = glyph.Data.IsDecomposed;
Expand Down
40 changes: 38 additions & 2 deletions src/SixLabors.Fonts/GlyphShapingData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ namespace SixLabors.Fonts;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal class GlyphShapingData
{
private ushort glyphId;

/// <summary>
/// Initializes a new instance of the <see cref="GlyphShapingData"/> class.
/// </summary>
Expand Down Expand Up @@ -62,6 +64,10 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false)
if (!clearFeatures)
{
this.Features.AddRange(data.Features);
foreach (Tag tag in data.EnabledFeatureTags)
{
this.EnabledFeatureTags.Add(tag);
}
}

foreach (Tag feature in data.AppliedFeatures)
Expand All @@ -70,12 +76,36 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false)
}

this.Bounds = data.Bounds;
this.CachedShapingClass = data.CachedShapingClass;
this.ShapingClassCacheKey = data.ShapingClassCacheKey;
}

/// <summary>
/// Gets or sets the glyph id. Setting this value invalidates the cached shaping class.
/// </summary>
public ushort GlyphId
{
get => this.glyphId;
set
{
if (this.glyphId != value)
{
this.glyphId = value;
this.ShapingClassCacheKey = -1;
}
}
}

/// <summary>
/// Gets or sets the glyph id.
/// Gets or sets the cached glyph shaping class, avoiding repeated GDEF lookups.
/// </summary>
internal GlyphShapingClass CachedShapingClass { get; set; }

/// <summary>
/// Gets or sets the cache key for <see cref="CachedShapingClass"/>.
/// A value of <c>-1</c> indicates the cache is invalid. Valid entries store the glyph id.
/// </summary>
public ushort GlyphId { get; set; }
internal int ShapingClassCacheKey { get; set; } = -1;

/// <summary>
/// Gets or sets the leading codepoint.
Expand Down Expand Up @@ -127,6 +157,12 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false)
/// </summary>
public List<TagEntry> Features { get; set; } = [];

/// <summary>
/// Gets the set of feature tags that are currently enabled, maintained
/// in sync with <see cref="Features"/> for O(1) lookup.
/// </summary>
internal HashSet<Tag> EnabledFeatureTags { get; } = [];

/// <summary>
/// Gets or sets the collection of applied features.
/// </summary>
Expand Down
37 changes: 25 additions & 12 deletions src/SixLabors.Fonts/GlyphSubstitutionCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,28 @@ internal GlyphShapingData GetGlyphShapingData(int index, out int offset)

/// <inheritdoc />
public void AddShapingFeature(int index, TagEntry feature)
=> this.glyphs[index].Data.Features.Add(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)
{
List<TagEntry> features = this.glyphs[index].Data.Features;
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;
}
}
Expand All @@ -82,14 +91,16 @@ public void EnableShapingFeature(int index, Tag feature)
/// <inheritdoc />
public void DisableShapingFeature(int index, Tag feature)
{
List<TagEntry> features = this.glyphs[index].Data.Features;
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;
}
}
Expand Down Expand Up @@ -189,27 +200,29 @@ public void ReverseRange(int startIndex, int endIndex)

/// <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(this[j - 1], this[i]) > 0)
while (j > startIndex && comparer(glyphs[j - 1].Data, glyphs[j].Data) > 0)
{
// Swap Data references between adjacent slots.
GlyphShapingData temp = glyphs[j - 1].Data;
glyphs[j - 1].Data = glyphs[j].Data;
glyphs[j].Data = temp;
j--;
}

if (i == j)
{
continue;
}

// Move item i to occupy place for item j, shift what's in between.
this.MoveGlyph(i, j);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,13 @@ public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, GlyphSha

public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData)
{
// Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups.
// The cache key stores the glyph id; -1 means "not cached".
if (shapingData.ShapingClassCacheKey == glyphId)
{
return shapingData.CachedShapingClass;
}

bool isMark;
bool isBase;
bool isLigature;
Expand All @@ -383,7 +390,10 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, us
isLigature = shapingData.CodePointCount > 1;
}

return new GlyphShapingClass(isMark, isBase, isLigature, markAttachmentType);
GlyphShapingClass result = new(isMark, isBase, isLigature, markAttachmentType);
shapingData.CachedShapingClass = result;
shapingData.ShapingClassCacheKey = glyphId;
return result;
}

public static bool IsInMarkFilteringSet(FontMetrics fontMetrics, ushort markFilteringSet, ushort glyphId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,24 @@ public static ClassDefinitionFormat2Table Load(BigEndianBinaryReader reader)
/// <inheritdoc />
public override int ClassIndexOf(ushort glyphId)
{
for (int i = 0; i < this.records.Length; i++)
// Records are ordered by StartGlyphId, so use binary search to find the
// candidate range whose StartGlyphId is <= glyphId.
ClassRangeRecord[] records = this.records;
int lo = 0;
int hi = records.Length - 1;
while (lo <= hi)
{
ClassRangeRecord rec = this.records[i];
if (rec.StartGlyphId <= glyphId && glyphId <= rec.EndGlyphId)
int mid = (int)(((uint)lo + (uint)hi) >> 1);
ClassRangeRecord rec = records[mid];
if (glyphId < rec.StartGlyphId)
{
hi = mid - 1;
}
else if (glyphId > rec.EndGlyphId)
{
lo = mid + 1;
}
else
{
return rec.Class;
}
Expand Down
20 changes: 17 additions & 3 deletions src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,24 @@ private CoverageFormat2Table(CoverageRangeRecord[] records)

public override int CoverageIndexOf(ushort glyphId)
{
for (int i = 0; i < this.records.Length; i++)
// Records are ordered by StartGlyphId, so use binary search to find the
// candidate range whose StartGlyphId is <= glyphId.
CoverageRangeRecord[] records = this.records;
int lo = 0;
int hi = records.Length - 1;
while (lo <= hi)
{
CoverageRangeRecord rec = this.records[i];
if (rec.StartGlyphId <= glyphId && glyphId <= rec.EndGlyphId)
int mid = (int)(((uint)lo + (uint)hi) >> 1);
CoverageRangeRecord rec = records[mid];
if (glyphId < rec.StartGlyphId)
{
hi = mid - 1;
}
else if (glyphId > rec.EndGlyphId)
{
lo = mid + 1;
}
else
{
return rec.Index + glyphId - rec.StartGlyphId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,25 @@ public static PairSetTable Load(BigEndianBinaryReader reader, long offset, Value

public bool TryGetPairValueRecord(ushort glyphId, [NotNullWhen(true)] out PairValueRecord pairValueRecord)
{
foreach (PairValueRecord pair in this.pairValueRecords)
// Records are ordered by SecondGlyph, so use binary search.
PairValueRecord[] records = this.pairValueRecords;
int lo = 0;
int hi = records.Length - 1;
while (lo <= hi)
{
if (pair.SecondGlyph == glyphId)
int mid = (int)(((uint)lo + (uint)hi) >> 1);
ushort midGlyph = records[mid].SecondGlyph;
if (glyphId < midGlyph)
{
pairValueRecord = pair;
hi = mid - 1;
}
else if (glyphId > midGlyph)
{
lo = mid + 1;
}
else
{
pairValueRecord = records[mid];
return true;
}
}
Expand Down
17 changes: 1 addition & 16 deletions src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas
goto EndLookups;
}

List<TagEntry> glyphFeatures = collection[iterator.Index].Features;
if (!HasFeature(glyphFeatures, in feature))
if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature))
{
iterator.Next();
continue;
Expand Down Expand Up @@ -367,20 +366,6 @@ private ScriptClass GetScriptClass(ScriptClass current)
return ScriptClass.Default;
}

private static bool HasFeature(List<TagEntry> glyphFeatures, in Tag feature)
{
for (int i = 0; i < glyphFeatures.Count; i++)
{
TagEntry entry = glyphFeatures[i];
if (entry.Tag == feature && entry.Enabled)
{
return true;
}
}

return false;
}

private static void FixCursiveAttachment(GlyphPositioningCollection collection, int index, int count)
{
LayoutMode layoutMode = collection.TextOptions.LayoutMode;
Expand Down
Loading
Loading