Skip to content

Commit f4577a3

Browse files
authored
Fixed richtext wrapping in new wrapper (#2275)
* Fixed richtext wrapping in new wrapper * Removed comments
1 parent ba1e7c0 commit f4577a3

5 files changed

Lines changed: 213 additions & 38 deletions

File tree

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

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
using Microsoft.VisualStudio.TestTools.UnitTesting;
2-
using EPPlus.Fonts.OpenType.Integration;
1+
using EPPlus.Fonts.OpenType.Integration;
32
using EPPlus.Fonts.OpenType.TextShaping;
3+
using EPPlus.Fonts.OpenType.TrueTypeMeasurer.DataHolders;
4+
using EPPlus.Fonts.OpenType.Utils;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
46
using OfficeOpenXml.Interfaces.Drawing.Text;
57
using System.Collections.Generic;
68
using System.Diagnostics;
7-
using EPPlus.Fonts.OpenType.Utils;
9+
using static System.Net.Mime.MediaTypeNames;
810

911
namespace EPPlus.Fonts.OpenType.Tests.Integration
1012
{
@@ -311,6 +313,124 @@ public void WrapRichText_DifferentFonts_WrapsCorrectly()
311313
Assert.AreEqual("This is mixed fonts", rejoined);
312314
}
313315

316+
317+
private FontSubFamily GetFontSubType(MeasurementFontStyles Style)
318+
{
319+
if ((Style & (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic)) == (MeasurementFontStyles.Bold | MeasurementFontStyles.Italic))
320+
{
321+
return FontSubFamily.BoldItalic;
322+
}
323+
else if ((Style & MeasurementFontStyles.Bold) == MeasurementFontStyles.Bold)
324+
{
325+
return FontSubFamily.Bold;
326+
}
327+
else if ((Style & MeasurementFontStyles.Italic) == MeasurementFontStyles.Italic)
328+
{
329+
return FontSubFamily.Italic;
330+
}
331+
332+
return FontSubFamily.Regular;
333+
}
334+
335+
[TestMethod]
336+
public void WrapLongRichTextWord()
337+
{
338+
var mFont = new MeasurementFont()
339+
{
340+
FontFamily = "Aptos Narrow",
341+
Size = 11,
342+
Style = MeasurementFontStyles.Regular
343+
};
344+
345+
var font = OpenTypeFonts.GetFontData(FontFolders, mFont.FontFamily, FontSubFamily.Regular);
346+
347+
var longWord = "pellentesquer";
348+
349+
var fragment = new TextFragment() { Text = longWord, Font = mFont };
350+
var fragLst = new List<TextFragment>() { fragment };
351+
352+
ITextShaper shaper = new TextShaper(font);
353+
using var layout = new TextLayoutEngine(shaper);
354+
355+
var wrappedLines = layout.WrapRichText(fragLst, 54);
356+
357+
Assert.AreEqual("pellentesqu", wrappedLines[0]);
358+
Assert.AreEqual("er", wrappedLines[1]);
359+
}
360+
361+
[TestMethod]
362+
public void WrapRichTextDifficultCase()
363+
{
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+
};
408+
409+
List<MeasurementFont> fonts = new() { font1, font2, font3, font4, font5, font6 };
410+
var fragments = new List<TextFragment>();
411+
412+
for (int i = 0; i < lstOfRichText.Count(); i++)
413+
{
414+
var currentFrag = new TextFragment() {Text = lstOfRichText[i], Font = fonts[i] };
415+
fragments.Add(currentFrag);
416+
}
417+
418+
var maxSizePoints = Math.Round(300d, 0, MidpointRounding.AwayFromZero).PixelToPoint();
419+
420+
var startFont = TextData.GetFontData(font1.FontFamily, GetFontSubType(font1.Style));
421+
422+
var shaper = new TextShaper(startFont);
423+
var layout = new TextLayoutEngine(shaper);
424+
425+
var wrappedLines = layout.WrapRichText(fragments, maxSizePoints);
426+
427+
Assert.AreEqual("TextBox", wrappedLines[0]);
428+
Assert.AreEqual("a", wrappedLines[1]);
429+
Assert.AreEqual("TextBox2ra underlineLa", wrappedLines[2]);
430+
Assert.AreEqual("StrikeGoudy size", wrappedLines[3]);
431+
Assert.AreEqual("16SvgSize 24", wrappedLines[4]);
432+
}
433+
314434
[TestMethod]
315435
public void WrapRichText_WordSpanningFragments_MeasuresCorrectly()
316436
{

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

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Date Author Change
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
1414
*************************************************************************************************/
15+
using EPPlus.Fonts.OpenType.Utilities;
1516
using OfficeOpenXml.Interfaces.Drawing.Text;
1617
using System;
1718
using System.Collections.Generic;
@@ -40,17 +41,18 @@ public List<string> WrapRichText(
4041
_lineListBuffer.Clear();
4142

4243
var lineBuilder = new StringBuilder(512);
43-
double lineWidth = 0;
44-
int lastSpaceIndex = -1;
44+
var state = new WrapStateRichText(0);
45+
state.WordStart = -1;
46+
state.LineStart = -1;
4547

4648
foreach (var fragment in fragments)
4749
{
4850
if (string.IsNullOrEmpty(fragment.Text)) continue;
4951

50-
ProcessFragment(fragment, maxWidthPoints, lineBuilder, ref lineWidth, ref lastSpaceIndex);
52+
ProcessFragment(fragment, maxWidthPoints, lineBuilder, state);
5153
}
5254

53-
FinalizeCurrentLine(lineBuilder, lineWidth, lastSpaceIndex);
55+
FinalizeCurrentLine(lineBuilder, state.CurrentLineWidth, state.WordStart);
5456

5557
if (_lineListBuffer.Count == 0)
5658
{
@@ -64,8 +66,7 @@ private void ProcessFragment(
6466
TextFragment fragment,
6567
double maxWidthPoints,
6668
StringBuilder lineBuilder,
67-
ref double lineWidth,
68-
ref int lastSpaceIndex)
69+
WrapStateRichText state)
6970
{
7071
var shaper = GetShaperForFont(fragment.Font);
7172
var options = fragment.Options ?? ShapingOptions.Default;
@@ -87,28 +88,48 @@ private void ProcessFragment(
8788

8889
if (IsLineBreak(c))
8990
{
90-
HandleLineBreak(lineBuilder, lineWidth, lastSpaceIndex);
91+
HandleLineBreak(lineBuilder, state);
9192
SkipLineBreakChars(fragment.Text, ref i);
92-
lineWidth = 0;
93-
lastSpaceIndex = -1; // Reset after line break
93+
state.CurrentLineWidth = 0;
94+
state.CurrentWordWidth = 0;
95+
state.WordStart = -1; // Reset after line break
96+
state.LineStart = -1;
9497
continue;
9598
}
9699

100+
state.CurrentLineWidth += charWidths[i];
101+
state.CurrentWordWidth += charWidths[i];
102+
97103
lineBuilder.Append(c);
98-
lineWidth += charWidths[i];
99104

100105
if (c == ' ')
101106
{
102-
lastSpaceIndex = lineBuilder.Length - 1;
107+
state.WordStart = lineBuilder.Length - 1;
108+
state.CurrentWordWidth = 0;
103109
}
104110

105-
if (lineWidth > maxWidthPoints)
111+
if (state.CurrentLineWidth > maxWidthPoints)
106112
{
107-
WrapCurrentLine(lineBuilder, lineWidth, lastSpaceIndex, maxWidthPoints);
108-
lineWidth = 0;
109-
lastSpaceIndex = -1; // Reset after wrap
113+
WrapCurrentLine(lineBuilder, state, maxWidthPoints);
114+
115+
state.CurrentWordWidth = state.CurrentLineWidth;
116+
117+
state.WordStart = -1;
118+
state.LineStart = -1;
119+
//We do not append ending spaces to the new line
120+
if (c != ' ')
121+
{
122+
//lineBuilder.Append(c);
123+
if(state.CurrentWordWidth == 0)
124+
{
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.
128+
state.CurrentWordWidth = charWidths[i];
129+
state.CurrentLineWidth = charWidths[i];
130+
}
131+
}
110132
}
111-
112133
i++;
113134
}
114135
}
@@ -130,7 +151,7 @@ private bool IsLineBreak(char c)
130151
return c == '\r' || c == '\n';
131152
}
132153

133-
private void HandleLineBreak(StringBuilder lineBuilder, double lineWidth, int lastSpaceIndex)
154+
private void HandleLineBreak(StringBuilder lineBuilder, WrapStateRichText state)
134155
{
135156
if (lineBuilder.Length > 0 && lineBuilder[lineBuilder.Length - 1] == ' ')
136157
{
@@ -140,7 +161,7 @@ private void HandleLineBreak(StringBuilder lineBuilder, double lineWidth, int la
140161
{
141162
_lineListBuffer.Add(lineBuilder.ToString());
142163
}
143-
else if (lineWidth > 0)
164+
else if (state.CurrentLineWidth > 0)
144165
{
145166
_lineListBuffer.Add(string.Empty);
146167
}
@@ -157,20 +178,28 @@ private void SkipLineBreakChars(string text, ref int i)
157178
i++;
158179
}
159180

160-
private void WrapCurrentLine(StringBuilder lineBuilder, double lineWidth, int lastSpaceIndex, double maxWidthPoints)
181+
private void WrapCurrentLine(StringBuilder lineBuilder, WrapStateRichText state, double maxWidthPoints)
161182
{
162183
// Bounds check to prevent ArgumentOutOfRangeException
163-
if (lastSpaceIndex >= 0 && lastSpaceIndex < lineBuilder.Length)
184+
if (state.WordStart >= 0 && state.WordStart < lineBuilder.Length)
164185
{
165-
string line = lineBuilder.ToString(0, lastSpaceIndex).TrimEnd();
186+
string line = lineBuilder.ToString(0, state.WordStart).TrimEnd();
166187
_lineListBuffer.Add(line);
167-
lineBuilder.Remove(0, lastSpaceIndex + 1);
188+
lineBuilder.Remove(0, state.WordStart + 1);
189+
190+
//A word was moved down. The new line must have the width and pos of the word.
191+
state.CurrentLineWidth = state.CurrentWordWidth;
192+
state.LineStart = state.WordStart;
168193
}
169194
else
170195
{
196+
var lastChar = lineBuilder[lineBuilder.Length-1];
171197
// No valid space found - wrap entire line
172-
_lineListBuffer.Add(lineBuilder.ToString());
198+
_lineListBuffer.Add(lineBuilder.ToString(0, lineBuilder.Length -1));
199+
state.CurrentLineWidth = 0;
173200
lineBuilder.Length = 0;
201+
lineBuilder.Append(lastChar);
202+
174203
}
175204
}
176205

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace EPPlus.Fonts.OpenType.Integration
7+
{
8+
internal abstract class WrapStateBase
9+
{
10+
public int LineStart { get; set; }
11+
public int WordStart { get; set; }
12+
public double CurrentLineWidth { get; set; }
13+
public double CurrentWordWidth { get; set; }
14+
15+
public bool IsCompleteWordReady(CharacterType charType, int currentPosition)
16+
{
17+
return (charType == CharacterType.Space || charType == CharacterType.EndOfText)
18+
&& WordStart < currentPosition;
19+
}
20+
}
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace EPPlus.Fonts.OpenType.Integration
7+
{
8+
internal class WrapStateRichText : WrapStateBase
9+
{
10+
public WrapStateRichText(double lineWidth)
11+
{
12+
CurrentLineWidth = lineWidth;
13+
}
14+
}
15+
}

src/EPPlus.Fonts.OpenType/Integration/WrapState.cs renamed to src/EPPlus.Fonts.OpenType/Integration/WrapStateText.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,14 @@ Date Author Change
1414

1515
namespace EPPlus.Fonts.OpenType.Integration
1616
{
17-
internal class WrapState
17+
internal class WrapStateText : WrapStateBase
1818
{
19-
public WrapState(double lineWidth, double spaceWidth)
19+
public WrapStateText(double lineWidth, double spaceWidth)
2020
{
2121
CurrentLineWidth = lineWidth;
2222
SpaceWidth = spaceWidth;
2323
}
2424

25-
public int LineStart { get; set; }
26-
public int WordStart { get; set; }
27-
public double CurrentLineWidth { get; set; }
28-
public double CurrentWordWidth { get; set; }
2925
public double SpaceWidth { get; set; }
30-
31-
public bool IsCompleteWordReady(CharacterType charType, int currentPosition)
32-
{
33-
return (charType == CharacterType.Space || charType == CharacterType.EndOfText)
34-
&& WordStart < currentPosition;
35-
}
3626
}
3727
}

0 commit comments

Comments
 (0)