Skip to content

Commit 48b8ab1

Browse files
Merge pull request #509 from SixLabors/js/optimize-fix
Implement Hebrew and Thai/Lao Font Shapers
2 parents ee58d0d + 07a5484 commit 48b8ab1

File tree

21 files changed

+949
-67
lines changed

21 files changed

+949
-67
lines changed

src/SixLabors.Fonts/GlyphPositioningCollection.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,28 @@ public GlyphShapingData this[int index]
4040

4141
/// <inheritdoc />
4242
public void AddShapingFeature(int index, TagEntry feature)
43-
=> this.glyphs[index].Data.Features.Add(feature);
43+
{
44+
GlyphShapingData data = this.glyphs[index].Data;
45+
data.Features.Add(feature);
46+
if (feature.Enabled)
47+
{
48+
data.EnabledFeatureTags.Add(feature.Tag);
49+
}
50+
}
4451

4552
/// <inheritdoc />
4653
public void EnableShapingFeature(int index, Tag feature)
4754
{
48-
List<TagEntry> features = this.glyphs[index].Data.Features;
55+
GlyphShapingData data = this.glyphs[index].Data;
56+
List<TagEntry> features = data.Features;
4957
for (int i = 0; i < features.Count; i++)
5058
{
5159
TagEntry tagEntry = features[i];
5260
if (tagEntry.Tag == feature)
5361
{
5462
tagEntry.Enabled = true;
5563
features[i] = tagEntry;
64+
data.EnabledFeatureTags.Add(feature);
5665
break;
5766
}
5867
}
@@ -61,14 +70,16 @@ public void EnableShapingFeature(int index, Tag feature)
6170
/// <inheritdoc />
6271
public void DisableShapingFeature(int index, Tag feature)
6372
{
64-
List<TagEntry> features = this.glyphs[index].Data.Features;
73+
GlyphShapingData data = this.glyphs[index].Data;
74+
List<TagEntry> features = data.Features;
6575
for (int i = 0; i < features.Count; i++)
6676
{
6777
TagEntry tagEntry = features[i];
6878
if (tagEntry.Tag == feature)
6979
{
7080
tagEntry.Enabled = false;
7181
features[i] = tagEntry;
82+
data.EnabledFeatureTags.Remove(feature);
7283
break;
7384
}
7485
}
@@ -78,6 +89,10 @@ public void DisableShapingFeature(int index, Tag feature)
7889
/// Gets the glyph metrics at the given codepoint offset.
7990
/// </summary>
8091
/// <param name="offset">The zero-based index within the input codepoint collection.</param>
92+
/// <param name="startIndex">
93+
/// The index within the glyph list to start searching from. Updated to the position of the match
94+
/// so that subsequent calls with increasing offsets avoid rescanning from the beginning.
95+
/// </param>
8196
/// <param name="pointSize">The font size in PT units of the font containing this glyph.</param>
8297
/// <param name="isSubstituted">Whether the glyph is the result of a substitution.</param>
8398
/// <param name="isVerticalSubstitution">Whether the glyph is the result of a vertical substitution.</param>
@@ -90,6 +105,7 @@ public void DisableShapingFeature(int index, Tag feature)
90105
/// <returns>The metrics.</returns>
91106
public bool TryGetGlyphMetricsAtOffset(
92107
int offset,
108+
ref int startIndex,
93109
out float pointSize,
94110
out bool isSubstituted,
95111
out bool isVerticalSubstitution,
@@ -106,10 +122,15 @@ public bool TryGetGlyphMetricsAtOffset(
106122
Tag vrt2 = FeatureTags.VerticalAlternatesAndRotation;
107123
Tag vrtr = FeatureTags.VerticalAlternatesForRotation;
108124

109-
for (int i = 0; i < this.glyphs.Count; i++)
125+
for (int i = startIndex; i < this.glyphs.Count; i++)
110126
{
111127
if (this.glyphs[i].Offset == offset)
112128
{
129+
if (match.Count == 0)
130+
{
131+
startIndex = i;
132+
}
133+
113134
GlyphPositioningData glyph = this.glyphs[i];
114135
isSubstituted = glyph.Data.IsSubstituted;
115136
isDecomposed = glyph.Data.IsDecomposed;

src/SixLabors.Fonts/GlyphShapingData.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace SixLabors.Fonts;
1414
[DebuggerDisplay("{DebuggerDisplay,nq}")]
1515
internal class GlyphShapingData
1616
{
17+
private ushort glyphId;
18+
1719
/// <summary>
1820
/// Initializes a new instance of the <see cref="GlyphShapingData"/> class.
1921
/// </summary>
@@ -62,6 +64,10 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false)
6264
if (!clearFeatures)
6365
{
6466
this.Features.AddRange(data.Features);
67+
foreach (Tag tag in data.EnabledFeatureTags)
68+
{
69+
this.EnabledFeatureTags.Add(tag);
70+
}
6571
}
6672

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

7278
this.Bounds = data.Bounds;
79+
this.CachedShapingClass = data.CachedShapingClass;
80+
this.ShapingClassCacheKey = data.ShapingClassCacheKey;
81+
}
82+
83+
/// <summary>
84+
/// Gets or sets the glyph id. Setting this value invalidates the cached shaping class.
85+
/// </summary>
86+
public ushort GlyphId
87+
{
88+
get => this.glyphId;
89+
set
90+
{
91+
if (this.glyphId != value)
92+
{
93+
this.glyphId = value;
94+
this.ShapingClassCacheKey = -1;
95+
}
96+
}
7397
}
7498

7599
/// <summary>
76-
/// Gets or sets the glyph id.
100+
/// Gets or sets the cached glyph shaping class, avoiding repeated GDEF lookups.
101+
/// </summary>
102+
internal GlyphShapingClass CachedShapingClass { get; set; }
103+
104+
/// <summary>
105+
/// Gets or sets the cache key for <see cref="CachedShapingClass"/>.
106+
/// A value of <c>-1</c> indicates the cache is invalid. Valid entries store the glyph id.
77107
/// </summary>
78-
public ushort GlyphId { get; set; }
108+
internal int ShapingClassCacheKey { get; set; } = -1;
79109

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

160+
/// <summary>
161+
/// Gets the set of feature tags that are currently enabled, maintained
162+
/// in sync with <see cref="Features"/> for O(1) lookup.
163+
/// </summary>
164+
internal HashSet<Tag> EnabledFeatureTags { get; } = [];
165+
130166
/// <summary>
131167
/// Gets or sets the collection of applied features.
132168
/// </summary>

src/SixLabors.Fonts/GlyphSubstitutionCollection.cs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,28 @@ internal GlyphShapingData GetGlyphShapingData(int index, out int offset)
6161

6262
/// <inheritdoc />
6363
public void AddShapingFeature(int index, TagEntry feature)
64-
=> this.glyphs[index].Data.Features.Add(feature);
64+
{
65+
GlyphShapingData data = this.glyphs[index].Data;
66+
data.Features.Add(feature);
67+
if (feature.Enabled)
68+
{
69+
data.EnabledFeatureTags.Add(feature.Tag);
70+
}
71+
}
6572

6673
/// <inheritdoc />
6774
public void EnableShapingFeature(int index, Tag feature)
6875
{
69-
List<TagEntry> features = this.glyphs[index].Data.Features;
76+
GlyphShapingData data = this.glyphs[index].Data;
77+
List<TagEntry> features = data.Features;
7078
for (int i = 0; i < features.Count; i++)
7179
{
7280
TagEntry tagEntry = features[i];
7381
if (tagEntry.Tag == feature)
7482
{
7583
tagEntry.Enabled = true;
7684
features[i] = tagEntry;
85+
data.EnabledFeatureTags.Add(feature);
7786
break;
7887
}
7988
}
@@ -82,14 +91,16 @@ public void EnableShapingFeature(int index, Tag feature)
8291
/// <inheritdoc />
8392
public void DisableShapingFeature(int index, Tag feature)
8493
{
85-
List<TagEntry> features = this.glyphs[index].Data.Features;
94+
GlyphShapingData data = this.glyphs[index].Data;
95+
List<TagEntry> features = data.Features;
8696
for (int i = 0; i < features.Count; i++)
8797
{
8898
TagEntry tagEntry = features[i];
8999
if (tagEntry.Tag == feature)
90100
{
91101
tagEntry.Enabled = false;
92102
features[i] = tagEntry;
103+
data.EnabledFeatureTags.Remove(feature);
93104
break;
94105
}
95106
}
@@ -189,27 +200,29 @@ public void ReverseRange(int startIndex, int endIndex)
189200

190201
/// <summary>
191202
/// Performs a stable sort of the glyphs by the comparison delegate starting at the specified index.
203+
/// Only the <see cref="GlyphShapingData"/> references are reordered; offsets remain in place.
192204
/// </summary>
193205
/// <param name="startIndex">The start index.</param>
194206
/// <param name="endIndex">The end index.</param>
195207
/// <param name="comparer">The comparison delegate.</param>
196208
public void Sort(int startIndex, int endIndex, Comparison<GlyphShapingData> comparer)
197209
{
210+
// Stable insertion sort using adjacent swaps of Data references.
211+
// The sorted ranges are typically small (syllable clusters of 2-10 glyphs),
212+
// so insertion sort is optimal and avoids allocations. Adjacent swaps
213+
// replace the previous MoveGlyph approach which shifted all intermediate elements.
214+
List<OffsetGlyphDataPair> glyphs = this.glyphs;
198215
for (int i = startIndex + 1; i < endIndex; i++)
199216
{
200217
int j = i;
201-
while (j > startIndex && comparer(this[j - 1], this[i]) > 0)
218+
while (j > startIndex && comparer(glyphs[j - 1].Data, glyphs[j].Data) > 0)
202219
{
220+
// Swap Data references between adjacent slots.
221+
GlyphShapingData temp = glyphs[j - 1].Data;
222+
glyphs[j - 1].Data = glyphs[j].Data;
223+
glyphs[j].Data = temp;
203224
j--;
204225
}
205-
206-
if (i == j)
207-
{
208-
continue;
209-
}
210-
211-
// Move item i to occupy place for item j, shift what's in between.
212-
this.MoveGlyph(i, j);
213226
}
214227
}
215228

src/SixLabors.Fonts/Tables/AdvancedTypographic/AdvancedTypographicUtils.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,13 @@ public static bool IsMarkGlyph(FontMetrics fontMetrics, ushort glyphId, GlyphSha
361361

362362
public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, ushort glyphId, GlyphShapingData shapingData)
363363
{
364+
// Cache the shaping class on the GlyphShapingData to avoid repeated GDEF lookups.
365+
// The cache key stores the glyph id; -1 means "not cached".
366+
if (shapingData.ShapingClassCacheKey == glyphId)
367+
{
368+
return shapingData.CachedShapingClass;
369+
}
370+
364371
bool isMark;
365372
bool isBase;
366373
bool isLigature;
@@ -383,7 +390,10 @@ public static GlyphShapingClass GetGlyphShapingClass(FontMetrics fontMetrics, us
383390
isLigature = shapingData.CodePointCount > 1;
384391
}
385392

386-
return new GlyphShapingClass(isMark, isBase, isLigature, markAttachmentType);
393+
GlyphShapingClass result = new(isMark, isBase, isLigature, markAttachmentType);
394+
shapingData.CachedShapingClass = result;
395+
shapingData.ShapingClassCacheKey = glyphId;
396+
return result;
387397
}
388398

389399
public static bool IsInMarkFilteringSet(FontMetrics fontMetrics, ushort markFilteringSet, ushort glyphId)

src/SixLabors.Fonts/Tables/AdvancedTypographic/ClassDefinitionTable.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,24 @@ public static ClassDefinitionFormat2Table Load(BigEndianBinaryReader reader)
143143
/// <inheritdoc />
144144
public override int ClassIndexOf(ushort glyphId)
145145
{
146-
for (int i = 0; i < this.records.Length; i++)
146+
// Records are ordered by StartGlyphId, so use binary search to find the
147+
// candidate range whose StartGlyphId is <= glyphId.
148+
ClassRangeRecord[] records = this.records;
149+
int lo = 0;
150+
int hi = records.Length - 1;
151+
while (lo <= hi)
147152
{
148-
ClassRangeRecord rec = this.records[i];
149-
if (rec.StartGlyphId <= glyphId && glyphId <= rec.EndGlyphId)
153+
int mid = (int)(((uint)lo + (uint)hi) >> 1);
154+
ClassRangeRecord rec = records[mid];
155+
if (glyphId < rec.StartGlyphId)
156+
{
157+
hi = mid - 1;
158+
}
159+
else if (glyphId > rec.EndGlyphId)
160+
{
161+
lo = mid + 1;
162+
}
163+
else
150164
{
151165
return rec.Class;
152166
}

src/SixLabors.Fonts/Tables/AdvancedTypographic/CoverageTable.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,24 @@ private CoverageFormat2Table(CoverageRangeRecord[] records)
8484

8585
public override int CoverageIndexOf(ushort glyphId)
8686
{
87-
for (int i = 0; i < this.records.Length; i++)
87+
// Records are ordered by StartGlyphId, so use binary search to find the
88+
// candidate range whose StartGlyphId is <= glyphId.
89+
CoverageRangeRecord[] records = this.records;
90+
int lo = 0;
91+
int hi = records.Length - 1;
92+
while (lo <= hi)
8893
{
89-
CoverageRangeRecord rec = this.records[i];
90-
if (rec.StartGlyphId <= glyphId && glyphId <= rec.EndGlyphId)
94+
int mid = (int)(((uint)lo + (uint)hi) >> 1);
95+
CoverageRangeRecord rec = records[mid];
96+
if (glyphId < rec.StartGlyphId)
97+
{
98+
hi = mid - 1;
99+
}
100+
else if (glyphId > rec.EndGlyphId)
101+
{
102+
lo = mid + 1;
103+
}
104+
else
91105
{
92106
return rec.Index + glyphId - rec.StartGlyphId;
93107
}

src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType2SubTable.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,25 @@ public static PairSetTable Load(BigEndianBinaryReader reader, long offset, Value
160160

161161
public bool TryGetPairValueRecord(ushort glyphId, [NotNullWhen(true)] out PairValueRecord pairValueRecord)
162162
{
163-
foreach (PairValueRecord pair in this.pairValueRecords)
163+
// Records are ordered by SecondGlyph, so use binary search.
164+
PairValueRecord[] records = this.pairValueRecords;
165+
int lo = 0;
166+
int hi = records.Length - 1;
167+
while (lo <= hi)
164168
{
165-
if (pair.SecondGlyph == glyphId)
169+
int mid = (int)(((uint)lo + (uint)hi) >> 1);
170+
ushort midGlyph = records[mid].SecondGlyph;
171+
if (glyphId < midGlyph)
166172
{
167-
pairValueRecord = pair;
173+
hi = mid - 1;
174+
}
175+
else if (glyphId > midGlyph)
176+
{
177+
lo = mid + 1;
178+
}
179+
else
180+
{
181+
pairValueRecord = records[mid];
168182
return true;
169183
}
170184
}

src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,7 @@ current is not ScriptClass.Common and not ScriptClass.Unknown and not ScriptClas
194194
goto EndLookups;
195195
}
196196

197-
List<TagEntry> glyphFeatures = collection[iterator.Index].Features;
198-
if (!HasFeature(glyphFeatures, in feature))
197+
if (!collection[iterator.Index].EnabledFeatureTags.Contains(feature))
199198
{
200199
iterator.Next();
201200
continue;
@@ -367,20 +366,6 @@ private ScriptClass GetScriptClass(ScriptClass current)
367366
return ScriptClass.Default;
368367
}
369368

370-
private static bool HasFeature(List<TagEntry> glyphFeatures, in Tag feature)
371-
{
372-
for (int i = 0; i < glyphFeatures.Count; i++)
373-
{
374-
TagEntry entry = glyphFeatures[i];
375-
if (entry.Tag == feature && entry.Enabled)
376-
{
377-
return true;
378-
}
379-
}
380-
381-
return false;
382-
}
383-
384369
private static void FixCursiveAttachment(GlyphPositioningCollection collection, int index, int count)
385370
{
386371
LayoutMode layoutMode = collection.TextOptions.LayoutMode;

0 commit comments

Comments
 (0)