Skip to content

Commit 80a3ee6

Browse files
committed
Performance fixes
1 parent cab5ca4 commit 80a3ee6

7 files changed

Lines changed: 72 additions & 253 deletions

File tree

src/EPPlus.Fonts.OpenType.Tests/Integration/TextLayoutEngineTests.cs

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -361,69 +361,47 @@ public void WrapLongRichTextWord()
361361
[TestMethod]
362362
public void WrapRichTextDifficultCase()
363363
{
364-
List<string> lstOfRichText = new() { "TextBox\r\na\r\n", "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" };
365-
366-
var font1 = new MeasurementFont()
367-
{
368-
FontFamily = "Aptos Narrow",
369-
Size = 11,
370-
Style = MeasurementFontStyles.Regular
371-
}; ;
372-
373-
var font2 = new MeasurementFont()
374-
{
375-
FontFamily = "Aptos Narrow",
376-
Size = 11,
377-
Style = MeasurementFontStyles.Bold
378-
};
379-
380-
var font3 = new MeasurementFont()
381-
{
382-
FontFamily = "Aptos Narrow",
383-
Size = 11,
384-
Style = MeasurementFontStyles.Underline
385-
};
386-
387-
var font4 = new MeasurementFont()
388-
{
389-
FontFamily = "Aptos Narrow",
390-
Size = 11,
391-
Style = MeasurementFontStyles.Strikeout
392-
};
393-
394-
var font5 = new MeasurementFont()
395-
{
396-
FontFamily = "Goudy Stout",
397-
Size = 16,
398-
Style = MeasurementFontStyles.Regular
399-
};
400-
401-
402-
var font6 = new MeasurementFont()
403-
{
404-
FontFamily = "Aptos Narrow",
405-
Size = 24,
406-
Style = MeasurementFontStyles.Regular
407-
};
364+
var sw = System.Diagnostics.Stopwatch.StartNew();
365+
var lap = sw.ElapsedMilliseconds;
408366

367+
List<string> lstOfRichText = new() { "TextBox\r\na\r\n", "TextBox2", "ra underline", "La Strike", "Goudy size 16", "SvgSize 24" };
368+
var font1 = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Regular };
369+
var font2 = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Bold };
370+
var font3 = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Underline };
371+
var font4 = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 11, Style = MeasurementFontStyles.Strikeout };
372+
var font5 = new MeasurementFont() { FontFamily = "Goudy Stout", Size = 16, Style = MeasurementFontStyles.Regular };
373+
var font6 = new MeasurementFont() { FontFamily = "Aptos Narrow", Size = 24, Style = MeasurementFontStyles.Regular };
409374
List<MeasurementFont> fonts = new() { font1, font2, font3, font4, font5, font6 };
410-
var fragments = new List<TextFragment>();
411375

376+
var fragments = new List<TextFragment>();
412377
for (int i = 0; i < lstOfRichText.Count(); i++)
413378
{
414-
var currentFrag = new TextFragment() {Text = lstOfRichText[i], Font = fonts[i] };
415-
fragments.Add(currentFrag);
379+
fragments.Add(new TextFragment() { Text = lstOfRichText[i], Font = fonts[i] });
416380
}
417381

382+
lap = sw.ElapsedMilliseconds;
383+
418384
var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint();
419385

386+
lap = sw.ElapsedMilliseconds;
387+
420388
var startFont = TextData.GetFontData(font1.FontFamily, GetFontSubType(font1.Style));
421389

390+
lap = sw.ElapsedMilliseconds;
391+
422392
var shaper = new TextShaper(startFont);
393+
394+
lap = sw.ElapsedMilliseconds;
395+
423396
var layout = new TextLayoutEngine(shaper);
424397

398+
lap = sw.ElapsedMilliseconds;
399+
425400
var wrappedLines = layout.WrapRichText(fragments, maxSizePoints);
426401

402+
lap = sw.ElapsedMilliseconds;
403+
404+
427405
Assert.AreEqual("TextBox", wrappedLines[0]);
428406
Assert.AreEqual("a", wrappedLines[1]);
429407
Assert.AreEqual("TextBox2ra underlineLa", wrappedLines[2]);

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

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Date Author Change
1111
01/20/2025 EPPlus Software AB TextLayoutEngine implementation
1212
01/22/2025 EPPlus Software AB Optimized with shaping cache
1313
01/23/2025 EPPlus Software AB Fixed lastSpaceIndex bug in multi-fragment wrapping
14+
02/23/2026 EPPlus Software AB Performance fix: Shape() → ShapeLight() in ProcessFragment
1415
*************************************************************************************************/
1516
using EPPlus.Fonts.OpenType.Utilities;
1617
using OfficeOpenXml.Interfaces.Drawing.Text;
@@ -75,11 +76,14 @@ private void ProcessFragment(
7576

7677
var charWidths = GetCharWidthBuffer(len);
7778

78-
var shaped = shaper.Shape(fragment.Text, options);
79+
// ShapeLight applies only kerning (sufficient for line-breaking).
80+
// Full Shape() runs SingleAdjustment + Kerning + MarkToBase which
81+
// is ~250x slower and irrelevant for wrapping decisions.
82+
var glyphWidths = shaper.ShapeLight(fragment.Text, options);
7983
double scale = fragment.Font.Size / shaper.UnitsPerEm;
8084

8185
Array.Clear(charWidths, 0, len);
82-
FillCharWidths(shaped.Glyphs, scale, len, charWidths);
86+
FillCharWidths(glyphWidths, scale, len, charWidths);
8387

8488
int i = 0;
8589
while (i < len)
@@ -92,7 +96,7 @@ private void ProcessFragment(
9296
SkipLineBreakChars(fragment.Text, ref i);
9397
state.CurrentLineWidth = 0;
9498
state.CurrentWordWidth = 0;
95-
state.WordStart = -1; // Reset after line break
99+
state.WordStart = -1;
96100
state.LineStart = -1;
97101
continue;
98102
}
@@ -116,15 +120,10 @@ private void ProcessFragment(
116120

117121
state.WordStart = -1;
118122
state.LineStart = -1;
119-
//We do not append ending spaces to the new line
120123
if (c != ' ')
121124
{
122-
//lineBuilder.Append(c);
123-
if(state.CurrentWordWidth == 0)
125+
if (state.CurrentWordWidth == 0)
124126
{
125-
//The char that made us move past maxWidth
126-
//must be added to the new line
127-
//A whole word being moved down is handled in wrapCurrentLine.
128127
state.CurrentWordWidth = charWidths[i];
129128
state.CurrentLineWidth = charWidths[i];
130129
}
@@ -134,14 +133,18 @@ private void ProcessFragment(
134133
}
135134
}
136135

137-
private void FillCharWidths(ShapedGlyph[] glyphs, double scale, int textLength, double[] charWidths)
136+
/// <summary>
137+
/// Fills character widths from lightweight GlyphWidth structs (8 bytes each).
138+
/// Used by the wrapping pipeline for optimal performance.
139+
/// </summary>
140+
private void FillCharWidths(GlyphWidth[] glyphs, double scale, int textLength, double[] charWidths)
138141
{
139-
foreach (var glyph in glyphs)
142+
for (int i = 0; i < glyphs.Length; i++)
140143
{
141-
int idx = glyph.ClusterIndex;
144+
int idx = glyphs[i].ClusterIndex;
142145
if (idx >= 0 && idx < textLength)
143146
{
144-
charWidths[idx] += glyph.XAdvance * scale;
147+
charWidths[idx] += glyphs[i].XAdvance * scale;
145148
}
146149
}
147150
}
@@ -180,26 +183,22 @@ private void SkipLineBreakChars(string text, ref int i)
180183

181184
private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints)
182185
{
183-
// Bounds check to prevent ArgumentOutOfRangeException
184186
if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length)
185187
{
186188
string line = lineBuilder.ToString(0, state.WordStart).TrimEnd();
187189
_lineListBuffer.Add(line);
188190
lineBuilder.Remove(0, state.WordStart + 1);
189191

190-
//A word was moved down. The new line must have the width and pos of the word.
191192
state.CurrentLineWidth = state.CurrentWordWidth;
192193
state.LineStart = state.WordStart;
193194
}
194195
else
195196
{
196-
var lastChar = lineBuilder[lineBuilder.Length-1];
197-
// No valid space found - wrap entire line
198-
_lineListBuffer.Add(lineBuilder.ToString(0, lineBuilder.Length -1));
197+
var lastChar = lineBuilder[lineBuilder.Length - 1];
198+
_lineListBuffer.Add(lineBuilder.ToString(0, lineBuilder.Length - 1));
199199
state.CurrentLineWidth = 0;
200200
lineBuilder.Length = 0;
201201
lineBuilder.Append(lastChar);
202-
203202
}
204203
}
205204

src/EPPlus.Fonts.OpenType/Subsetting/CmapSubsetProcessor.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,6 @@ public void Discover(FontSubsettingContext context)
3131
{
3232
ushort oldGid;
3333
if (context.OriginalFont.CmapTable.TryGetGlyphId(codePoint, out oldGid))
34-
{
35-
// DEBUGGA: Skriv ut mappningen
36-
Console.WriteLine($"Code point 0x{codePoint:X} → Glyph ID {oldGid}");
37-
}
38-
else
39-
{
40-
// VIKTIGT: Om detta körs för emoji betyder det att fonten inte har den!
41-
Console.WriteLine($"Code point 0x{codePoint:X} NOT FOUND in font!");
42-
}
43-
if (context.OriginalFont.CmapTable.TryGetGlyphId(codePoint, out oldGid))
4434
{
4535
if (!context.IncludedGlyphs.Contains(oldGid))
4636
{

src/EPPlus.Fonts.OpenType/Subsetting/GlyphAndLocaSubsetProcessor.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,6 @@ public void Rewrite(FontSubsettingContext context)
8888
: HeadTable.IndexToLocFormats.Offset32;
8989

9090
context.SubsetFont.AddOrReplaceTable(LocaTable.CreateSubset(offsets, context.SubsetFont.HeadTable.IndexToLocFormat));
91-
Console.WriteLine($"=== GlyfAndLocaSubsetProcessor ===");
92-
Console.WriteLine($"NewToOldGlyphId count: {context.NewToOldGlyphId.Count}");
9391
}
9492

9593
private static bool IsEmpty(Glyph g)

src/EPPlus.Fonts.OpenType/Tables/Gpos/Data/Lookups/LookupType2/PairPosSubTableFormat2.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,54 +64,41 @@ public override bool TryGetPairAdjustment(
6464

6565
// First glyph must be in coverage
6666
int coverageIndex = Coverage.GetGlyphIndex(firstGlyph);
67-
System.Diagnostics.Debug.WriteLine($" Coverage index for {firstGlyph}: {coverageIndex}");
6867

6968
if (coverageIndex < 0)
7069
{
71-
System.Diagnostics.Debug.WriteLine($" ✗ First glyph {firstGlyph} not in coverage");
7270
return false;
7371
}
7472

7573
int class1 = ClassDef1.GetClass(firstGlyph);
7674
int class2 = ClassDef2.GetClass(secondGlyph);
7775

78-
System.Diagnostics.Debug.WriteLine($" Classes: class1={class1}, class2={class2}");
79-
System.Diagnostics.Debug.WriteLine($" Matrix bounds: Class1Count={Class1Count}, Class2Count={Class2Count}");
8076

8177
if (class1 < 0 || class2 < 0)
8278
{
83-
System.Diagnostics.Debug.WriteLine($" ✗ Negative class!");
8479
return false;
8580
}
8681

8782
if (class1 >= Class1Count || class2 >= Class2Count)
8883
{
89-
System.Diagnostics.Debug.WriteLine($" ✗ Class out of bounds!");
9084
return false;
9185
}
9286

9387
var record = ClassMatrix[class1, class2];
9488

9589
if (record == null)
9690
{
97-
System.Diagnostics.Debug.WriteLine($" ✗ Matrix[{class1},{class2}] is null");
9891
return false;
9992
}
10093

101-
System.Diagnostics.Debug.WriteLine($" Matrix[{class1},{class2}] exists:");
102-
System.Diagnostics.Debug.WriteLine($" Value1: {(record.Value1 != null ? $"XAdv={record.Value1.XAdvance}" : "null")}");
103-
System.Diagnostics.Debug.WriteLine($" Value2: {(record.Value2 != null ? $"XAdv={record.Value2.XAdvance}" : "null")}");
104-
10594
if (record.Value1 == null && record.Value2 == null)
10695
{
107-
System.Diagnostics.Debug.WriteLine($" ✗ Both values are null");
10896
return false;
10997
}
11098

11199
value1 = record.Value1;
112100
value2 = record.Value2;
113101

114-
System.Diagnostics.Debug.WriteLine($" ✓ SUCCESS! Returning XAdvance={value1?.XAdvance ?? 0}");
115102
return true;
116103
}
117104

0 commit comments

Comments
 (0)