Skip to content

Commit de43266

Browse files
committed
Refactored TextLayoutEngine.WrapParagraph and added support for linebreak inside word
1 parent ff9ec97 commit de43266

7 files changed

Lines changed: 450 additions & 126 deletions

File tree

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,5 +380,25 @@ public void WrapRichText_SameFontMultipleTimes_UsesCache()
380380
}
381381

382382
#endregion
383+
384+
[TestMethod]
385+
public void WrapText_Continous_Long_Word()
386+
{
387+
var font = OpenTypeFonts.GetFontData(FontFolders, "Aptos Narrow", FontSubFamily.Regular);
388+
389+
var longWord = "pellentesquer";
390+
391+
ITextShaper shaper = new TextShaper(font);
392+
using var layoutEngine = new TextLayoutEngine(shaper);
393+
var wrappedLines = layoutEngine.WrapText(
394+
longWord,
395+
11f,
396+
54,
397+
ShapingOptions.Full
398+
);
399+
400+
Assert.AreEqual("pellentesqu", wrappedLines[0]);
401+
Assert.AreEqual("er", wrappedLines[1]);
402+
}
383403
}
384404
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*************************************************************************************************
2+
Required Notice: Copyright (C) EPPlus Software AB.
3+
This software is licensed under PolyForm Noncommercial License 1.0.0
4+
and may only be used for noncommercial purposes
5+
https://polyformproject.org/licenses/noncommercial/1.0.0/
6+
7+
A commercial license to use this software can be purchased at https://epplussoftware.com
8+
*************************************************************************************************
9+
Date Author Change
10+
*************************************************************************************************
11+
01/20/2025 EPPlus Software AB TextLayoutEngine implementation
12+
*************************************************************************************************/
13+
namespace EPPlus.Fonts.OpenType.Integration
14+
{
15+
internal enum CharacterType
16+
{
17+
Space,
18+
EndOfText,
19+
Regular
20+
}
21+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using OfficeOpenXml.Interfaces.Drawing.Text;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using EPPlus.Fonts.OpenType.Utilities;
7+
8+
9+
namespace EPPlus.Fonts.OpenType.Integration
10+
{
11+
public partial class TextLayoutEngine
12+
{
13+
private List<string> CreateEmptyResult()
14+
{
15+
_lineListBuffer.Clear();
16+
_lineListBuffer.Add(string.Empty);
17+
return new List<string>(_lineListBuffer);
18+
}
19+
20+
private void PrepareLineBuilder(int textLength)
21+
{
22+
_lineBuilder.Clear();
23+
_lineBuilder.EnsureCapacity(textLength / 4 + 20);
24+
}
25+
26+
private double[] CalculateCharacterWidths(string text, float fontSize, ShapingOptions options)
27+
{
28+
var glyphs = _shaper.ShapeLight(text, options);
29+
var charWidths = GetCharWidthBuffer(text.Length);
30+
Array.Clear(charWidths, 0, text.Length);
31+
32+
double scaleFactor = fontSize / _shaper.UnitsPerEm;
33+
34+
foreach (var glyph in glyphs)
35+
{
36+
int charIndex = glyph.ClusterIndex;
37+
if (charIndex >= 0 && charIndex < text.Length)
38+
{
39+
charWidths[charIndex] += glyph.XAdvance * scaleFactor;
40+
}
41+
}
42+
43+
return charWidths;
44+
}
45+
46+
47+
48+
private CharacterType GetCharacterType(string text, int position)
49+
{
50+
if (position >= text.Length)
51+
return CharacterType.EndOfText;
52+
53+
if (text[position] == ' ')
54+
return CharacterType.Space;
55+
56+
return CharacterType.Regular;
57+
}
58+
}
59+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*************************************************************************************************
2+
Required Notice: Copyright (C) EPPlus Software AB.
3+
This software is licensed under PolyForm Noncommercial License 1.0.0
4+
and may only be used for noncommercial purposes
5+
https://polyformproject.org/licenses/noncommercial/1.0.0/
6+
7+
A commercial license to use this software can be purchased at https://epplussoftware.com
8+
*************************************************************************************************
9+
Date Author Change
10+
*************************************************************************************************
11+
01/20/2025 EPPlus Software AB TextLayoutEngine implementation
12+
01/22/2025 EPPlus Software AB Optimized with shaping cache
13+
01/23/2025 EPPlus Software AB Fixed lastSpaceIndex bug in multi-fragment wrapping
14+
*************************************************************************************************/
15+
using EPPlus.Fonts.OpenType.Utilities;
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using System.Text;
20+
21+
namespace EPPlus.Fonts.OpenType.Integration
22+
{
23+
public partial class TextLayoutEngine
24+
{
25+
/// <summary>
26+
/// Processes a complete word (reached space or end of text).
27+
/// Decides whether to add it to current line or start a new line.
28+
/// </summary>
29+
private void ProcessCompleteWord(
30+
string text,
31+
WrapState state,
32+
int currentPos,
33+
double maxWidth)
34+
{
35+
double totalWidth = state.CurrentLineWidth + state.CurrentWordWidth;
36+
37+
if (state.LineStart < state.WordStart)
38+
{
39+
totalWidth += state.SpaceWidth;
40+
}
41+
42+
if (totalWidth <= maxWidth || state.LineStart == state.WordStart)
43+
{
44+
// Word fits on current line
45+
_lineBuilder.AppendSpaceIfNotEmpty();
46+
_lineBuilder.AppendSubstring(text, state.WordStart, currentPos - state.WordStart);
47+
state.CurrentLineWidth = totalWidth;
48+
}
49+
else
50+
{
51+
// Word doesn't fit - start new line
52+
_lineBuilder.FlushToList(_lineListBuffer);
53+
54+
state.LineStart = state.WordStart;
55+
state.CurrentLineWidth = state.CurrentWordWidth;
56+
57+
_lineBuilder.AppendSubstring(text, state.WordStart, currentPos - state.WordStart);
58+
}
59+
60+
state.WordStart = currentPos + 1;
61+
state.CurrentWordWidth = 0;
62+
}
63+
64+
private void ProcessCharacterInWord(
65+
string text,
66+
double[] charWidths,
67+
WrapState state,
68+
int currentPos,
69+
double maxWidth)
70+
{
71+
state.CurrentWordWidth += charWidths[currentPos];
72+
73+
// CASE 1: Line has content and word grows too large
74+
if (state.CurrentWordWidth > maxWidth &&
75+
state.LineStart < state.WordStart &&
76+
state.CurrentLineWidth > 0)
77+
{
78+
_lineBuilder.FlushToList(_lineListBuffer);
79+
state.LineStart = state.WordStart;
80+
state.CurrentLineWidth = 0;
81+
}
82+
83+
// CASE 2: Word is alone on line and too long - break it
84+
if (state.LineStart == state.WordStart && state.CurrentWordWidth > maxWidth)
85+
{
86+
BreakLongWord(text, charWidths, state, currentPos, maxWidth);
87+
}
88+
}
89+
90+
/// <summary>
91+
/// Breaks a word that is too long to fit on a single line.
92+
/// Uses backward removal strategy: removes characters from the end until the word fits.
93+
/// </summary>
94+
private void BreakLongWord(
95+
string text,
96+
double[] charWidths,
97+
WrapState state,
98+
int currentPos,
99+
double maxWidth)
100+
{
101+
int breakPoint = currentPos + 1;
102+
double currentWidth = state.CurrentWordWidth;
103+
104+
// Remove characters from the end until it fits
105+
while (breakPoint > state.WordStart + 1 && currentWidth > maxWidth)
106+
{
107+
breakPoint--;
108+
currentWidth -= charWidths[breakPoint];
109+
}
110+
111+
// Safety: at least 1 character must fit
112+
if (breakPoint <= state.WordStart)
113+
{
114+
breakPoint = state.WordStart + 1;
115+
}
116+
117+
// Add what fits on current line
118+
_lineBuilder.AppendSubstring(text, state.WordStart, breakPoint - state.WordStart);
119+
_lineListBuffer.Add(_lineBuilder.ToString());
120+
_lineBuilder.Clear();
121+
122+
// Calculate width of remaining part
123+
state.CurrentWordWidth = 0;
124+
for (int j = breakPoint; j <= currentPos; j++)
125+
{
126+
if (j < text.Length)
127+
{
128+
state.CurrentWordWidth += charWidths[j];
129+
}
130+
}
131+
132+
// Update state
133+
state.WordStart = breakPoint;
134+
state.LineStart = breakPoint;
135+
state.CurrentLineWidth = 0;
136+
}
137+
138+
private List<string> FinalizeWrapping()
139+
{
140+
_lineBuilder.FlushToList(_lineListBuffer);
141+
142+
if (_lineListBuffer.Count == 0)
143+
{
144+
_lineListBuffer.Add(string.Empty);
145+
}
146+
147+
return new List<string>(_lineListBuffer);
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)