|
| 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