Skip to content

Commit ead3f20

Browse files
committed
Performance improvements
1 parent 2c371ea commit ead3f20

8 files changed

Lines changed: 335 additions & 144 deletions

File tree

src/EPPlus.Fonts.OpenType/Integration/TextLayoutEngine.cs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ public List<string> WrapText(
143143
}
144144

145145
private List<string> WrapParagraph(
146-
string text,
147-
float fontSize,
148-
double maxWidthPoints,
149-
double startingWidthPoints,
150-
ShapingOptions options)
146+
string text,
147+
float fontSize,
148+
double maxWidthPoints,
149+
double startingWidthPoints,
150+
ShapingOptions options)
151151
{
152152
_lineListBuffer.Clear();
153153

@@ -157,22 +157,35 @@ private List<string> WrapParagraph(
157157
return new List<string>(_lineListBuffer);
158158
}
159159

160-
// Get buffer from pool and extract widths
160+
// CHANGED: Use light shaping pipeline instead of full shaping
161+
// This gives us 8 bytes/glyph instead of 56 bytes/glyph (85% reduction!)
162+
var glyphs = _shaper.ShapeLight(text, options);
163+
164+
// Convert glyph widths to character widths
161165
var charWidths = GetCharWidthBuffer(text.Length);
162-
_shaper.ExtractCharWidths(text, fontSize, options, charWidths);
166+
Array.Clear(charWidths, 0, text.Length);
167+
168+
double scaleFactor = fontSize / _shaper.UnitsPerEm;
169+
170+
foreach (var glyph in glyphs)
171+
{
172+
int charIndex = glyph.ClusterIndex;
173+
if (charIndex >= 0 && charIndex < text.Length)
174+
{
175+
charWidths[charIndex] += glyph.XAdvance * scaleFactor;
176+
}
177+
}
163178

164-
// Use cached space width instead of measuring every time
179+
// Use cached space width
165180
double spaceWidth = GetCachedSpaceWidth(fontSize, options);
166181

182+
// Rest of wrapping logic unchanged...
167183
int lineStart = 0;
168184
int wordStart = 0;
169185
double currentLineWidth = startingWidthPoints;
170186
double currentWordWidth = 0;
171187

172-
// Reuse pooled StringBuilder instead of creating new
173-
// .NET 3.5 compatible: use Length = 0 instead of Clear()
174188
_lineBuilder.Length = 0;
175-
// Ensure capacity for typical line length
176189
if (_lineBuilder.Capacity < text.Length / 4 + 20)
177190
{
178191
_lineBuilder.Capacity = text.Length / 4 + 20;

src/EPPlus.Fonts.OpenType/TextShaping/Contextual/ChainingContextualProcessor.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ private List<ShapedGlyph> ApplyLigatureSubstitutionAtPosition(
363363
{
364364
// Create ligature
365365
var result = new List<ShapedGlyph>(glyphs);
366-
var ligatureGlyph = CreateLigatureGlyph(glyphs, position, componentCount, ligature.LigatureGlyph);
366+
var ligatureGlyph = CreateLigatureGlyph(glyphs, position, (byte)componentCount, ligature.LigatureGlyph);
367367

368368
result.RemoveRange(position, componentCount);
369369
result.Insert(position, ligatureGlyph);
@@ -379,7 +379,7 @@ private List<ShapedGlyph> ApplyLigatureSubstitutionAtPosition(
379379

380380
private ShapedGlyph CreateSubstitutedGlyph(ShapedGlyph original, ushort newGlyphId)
381381
{
382-
int advanceWidth = _font.HmtxTable.GetAdvanceWidth(newGlyphId);
382+
var advanceWidth = (short)_font.HmtxTable.GetAdvanceWidth(newGlyphId);
383383

384384
return new ShapedGlyph
385385
{
@@ -396,11 +396,11 @@ private ShapedGlyph CreateSubstitutedGlyph(ShapedGlyph original, ushort newGlyph
396396
private ShapedGlyph CreateLigatureGlyph(
397397
List<ShapedGlyph> glyphs,
398398
int startIndex,
399-
int componentCount,
399+
byte componentCount,
400400
ushort ligatureGlyphId)
401401
{
402-
int advanceWidth = _font.HmtxTable.GetAdvanceWidth(ligatureGlyphId);
403-
int clusterIndex = glyphs[startIndex].ClusterIndex;
402+
var advanceWidth = (short)_font.HmtxTable.GetAdvanceWidth(ligatureGlyphId);
403+
var clusterIndex = glyphs[startIndex].ClusterIndex;
404404

405405
return new ShapedGlyph
406406
{

src/EPPlus.Fonts.OpenType/TextShaping/Ligatures/LigatureProcessor.cs

Lines changed: 75 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ Date Author Change
1515
using EPPlus.Fonts.OpenType.Tables.Gsub.Data.Lookups;
1616
using OfficeOpenXml.Interfaces.Drawing.Text;
1717
using System.Collections.Generic;
18+
using System.Linq;
1819

1920
namespace EPPlus.Fonts.OpenType.TextShaping.Ligatures
2021
{
2122
internal class LigatureProcessor
2223
{
24+
private readonly List<LookupTable> _ligaLookups;
25+
2326
public LigatureProcessor(OpenTypeFont font)
2427
{
2528
_font = font;
29+
_ligaLookups = FindLookupsForFeature(font.GsubTable, "liga");
2630
}
2731

2832
private readonly OpenTypeFont _font;
@@ -45,162 +49,137 @@ internal List<ShapedGlyph> ApplyLigatures(List<ShapedGlyph> glyphs)
4549
// Apply each lookup in order
4650
foreach (var lookup in ligaLookups)
4751
{
48-
glyphs = ApplyLigatureLookup(glyphs, lookup);
52+
ApplyLigaturesInPlace(glyphs);
4953
}
5054

5155
return glyphs;
5256
}
5357

54-
/// <summary>
55-
/// Finds all lookups associated with a feature tag.
56-
/// </summary>
57-
private List<LookupTable> FindLookupsForFeature(GsubTable gsub, string featureTag)
58+
internal void ApplyLigaturesInPlace(List<ShapedGlyph> glyphs)
5859
{
59-
var lookups = new List<LookupTable>();
60+
if (_ligaLookups.Count == 0) return;
6061

61-
foreach (var featureRecord in gsub.FeatureList.FeatureRecords)
62+
foreach (var lookup in _ligaLookups)
6263
{
63-
if (featureRecord.FeatureTag.Value == featureTag)
64+
if (lookup.LookupType != 4) continue;
65+
66+
int i = 0;
67+
while (i < glyphs.Count)
6468
{
65-
var feature = featureRecord.FeatureTable;
69+
bool substituted = false;
6670

67-
foreach (var lookupIndex in feature.LookupListIndices)
71+
foreach (var subtableObj in lookup.SubTables)
6872
{
69-
if (lookupIndex < gsub.LookupList.Lookups.Count)
73+
if (subtableObj is not LigatureSubstSubTable subtable) continue;
74+
75+
if (TryApplyLigatureInPlace(glyphs, i, subtable, out int consumed))
7076
{
71-
lookups.Add(gsub.LookupList.Lookups[lookupIndex]);
77+
substituted = true;
78+
i += consumed; // Oftast 1 efter ersättning
79+
break; // Första match vinner – hoppa ur
7280
}
7381
}
82+
83+
if (!substituted) i++;
7484
}
7585
}
76-
77-
return lookups;
7886
}
7987

80-
/// <summary>
81-
/// Applies a single ligature lookup to the glyph sequence.
82-
/// Processes left-to-right, replacing matching sequences with ligatures.
83-
/// </summary>
84-
private List<ShapedGlyph> ApplyLigatureLookup(List<ShapedGlyph> glyphs, LookupTable lookup)
88+
private bool TryApplyLigatureInPlace(
89+
List<ShapedGlyph> glyphs,
90+
int startIndex,
91+
LigatureSubstSubTable subtable,
92+
out int componentsConsumed)
8593
{
86-
if (lookup.LookupType != 4) // Must be Ligature Substitution
87-
return glyphs;
94+
componentsConsumed = 0;
95+
96+
if (startIndex >= glyphs.Count) return false;
8897

89-
var result = new List<ShapedGlyph>();
90-
int i = 0;
98+
ushort first = glyphs[startIndex].GlyphId;
99+
int covIdx = subtable.Coverage.GetGlyphIndex(first);
100+
if (covIdx < 0) return false;
91101

92-
while (i < glyphs.Count)
102+
if (!subtable.LigatureSets.TryGetValue(first, out var ligSet) || ligSet?.Ligatures.Count == 0)
103+
return false;
104+
105+
// Försök längre ligaturer först (rekommenderas av OpenType-spec)
106+
var sortedLigs = ligSet.Ligatures
107+
.OrderByDescending(l => 1 + (l.Components?.Length ?? 0))
108+
.ToList();
109+
110+
foreach (var lig in sortedLigs)
93111
{
94-
bool substituted = false;
112+
int compCount = 1 + (lig.Components?.Length ?? 0);
113+
if (startIndex + compCount > glyphs.Count) continue;
95114

96-
// Try each subtable
97-
foreach (var subtable in lookup.SubTables)
115+
bool match = true;
116+
for (int j = 0; j < lig.Components?.Length; j++)
98117
{
99-
if (subtable is LigatureSubstSubTable ligSubtable)
118+
if (glyphs[startIndex + 1 + j].GlyphId != lig.Components[j])
100119
{
101-
// Try to match ligature starting at position i
102-
if (TryApplyLigature(glyphs, i, ligSubtable, out var ligatureGlyph, out int componentsConsumed))
103-
{
104-
result.Add(ligatureGlyph);
105-
i += componentsConsumed;
106-
substituted = true;
107-
break; // Found a match, move to next position
108-
}
120+
match = false;
121+
break;
109122
}
110123
}
111124

112-
if (!substituted)
125+
if (match)
113126
{
114-
// No ligature found, keep original glyph
115-
result.Add(glyphs[i]);
116-
i++;
127+
var ligGlyph = CreateLigatureGlyph(glyphs, startIndex, (byte)compCount, lig.LigatureGlyph);
128+
129+
// MUTERA DIREKT
130+
glyphs.RemoveRange(startIndex, compCount);
131+
glyphs.Insert(startIndex, ligGlyph);
132+
133+
componentsConsumed = 1; // ligatur tar platsen → nästa steg flyttar förbi den
134+
return true;
117135
}
118136
}
119137

120-
return result;
138+
return false;
121139
}
122140

141+
123142
/// <summary>
124-
/// Attempts to find and apply a ligature substitution starting at the given position.
143+
/// Finds all lookups associated with a feature tag.
125144
/// </summary>
126-
private bool TryApplyLigature(
127-
List<ShapedGlyph> glyphs,
128-
int startIndex,
129-
LigatureSubstSubTable subtable,
130-
out ShapedGlyph ligatureGlyph,
131-
out int componentsConsumed)
145+
private List<LookupTable> FindLookupsForFeature(GsubTable gsub, string featureTag)
132146
{
133-
ligatureGlyph = null;
134-
componentsConsumed = 0;
135-
136-
if (startIndex >= glyphs.Count)
137-
return false;
138-
139-
ushort firstGlyph = glyphs[startIndex].GlyphId;
140-
141-
// Check if first glyph is in coverage
142-
int coverageIndex = subtable.Coverage.GetGlyphIndex(firstGlyph);
143-
if (coverageIndex < 0)
144-
return false;
145-
146-
// LigatureSets is a Dictionary<ushort, LigatureSet>
147-
// Key is the GLYPH ID, not coverage index!
148-
if (!subtable.LigatureSets.TryGetValue(firstGlyph, out var ligatureSet))
149-
return false;
150-
151-
if (ligatureSet?.Ligatures == null)
152-
return false;
147+
var lookups = new List<LookupTable>();
153148

154-
// Try each ligature in the set
155-
foreach (var ligature in ligatureSet.Ligatures)
149+
foreach (var featureRecord in gsub.FeatureList.FeatureRecords)
156150
{
157-
int componentCount = 1 + (ligature.Components?.Length ?? 0);
158-
159-
// Check if we have enough glyphs remaining
160-
if (startIndex + componentCount > glyphs.Count)
161-
continue;
162-
163-
// Check if all component glyphs match
164-
bool matches = true;
165-
166-
if (ligature.Components != null)
151+
if (featureRecord.FeatureTag.Value == featureTag)
167152
{
168-
for (int j = 0; j < ligature.Components.Length; j++)
153+
var feature = featureRecord.FeatureTable;
154+
155+
foreach (var lookupIndex in feature.LookupListIndices)
169156
{
170-
if (glyphs[startIndex + 1 + j].GlyphId != ligature.Components[j])
157+
if (lookupIndex < gsub.LookupList.Lookups.Count)
171158
{
172-
matches = false;
173-
break;
159+
lookups.Add(gsub.LookupList.Lookups[lookupIndex]);
174160
}
175161
}
176162
}
177-
178-
if (matches)
179-
{
180-
// Found a match! Create ligature glyph
181-
ligatureGlyph = CreateLigatureGlyph(glyphs, startIndex, componentCount, ligature.LigatureGlyph);
182-
componentsConsumed = componentCount;
183-
return true;
184-
}
185163
}
186164

187-
return false;
165+
return lookups;
188166
}
189167

168+
190169
/// <summary>
191170
/// Creates a new shaped glyph for a ligature, combining metrics from components.
192171
/// </summary>
193172
private ShapedGlyph CreateLigatureGlyph(
194173
List<ShapedGlyph> glyphs,
195174
int startIndex,
196-
int componentCount,
175+
byte componentCount,
197176
ushort ligatureGlyphId)
198177
{
199178
// Get advance width for ligature glyph
200-
int advanceWidth = _font.HmtxTable.GetAdvanceWidth(ligatureGlyphId);
179+
var advanceWidth = (short)_font.HmtxTable.GetAdvanceWidth(ligatureGlyphId);
201180

202181
// Preserve cluster index from first component
203-
int clusterIndex = glyphs[startIndex].ClusterIndex;
182+
var clusterIndex = glyphs[startIndex].ClusterIndex;
204183

205184
return new ShapedGlyph
206185
{

src/EPPlus.Fonts.OpenType/TextShaping/Positioning/MarkToBaseProvider.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,12 @@ private bool TryPositionMark(
128128

129129
// Calculate mark position relative to base
130130
// Mark is positioned so its anchor aligns with base anchor
131-
int xOffset = baseAnchor.XCoordinate - markAnchor.XCoordinate;
132-
int yOffset = baseAnchor.YCoordinate - markAnchor.YCoordinate;
131+
var xOffset = baseAnchor.XCoordinate - markAnchor.XCoordinate;
132+
var yOffset = baseAnchor.YCoordinate - markAnchor.YCoordinate;
133133

134134
// Apply positioning to mark glyph
135-
markGlyph.XOffset = xOffset;
136-
markGlyph.YOffset = yOffset;
135+
markGlyph.XOffset = (short)xOffset;
136+
markGlyph.YOffset = (short)yOffset;
137137

138138
// Mark should not advance (it's positioned over base)
139139
markGlyph.XAdvance = 0;

0 commit comments

Comments
 (0)